Skip to content

Commit cb2eba2

Browse files
committed
fix: preserve enum split branch order in staged builder
Symptom: enum split roundtrips could reorder an explicit `(empty)` case after named cases, and an empty-body grouped case could move behind a terminal branch. Root cause: the staged enum split builder selected split-flow anchors from branch geometry. `(empty)` branches used bottom-origin geometry, and empty-body branch tails were emitted after body branches, so the describer could not recover the original authoring order. Fix: carry the authoring branch index onto empty-body tails, encode the first split flow of order-sensitive branches using the existing split-case order anchors, and sort enum split flows by that encoded order before grouping cases during describe. Tests: added synthetic enum split regressions for `(empty)` before named cases and grouped empty-body cases before terminal cases; ran make build, make test, make lint-go, and a targeted two-microflow audit with clean mxcli and Mendix mx check results.
1 parent efa3acf commit cb2eba2

3 files changed

Lines changed: 169 additions & 2 deletions

File tree

mdl/executor/cmd_microflows_builder_actions.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ func (fb *flowBuilder) addEnumSplit(s *ast.EnumSplitStmt) model.ID {
292292
id model.ID
293293
values []string
294294
fromSplit bool
295+
order int
295296
}
296297
var branchTails []branchTail
297298
routePendingErrorToElse := len(s.ElseBody) > 0 && fb.errorHandlerSkipVar != "" && s.Variable == fb.errorHandlerSkipVar
@@ -303,7 +304,7 @@ func (fb *flowBuilder) addEnumSplit(s *ast.EnumSplitStmt) model.ID {
303304
branchIndex++
304305
if len(body) == 0 {
305306
allBranchesReturn = false
306-
branchTails = append(branchTails, branchTail{id: splitID, values: values, fromSplit: true})
307+
branchTails = append(branchTails, branchTail{id: splitID, values: values, fromSplit: true, order: branchNumber})
307308
return
308309
}
309310

@@ -366,6 +367,9 @@ func (fb *flowBuilder) addEnumSplit(s *ast.EnumSplitStmt) model.ID {
366367
}
367368
baseDestinationAnchor := flow.DestinationConnectionIndex
368369
if branchEntryIsSplit {
370+
if len(values) <= 1 && (thisAnchor == nil || thisAnchor.To == ast.AnchorSideUnset) {
371+
applySplitCaseOrder(flow, branchNumber)
372+
}
369373
applyEnumGroupedControlVectors(flow, 0, len(values))
370374
}
371375
fb.flows = append(fb.flows, flow)
@@ -463,6 +467,7 @@ func (fb *flowBuilder) addEnumSplit(s *ast.EnumSplitStmt) model.ID {
463467
} else {
464468
flow = newDownwardFlowWithEnumCase(splitID, merge.ID, enumSplitFlowCaseValue(tail.values, 0))
465469
}
470+
applySplitCaseOrder(flow, tail.order)
466471
baseDestinationAnchor := flow.DestinationConnectionIndex
467472
applyEnumGroupedControlVectors(flow, 0, len(tail.values))
468473
fb.flows = append(fb.flows, flow)
@@ -524,6 +529,39 @@ func enumSplitGroupedDestinationAnchor(baseAnchor, index int) int {
524529
return baseAnchor
525530
}
526531

532+
type splitCaseOrderAnchor struct {
533+
origin int
534+
destination int
535+
}
536+
537+
var splitCaseOrderAnchors = []splitCaseOrderAnchor{
538+
{AnchorTop, AnchorLeft},
539+
{AnchorRight, AnchorLeft},
540+
{AnchorBottom, AnchorLeft},
541+
{AnchorLeft, AnchorLeft},
542+
{AnchorTop, AnchorTop},
543+
{AnchorRight, AnchorTop},
544+
{AnchorBottom, AnchorTop},
545+
{AnchorLeft, AnchorTop},
546+
{AnchorTop, AnchorRight},
547+
{AnchorRight, AnchorRight},
548+
{AnchorBottom, AnchorRight},
549+
{AnchorLeft, AnchorRight},
550+
{AnchorTop, AnchorBottom},
551+
{AnchorRight, AnchorBottom},
552+
{AnchorBottom, AnchorBottom},
553+
{AnchorLeft, AnchorBottom},
554+
}
555+
556+
func applySplitCaseOrder(flow *microflows.SequenceFlow, order int) {
557+
if flow == nil || order < 0 || order >= len(splitCaseOrderAnchors) {
558+
return
559+
}
560+
pair := splitCaseOrderAnchors[order]
561+
flow.OriginConnectionIndex = pair.origin
562+
flow.DestinationConnectionIndex = pair.destination
563+
}
564+
527565
func applyEnumGroupedControlVectors(flow *microflows.SequenceFlow, index, total int) {
528566
if flow == nil || total == 0 {
529567
return

mdl/executor/cmd_microflows_builder_enum_split_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package executor
44

55
import (
6+
"strings"
67
"testing"
78

89
"github.com/mendixlabs/mxcli/mdl/ast"
@@ -156,6 +157,86 @@ func TestBuildFlowGraph_EnumSplitGroupedCasesShareDestination(t *testing.T) {
156157
}
157158
}
158159

160+
func TestBuildFlowGraph_EnumSplitPreservesEmptyCaseBeforeNamedCases(t *testing.T) {
161+
out := describeBuiltEnumSplitBody(t, []ast.MicroflowStatement{
162+
&ast.EnumSplitStmt{
163+
Variable: "ImageKind",
164+
Cases: []ast.EnumSplitCase{
165+
{
166+
Value: "(empty)",
167+
Body: []ast.MicroflowStatement{
168+
&ast.LogStmt{
169+
Level: ast.LogError,
170+
Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "missing kind"},
171+
Annotations: &ast.ActivityAnnotations{Position: &ast.Position{X: -300, Y: -200}},
172+
},
173+
},
174+
},
175+
{
176+
Value: "Cover",
177+
Body: []ast.MicroflowStatement{
178+
&ast.LogStmt{
179+
Level: ast.LogInfo,
180+
Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "cover"},
181+
Annotations: &ast.ActivityAnnotations{Position: &ast.Position{X: -300, Y: 100}},
182+
},
183+
},
184+
},
185+
{
186+
Value: "Logo",
187+
Body: []ast.MicroflowStatement{
188+
&ast.LogStmt{
189+
Level: ast.LogInfo,
190+
Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "logo"},
191+
Annotations: &ast.ActivityAnnotations{Position: &ast.Position{X: -300, Y: -40}},
192+
},
193+
},
194+
},
195+
},
196+
},
197+
})
198+
199+
assertTextOrder(t, out, "case (empty)", "case Cover", "case Logo")
200+
}
201+
202+
func TestBuildFlowGraph_EnumSplitPreservesEmptyBodyCaseOrder(t *testing.T) {
203+
out := describeBuiltEnumSplitBody(t, []ast.MicroflowStatement{
204+
&ast.EnumSplitStmt{
205+
Variable: "Event/Type",
206+
Cases: []ast.EnumSplitCase{
207+
{
208+
Values: []string{"CREATE", "DELETE"},
209+
},
210+
{
211+
Value: "UPDATE",
212+
Body: []ast.MicroflowStatement{
213+
&ast.LogStmt{Level: ast.LogInfo, Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "update"}},
214+
&ast.ReturnStmt{},
215+
},
216+
},
217+
{
218+
Value: "(empty)",
219+
Body: []ast.MicroflowStatement{
220+
&ast.LogStmt{
221+
Level: ast.LogInfo,
222+
Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "empty"},
223+
Annotations: &ast.ActivityAnnotations{
224+
Anchor: &ast.FlowAnchors{From: ast.AnchorSideBottom, To: ast.AnchorSideTop},
225+
},
226+
},
227+
&ast.ReturnStmt{
228+
Annotations: &ast.ActivityAnnotations{Anchor: &ast.FlowAnchors{To: ast.AnchorSideTop}},
229+
},
230+
},
231+
},
232+
},
233+
},
234+
&ast.LogStmt{Level: ast.LogInfo, Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "shared tail"}},
235+
})
236+
237+
assertTextOrder(t, out, "case CREATE, DELETE", "case UPDATE", "case (empty)")
238+
}
239+
159240
func TestBuildFlowGraph_EnumSplitAllTerminalCasesDoesNotAddDefaultFlow(t *testing.T) {
160241
body := []ast.MicroflowStatement{
161242
&ast.EnumSplitStmt{
@@ -200,6 +281,33 @@ func TestBuildFlowGraph_EnumSplitAllTerminalCasesDoesNotAddDefaultFlow(t *testin
200281
}
201282
}
202283

284+
func describeBuiltEnumSplitBody(t *testing.T, body []ast.MicroflowStatement) string {
285+
t.Helper()
286+
287+
fb := &flowBuilder{posX: 100, posY: 100, spacing: HorizontalSpacing, measurer: &layoutMeasurer{}}
288+
oc := fb.buildFlowGraph(body, nil)
289+
mf := &microflows.Microflow{ObjectCollection: oc}
290+
e := newTestExecutor()
291+
292+
return strings.Join(formatMicroflowActivities(e.newExecContext(t.Context()), mf, nil, nil), "\n")
293+
}
294+
295+
func assertTextOrder(t *testing.T, out string, items ...string) {
296+
t.Helper()
297+
298+
lastIdx := -1
299+
for _, item := range items {
300+
idx := strings.Index(out, item)
301+
if idx == -1 {
302+
t.Fatalf("missing %q in output:\n%s", item, out)
303+
}
304+
if idx < lastIdx {
305+
t.Fatalf("expected %q after previous item in output:\n%s", item, out)
306+
}
307+
lastIdx = idx
308+
}
309+
}
310+
203311
func TestBuildFlowGraph_EnumSplitSiblingBranchesDeclareRepeatedCallOutputs(t *testing.T) {
204312
body := []ast.MicroflowStatement{
205313
&ast.EnumSplitStmt{

mdl/executor/cmd_microflows_show_helpers.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package executor
66
import (
77
"context"
88
"fmt"
9+
"sort"
910
"strings"
1011

1112
"github.com/mendixlabs/mxcli/model"
@@ -1047,7 +1048,7 @@ func emitEnumSplitStatement(
10471048
var branches []enumBranch
10481049
branchByDestination := make(map[model.ID]int)
10491050
var elseFlow *microflows.SequenceFlow
1050-
for _, flow := range findNormalFlows(flowsByOrigin[currentID]) {
1051+
for _, flow := range orderedEnumSplitFlows(findNormalFlows(flowsByOrigin[currentID])) {
10511052
caseValue, isEnum := enumCaseValue(flow.CaseValue)
10521053
if !isEnum {
10531054
if isNoCaseValue(flow.CaseValue) {
@@ -1141,6 +1142,26 @@ func enumCaseValue(cv microflows.CaseValue) (string, bool) {
11411142
}
11421143
}
11431144

1145+
func orderedEnumSplitFlows(flows []*microflows.SequenceFlow) []*microflows.SequenceFlow {
1146+
ordered := append([]*microflows.SequenceFlow(nil), flows...)
1147+
sort.SliceStable(ordered, func(i, j int) bool {
1148+
return splitCaseOrder(ordered[i]) < splitCaseOrder(ordered[j])
1149+
})
1150+
return ordered
1151+
}
1152+
1153+
func splitCaseOrder(flow *microflows.SequenceFlow) int {
1154+
if flow == nil {
1155+
return 1 << 20
1156+
}
1157+
for i, pair := range splitCaseOrderAnchors {
1158+
if flow.OriginConnectionIndex == pair.origin && flow.DestinationConnectionIndex == pair.destination {
1159+
return i
1160+
}
1161+
}
1162+
return (1 << 10) + flow.OriginConnectionIndex*4 + flow.DestinationConnectionIndex
1163+
}
1164+
11441165
func isNoCaseValue(cv microflows.CaseValue) bool {
11451166
switch cv.(type) {
11461167
case *microflows.NoCase, microflows.NoCase:

0 commit comments

Comments
 (0)