Skip to content

Commit 4270259

Browse files
authored
Merge pull request #504 from hjotha/submit/error-handler-rejoin-if-else
fix: rejoin empty custom error handler at IF ELSE first activity
2 parents 29779d6 + b351068 commit 4270259

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

mdl/executor/cmd_microflows_builder_control.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,36 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
4141
// a branch's RETURN affecting the parent flow state prematurely
4242
savedEndsWithReturn := fb.endsWithReturn
4343

44+
// Capture an incoming empty custom error handler whose source is the
45+
// previous call activity (set by finishCustomErrorHandler via
46+
// registerEmptyCustomErrorHandlerWithSkip) when the call has a
47+
// non-empty output variable (skipVar). The Studio Pro authored pattern
48+
// wires the error edge into the IF's ELSE branch — at the first
49+
// activity in the ELSE body — so the fallback path handles both "no
50+
// value" and "error" cases. Left un-rejoined,
51+
// terminatePendingErrorHandlersAtEnd would synthesise a phantom
52+
// EndEvent; naively rejoining at the split would bypass the output
53+
// variable declaration and trigger CE0108 "variable not in scope" on
54+
// subsequent activities.
55+
//
56+
// We consume the pending state here so the rejoin is emitted in the
57+
// `lastElseID == ""` block below (when the first ELSE activity is
58+
// created); the corresponding builder fields are cleared so downstream
59+
// logic does not see the handler twice.
60+
pendingElseErrorOrigin := model.ID("")
61+
if fb.errorHandlerSource != "" && fb.errorHandlerTailFrom == fb.errorHandlerSource &&
62+
fb.errorHandlerSkipVar != "" && fb.errorHandlerTailIsSource &&
63+
hasElseBody && len(s.ElseBody) > 0 &&
64+
bothReturn && // only when both branches terminate — a continuation in THEN would need a different rejoin target
65+
statementReferencesVar(s, fb.errorHandlerSkipVar) &&
66+
firstElseIsLeafActivity(s.ElseBody) {
67+
pendingElseErrorOrigin = fb.errorHandlerSource
68+
fb.errorHandlerSource = ""
69+
fb.errorHandlerTailFrom = ""
70+
fb.errorHandlerSkipVar = ""
71+
fb.errorHandlerTailIsSource = false
72+
}
73+
4474
// Save current center line position
4575
splitX := fb.posX
4676
centerY := fb.posY // This is the center line for the happy path
@@ -223,6 +253,17 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
223253
flow := newDownwardFlowWithCase(splitID, actID, "false")
224254
applyUserAnchors(flow, falseBranchAnchor, branchDestinationAnchor(falseBranchAnchor, thisAnchor))
225255
fb.flows = append(fb.flows, flow)
256+
// Rejoin a pending empty custom error handler from the
257+
// previous call activity at the first ELSE activity.
258+
// Mendix authors the error edge directly into the ELSE
259+
// fallback path (same entry point as the split `false`
260+
// branch), preserving output-variable scope while giving
261+
// the error case the same handling as the happy-path
262+
// fallback.
263+
if pendingElseErrorOrigin != "" {
264+
fb.addEmptyErrorHandlerRejoinFlowFrom(splitID, pendingElseErrorOrigin, actID)
265+
pendingElseErrorOrigin = ""
266+
}
226267
} else {
227268
var flow *microflows.SequenceFlow
228269
originAnchor := prevElseAnchor
@@ -463,6 +504,22 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
463504
return splitID
464505
}
465506

507+
// firstElseIsLeafActivity reports whether the ELSE body's first statement is
508+
// a plain activity (not a compound split/if/loop). Rejoining an error flow
509+
// onto a compound statement's internal split/merge would need extra plumbing
510+
// that the caller is not equipped for; fall back to the default phantom-
511+
// EndEvent terminator in that case.
512+
func firstElseIsLeafActivity(stmts []ast.MicroflowStatement) bool {
513+
if len(stmts) == 0 {
514+
return false
515+
}
516+
switch stmts[0].(type) {
517+
case *ast.IfStmt, *ast.LoopStmt, *ast.WhileStmt, *ast.EnumSplitStmt:
518+
return false
519+
}
520+
return true
521+
}
522+
466523
func bodyHasContinuingCustomErrorHandler(stmts []ast.MicroflowStatement) bool {
467524
for _, stmt := range stmts {
468525
if eh := statementErrorHandling(stmt); eh != nil {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package executor
2+
3+
import (
4+
"testing"
5+
6+
"github.com/mendixlabs/mxcli/mdl/ast"
7+
"github.com/mendixlabs/mxcli/model"
8+
"github.com/mendixlabs/mxcli/sdk/microflows"
9+
)
10+
11+
// Empty custom error handler followed by an IF that checks the call's
12+
// output variable: the error edge must rejoin the fallback path at the
13+
// first ELSE activity, sharing a merge with the split's `false` branch.
14+
// Without this rejoin, terminatePendingErrorHandlersAtEnd synthesises a
15+
// phantom EndEvent at the microflow tail (CE0079); wiring the error edge
16+
// straight to the IF split bypasses the output-variable declaration and
17+
// triggers CE0108 "variable not in scope" downstream.
18+
func TestEmptyErrorHandlerRejoinsAtIfElseFirstActivity(t *testing.T) {
19+
fb := &flowBuilder{
20+
spacing: HorizontalSpacing,
21+
measurer: &layoutMeasurer{},
22+
declaredVars: map[string]string{"R": "String"},
23+
}
24+
25+
oc := fb.buildFlowGraph([]ast.MicroflowStatement{
26+
&ast.CallMicroflowStmt{
27+
MicroflowName: ast.QualifiedName{Module: "M", Name: "Helper"},
28+
OutputVariable: "R",
29+
ErrorHandling: &ast.ErrorHandlingClause{
30+
Type: ast.ErrorHandlingCustomWithoutRollback,
31+
Body: nil,
32+
},
33+
},
34+
&ast.IfStmt{
35+
Condition: &ast.VariableExpr{Name: "R"},
36+
ThenBody: []ast.MicroflowStatement{
37+
&ast.LogStmt{Level: ast.LogInfo, Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "ok"}},
38+
&ast.ReturnStmt{},
39+
},
40+
ElseBody: []ast.MicroflowStatement{
41+
&ast.LogStmt{Level: ast.LogError, Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "fail"}},
42+
&ast.ReturnStmt{},
43+
},
44+
},
45+
}, nil)
46+
47+
// Count EndEvents — should be 2 (one per IF branch), no phantom at mf end.
48+
endEvents := 0
49+
for _, obj := range oc.Objects {
50+
if _, ok := obj.(*microflows.EndEvent); ok {
51+
endEvents++
52+
}
53+
}
54+
if endEvents != 2 {
55+
t.Errorf("got %d EndEvents, want 2", endEvents)
56+
}
57+
58+
// Find call activity, if split, first-else activity
59+
var callID, splitID, firstElseID model.ID
60+
logCount := 0
61+
for _, obj := range oc.Objects {
62+
switch o := obj.(type) {
63+
case *microflows.ActionActivity:
64+
if _, ok := o.Action.(*microflows.MicroflowCallAction); ok {
65+
callID = o.ID
66+
}
67+
if _, ok := o.Action.(*microflows.LogMessageAction); ok {
68+
logCount++
69+
if logCount == 2 {
70+
// Second log in visitation order = the else body log ("fail")
71+
firstElseID = o.ID
72+
}
73+
}
74+
case *microflows.ExclusiveSplit:
75+
splitID = o.ID
76+
}
77+
}
78+
if callID == "" || splitID == "" || firstElseID == "" {
79+
t.Fatal("missing call/split/firstElse")
80+
}
81+
82+
// Error flow must originate at call and terminate at a merge reachable from split(false).
83+
// Trace error flow from call:
84+
var errorDest model.ID
85+
for _, f := range oc.Flows {
86+
if f.OriginID == callID && f.IsErrorHandler {
87+
errorDest = f.DestinationID
88+
break
89+
}
90+
}
91+
if errorDest == "" {
92+
t.Fatal("no error-handler flow from call")
93+
}
94+
// errorDest should be a merge that also receives the split false branch and leads to firstElseID
95+
mergeReachesFirstElse := false
96+
splitFalseReachesMerge := false
97+
for _, f := range oc.Flows {
98+
if f.OriginID == errorDest && f.DestinationID == firstElseID {
99+
mergeReachesFirstElse = true
100+
}
101+
if f.OriginID == splitID && f.DestinationID == errorDest {
102+
splitFalseReachesMerge = true
103+
}
104+
}
105+
if !mergeReachesFirstElse {
106+
t.Errorf("merge %s does not flow to first-else activity %s", string(errorDest)[:6], string(firstElseID)[:6])
107+
}
108+
if !splitFalseReachesMerge {
109+
t.Errorf("split %s (false) does not flow to merge %s", string(splitID)[:6], string(errorDest)[:6])
110+
}
111+
}

0 commit comments

Comments
 (0)