Skip to content

Commit b1d099c

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 e2cbaf0 commit b1d099c

2 files changed

Lines changed: 95 additions & 4 deletions

File tree

mdl/executor/cmd_microflows_inheritance_test.go

Lines changed: 86 additions & 0 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{

mdl/executor/cmd_microflows_show_helpers.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@ func traverseFlowUntilMerge(
656656
startLine := len(*lines) + headerLineCount
657657
nestedMergeID := splitMergeMap[currentID]
658658
emitObjectAnnotations(obj, lines, indentStr, annotationsByTarget, flowsByOrigin, flowsByDest)
659-
emitInheritanceSplitStatement(ctx, currentID, nestedMergeID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, visited, entityNames, microflowNames, lines, indent, sourceMap, headerLineCount, annotationsByTarget)
659+
emitInheritanceSplitStatement(ctx, currentID, mergeID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, visited, entityNames, microflowNames, lines, indent, sourceMap, headerLineCount, annotationsByTarget)
660660
recordSourceMap(sourceMap, currentID, startLine, len(*lines)+headerLineCount-1)
661661
if nestedMergeID != "" && nestedMergeID != mergeID {
662662
visited[nestedMergeID] = true
@@ -949,7 +949,7 @@ func isMergePairedWithSplit(mergeID model.ID, splitMergeMap map[model.ID]model.I
949949
func emitInheritanceSplitStatement(
950950
ctx *ExecContext,
951951
currentID model.ID,
952-
mergeID model.ID,
952+
stopID model.ID,
953953
activityMap map[model.ID]microflows.MicroflowObject,
954954
flowsByOrigin map[model.ID][]*microflows.SequenceFlow,
955955
flowsByDest map[model.ID][]*microflows.SequenceFlow,
@@ -974,6 +974,11 @@ func emitInheritanceSplitStatement(
974974
indentStr := strings.Repeat(" ", indent)
975975
*lines = append(*lines, indentStr+"split type "+varName)
976976

977+
branchStopID := splitMergeMap[currentID]
978+
if branchStopID == "" {
979+
branchStopID = stopID
980+
}
981+
977982
var elseFlow *microflows.SequenceFlow
978983
for _, flow := range orderedInheritanceSplitFlows(findNormalFlows(flowsByOrigin[currentID])) {
979984
caseName, ok := inheritanceCaseName(flow, entityNames)
@@ -982,11 +987,11 @@ func emitInheritanceSplitStatement(
982987
continue
983988
}
984989
*lines = append(*lines, indentStr+"case "+caseName)
985-
traverseFlowUntilMerge(ctx, flow.DestinationID, mergeID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, cloneVisited(visited), entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
990+
traverseFlowUntilMerge(ctx, flow.DestinationID, branchStopID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, cloneVisited(visited), entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
986991
}
987992
if elseFlow != nil {
988993
*lines = append(*lines, indentStr+"else")
989-
traverseFlowUntilMerge(ctx, elseFlow.DestinationID, mergeID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, cloneVisited(visited), entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
994+
traverseFlowUntilMerge(ctx, elseFlow.DestinationID, branchStopID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, cloneVisited(visited), entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
990995
}
991996
*lines = append(*lines, indentStr+"end split;")
992997
}

0 commit comments

Comments
 (0)