Skip to content

Commit c54165d

Browse files
committed
fix: keep nested inheritance split tails outside cases
Symptom: describing a microflow with an inheritance split inside an if could emit the parent continuation inside the matching split case. Re-executing that MDL made variables declared in the continuation branch-scoped, so Mendix mx check reported invalid or missing return/variable state. Root cause: nested inheritance split emission stopped branches only at the split's own merge. When the inheritance split had no merge because one branch returned and the other fell through to the parent if merge, branch traversal used an empty stop ID and consumed the parent tail. Fix: when emitting an inheritance split, prefer the split's own merge but fall back to the caller's stop ID. This keeps parent continuation statements outside the split cases while preserving standalone inheritance split behavior. Tests: added a synthetic nested if/split-type traversal regression that verifies the parent tail is emitted after both end split and end if.
1 parent 9bee0fd commit c54165d

2 files changed

Lines changed: 111 additions & 6 deletions

File tree

mdl/executor/cmd_microflows_inheritance_test.go

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,92 @@ func TestTraverseFlow_InheritanceSplitPreservesExplicitCaseOrder(t *testing.T) {
150150
}
151151
}
152152

153+
func TestTraverseFlow_NestedInheritanceSplitKeepsParentTailOutsideCase(t *testing.T) {
154+
e := newTestExecutor()
155+
entityID := mkID("entity-specialized")
156+
157+
activityMap := map[model.ID]microflows.MicroflowObject{
158+
mkID("start"): &microflows.StartEvent{BaseMicroflowObject: mkObj("start")},
159+
mkID("init"): &microflows.ActionActivity{
160+
BaseActivity: microflows.BaseActivity{BaseMicroflowObject: mkObj("init")},
161+
Action: &microflows.CreateVariableAction{
162+
VariableName: "TokenValue",
163+
InitialValue: "''",
164+
},
165+
},
166+
mkID("outer_split"): &microflows.ExclusiveSplit{
167+
BaseMicroflowObject: mkObj("outer_split"),
168+
SplitCondition: &microflows.ExpressionSplitCondition{Expression: "$UseToken"},
169+
},
170+
mkID("before_type_split"): &microflows.ActionActivity{
171+
BaseActivity: microflows.BaseActivity{BaseMicroflowObject: mkObj("before_type_split")},
172+
Action: &microflows.LogMessageAction{LogLevel: "Info", LogNodeName: "'App'", MessageTemplate: &model.Text{Translations: map[string]string{"en_US": "before type split"}}},
173+
},
174+
mkID("type_split"): &microflows.InheritanceSplit{
175+
BaseMicroflowObject: mkObj("type_split"),
176+
VariableName: "Input",
177+
},
178+
mkID("set_token"): &microflows.ActionActivity{
179+
BaseActivity: microflows.BaseActivity{BaseMicroflowObject: mkObj("set_token")},
180+
Action: &microflows.ChangeVariableAction{VariableName: "TokenValue", Value: "$Input/Value"},
181+
},
182+
mkID("failed_log"): &microflows.ActionActivity{
183+
BaseActivity: microflows.BaseActivity{BaseMicroflowObject: mkObj("failed_log")},
184+
Action: &microflows.LogMessageAction{LogLevel: "Info", LogNodeName: "'App'", MessageTemplate: &model.Text{Translations: map[string]string{"en_US": "no token"}}},
185+
},
186+
mkID("failed_return"): &microflows.EndEvent{
187+
BaseMicroflowObject: mkObj("failed_return"),
188+
ReturnValue: "empty",
189+
},
190+
mkID("outer_merge"): &microflows.ExclusiveMerge{BaseMicroflowObject: mkObj("outer_merge")},
191+
mkID("tail"): &microflows.ActionActivity{
192+
BaseActivity: microflows.BaseActivity{BaseMicroflowObject: mkObj("tail")},
193+
Action: &microflows.LogMessageAction{LogLevel: "Info", LogNodeName: "'App'", MessageTemplate: &model.Text{Translations: map[string]string{"en_US": "tail after split"}}},
194+
},
195+
mkID("end"): &microflows.EndEvent{
196+
BaseMicroflowObject: mkObj("end"),
197+
ReturnValue: "'ok'",
198+
},
199+
}
200+
flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{
201+
mkID("start"): {mkFlow("start", "init")},
202+
mkID("init"): {mkFlow("init", "outer_split")},
203+
mkID("outer_split"): {
204+
mkBranchFlow("outer_split", "before_type_split", &microflows.ExpressionCase{Expression: "true"}),
205+
mkBranchFlow("outer_split", "outer_merge", &microflows.ExpressionCase{Expression: "false"}),
206+
},
207+
mkID("before_type_split"): {mkFlow("before_type_split", "type_split")},
208+
mkID("type_split"): {
209+
mkBranchFlow("type_split", "set_token", &microflows.InheritanceCase{EntityID: entityID}),
210+
mkBranchFlow("type_split", "failed_log", &microflows.InheritanceCase{}),
211+
},
212+
mkID("set_token"): {mkFlow("set_token", "outer_merge")},
213+
mkID("failed_log"): {mkFlow("failed_log", "failed_return")},
214+
mkID("outer_merge"): {mkFlow("outer_merge", "tail")},
215+
mkID("tail"): {mkFlow("tail", "end")},
216+
}
217+
splitMergeMap := map[model.ID]model.ID{mkID("outer_split"): mkID("outer_merge")}
218+
entityNames := map[model.ID]string{entityID: "Sample.SpecializedInput"}
219+
220+
var lines []string
221+
visited := make(map[model.ID]bool)
222+
e.traverseFlow(mkID("start"), activityMap, flowsByOrigin, splitMergeMap, visited, entityNames, nil, &lines, 0, nil, 0, nil)
223+
224+
out := strings.Join(lines, "\n")
225+
tail := strings.Index(out, "tail after split")
226+
endSplit := strings.Index(out, "end split;")
227+
endIf := strings.Index(out, "end if;")
228+
if tail == -1 {
229+
t.Fatalf("expected parent tail after nested inheritance split:\n%s", out)
230+
}
231+
if endSplit == -1 || tail < endSplit {
232+
t.Fatalf("parent tail must not be emitted inside the inheritance case:\n%s", out)
233+
}
234+
if endIf == -1 || tail < endIf {
235+
t.Fatalf("parent tail must remain after the outer IF closes:\n%s", out)
236+
}
237+
}
238+
153239
func TestLastStmtIsReturn_InheritanceSplitAllBranchesReturn(t *testing.T) {
154240
body := []ast.MicroflowStatement{
155241
&ast.InheritanceSplitStmt{
@@ -219,8 +305,22 @@ func TestBuilder_InheritanceSplitNestedEmptyThenBranchKeepsContinuationCase(t *t
219305
if flow.OriginID != nestedSplitID {
220306
continue
221307
}
222-
caseValue, ok := flow.CaseValue.(microflows.EnumerationCase)
223-
if !ok || caseValue.Value != "true" {
308+
// After PR #337 the expression split uses ExpressionCase (pointer or
309+
// value receiver) with Expression="true"/"false" rather than
310+
// EnumerationCase. Accept either representation so the test
311+
// documents the intent without pinning the case shape.
312+
value := ""
313+
switch c := flow.CaseValue.(type) {
314+
case microflows.EnumerationCase:
315+
value = c.Value
316+
case *microflows.EnumerationCase:
317+
value = c.Value
318+
case microflows.ExpressionCase:
319+
value = c.Expression
320+
case *microflows.ExpressionCase:
321+
value = c.Expression
322+
}
323+
if value != "true" {
224324
continue
225325
}
226326
if _, ok := objects[flow.DestinationID].(*microflows.ExclusiveMerge); ok {

mdl/executor/cmd_microflows_show_helpers.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,7 @@ func traverseFlowUntilMerge(
833833
startLine := len(*lines) + headerLineCount
834834
nestedMergeID := splitMergeMap[currentID]
835835
emitObjectAnnotations(obj, lines, indentStr, annotationsByTarget, flowsByOrigin, flowsByDest, activityMap)
836-
emitInheritanceSplitStatement(ctx, currentID, nestedMergeID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, visited, entityNames, microflowNames, lines, indent, sourceMap, headerLineCount, annotationsByTarget)
836+
emitInheritanceSplitStatement(ctx, currentID, mergeID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, visited, entityNames, microflowNames, lines, indent, sourceMap, headerLineCount, annotationsByTarget)
837837
recordSourceMap(sourceMap, currentID, startLine, len(*lines)+headerLineCount-1)
838838
if nestedMergeID != "" && nestedMergeID != mergeID {
839839
visited[nestedMergeID] = true
@@ -1353,7 +1353,7 @@ func emitEnumSplitStatement(
13531353
func emitInheritanceSplitStatement(
13541354
ctx *ExecContext,
13551355
currentID model.ID,
1356-
mergeID model.ID,
1356+
stopID model.ID,
13571357
activityMap map[model.ID]microflows.MicroflowObject,
13581358
flowsByOrigin map[model.ID][]*microflows.SequenceFlow,
13591359
flowsByDest map[model.ID][]*microflows.SequenceFlow,
@@ -1378,6 +1378,11 @@ func emitInheritanceSplitStatement(
13781378
indentStr := strings.Repeat(" ", indent)
13791379
*lines = append(*lines, indentStr+"split type "+varName)
13801380

1381+
branchStopID := splitMergeMap[currentID]
1382+
if branchStopID == "" {
1383+
branchStopID = stopID
1384+
}
1385+
13811386
var elseFlow *microflows.SequenceFlow
13821387
for _, flow := range orderedInheritanceSplitFlows(findNormalFlows(flowsByOrigin[currentID])) {
13831388
caseName, ok := inheritanceCaseName(flow, entityNames)
@@ -1386,11 +1391,11 @@ func emitInheritanceSplitStatement(
13861391
continue
13871392
}
13881393
*lines = append(*lines, indentStr+"case "+caseName)
1389-
traverseFlowUntilMerge(ctx, flow.DestinationID, mergeID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, cloneVisited(visited), entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
1394+
traverseFlowUntilMerge(ctx, flow.DestinationID, branchStopID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, cloneVisited(visited), entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
13901395
}
13911396
if elseFlow != nil {
13921397
*lines = append(*lines, indentStr+"else")
1393-
traverseFlowUntilMerge(ctx, elseFlow.DestinationID, mergeID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, cloneVisited(visited), entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
1398+
traverseFlowUntilMerge(ctx, elseFlow.DestinationID, branchStopID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, cloneVisited(visited), entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
13941399
}
13951400
*lines = append(*lines, indentStr+"end split;")
13961401
}

0 commit comments

Comments
 (0)