From 76e198912cce0abe5e800579543e229826ec1601 Mon Sep 17 00:00:00 2001 From: Henrique Costa Date: Thu, 30 Apr 2026 09:40:43 +0200 Subject: [PATCH 1/2] fix: address merged microflow review followups 1. Manual while-true detection over-triggered when the only continue lived inside a nested loop. The continue scan crossed loop boundaries while the break scan did not, so the outer while could be rebuilt as a manual back-edge. Stop the continue scan at nested loops and add a regression test. 2. Owner-both reverse retrieves feeding add/remove-to-list were still treated as object-only consumers. The list pre-scan ignored AddToListStmt and RemoveFromListStmt target lists, so AssociationRetrieveSource could be suppressed. Track those list consumers and add coverage for the helper. 3. Direct nanoflow describe did not set the return-value render context used by EndEvent formatting. Thread the wrapped nanoflow return type through the formatter so value-returning nanoflows do not emit bare return; for empty EndEvents. Also folds in low-risk review followups: commit-action writer coverage, change-object refresh negative coverage, download-file formatter coverage, reverse-retrieve name validation tightening, and documentation for MXCLI_EXEC_TIMEOUT. Tests: make build Tests: make test Tests: make lint-go Closes #404. Closes #405. Closes #406. --- docs/01-project/MDL_QUICK_REFERENCE.md | 2 + mdl/executor/bugfix_regression_test.go | 22 ++++++ .../cmd_microflows_builder_actions.go | 3 +- ...croflows_builder_collectlistinputs_test.go | 31 ++++++++ .../cmd_microflows_builder_control.go | 18 ++--- mdl/executor/cmd_microflows_builder_graph.go | 8 ++ ...oflows_builder_manual_while_nested_test.go | 74 +++++++++++++++++++ mdl/executor/cmd_microflows_format_action.go | 9 ++- .../cmd_microflows_format_action_test.go | 16 ++++ mdl/executor/cmd_microflows_show.go | 13 +++- .../cmd_microflows_show_helpers_test.go | 2 +- mdl/executor/cmd_nanoflows_mock_test.go | 31 ++++++++ sdk/mpr/writer_microflow_action_items_test.go | 13 ++++ 13 files changed, 225 insertions(+), 17 deletions(-) create mode 100644 mdl/executor/cmd_microflows_builder_collectlistinputs_test.go create mode 100644 mdl/executor/cmd_microflows_builder_manual_while_nested_test.go diff --git a/docs/01-project/MDL_QUICK_REFERENCE.md b/docs/01-project/MDL_QUICK_REFERENCE.md index 0aeb758a..8087fd04 100644 --- a/docs/01-project/MDL_QUICK_REFERENCE.md +++ b/docs/01-project/MDL_QUICK_REFERENCE.md @@ -1054,6 +1054,8 @@ Cross-reference commands require `refresh catalog full` to populate reference da | Setup mxcli | `mxcli setup mxcli [--os linux]` | Download platform-specific mxcli binary | | LSP server | `mxcli lsp --stdio` | Language server for VS Code | +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`). + ## ANTLR4 Parser Architecture The MDL parser uses ANTLR4 for grammar definition, enabling cross-language grammar sharing (Go, TypeScript, Java, Python). diff --git a/mdl/executor/bugfix_regression_test.go b/mdl/executor/bugfix_regression_test.go index b0072a42..6508f7a9 100644 --- a/mdl/executor/bugfix_regression_test.go +++ b/mdl/executor/bugfix_regression_test.go @@ -694,6 +694,28 @@ func TestEmptyChangeObjectRefreshesInClient(t *testing.T) { if !action.RefreshInClient { t.Fatal("empty change object must refresh in client to remain valid without member changes or commit") } + + id = fb.addChangeObjectAction(&ast.ChangeObjectStmt{ + Variable: "Object", + Changes: []ast.ChangeItem{{ + Attribute: "Name", + Value: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "changed"}, + }}, + }) + if id == "" || len(fb.objects) != 2 { + t.Fatalf("expected second change object activity, got id=%q objects=%d", id, len(fb.objects)) + } + activity, ok = fb.objects[1].(*microflows.ActionActivity) + if !ok { + t.Fatalf("object type = %T, want *microflows.ActionActivity", fb.objects[1]) + } + action, ok = activity.Action.(*microflows.ChangeObjectAction) + if !ok { + t.Fatalf("action type = %T, want *microflows.ChangeObjectAction", activity.Action) + } + if action.RefreshInClient { + t.Fatal("non-empty change object must not infer refresh in client") + } } func TestListFindAttributeEqualsExpressionUsesAttributeOperation(t *testing.T) { diff --git a/mdl/executor/cmd_microflows_builder_actions.go b/mdl/executor/cmd_microflows_builder_actions.go index e784f79b..90ffcc4a 100644 --- a/mdl/executor/cmd_microflows_builder_actions.go +++ b/mdl/executor/cmd_microflows_builder_actions.go @@ -543,13 +543,14 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID { outputUsedAsObject := fb.objectInputVariables != nil && fb.objectInputVariables[s.Variable] // Owner-both Reference associations need later usage context: the same // compact retrieve can be consumed as either a list or a single object. + // Owner="" means metadata was unavailable, so keep the association source. expandReverseReference := assocInfo != nil && assocInfo.Type == domainmodel.AssociationTypeReference && assocInfo.Owner != "" && assocInfo.parentPersistable && assocInfo.childEntityQN != "" && startVarType == assocInfo.childEntityQN && - (assocInfo.Owner != domainmodel.AssociationOwnerBoth || outputUsedAsList && !outputUsedAsObject) + (assocInfo.Owner != domainmodel.AssociationOwnerBoth || (outputUsedAsList && !outputUsedAsObject)) if expandReverseReference { // Reverse traversal on Reference: child → parent (one-to-many) diff --git a/mdl/executor/cmd_microflows_builder_collectlistinputs_test.go b/mdl/executor/cmd_microflows_builder_collectlistinputs_test.go new file mode 100644 index 00000000..ade44df7 --- /dev/null +++ b/mdl/executor/cmd_microflows_builder_collectlistinputs_test.go @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" +) + +// TestCollectListInputVariables_AddRemoveFromList pins issue #405: +// `add $X to $List` and `remove $Y from $List` consume the target list, so +// the list variable must be tracked as a list input. Without it, the output +// of an Owner=Both reverse retrieve fed straight into add/remove was +// misclassified as object-only and the AssociationRetrieveSource was +// suppressed, re-introducing the original #383 bug for this usage shape. +func TestCollectListInputVariables_AddRemoveFromList(t *testing.T) { + stmts := []ast.MicroflowStatement{ + &ast.AddToListStmt{Item: "NewItem", List: "Items"}, + &ast.RemoveFromListStmt{Item: "OldItem", List: "Backlog"}, + } + + got := collectListInputVariables(stmts) + + if !got["Items"] { + t.Errorf("AddToListStmt target `Items` must be marked as list input; got %v", got) + } + if !got["Backlog"] { + t.Errorf("RemoveFromListStmt target `Backlog` must be marked as list input; got %v", got) + } +} diff --git a/mdl/executor/cmd_microflows_builder_control.go b/mdl/executor/cmd_microflows_builder_control.go index 8a71fb52..f46d9cb0 100644 --- a/mdl/executor/cmd_microflows_builder_control.go +++ b/mdl/executor/cmd_microflows_builder_control.go @@ -626,7 +626,7 @@ func (fb *flowBuilder) addLoopStatement(s *ast.LoopStmt) model.ID { } func isManualWhileTrueCandidate(s *ast.WhileStmt) bool { - if s == nil || containsBreakForCurrentLoop(s.Body) || (!containsContinueStmt(s.Body) && !containsTerminalStmt(s.Body)) { + if s == nil || containsBreakForCurrentLoop(s.Body) || (!containsContinueForCurrentLoop(s.Body) && !containsTerminalStmt(s.Body)) { return false } lit, ok := s.Condition.(*ast.LiteralExpr) @@ -696,23 +696,19 @@ func statementErrorHandling(stmt ast.MicroflowStatement) *ast.ErrorHandlingClaus } } -func containsContinueStmt(stmts []ast.MicroflowStatement) bool { +// containsContinueForCurrentLoop mirrors containsBreakForCurrentLoop: +// a continue inside a nested loop targets that nested loop, not this one. +func containsContinueForCurrentLoop(stmts []ast.MicroflowStatement) bool { for _, stmt := range stmts { switch s := stmt.(type) { case *ast.ContinueStmt: return true case *ast.IfStmt: - if containsContinueStmt(s.ThenBody) || containsContinueStmt(s.ElseBody) { - return true - } - case *ast.LoopStmt: - if containsContinueStmt(s.Body) { - return true - } - case *ast.WhileStmt: - if containsContinueStmt(s.Body) { + if containsContinueForCurrentLoop(s.ThenBody) || containsContinueForCurrentLoop(s.ElseBody) { return true } + case *ast.LoopStmt, *ast.WhileStmt: + continue } } return false diff --git a/mdl/executor/cmd_microflows_builder_graph.go b/mdl/executor/cmd_microflows_builder_graph.go index a3196562..bef9c19b 100644 --- a/mdl/executor/cmd_microflows_builder_graph.go +++ b/mdl/executor/cmd_microflows_builder_graph.go @@ -198,6 +198,14 @@ func collectListInputVariables(stmts []ast.MicroflowStatement) map[string]bool { inputs[s.ListVariable] = true } walk(s.Body) + case *ast.AddToListStmt: + if s.List != "" { + inputs[s.List] = true + } + case *ast.RemoveFromListStmt: + if s.List != "" { + inputs[s.List] = true + } case *ast.WhileStmt: walk(s.Body) case *ast.IfStmt: diff --git a/mdl/executor/cmd_microflows_builder_manual_while_nested_test.go b/mdl/executor/cmd_microflows_builder_manual_while_nested_test.go new file mode 100644 index 00000000..111d3162 --- /dev/null +++ b/mdl/executor/cmd_microflows_builder_manual_while_nested_test.go @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/sdk/microflows" +) + +// TestBuildFlowGraph_ManualWhileTrueIgnoresNestedLoopContinue pins issue #404: +// a `while true` whose only `continue` lives inside a nested collection loop +// must NOT be classified as a manual back-edge candidate. The outer flow +// should be built as a regular LoopedActivity (with a WhileLoopCondition). +// +// Before the fix, containsContinueStmt recursed into nested LoopStmt bodies +// asymmetrically with containsBreakForCurrentLoop, so isManualWhileTrueCandidate +// returned true and the outer while was rebuilt as an ExclusiveMerge back-edge, +// creating an unconditional infinite loop in the BSON graph. +func TestBuildFlowGraph_ManualWhileTrueIgnoresNestedLoopContinue(t *testing.T) { + body := []ast.MicroflowStatement{ + &ast.WhileStmt{ + Condition: &ast.LiteralExpr{Kind: ast.LiteralBoolean, Value: true}, + Body: []ast.MicroflowStatement{ + &ast.LoopStmt{ + LoopVariable: "item", + ListVariable: "items", + Body: []ast.MicroflowStatement{ + &ast.ContinueStmt{}, + }, + }, + // No outer-scope continue / return / raise: the outer while + // has no terminal signal of its own. + }, + }, + } + + fb := &flowBuilder{ + posX: 100, + posY: 100, + spacing: HorizontalSpacing, + measurer: &layoutMeasurer{}, + varTypes: map[string]string{"items": "List of Sample.Item"}, + declaredVars: map[string]string{"items": "List of Sample.Item"}, + } + oc := fb.buildFlowGraph(body, nil) + + var ( + outerLoop *microflows.LoopedActivity + mergeCount int + ) + for _, obj := range oc.Objects { + switch o := obj.(type) { + case *microflows.LoopedActivity: + // The first looped activity at this scope is the outer while. + if outerLoop == nil { + outerLoop = o + } + case *microflows.ExclusiveMerge: + mergeCount++ + } + } + + if outerLoop == nil { + t.Fatal("outer `while true` must be built as a LoopedActivity, not an ExclusiveMerge back-edge") + } + if outerLoop.LoopSource == nil { + t.Fatal("outer LoopedActivity must have a LoopSource (WhileLoopCondition for `while true`)") + } + if mergeCount != 0 { + t.Errorf("manual back-edge ExclusiveMerge must not be emitted; got %d ExclusiveMerge node(s)", mergeCount) + } +} diff --git a/mdl/executor/cmd_microflows_format_action.go b/mdl/executor/cmd_microflows_format_action.go index f9ca0532..93cddd01 100644 --- a/mdl/executor/cmd_microflows_format_action.go +++ b/mdl/executor/cmd_microflows_format_action.go @@ -83,6 +83,7 @@ func formatActivity( if ctx != nil && ctx.DescribingMicroflowHasReturnValue { return "" } + // Without render context, default to the void-flow form. return "return;" case *microflows.ActionActivity: @@ -1576,7 +1577,13 @@ func isSimpleMendixName(name string) bool { return false } for i, r := range name { - if r == '_' || r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || i > 0 && r >= '0' && r <= '9' { + if i == 0 { + if r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' { + continue + } + return false + } + if r == '_' || r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9' { continue } return false diff --git a/mdl/executor/cmd_microflows_format_action_test.go b/mdl/executor/cmd_microflows_format_action_test.go index 4e5307ad..0b2c2879 100644 --- a/mdl/executor/cmd_microflows_format_action_test.go +++ b/mdl/executor/cmd_microflows_format_action_test.go @@ -560,6 +560,19 @@ func TestFormatAction_DownloadFile(t *testing.T) { } } +func TestFormatAction_DownloadFileWithoutBrowserFlag(t *testing.T) { + e := newTestExecutor() + action := µflows.DownloadFileAction{ + FileDocument: "GeneratedReport", + } + + got := e.formatAction(action, nil, nil) + want := "download file $GeneratedReport;" + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + func TestFormatAction_ValidationFeedback(t *testing.T) { e := newTestExecutor() action := µflows.ValidationFeedbackAction{ @@ -836,6 +849,9 @@ func TestParseReverseAssociationXPathRejectsComplexPredicates(t *testing.T) { "[SampleRuntime.Domain_Runtime != $Runtime]", "[SampleRuntime.Domain_Runtime = $Runtime/Other.Assoc]", "[SampleRuntime.Domain_Runtime = 'literal']", + "[_SampleRuntime.Domain_Runtime = $Runtime]", + "[SampleRuntime._Domain_Runtime = $Runtime]", + "[SampleRuntime.Domain_Runtime = $_Runtime]", "SampleRuntime.Domain_Runtime = $Runtime", } diff --git a/mdl/executor/cmd_microflows_show.go b/mdl/executor/cmd_microflows_show.go index 5f3990af..71a11c61 100644 --- a/mdl/executor/cmd_microflows_show.go +++ b/mdl/executor/cmd_microflows_show.go @@ -432,10 +432,17 @@ func describeNanoflow(ctx *ExecContext, name ast.QualifiedName) error { lines = append(lines, "begin") // Wrap nanoflow in a Microflow to reuse formatMicroflowActivities + wrapperMf := µflows.Microflow{ + ReturnType: targetNf.ReturnType, + ObjectCollection: targetNf.ObjectCollection, + } + prevDescribingReturnValue := ctx.DescribingMicroflowHasReturnValue + ctx.DescribingMicroflowHasReturnValue = microflowHasReturnValue(wrapperMf) + defer func() { + ctx.DescribingMicroflowHasReturnValue = prevDescribingReturnValue + }() + if targetNf.ObjectCollection != nil && len(targetNf.ObjectCollection.Objects) > 0 { - wrapperMf := µflows.Microflow{ - ObjectCollection: targetNf.ObjectCollection, - } activityLines := formatMicroflowActivities(ctx, wrapperMf, entityNames, microflowNames) for _, line := range activityLines { lines = append(lines, " "+line) diff --git a/mdl/executor/cmd_microflows_show_helpers_test.go b/mdl/executor/cmd_microflows_show_helpers_test.go index cd02c4d7..4ce2beda 100644 --- a/mdl/executor/cmd_microflows_show_helpers_test.go +++ b/mdl/executor/cmd_microflows_show_helpers_test.go @@ -381,7 +381,7 @@ func TestFormatActivity_StartEvent(t *testing.T) { } } -func TestFormatActivity_EndEvent_NoReturn(t *testing.T) { +func TestFormatActivity_EndEvent_VoidOrUnknownContext(t *testing.T) { e := newTestExecutor() obj := µflows.EndEvent{BaseMicroflowObject: mkObj("1")} got := e.formatActivity(obj, nil, nil) diff --git a/mdl/executor/cmd_nanoflows_mock_test.go b/mdl/executor/cmd_nanoflows_mock_test.go index 9612f5c5..e6686f29 100644 --- a/mdl/executor/cmd_nanoflows_mock_test.go +++ b/mdl/executor/cmd_nanoflows_mock_test.go @@ -114,6 +114,37 @@ func TestDescribeNanoflow_Mock_WithReturnType(t *testing.T) { assertContainsStr(t, out, "nanoflow MyModule.NF_GetName") } +func TestDescribeNanoflow_ReturningFlowSkipsEmptyEndEvent(t *testing.T) { + mod := mkModule("MyModule") + nf := mkNanoflow(mod.ID, "NF_Value") + nf.ReturnType = µflows.StringType{} + nf.ObjectCollection = µflows.MicroflowObjectCollection{ + Objects: []microflows.MicroflowObject{ + µflows.StartEvent{BaseMicroflowObject: mkObj("start")}, + µflows.EndEvent{BaseMicroflowObject: mkObj("end")}, + }, + Flows: []*microflows.SequenceFlow{mkFlow("start", "end")}, + } + + h := mkHierarchy(mod) + withContainer(h, nf.ContainerID, mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListMicroflowsFunc: func() ([]*microflows.Microflow, error) { return nil, nil }, + ListNanoflowsFunc: func() ([]*microflows.Nanoflow, error) { return []*microflows.Nanoflow{nf}, nil }, + ListDomainModelsFunc: func() ([]*domainmodel.DomainModel, error) { return nil, nil }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, describeNanoflow(ctx, ast.QualifiedName{Module: "MyModule", Name: "NF_Value"})) + + out := buf.String() + assertContainsStr(t, out, "returns String") + assertNotContainsStr(t, out, "return;") +} + // --- DROP NANOFLOW --- func TestDropNanoflow_Mock(t *testing.T) { diff --git a/sdk/mpr/writer_microflow_action_items_test.go b/sdk/mpr/writer_microflow_action_items_test.go index a0f5c277..8fce7db6 100644 --- a/sdk/mpr/writer_microflow_action_items_test.go +++ b/sdk/mpr/writer_microflow_action_items_test.go @@ -71,3 +71,16 @@ func TestSerializeChangeObjectActionItemsUseStorageListMarkerAndDefaultErrorHand t.Fatalf("Items marker = %#v, want int32(2)", items[0]) } } + +func TestSerializeCommitActionAlwaysWritesDefaultErrorHandling(t *testing.T) { + action := µflows.CommitObjectsAction{ + BaseElement: model.BaseElement{ID: "commit-1"}, + CommitVariable: "Order", + } + + doc := serializeMicroflowAction(action) + + if got := getBSONField(doc, "ErrorHandlingType"); got != "Rollback" { + t.Fatalf("ErrorHandlingType = %#v, want Rollback", got) + } +} From 1568e64c30e30e11f077ac127cc87ec8816ba85c Mon Sep 17 00:00:00 2001 From: Henrique Costa Date: Fri, 1 May 2026 15:56:47 +0200 Subject: [PATCH 2/2] fix: honor UseReturnVariable when emitting call-action assignments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit formatAction emitted `$Var = call microflow/nanoflow/java/javascript/external` whenever the action's ResultVariableName/OutputVariableName was non-empty, ignoring UseReturnVariable. Studio Pro stores the variable name as a UI fallback even when UseReturnVariable=false (the action discards its return), so re-exec of described MDL introduced phantom output variable declarations. Symptom: microflows with two call actions pointing at the same sub-microflow/Java action, both with UseReturnVariable=false but both carrying the same ResultVariableName, round-tripped as two declarations of the same output variable, triggering CE0111 "Duplicate variable name" in Studio Pro. Fix: emit the `$Var = ` prefix only when UseReturnVariable is true. Applies to MicroflowCallAction, NanoflowCallAction, JavaActionCallAction, JavaScriptActionCallAction, and CallExternalAction. Existing tests that relied on the old behavior now set UseReturnVariable explicitly — they were exercising the wrong BSON shape. Co-Authored-By: Claude Opus 4.7 (1M context) --- mdl/executor/cmd_javaactions_test.go | 4 ++++ mdl/executor/cmd_microflows_builder_control.go | 8 ++++++-- mdl/executor/cmd_microflows_format_action.go | 12 ++++++------ mdl/executor/cmd_microflows_format_action_test.go | 6 ++++++ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/mdl/executor/cmd_javaactions_test.go b/mdl/executor/cmd_javaactions_test.go index 978ff6bb..70204630 100644 --- a/mdl/executor/cmd_javaactions_test.go +++ b/mdl/executor/cmd_javaactions_test.go @@ -19,6 +19,7 @@ func TestFormatAction_JavaActionCall_EntityTypeParam(t *testing.T) { action := µflows.JavaActionCallAction{ JavaAction: "MyModule.Validate", ResultVariableName: "IsValid", + UseReturnVariable: true, ParameterMappings: []*microflows.JavaActionParameterMapping{ { Parameter: "MyModule.Validate.InputObject", @@ -40,6 +41,7 @@ func TestFormatAction_JavaActionCall_MixedParamTypes(t *testing.T) { action := µflows.JavaActionCallAction{ JavaAction: "MyModule.ProcessEntity", ResultVariableName: "Result", + UseReturnVariable: true, ParameterMappings: []*microflows.JavaActionParameterMapping{ { Parameter: "MyModule.ProcessEntity.InputObject", @@ -67,6 +69,7 @@ func TestFormatAction_JavaActionCall_EntityTypeParam_EmptyEntity(t *testing.T) { action := µflows.JavaActionCallAction{ JavaAction: "MyModule.Validate", ResultVariableName: "IsValid", + UseReturnVariable: true, ParameterMappings: []*microflows.JavaActionParameterMapping{ { Parameter: "MyModule.Validate.InputObject", @@ -280,6 +283,7 @@ func TestFormatAction_JavaActionCall_EntityTypeAndParameterizedParams(t *testing action := µflows.JavaActionCallAction{ JavaAction: "MyModule.CopyAttributes", ResultVariableName: "Result", + UseReturnVariable: true, ParameterMappings: []*microflows.JavaActionParameterMapping{ { Parameter: "MyModule.CopyAttributes.EntityType", diff --git a/mdl/executor/cmd_microflows_builder_control.go b/mdl/executor/cmd_microflows_builder_control.go index f46d9cb0..9ceade23 100644 --- a/mdl/executor/cmd_microflows_builder_control.go +++ b/mdl/executor/cmd_microflows_builder_control.go @@ -696,8 +696,12 @@ func statementErrorHandling(stmt ast.MicroflowStatement) *ast.ErrorHandlingClaus } } -// containsContinueForCurrentLoop mirrors containsBreakForCurrentLoop: -// a continue inside a nested loop targets that nested loop, not this one. +// containsContinueForCurrentLoop reports whether stmts contain a continue +// that targets the enclosing loop — i.e. one that is NOT inside a nested +// LoopStmt/WhileStmt. Nested loops trap their own continues just like they +// trap their own breaks, so the scan stops at nested-loop boundaries. +// This mirrors containsBreakForCurrentLoop in intent; it differs only in +// which statement type it looks for. func containsContinueForCurrentLoop(stmts []ast.MicroflowStatement) bool { for _, stmt := range stmts { switch s := stmt.(type) { diff --git a/mdl/executor/cmd_microflows_format_action.go b/mdl/executor/cmd_microflows_format_action.go index 93cddd01..00192631 100644 --- a/mdl/executor/cmd_microflows_format_action.go +++ b/mdl/executor/cmd_microflows_format_action.go @@ -494,7 +494,7 @@ func formatAction( paramStr = strings.Join(params, ", ") } - if a.ResultVariableName != "" { + if a.UseReturnVariable && a.ResultVariableName != "" { return fmt.Sprintf("$%s = call microflow %s(%s);", a.ResultVariableName, mfName, paramStr) } return fmt.Sprintf("call microflow %s(%s);", mfName, paramStr) @@ -523,7 +523,7 @@ func formatAction( paramStr = strings.Join(params, ", ") } - if a.OutputVariableName != "" { + if a.UseReturnVariable && a.OutputVariableName != "" { return fmt.Sprintf("$%s = call nanoflow %s(%s);", a.OutputVariableName, nfName, paramStr) } return fmt.Sprintf("call nanoflow %s(%s);", nfName, paramStr) @@ -577,7 +577,7 @@ func formatAction( paramStr = strings.Join(params, ", ") } - if a.ResultVariableName != "" { + if a.UseReturnVariable && a.ResultVariableName != "" { return fmt.Sprintf("$%s = call java action %s(%s);", a.ResultVariableName, javaActionName, paramStr) } return fmt.Sprintf("call java action %s(%s);", javaActionName, paramStr) @@ -602,7 +602,7 @@ func formatAction( paramStr = strings.Join(params, ", ") } - if a.ResultVariableName != "" { + if a.UseReturnVariable && a.ResultVariableName != "" { return fmt.Sprintf("$%s = call external action %s.%s(%s);", a.ResultVariableName, serviceName, actionName, paramStr) } return fmt.Sprintf("call external action %s.%s(%s);", serviceName, actionName, paramStr) @@ -744,7 +744,7 @@ func formatAction( return fmt.Sprintf("get workflow data $%s as %s;", a.WorkflowVariable, a.Workflow) case *microflows.WorkflowCallAction: - if a.OutputVariableName != "" { + if a.UseReturnVariable && a.OutputVariableName != "" { return fmt.Sprintf("$%s = call workflow %s ($%s);", a.OutputVariableName, a.Workflow, a.WorkflowContextVariable) } return fmt.Sprintf("call workflow %s ($%s);", a.Workflow, a.WorkflowContextVariable) @@ -841,7 +841,7 @@ func formatAction( paramStr = strings.Join(params, ", ") } - if a.OutputVariableName != "" { + if a.UseReturnVariable && a.OutputVariableName != "" { return fmt.Sprintf("$%s = call javascript action %s(%s);", a.OutputVariableName, jsActionName, paramStr) } return fmt.Sprintf("call javascript action %s(%s);", jsActionName, paramStr) diff --git a/mdl/executor/cmd_microflows_format_action_test.go b/mdl/executor/cmd_microflows_format_action_test.go index 0b2c2879..39c60f64 100644 --- a/mdl/executor/cmd_microflows_format_action_test.go +++ b/mdl/executor/cmd_microflows_format_action_test.go @@ -368,6 +368,7 @@ func TestFormatAction_MicroflowCall_WithResult(t *testing.T) { e := newTestExecutor() action := µflows.MicroflowCallAction{ ResultVariableName: "Result", + UseReturnVariable: true, MicroflowCall: µflows.MicroflowCall{ Microflow: "MyModule.ProcessOrder", ParameterMappings: []*microflows.MicroflowCallParameterMapping{ @@ -400,6 +401,7 @@ func TestFormatAction_JavaActionCall(t *testing.T) { action := µflows.JavaActionCallAction{ JavaAction: "MyModule.SendEmail", ResultVariableName: "Success", + UseReturnVariable: true, ParameterMappings: []*microflows.JavaActionParameterMapping{ { Parameter: "MyModule.SendEmail.To", @@ -448,6 +450,7 @@ func TestFormatAction_CallExternal(t *testing.T) { ConsumedODataService: "MyModule.OrderService", Name: "GetOrders", ResultVariableName: "Orders", + UseReturnVariable: true, } got := e.formatAction(action, nil, nil) want := "$Orders = call external action MyModule.OrderService.GetOrders();" @@ -1033,6 +1036,7 @@ func TestFormatAction_JavaScriptActionCall_WithReturn(t *testing.T) { action := µflows.JavaScriptActionCallAction{ JavaScriptAction: "MyModule.MyJSAction", OutputVariableName: "Result", + UseReturnVariable: true, } got := e.formatAction(action, nil, nil) want := "$Result = call javascript action MyModule.MyJSAction();" @@ -1054,6 +1058,7 @@ func TestFormatAction_JavaScriptActionCall_WithParams(t *testing.T) { }, }, OutputVariableName: "Result", + UseReturnVariable: true, } got := e.formatAction(action, nil, nil) want := "$Result = call javascript action MyModule.MyJSAction(Input = $MyVar);" @@ -1192,6 +1197,7 @@ func TestFormatAction_JavaScriptActionCall_WithOutputAndEmptyParam(t *testing.T) action := µflows.JavaScriptActionCallAction{ JavaScriptAction: "MyModule.MyJSAction", OutputVariableName: "Result", + UseReturnVariable: true, ParameterMappings: []*microflows.JavaScriptActionParameterMapping{ { Parameter: "MyModule.MyJSAction.Input",