Skip to content

Commit 74595ec

Browse files
authored
Merge pull request #408 from hjotha/submit/merged-pr-review-followups
fix: address merged microflow review followups
2 parents 5a5b743 + 1568e64 commit 74595ec

14 files changed

Lines changed: 245 additions & 23 deletions

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,8 @@ Cross-reference commands require `refresh catalog full` to populate reference da
10551055
| Setup mxcli | `mxcli setup mxcli [--os linux]` | Download platform-specific mxcli binary |
10561056
| LSP server | `mxcli lsp --stdio` | Language server for VS Code |
10571057

1058+
Set `MXCLI_EXEC_TIMEOUT` to override the per-statement execution timeout used by `mxcli exec` (for example `MXCLI_EXEC_TIMEOUT=12m` or `MXCLI_EXEC_TIMEOUT=900`).
1059+
10581060
## ANTLR4 Parser Architecture
10591061

10601062
The MDL parser uses ANTLR4 for grammar definition, enabling cross-language grammar sharing (Go, TypeScript, Java, Python).

mdl/executor/bugfix_regression_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,28 @@ func TestEmptyChangeObjectRefreshesInClient(t *testing.T) {
694694
if !action.RefreshInClient {
695695
t.Fatal("empty change object must refresh in client to remain valid without member changes or commit")
696696
}
697+
698+
id = fb.addChangeObjectAction(&ast.ChangeObjectStmt{
699+
Variable: "Object",
700+
Changes: []ast.ChangeItem{{
701+
Attribute: "Name",
702+
Value: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "changed"},
703+
}},
704+
})
705+
if id == "" || len(fb.objects) != 2 {
706+
t.Fatalf("expected second change object activity, got id=%q objects=%d", id, len(fb.objects))
707+
}
708+
activity, ok = fb.objects[1].(*microflows.ActionActivity)
709+
if !ok {
710+
t.Fatalf("object type = %T, want *microflows.ActionActivity", fb.objects[1])
711+
}
712+
action, ok = activity.Action.(*microflows.ChangeObjectAction)
713+
if !ok {
714+
t.Fatalf("action type = %T, want *microflows.ChangeObjectAction", activity.Action)
715+
}
716+
if action.RefreshInClient {
717+
t.Fatal("non-empty change object must not infer refresh in client")
718+
}
697719
}
698720

699721
func TestListFindAttributeEqualsExpressionUsesAttributeOperation(t *testing.T) {

mdl/executor/cmd_javaactions_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func TestFormatAction_JavaActionCall_EntityTypeParam(t *testing.T) {
1919
action := &microflows.JavaActionCallAction{
2020
JavaAction: "MyModule.Validate",
2121
ResultVariableName: "IsValid",
22+
UseReturnVariable: true,
2223
ParameterMappings: []*microflows.JavaActionParameterMapping{
2324
{
2425
Parameter: "MyModule.Validate.InputObject",
@@ -40,6 +41,7 @@ func TestFormatAction_JavaActionCall_MixedParamTypes(t *testing.T) {
4041
action := &microflows.JavaActionCallAction{
4142
JavaAction: "MyModule.ProcessEntity",
4243
ResultVariableName: "Result",
44+
UseReturnVariable: true,
4345
ParameterMappings: []*microflows.JavaActionParameterMapping{
4446
{
4547
Parameter: "MyModule.ProcessEntity.InputObject",
@@ -67,6 +69,7 @@ func TestFormatAction_JavaActionCall_EntityTypeParam_EmptyEntity(t *testing.T) {
6769
action := &microflows.JavaActionCallAction{
6870
JavaAction: "MyModule.Validate",
6971
ResultVariableName: "IsValid",
72+
UseReturnVariable: true,
7073
ParameterMappings: []*microflows.JavaActionParameterMapping{
7174
{
7275
Parameter: "MyModule.Validate.InputObject",
@@ -280,6 +283,7 @@ func TestFormatAction_JavaActionCall_EntityTypeAndParameterizedParams(t *testing
280283
action := &microflows.JavaActionCallAction{
281284
JavaAction: "MyModule.CopyAttributes",
282285
ResultVariableName: "Result",
286+
UseReturnVariable: true,
283287
ParameterMappings: []*microflows.JavaActionParameterMapping{
284288
{
285289
Parameter: "MyModule.CopyAttributes.EntityType",

mdl/executor/cmd_microflows_builder_actions.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,13 +543,14 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID {
543543
outputUsedAsObject := fb.objectInputVariables != nil && fb.objectInputVariables[s.Variable]
544544
// Owner-both Reference associations need later usage context: the same
545545
// compact retrieve can be consumed as either a list or a single object.
546+
// Owner="" means metadata was unavailable, so keep the association source.
546547
expandReverseReference := assocInfo != nil &&
547548
assocInfo.Type == domainmodel.AssociationTypeReference &&
548549
assocInfo.Owner != "" &&
549550
assocInfo.parentPersistable &&
550551
assocInfo.childEntityQN != "" &&
551552
startVarType == assocInfo.childEntityQN &&
552-
(assocInfo.Owner != domainmodel.AssociationOwnerBoth || outputUsedAsList && !outputUsedAsObject)
553+
(assocInfo.Owner != domainmodel.AssociationOwnerBoth || (outputUsedAsList && !outputUsedAsObject))
553554

554555
if expandReverseReference {
555556
// Reverse traversal on Reference: child → parent (one-to-many)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package executor
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/mdl/ast"
9+
)
10+
11+
// TestCollectListInputVariables_AddRemoveFromList pins issue #405:
12+
// `add $X to $List` and `remove $Y from $List` consume the target list, so
13+
// the list variable must be tracked as a list input. Without it, the output
14+
// of an Owner=Both reverse retrieve fed straight into add/remove was
15+
// misclassified as object-only and the AssociationRetrieveSource was
16+
// suppressed, re-introducing the original #383 bug for this usage shape.
17+
func TestCollectListInputVariables_AddRemoveFromList(t *testing.T) {
18+
stmts := []ast.MicroflowStatement{
19+
&ast.AddToListStmt{Item: "NewItem", List: "Items"},
20+
&ast.RemoveFromListStmt{Item: "OldItem", List: "Backlog"},
21+
}
22+
23+
got := collectListInputVariables(stmts)
24+
25+
if !got["Items"] {
26+
t.Errorf("AddToListStmt target `Items` must be marked as list input; got %v", got)
27+
}
28+
if !got["Backlog"] {
29+
t.Errorf("RemoveFromListStmt target `Backlog` must be marked as list input; got %v", got)
30+
}
31+
}

mdl/executor/cmd_microflows_builder_control.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -683,7 +683,7 @@ func (fb *flowBuilder) addLoopStatement(s *ast.LoopStmt) model.ID {
683683
}
684684

685685
func isManualWhileTrueCandidate(s *ast.WhileStmt) bool {
686-
if s == nil || containsBreakForCurrentLoop(s.Body) || (!containsContinueStmt(s.Body) && !containsTerminalStmt(s.Body)) {
686+
if s == nil || containsBreakForCurrentLoop(s.Body) || (!containsContinueForCurrentLoop(s.Body) && !containsTerminalStmt(s.Body)) {
687687
return false
688688
}
689689
lit, ok := s.Condition.(*ast.LiteralExpr)
@@ -753,23 +753,23 @@ func statementErrorHandling(stmt ast.MicroflowStatement) *ast.ErrorHandlingClaus
753753
}
754754
}
755755

756-
func containsContinueStmt(stmts []ast.MicroflowStatement) bool {
756+
// containsContinueForCurrentLoop reports whether stmts contain a continue
757+
// that targets the enclosing loop — i.e. one that is NOT inside a nested
758+
// LoopStmt/WhileStmt. Nested loops trap their own continues just like they
759+
// trap their own breaks, so the scan stops at nested-loop boundaries.
760+
// This mirrors containsBreakForCurrentLoop in intent; it differs only in
761+
// which statement type it looks for.
762+
func containsContinueForCurrentLoop(stmts []ast.MicroflowStatement) bool {
757763
for _, stmt := range stmts {
758764
switch s := stmt.(type) {
759765
case *ast.ContinueStmt:
760766
return true
761767
case *ast.IfStmt:
762-
if containsContinueStmt(s.ThenBody) || containsContinueStmt(s.ElseBody) {
763-
return true
764-
}
765-
case *ast.LoopStmt:
766-
if containsContinueStmt(s.Body) {
767-
return true
768-
}
769-
case *ast.WhileStmt:
770-
if containsContinueStmt(s.Body) {
768+
if containsContinueForCurrentLoop(s.ThenBody) || containsContinueForCurrentLoop(s.ElseBody) {
771769
return true
772770
}
771+
case *ast.LoopStmt, *ast.WhileStmt:
772+
continue
773773
}
774774
}
775775
return false

mdl/executor/cmd_microflows_builder_graph.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,14 @@ func collectListInputVariables(stmts []ast.MicroflowStatement) map[string]bool {
206206
inputs[s.ListVariable] = true
207207
}
208208
walk(s.Body)
209+
case *ast.AddToListStmt:
210+
if s.List != "" {
211+
inputs[s.List] = true
212+
}
213+
case *ast.RemoveFromListStmt:
214+
if s.List != "" {
215+
inputs[s.List] = true
216+
}
209217
case *ast.WhileStmt:
210218
walk(s.Body)
211219
case *ast.IfStmt:
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package executor
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/mdl/ast"
9+
"github.com/mendixlabs/mxcli/sdk/microflows"
10+
)
11+
12+
// TestBuildFlowGraph_ManualWhileTrueIgnoresNestedLoopContinue pins issue #404:
13+
// a `while true` whose only `continue` lives inside a nested collection loop
14+
// must NOT be classified as a manual back-edge candidate. The outer flow
15+
// should be built as a regular LoopedActivity (with a WhileLoopCondition).
16+
//
17+
// Before the fix, containsContinueStmt recursed into nested LoopStmt bodies
18+
// asymmetrically with containsBreakForCurrentLoop, so isManualWhileTrueCandidate
19+
// returned true and the outer while was rebuilt as an ExclusiveMerge back-edge,
20+
// creating an unconditional infinite loop in the BSON graph.
21+
func TestBuildFlowGraph_ManualWhileTrueIgnoresNestedLoopContinue(t *testing.T) {
22+
body := []ast.MicroflowStatement{
23+
&ast.WhileStmt{
24+
Condition: &ast.LiteralExpr{Kind: ast.LiteralBoolean, Value: true},
25+
Body: []ast.MicroflowStatement{
26+
&ast.LoopStmt{
27+
LoopVariable: "item",
28+
ListVariable: "items",
29+
Body: []ast.MicroflowStatement{
30+
&ast.ContinueStmt{},
31+
},
32+
},
33+
// No outer-scope continue / return / raise: the outer while
34+
// has no terminal signal of its own.
35+
},
36+
},
37+
}
38+
39+
fb := &flowBuilder{
40+
posX: 100,
41+
posY: 100,
42+
spacing: HorizontalSpacing,
43+
measurer: &layoutMeasurer{},
44+
varTypes: map[string]string{"items": "List of Sample.Item"},
45+
declaredVars: map[string]string{"items": "List of Sample.Item"},
46+
}
47+
oc := fb.buildFlowGraph(body, nil)
48+
49+
var (
50+
outerLoop *microflows.LoopedActivity
51+
mergeCount int
52+
)
53+
for _, obj := range oc.Objects {
54+
switch o := obj.(type) {
55+
case *microflows.LoopedActivity:
56+
// The first looped activity at this scope is the outer while.
57+
if outerLoop == nil {
58+
outerLoop = o
59+
}
60+
case *microflows.ExclusiveMerge:
61+
mergeCount++
62+
}
63+
}
64+
65+
if outerLoop == nil {
66+
t.Fatal("outer `while true` must be built as a LoopedActivity, not an ExclusiveMerge back-edge")
67+
}
68+
if outerLoop.LoopSource == nil {
69+
t.Fatal("outer LoopedActivity must have a LoopSource (WhileLoopCondition for `while true`)")
70+
}
71+
if mergeCount != 0 {
72+
t.Errorf("manual back-edge ExclusiveMerge must not be emitted; got %d ExclusiveMerge node(s)", mergeCount)
73+
}
74+
}

mdl/executor/cmd_microflows_format_action.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func formatActivity(
8383
if ctx != nil && ctx.DescribingMicroflowHasReturnValue {
8484
return ""
8585
}
86+
// Without render context, default to the void-flow form.
8687
return "return;"
8788

8889
case *microflows.ActionActivity:
@@ -508,7 +509,7 @@ func formatAction(
508509
paramStr = strings.Join(params, ", ")
509510
}
510511

511-
if a.ResultVariableName != "" {
512+
if a.UseReturnVariable && a.ResultVariableName != "" {
512513
return fmt.Sprintf("$%s = call microflow %s(%s);", a.ResultVariableName, mfName, paramStr)
513514
}
514515
return fmt.Sprintf("call microflow %s(%s);", mfName, paramStr)
@@ -537,7 +538,7 @@ func formatAction(
537538
paramStr = strings.Join(params, ", ")
538539
}
539540

540-
if a.OutputVariableName != "" {
541+
if a.UseReturnVariable && a.OutputVariableName != "" {
541542
return fmt.Sprintf("$%s = call nanoflow %s(%s);", a.OutputVariableName, nfName, paramStr)
542543
}
543544
return fmt.Sprintf("call nanoflow %s(%s);", nfName, paramStr)
@@ -591,7 +592,7 @@ func formatAction(
591592
paramStr = strings.Join(params, ", ")
592593
}
593594

594-
if a.ResultVariableName != "" {
595+
if a.UseReturnVariable && a.ResultVariableName != "" {
595596
return fmt.Sprintf("$%s = call java action %s(%s);", a.ResultVariableName, javaActionName, paramStr)
596597
}
597598
return fmt.Sprintf("call java action %s(%s);", javaActionName, paramStr)
@@ -616,7 +617,7 @@ func formatAction(
616617
paramStr = strings.Join(params, ", ")
617618
}
618619

619-
if a.ResultVariableName != "" {
620+
if a.UseReturnVariable && a.ResultVariableName != "" {
620621
return fmt.Sprintf("$%s = call external action %s.%s(%s);", a.ResultVariableName, serviceName, actionName, paramStr)
621622
}
622623
return fmt.Sprintf("call external action %s.%s(%s);", serviceName, actionName, paramStr)
@@ -758,7 +759,7 @@ func formatAction(
758759
return fmt.Sprintf("get workflow data $%s as %s;", a.WorkflowVariable, a.Workflow)
759760

760761
case *microflows.WorkflowCallAction:
761-
if a.OutputVariableName != "" {
762+
if a.UseReturnVariable && a.OutputVariableName != "" {
762763
return fmt.Sprintf("$%s = call workflow %s ($%s);", a.OutputVariableName, a.Workflow, a.WorkflowContextVariable)
763764
}
764765
return fmt.Sprintf("call workflow %s ($%s);", a.Workflow, a.WorkflowContextVariable)
@@ -855,7 +856,7 @@ func formatAction(
855856
paramStr = strings.Join(params, ", ")
856857
}
857858

858-
if a.OutputVariableName != "" {
859+
if a.UseReturnVariable && a.OutputVariableName != "" {
859860
return fmt.Sprintf("$%s = call javascript action %s(%s);", a.OutputVariableName, jsActionName, paramStr)
860861
}
861862
return fmt.Sprintf("call javascript action %s(%s);", jsActionName, paramStr)
@@ -1591,7 +1592,13 @@ func isSimpleMendixName(name string) bool {
15911592
return false
15921593
}
15931594
for i, r := range name {
1594-
if r == '_' || r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || i > 0 && r >= '0' && r <= '9' {
1595+
if i == 0 {
1596+
if r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' {
1597+
continue
1598+
}
1599+
return false
1600+
}
1601+
if r == '_' || r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9' {
15951602
continue
15961603
}
15971604
return false

0 commit comments

Comments
 (0)