diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index 713002bfb4..d05e03d1d6 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -735,3 +735,4 @@ {"area": "mdl/executor", "date": "2026-09-27", "symptom": "describe of a published REST service with no resources printed `create or modify published rest service M.S ( \u2026 )` followed by a bare `;`, which does not parse (`mismatched input ';' expecting '{'`); found while making describe end every statement with `;` (ako/mxcli#744).", "cause": "The resource block `{ \u2026 }` is mandatory in the grammar (publishedRestService: \u2026 RPAREN LBRACE publishedRestResource* RBRACE), but the describer only printed it when the service had resources and otherwise closed the statement with `;` alone.", "file": "mdl/executor/cmd_published_rest.go", "fix": "Always print the block: `{\\n};` when there are no resources.", "insight": "A describe test that asserts substrings never parses the output; feeding every Describe*_Mock output through the parser (assertTerminated, #744) found this at once. The empty-collection branch of a describer is the one no fixture exercises.", "test": "TestDescribePublishedRestService_Mock (assertTerminated)"} {"area": "mdl/executor", "date": "2026-09-27", "symptom": "Running an external entity's describe output (create or modify external entity, either form) turns each attribute's Rest$ODataMappedValue into a DomainModels$StoredValue (RemoteName/RemoteType/Filterable... gone), re-mints attribute $IDs on the `from odata client` form, and rewrites String(36) as unlimited", "cause": "Both rewrite paths rebuilt attributes from the AST, which has no spelling for the OData mapping; the `from odata client` path minted fresh IDs; describe external entity printed GetTypeName() (`String`) instead of formatAttributeType", "file": "mdl/executor/cmd_entities.go (carryStoredAttributeState), cmd_odata.go", "insight": "Carry per attribute by name what the statement cannot spell, in the shared merge, and enumerate every path that rebuilds attributes: there were two statements for one document kind", "refs": ["ako/mxcli#743"]} {"area": "mdl/executor", "date": "2026-09-27", "symptom": "create or modify odata service over a Studio Pro service: EntitySet PageSize 10000 -> 0, entity sets reordered, member CanBeEmpty recomputed, ExportLevel deleted, AuthenticationTypes marker 1 -> 3", "cause": "The modify path rebuilt entity types/sets from the publish block (no spelling for PageSize without paging, set order, CanBeEmpty); the model had no ExportLevel; the writer used a marker-3 list copied from the legacy writer, not Studio Pro", "file": "mdl/executor/cmd_odata.go (carryPublishedEntityState), mdl/backend/modelsdk/odata_write.go", "insight": "Parity with the legacy writer is not evidence of what Studio Pro writes; measure markers on a Studio Pro fixture. A probe that runs describe output with the verb rewritten to create or modify (mdl/roundtrip TestTestAppCreateOrModifyProbe) shows what a verb switch would lose before switching it", "refs": ["ako/mxcli#743"]} +{"area": "mdl/executor", "date": "2026-09-27", "symptom": "describe microflow printed `validation feedback $O/A message '{1} \u2026';` with no template arguments; describe -> exec stored the feedback with none", "cause": "formatAction's ValidationFeedbackAction case never read TemplateParameters, although the reader (textTemplateFromGen) and the writer both carried them", "file": "mdl/executor/cmd_microflows_format_action.go (templateArgsClause)", "insight": "A text template has two halves, the text and its parameters; check every describe case that prints one prints both. Found while switching the template form to `with ({1} = \u2026)` (ako/mxcli#751)."} diff --git a/.claude/skills/fix-issue/findings/mdl-visitor.jsonl b/.claude/skills/fix-issue/findings/mdl-visitor.jsonl index 78b509ca39..81eedde092 100644 --- a/.claude/skills/fix-issue/findings/mdl-visitor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-visitor.jsonl @@ -36,3 +36,4 @@ {"area":"mdl-visitor","date":"2026-09-24","refs":["#653"],"symptom":"describe entity on a view entity, exec'd back, was never idempotent: \"Each cycle reports `Modified view entity` and stores the query with every line after the first indented two spaces further.\" Re-exec of the SAME described file reported Unchanged, so it looked stable until you described again","cause":"The two directions did not mirror: describe (cmd_entities_describe.go, and cmd_diff_mdl.go) prefixes two spaces to every stored OQL line; the visitor stored extractOriginalText(oqlCtx), which starts at the query's first token, so line 1 lost its indentation and lines 2…n kept all of it — +2 per cycle","file":"`mdl/visitor/visitor_entity.go` (dedentOQL, leadingLineWhitespace); tests `mdl/visitor/visitor_view_entity_oql_indent_test.go`, `mdl/executor/view_entity_oql_roundtrip_test.go`; bug-test `mdl-examples/bug-tests/653-view-entity-oql-indent-drift.mdl`","insight":"**Any verbatim-source capture that starts at the first token is asymmetric**: line 1 is dedented for free, the continuation lines are not. Fix it on the way IN (exec), not by making describe emit less: strip the common leading-whitespace prefix of the non-blank lines, counting line 1 at its column when only whitespace precedes it (read it from the input stream, start.GetStart()-GetColumn()). Compare prefixes byte-wise, not by width, so a Studio Pro query indented with tabs comes back byte-identical under describe's two spaces. A round-trip test must run describe → exec at least twice AND start from stored text mxcli did not write (flat, tabs, blank lines, comments): one pass from a script is exactly how this went unnoticed. Control: stubbing dedentOQL to return raw fails every round-trip case with lines 2…n two spaces deeper; a real 11.12.1 project with the old binary printed Modified ×3 with growing indent, the fixed one Unchanged ×3. Separate, not fixed here: a comment AFTER the query's last token is outside the captured span and is dropped on exec"} {"area": "mdl/visitor", "date": "2026-09-25", "symptom": "A page action's microflow argument `Flag: true and false` was stored as the expression \"trueandfalse\", `Mode: if true then 'a' else 'b'` as \"iftruethen'a'else'b'\", and a REST call parameter `$OrderId = if $x then $id else 'none'` as \"if$xthen$idelse'none'\" (measured by decoding the units on a copy of ako/TestApp). `check -p --references` passed; describe printed the fused text back", "cause": "Four sites stored an expression as text via ANTLR's ctx.GetText(), which concatenates tokens without the hidden-channel whitespace: microflowArgV3 values (page/nanoflow call arguments), contentparams values, send-rest-request WITH parameters, and a dynamic `execute database query`. Literals, `+` and a lone $currentObject are single tokens or need no spacing, so every common case looked correct", "file": "`mdl/visitor/visitor_helpers.go` (`expressionSourceText`), `mdl/visitor/visitor_page_v3.go` (`buildMicroflowArgV3`, `buildParamAssignmentV3`), `mdl/visitor/visitor_microflow_actions.go` (dynamic query, send rest params)", "insight": "**GetText() on an expression context is always a bug** — grep `Expression().*GetText()` / `expr.GetText()` in mdl/visitor; each hit either builds the AST or must use expressionSourceText (whitespace kept, MDL comments stripped). The earlier comment-leak fix moved the six microflow sites to extractExpressionText and missed these four because they lived in page/REST code, not microflow statements: fix a text-extraction defect by searching for the call, not the feature. The tell that hid it: tests used `$currentObject` and `'a' + 'b'`, both immune; a probe with `and` / `if…then` exposed all four at once. Found while writing PROPOSAL_first_class_expressions.md §5.1. Tests `visitor_expression_source_text_test.go`", "refs": ["mendixlabs/mxcli#750"]} {"area": "mdl/visitor", "date": "2026-09-26", "refs": ["ako/mxcli#706"], "rules": ["MDL-IDX01", "MDL-ENUMDOC01"], "symptom": "Five forms parsed and `check` printed \"Syntax OK\", then the model lost or changed them: `throw 'x';` vanished from the microflow; `Amount: float` / `currency` became String(unlimited) (Void as a microflow type) and `date` became DateTime; `create association X (from A to B, type: referenceset, storage: table)` was stored as a Reference with column storage; a `/** … */` on an enumeration value and an index name (`index Idx on (…)`, `create index Idx on E (…)`) were read and written nowhere", "cause": "Three different gaps behind one symptom: throwStatement had a grammar rule and no listener (a whole statement); buildDataType/buildMicroflowDataType end in a default return (String / Void) that the FLOAT_TYPE and CURRENCY_TYPE tokens fell into, and DATE_TYPE mapped to an ast.TypeDate the writers alias to DateTime; ExitCreateAssociationStatement read only ctx.AssociationOptions(), while the parenthesised alternative puts its options directly under the statement as `(COMMA associationOption)*`. Enumeration-value docs and index names have no home in the metamodel (EnumerationValue and Index have no such property)", "file": "`mdl/visitor/visitor_silent_drops.go` (ExitThrowStatement, EnterDataType, EnterNonListDataType, rejectParenthesisedAssociation), `mdl/executor/validate_unstored_text.go` (MDL-IDX01, MDL-ENUMDOC01); tests `mdl/visitor/silent_drops_706_test.go`, `mdl/executor/validate_unstored_text_test.go`", "insight": "**Split by what is lost: refuse when the MODEL would be wrong, warn when only WORDS are lost.** throw/types/association write a different model, so they are visitor errors with the working spelling in the message; doc comments on values and index names leave a correct model, and were used in 56 places in our own examples, so they are check warnings. **Refuse from the parse tree, not by deleting the grammar alternative**: `float`, `currency`, `date` and `throw` are also keywords-as-identifiers, so removing `| FLOAT_TYPE` from dataType would let `Amount: float` re-parse as an enumeration reference named float — the same silence again. A listener on EnterDataType covers every place a type is written (attribute, parameter, return, declare, constant, service rules) in one method; checking only the direct child token keeps `M.Currency` and `enumeration(M.Float)` legal (the control). To find these: any `if … ; return ast.DataType{Kind: …}` fall-through default in a builder is a silent-substitution site, and a grammar alternative whose sub-rules differ from its sibling's (`associationOption` vs `associationOptions`) needs its own read. Sweep: run old vs new `mxcli check` over `git ls-files '*.mdl'` and diff exit codes — it found every example that relied on the dropped forms (5 files)"} +{"date": "2026-09-27", "area": "mdl/visitor", "symptom": "A member path after `*`, `div`, `mod` (`$a/X * $b/Y`) is stored as division: `describe` shows `$a/X * $b / Y`, and inside a list `filter` `$b / $currentObject/Y`. `mxcli check`, `mx check` and the build are all green; Mendix evaluates a different expression.", "cause": "`/` shares a precedence level with `*`, `div`, `mod` in `multiplicativeExpression` (MDLSettings.g4), so the left-associative chain parses `$a/X * $b/Y` as `(($a/X) * $b) / Y`. `buildMultiplicativeExpression` only turned `/ Member` into an AttributePathExpr when the WHOLE left side was a variable or path; here `$b` was already the right operand of the product, so the `/` stayed a BinaryExpr. The serializer then printed a spaced `/` and the filter's `qualifyIteratorAttributes` read the stranded `Y` as a bare attribute.", "file": "mdl/visitor/visitor_microflow_expression.go (attachPathToLastOperand)", "fix": "When a `/ Member` step cannot attach to the whole left side, attach it to the last operand of the preceding multiplicative BinaryExpr (`*`, `div`, `mod`, `%`, `:`): `(L op $b) / Y` becomes `L op ($b/Y)`. Mendix has no `/` division, so `/` followed by a member name is always navigation. Division by a parenthesised expression is left alone (control test).", "insight": "Fixing it where the AST is BUILT fixes every consumer at once (serializer, iterator qualification, MDL045's slash-division check), where the earlier #52 fix only taught MDL045 to tolerate the mis-nested tree. Test: `TestMemberPathAfterMultiplicativeOperatorIsStoredAsWritten` (flow builder, stored strings for sum, filter, declare, div chain) plus a control `TestSlashAfterNonPathIsNotMadeAMemberPath`. Revert check: stubbing the fix brings back all four wrong strings. A unary-minus branch was dropped because removing it failed nothing (`2 * -$b/Y` was already stored correctly).", "refs": ["#768", "#52"], "rules": []} diff --git a/.claude/skills/mendix/cheatsheet-errors/SKILL.md b/.claude/skills/mendix/cheatsheet-errors/SKILL.md index 8565ba1801..90b496a68f 100644 --- a/.claude/skills/mendix/cheatsheet-errors/SKILL.md +++ b/.claude/skills/mendix/cheatsheet-errors/SKILL.md @@ -265,7 +265,7 @@ Run with `-p` for the fullest coverage. | CE0104 | Action activity is unreachable | Code after RETURN | | CE0105 | Must end with end event | Missing RETURN | | CE0117 | Error in expression | Unqualified association path | -| CE1571 | No argument selected for parameter | A microflow/nanoflow call with a parameter nothing fills — as a `datasource:` **or** an `action:`. Give it an argument (`action: nanoflow M.NF($P = $value)`), or nest the widget in a data container of the parameter's type. `check -p` reports both | +| CE1571 | No argument selected for parameter | A microflow/nanoflow call with a parameter nothing fills — as a `datasource:` **or** an `action:`. Give it an argument (`action: nanoflow M.NF(P = $value)`), or nest the widget in a data container of the parameter's type. `check -p` reports both | | CE1571 | …in a control bar | A control bar is **not** row-scoped, so the grid's row does not fill it: pass the grid's selection (`$dgOrders`, with `Selection:` set) or move the widget into a column. `$currentObject` there is MDL-BUTTON01 | | CE1834 | The 'Page' property is required | Workflow user task without a `page` — `check` flags MDL-WF01 | | CE1876 | Single outcome must not contain activities | Single-outcome user task with a nested activity flow — `check` flags MDL-WF02 | diff --git a/.claude/skills/mendix/check-syntax/SKILL.md b/.claude/skills/mendix/check-syntax/SKILL.md index dbed9603e8..1dd142eb41 100644 --- a/.claude/skills/mendix/check-syntax/SKILL.md +++ b/.claude/skills/mendix/check-syntax/SKILL.md @@ -249,7 +249,7 @@ Before writing any MDL, verify these requirements: - `retrieve $Var from Module.Entity [where condition];` - `$Result = call microflow Module.Name (Param = $value);` (NOT `set $Result = ...`) - `$Result = call nanoflow Module.Name (Param = $value);` -- `show page Module.PageName ($Param = $value);` +- `show page Module.PageName (Param = $value);` - `close page;` - `validation feedback $entity/attribute message 'message';` - `log info|warning|error [node 'name'] 'message';` diff --git a/.claude/skills/mendix/create-page/reference/widgets.md b/.claude/skills/mendix/create-page/reference/widgets.md index a2a3059599..74edc8f647 100644 --- a/.claude/skills/mendix/create-page/reference/widgets.md +++ b/.claude/skills/mendix/create-page/reference/widgets.md @@ -123,10 +123,9 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference - `action: close_page` - Close the page - `action: delete` - Delete object - `action: microflow Module.MicroflowName` - Call microflow -- `action: microflow Module.MicroflowName(Param: $value)` - Call microflow with parameters +- `action: microflow Module.MicroflowName(Param = $value)` - Call microflow with parameters - `action: nanoflow Module.NanoflowName` - Call nanoflow (client-side) -- `action: nanoflow Module.NanoflowName(Param: $value)` - Call nanoflow with parameters -- `action: nanoflow Module.NanoflowName($Param = $value)` - Also accepted (microflow-style) +- `action: nanoflow Module.NanoflowName(Param = $value)` - Call nanoflow with parameters - `action: open_link 'https://example.com'` - Open a fixed web address - `action: open_link $currentObject/URL` - Open the address held in an attribute of the enclosing data container's object (inside a data container only; not over an association) - **Every parameter needs an argument, or an enclosing data container of its @@ -134,13 +133,12 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference check -p` reports it. This is the same on every widget that takes an action, a clickable `container` included. - `action: show_page Module.PageName` - Navigate to page -- `action: show_page Module.PageName(Param: $value)` - Navigate with parameters -- `action: show_page Module.PageName($Param = $value)` - Also accepted (microflow-style) +- `action: show_page Module.PageName(Param = $value)` - Navigate with parameters - `action: create_object Module.Entity then show_page Module.PageName` - Create and navigate - **A `show_page` argument must be the context object, and there has to BE one.** Mendix takes the page argument from the enclosing data widget, so the only spellings that mean anything are `$currentObject` or the name of the variable - that widget is bound to (`datasource: $Customer` → `(Customer: $Customer)` is + that widget is bound to (`datasource: $Customer` → `(Customer = $Customer)` is fine). Naming any other variable is refused as **MDL-PAGEARG01** — it used to be accepted and silently opened the page with the context object anyway. - **Outside a data widget the same rule leaves nothing at all**, so a button sitting @@ -150,7 +148,7 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference mxbuild reports **CE1571** per parameter of the target page, and a page whose parameters are optional would simply show the wrong data. MDL-PAGEARG01 refuses that too (mendixlabs/mxcli#1029). To open a parameterised page from such a - button, call a microflow that does `show page Module.Page(Param: $value)` — + button, call a microflow that does `show page Module.Page(Param = $value)` — that path wires the arguments properly. - **The list above is the whole vocabulary, and a keyword without its argument is not in it.** `action: open_link` with no URL, `action: show_page` with no page, @@ -173,16 +171,16 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) -- Navigate with parameter (inside DATAVIEW) -actionbutton btnEdit (caption: 'Edit', action: show_page Module.EditPage(Product: $Product)) +actionbutton btnEdit (caption: 'Edit', action: show_page Module.EditPage(Product = $Product)) -- Navigate with $currentObject (inside DATAGRID column) -actionbutton btnEdit (caption: 'Edit', action: show_page Module.EditPage(Product: $currentObject)) +actionbutton btnEdit (caption: 'Edit', action: show_page Module.EditPage(Product = $currentObject)) -- Call microflow with page/dataview parameter -actionbutton btnProcess (caption: 'Process', action: microflow Module.ACT_Process(Order: $Order), buttonstyle: success) +actionbutton btnProcess (caption: 'Process', action: microflow Module.ACT_Process(Order = $Order), buttonstyle: success) -- Call microflow with $currentObject (inside DATAGRID/LISTVIEW column) -actionbutton btnDelete (caption: 'Delete', action: microflow Module.ACT_Delete(Target: $currentObject), buttonstyle: danger) +actionbutton btnDelete (caption: 'Delete', action: microflow Module.ACT_Delete(Target = $currentObject), buttonstyle: danger) -- Create object and show page actionbutton btnNew (caption: 'New', action: create_object Module.Product then show_page Module.Product_Edit, buttonstyle: primary) @@ -645,7 +643,7 @@ Create a menu with action items: ```sql navigationlist navName { - item itemEdit (caption: 'Edit', action: show_page Module.EditPage(entity: $EntityParameter)) + item itemEdit (caption: 'Edit', action: show_page Module.EditPage(entity = $EntityParameter)) item itemDelete (caption: 'Delete', action: delete) item itemBack (caption: 'Back', action: close_page) } @@ -947,7 +945,7 @@ as an `actionbutton`'s `action:`: ```sql -- Rich, parameterised trigger: a card that opens the object it represents -container tileCard (OnClick: microflow MyModule.ACT_Open(Item: $currentObject), class: 'tile') { +container tileCard (OnClick: microflow MyModule.ACT_Open(Item = $currentObject), class: 'tile') { dynamictext tileValue (content: '4') dynamictext tileLabel (content: '4 LEFT', class: 'tile-label') } @@ -1013,7 +1011,7 @@ datagrid dgMaterials ( -- Not row-scoped: pass the selection explicitly. container cUnlink ( class: 'command', - action: nanoflow Module.ACT_UnLink ($Material = $dgMaterials) + action: nanoflow Module.ACT_UnLink (Material = $dgMaterials) ) { actionbutton btnUnlink (caption: 'Unlink') } diff --git a/.claude/skills/mendix/migrate-k2-nintex/SKILL.md b/.claude/skills/mendix/migrate-k2-nintex/SKILL.md index 502a4d9039..e666fccd5c 100644 --- a/.claude/skills/mendix/migrate-k2-nintex/SKILL.md +++ b/.claude/skills/mendix/migrate-k2-nintex/SKILL.md @@ -241,7 +241,7 @@ begin commit $Order; -- Show page for review (like K2 "Task" with form) - show page CRM.Order_Review ($Order = $Order); + show page CRM.Order_Review (Order = $Order); end; -- K2 Decision "Order > $5000?" → Mendix microflow with decision @@ -252,7 +252,7 @@ begin if $Order/TotalAmount > 5000 then -- Route to manager (K2 Destination Rule equivalent) - call microflow CRM.ACT_Order_SubmitForManagerReview ($Order = $Order); + call microflow CRM.ACT_Order_SubmitForManagerReview (Order = $Order); else -- Auto-approve (K2 "Go To" equivalent) change $Order (status = CRM.OrderStatus.Approved); diff --git a/.claude/skills/mendix/overview-pages/SKILL.md b/.claude/skills/mendix/overview-pages/SKILL.md index 2fdc531dde..19d77f784a 100644 --- a/.claude/skills/mendix/overview-pages/SKILL.md +++ b/.claude/skills/mendix/overview-pages/SKILL.md @@ -105,7 +105,7 @@ create page Module.Entity_Overview textfilter textFilter2 } column (caption: 'Actions') { - actionbutton btnEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity": $currentObject)) + actionbutton btnEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity" = $currentObject)) actionbutton btnDelete (caption: 'Delete', action: delete, buttonstyle: danger) } } @@ -147,7 +147,7 @@ datagrid GridName ( textfilter textFilter1 } column (caption: 'Actions') { - actionbutton btnEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity": $currentObject)) + actionbutton btnEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity" = $currentObject)) } } ``` @@ -276,8 +276,8 @@ create page Module.PageName - Parameter name conventionally matches the entity name (e.g., `$store`, `$Customer`) - The DataView's binding references this parameter (`datasource: $ParamName`) -- When calling the page via SHOW_PAGE, pass the object by parameter name: `show_page Module.PageName(ParamName: $value)` -- **Reserved-word parameter names must be quoted in the SHOW_PAGE args.** The generic examples above name the parameter `entity`, which is a reserved keyword, so the call quotes it: `show_page Module.Entity_NewEdit("entity": $currentObject)`. A non-reserved name (`$store`, `$Customer`) needs no quotes: `show_page Module.Store_NewEdit(store: $currentObject)`. +- When calling the page via SHOW_PAGE, pass the object by parameter name: `show_page Module.PageName(ParamName = $value)` +- **Reserved-word parameter names must be quoted in the SHOW_PAGE args.** The generic examples above name the parameter `entity`, which is a reserved keyword, so the call quotes it: `show_page Module.Entity_NewEdit("entity" = $currentObject)`. A non-reserved name (`$store`, `$Customer`) needs no quotes: `show_page Module.Store_NewEdit(store = $currentObject)`. ### NewEdit Page Components @@ -535,7 +535,7 @@ create snippet Module.Entity_Menu ) { navigationlist EntityMenuNav { - item itemEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity": $EntityParameter)) + item itemEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity" = $EntityParameter)) item itemDelete (caption: 'Delete', action: delete) item itemBack (caption: 'Back', action: close_page) } @@ -556,13 +556,13 @@ navigationlist widgetName { - `action: close_page` - Close current page - `action: delete` - Delete object - `action: microflow Module.MicroflowName` - Call microflow -- `action: microflow Module.MicroflowName(Param: $value)` - Call microflow with parameters +- `action: microflow Module.MicroflowName(Param = $value)` - Call microflow with parameters - `action: show_page Module.PageName` - Navigate to page -- `action: show_page Module.PageName(Param: $value)` - Navigate with parameters +- `action: show_page Module.PageName(Param = $value)` - Navigate with parameters - **A `show_page` argument must be the context object, and there has to BE one.** Mendix takes the page argument from the enclosing data widget, so the only spellings that mean anything are `$currentObject` or the name of the variable - that widget is bound to (`datasource: $Customer` → `(Customer: $Customer)` is + that widget is bound to (`datasource: $Customer` → `(Customer = $Customer)` is fine). Naming any other variable is refused as **MDL-PAGEARG01** — it used to be accepted and silently opened the page with the context object anyway. - **Outside a data widget the same rule leaves nothing at all**, so a button sitting @@ -572,7 +572,7 @@ navigationlist widgetName { mxbuild reports **CE1571** per parameter of the target page, and a page whose parameters are optional would simply show the wrong data. MDL-PAGEARG01 refuses that too (mendixlabs/mxcli#1029). To open a parameterised page from such a - button, call a microflow that does `show page Module.Page(Param: $value)` — + button, call a microflow that does `show page Module.Page(Param = $value)` — that path wires the arguments properly. ## Handling Circular Dependencies diff --git a/.claude/skills/mendix/patterns-crud/SKILL.md b/.claude/skills/mendix/patterns-crud/SKILL.md index 40e5429d9f..b20fbd8aae 100644 --- a/.claude/skills/mendix/patterns-crud/SKILL.md +++ b/.claude/skills/mendix/patterns-crud/SKILL.md @@ -35,7 +35,7 @@ returns boolean begin -- Validate first declare $IsValid boolean = true; - $IsValid = call microflow Module.VAL_Customer_Save($Customer = $Customer); + $IsValid = call microflow Module.VAL_Customer_Save(Customer = $Customer); if $IsValid then commit $Customer; @@ -153,7 +153,7 @@ begin CreatedDate = [%CurrentDateTime%] ); - show page Module.Customer_NewEdit ($Customer = $NewCustomer); + show page Module.Customer_NewEdit (Customer = $NewCustomer); return true; end; / @@ -198,7 +198,7 @@ create microflow Module.ACT_Customer_Edit ( ) returns boolean begin - show page Module.Customer_NewEdit ($Customer = $Customer); + show page Module.Customer_NewEdit (Customer = $Customer); return true; end; / diff --git a/.claude/skills/mendix/resolve-forward-references/SKILL.md b/.claude/skills/mendix/resolve-forward-references/SKILL.md index e72f1f00e4..e71ea72a64 100644 --- a/.claude/skills/mendix/resolve-forward-references/SKILL.md +++ b/.claude/skills/mendix/resolve-forward-references/SKILL.md @@ -198,7 +198,7 @@ create page MyModule.Order_Detail create microflow MyModule.ACT_OpenOrder ($Order: MyModule.Order) begin @position(200,200) - show page MyModule.Order_Detail ($Order = $Order); + show page MyModule.Order_Detail (Order = $Order); @position(400,200) return; end; / @@ -313,7 +313,7 @@ begin @position(200,200) $c = create MyModule.Customer; @position(400,200) - show page MyModule.Customer_NewEdit ($Customer = $c); + show page MyModule.Customer_NewEdit (Customer = $c); @position(600,200) return; end; / diff --git a/.claude/skills/mendix/validation-microflows/SKILL.md b/.claude/skills/mendix/validation-microflows/SKILL.md index 1f7e0181e7..c3a2ece3a7 100644 --- a/.claude/skills/mendix/validation-microflows/SKILL.md +++ b/.claude/skills/mendix/validation-microflows/SKILL.md @@ -42,7 +42,7 @@ validation feedback $VariableName/attributename message 'Error message'; With template arguments (for dynamic messages): ```mdl -validation feedback $VariableName/attributename message '{1}' objects [$MessageVariable]; +validation feedback $VariableName/attributename message '{1}' with ({1} = $MessageVariable); ``` ### CLOSE PAGE Statement @@ -131,7 +131,7 @@ returns boolean as $IsValid folder 'OverviewPages' begin -- Call validation microflow - $IsValid = call microflow MdlTemplates.VAL_Car_NewEdit($param = $Car); + $IsValid = call microflow MdlTemplates.VAL_Car_NewEdit(param = $Car); -- Only save if validation passed if $IsValid then @@ -240,7 +240,7 @@ end if; if trim($ValidationMessage) != '' then set $IsValid = false; - validation feedback $entity/value message '{1}' objects [$ValidationMessage]; + validation feedback $entity/value message '{1}' with ({1} = $ValidationMessage); end if; ``` diff --git a/.claude/skills/mendix/write-microflows/SKILL.md b/.claude/skills/mendix/write-microflows/SKILL.md index 2b31169d57..03dbfb0885 100644 --- a/.claude/skills/mendix/write-microflows/SKILL.md +++ b/.claude/skills/mendix/write-microflows/SKILL.md @@ -524,14 +524,15 @@ When calling microflows, always check the target's parameter list. Use `describe ### SHOW PAGE ```mdl --- Open page with parameter (canonical syntax) -show page Module.EditPage($Product = $Product); - --- Widget-style syntax also accepted in microflows -show page Module.EditPage(Product: $Product); +-- Open page with parameter +show page Module.EditPage(Product = $Product); ``` -Both `($Param = $value)` and `(Param: $value)` syntaxes are accepted in microflow SHOW PAGE statements. Similarly, widget Action: properties accept both `show_page Module.Page(Param: $value)` and `show_page Module.Page($Param = $value)`. +Every call site binds an argument as `Param = expression`, with no `$` on the +parameter name: `call microflow`, `show page`, and widget actions +(`action: show_page Module.Page(Param = $value)`) alike. `$Param = $value` and +`Param: $value` still parse but are deprecated (MDL-DEPR006/007); `mxcli fmt +--upgrade` rewrites them. ### CLOSE PAGE @@ -646,8 +647,7 @@ the short `@annotation 'text'` form. ### Page Navigation Pattern ```mdl -show page Module.Page($Param = $value); -- Canonical -show page Module.Page(Param: $value); -- Widget-style (also valid) +show page Module.Page(Param = $value); close page; show home page; ``` @@ -670,7 +670,7 @@ log info node 'App' 'starting' on error { return; }; show message 'saved' on error { return; }; -- BLOCKING halts the client until dismissed; after `objects`, before `on error`. -show message 'Hello {1}' type Warning objects [$Name] blocking; +show message 'Hello {1}' type Warning with ({1} = $Name) blocking; validation feedback $Order/Total message 'must be positive' on error { return; }; show page Module.Page on error { return; }; close page on error { return; }; diff --git a/.claude/skills/mendix/write-microflows/reference/integration.md b/.claude/skills/mendix/write-microflows/reference/integration.md index b342c85232..36a2fae24a 100644 --- a/.claude/skills/mendix/write-microflows/reference/integration.md +++ b/.claude/skills/mendix/write-microflows/reference/integration.md @@ -95,6 +95,10 @@ send rest request Module.ServiceName.OperationName; -- With output variable (RESPONSE JSON operation — maps to entity) $Result = send rest request Module.ServiceName.OperationName; +-- With path/query parameters, bound as at every call site: `Param = expression` +$Result = send rest request Module.ServiceName.GetItem + with (id = $ItemId, lang = 'en'); + -- With request body (POST/PUT operations) $Result = send rest request Module.ServiceName.CreateItem body $NewItem; diff --git a/.claude/skills/mendix/write-nanoflows/SKILL.md b/.claude/skills/mendix/write-nanoflows/SKILL.md index afcae59da4..0ded55a99d 100644 --- a/.claude/skills/mendix/write-nanoflows/SKILL.md +++ b/.claude/skills/mendix/write-nanoflows/SKILL.md @@ -121,7 +121,7 @@ $JsResult = CALL JAVASCRIPT ACTION NanoflowCommons.SignIn (userName = $Name, pas ### UI Activities ```mdl -SHOW PAGE Sales.CartDetail ($Cart = $Cart); +SHOW PAGE Sales.CartDetail (Cart = $Cart); CLOSE PAGE; SHOW MESSAGE WARNING 'Connection unavailable. Working offline.'; VALIDATION FEEDBACK $Item/Quantity MESSAGE 'Quantity must be at least 1'; @@ -156,7 +156,7 @@ IF $Cart/ItemCount = 0 THEN VALIDATION FEEDBACK $Cart/ItemCount MESSAGE 'Cart is empty'; RETURN false; ELSE - SHOW PAGE Sales.Checkout ($Cart = $Cart); + SHOW PAGE Sales.Checkout (Cart = $Cart); RETURN true; END IF; ``` @@ -217,11 +217,11 @@ CREATE OR MODIFY NANOFLOW Inventory.NAV_OpenProductDetail ( ) FOLDER 'Navigation' BEGIN - $IsValid = CALL NANOFLOW Inventory.NAV_ValidateProduct ($Product = $Product); + $IsValid = CALL NANOFLOW Inventory.NAV_ValidateProduct (Product = $Product); IF NOT ($IsValid) THEN RETURN; END IF; - SHOW PAGE Inventory.ProductDetail ($Product = $Product); + SHOW PAGE Inventory.ProductDetail (Product = $Product); END; ``` diff --git a/.claude/skills/mendix/write-workflows/SKILL.md b/.claude/skills/mendix/write-workflows/SKILL.md index 57972e907c..a610601698 100644 --- a/.claude/skills/mendix/write-workflows/SKILL.md +++ b/.claude/skills/mendix/write-workflows/SKILL.md @@ -109,8 +109,7 @@ begin outcomes 'Done' { }; -- Call a microflow (server logic); optional name, parameter mapping + outcomes - call microflow Module.ACT_Validate as callMicroflow1 - with (Module.ACT_Validate.Item = '$WorkflowContext'); + call microflow Module.ACT_Validate(Item = $WorkflowContext) as callMicroflow1; -- Decision: a boolean or enum exclusive split. The name is optional; give one -- when a `jump to` targets it. @@ -466,11 +465,11 @@ values. The full list and the System **entities** are in `system-module`. users` is refused — each omitted clause would reset it. - **An AI agent task is `call agent microflow`** (Mendix 11.9+) — the call microflow statement stored as `Workflows$AIAgentTaskActivity`, with the same - `as`, `comment`, `with (…)`, `outcomes` and boundary events. The microflow is + argument list, `as`, `comment`, `outcomes` and boundary events. The microflow is where the agent is invoked. Measured on mxbuild 11.13 against the identical call microflow, one rule differs: **its microflow must take a parameter** - (`CE1590 "Missing parameter"`), usually the context object mapped with - `with (Param = '$WorkflowContext')`. Return Boolean or an enumeration to + (`CE1590 "Missing parameter"`), usually the context object passed as + `(Param = $WorkflowContext)`. Return Boolean or an enumeration to branch on the answer. - **Handler microflows have fixed signatures** (measured, mxbuild 11.13): - `on created microflow` takes exactly `System.WorkflowUserTask` and the context @@ -525,9 +524,11 @@ values. The full list and the System **entities** are in `system-module`. `REPLACE ACTIVITY`. A **required (`not null`) attribute does not exempt it** — measured, the empty outcome is still required. Boolean (`true`/`false`) decisions do not take one. -- **A `with (...)` parameter value is a quoted string**, not a bare variable: - `with (Request = '$WorkflowContext')`. The unquoted spelling used elsewhere in - MDL is a syntax error here (it used to crash the binary — ako/mxcli#1023). +- **Arguments go right after the callee, as bare expressions**, like every other + call: `call microflow HR.Escalate(Request = $WorkflowContext) as callMicroflow1`. + The older `with (Request = '$WorkflowContext')`, the expression inside a + string, still parses with the same meaning but is deprecated (MDL-DEPR008); + `mxcli fmt --upgrade` rewrites it. - The context **Parameter entity must be persistent**. - Write the context variable as **`$WorkflowContext`**, matching the parameter name exactly. Mendix expressions are case-sensitive on 11.9+, so a lowercase diff --git a/.claude/skills/packs/mendix-vega-charts/references/properties.md b/.claude/skills/packs/mendix-vega-charts/references/properties.md index 36d3e2bb8b..0b5a9753d7 100644 --- a/.claude/skills/packs/mendix-vega-charts/references/properties.md +++ b/.claude/skills/packs/mendix-vega-charts/references/properties.md @@ -61,7 +61,7 @@ To let a click select something: ``` selection: SelectedPoint, -onClick: microflow MyModule.ACT_SelectPoint(Context: $currentObject) +onClick: microflow MyModule.ACT_SelectPoint(Context = $currentObject) ``` `selection` receives the clicked datum as JSON, reduced to its own scalar fields — Vega's diff --git a/CHANGELOG.md b/CHANGELOG.md index 40236a6316..a9a29f5ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed +- **One argument form at every call site: `Param = expression`; one text-template form: `with ({1} = …)`** (ako/mxcli#751, R3/R4) — `call microflow`/`nanoflow`/`java action`/…, `execute database query`, `send rest request`, `show page`, and page/button actions and microflow/nanoflow data sources all bind an argument as `Order = $Order`, with no `$` on the parameter name and `=` rather than `:`. A workflow's `call microflow` / `call workflow` takes its arguments the same way, right after the callee, as bare expressions: `call microflow Shop.ACT_Process(Order = $WorkflowContext) as act1 comment 'Process'`. `show message` and `validation feedback` take `with ({1} = $a, {2} = $b)`, like `log` and the REST templates. `describe` writes these forms; it also writes a validation feedback's template arguments, which it used to drop, so describe → exec lost them. Measured on a copy of the Studio Pro-authored TestApp (Mendix 11.14.0): a script in the new forms and the same script in the old forms describe identically, and `mx check` reports 0 errors. **Migrating a script:** nothing breaks — `$Param = e` (**MDL-DEPR006**), `Param: e` (**MDL-DEPR007**), a workflow's `with (Param = '')` (**MDL-DEPR008**, same meaning: the string's content is the expression) and `objects [..]` / `parameters [..]` (**MDL-DEPR009**) still parse and build the same model, `check` / `exec` warn, and `mxcli fmt --upgrade` rewrites them. A workflow string whose content does not read back as the same bare expression is left in place and reported, and `describe` keeps the string form for such a stored expression. - **`describe page` no longer invents names Mendix does not store; `alter page` addresses a DataGrid 2 column by what it shows** (ako/mxcli#749, R12) — a layout grid's rows and columns, a data grid's columns and control bar, a gallery's template and filter, a chart's series and other object-list items have no Name in the model, and describe used to print one it made up (`row row1`, `column col3`, `controlbar controlBar1`, and a data-grid column named after its attribute, so two columns over `FullName` were both `column FullName`). They now print without one — `row { column (DesktopWidth: 6) { … } }`, `column (Attribute: FullName, Caption: 'Full name')` — and the widget name is optional in the grammar for exactly those elements. A column is addressed as `grid column(Attr)`, written as describe writes the attribute (`Owner/Name` over an association), or `grid column('Caption')`; an address two columns share is refused, listing the matches, unless `@n` picks one (`drop dg column(FullName)@2`). A widget Mendix does store a name for still needs one: `check` reports a missing one as **MDL-WIDGET35** and the page builder refuses it. Measured on the Studio Pro-authored PedApp fixture: the round-trip harness stays green, and `mx check` 11.13 shows no new error after `alter page … column(…)` edits and a create with nameless rows, grid columns and gallery slots. **Migrating a script:** nothing breaks — a name written on one of those elements still parses, is ignored as before, and `check` / `exec` warn **MDL-DEPR005**; `mxcli fmt --upgrade` removes it. The older `grid.Column` address keeps working. The info-level MDL-WIDGET16 note about dropped column names is gone, replaced by that warning. - **`describe microflow` / `describe nanoflow` leave out layout the engine derives** (ako/mxcli#748, R12) — `@position`, `@merge`, `@anchor` and `@curve` now follow the rule `@start` has followed since #951: an annotation is printed only when re-executing the description without it would put the node, or route the flow, somewhere else. That is decided by building the flow again from the description exactly as `create or modify` would (nothing is written) and comparing it node by node with the stored flow, pinning the first misplaced node of each run and repeating until nothing moves — so a statement placed after a hand-placed one needs no annotation of its own. A flow authored with no annotations describes with none; a Studio Pro-drawn flow keeps its layout. Measured on the Studio Pro-authored PedApp fixture: re-executing the new description lands every node, anchor and curve where the old one did (all 16 microflows, and all 224 microflows of a second Studio Pro project that re-execute at all); there the output barely shrinks (433 layout lines to 430), as it should, while after `mxcli layout flows` the 24 flows it could lay out go from 163 layout lines to 3. **Migrating a script:** nothing to do — every annotation still parses and means the same, and committed describe output re-executes as before; only a fresh `describe` is shorter. diff --git a/cmd/mxcli/syntax/features_microflow.go b/cmd/mxcli/syntax/features_microflow.go index 74d1d3af0e..657d13e390 100644 --- a/cmd/mxcli/syntax/features_microflow.go +++ b/cmd/mxcli/syntax/features_microflow.go @@ -501,8 +501,8 @@ func init() { "show page", "open page", "close page", "display page", "navigate", "page action", }, - Syntax: "SHOW PAGE Module.Page;\nSHOW PAGE Module.Page ($Param = $value);\nCLOSE PAGE;", - Example: "SHOW PAGE MyModule.OrderDetail ($Order = $NewOrder);\nCLOSE PAGE;", + Syntax: "SHOW PAGE Module.Page;\nSHOW PAGE Module.Page (Param = $value);\nCLOSE PAGE;", + Example: "SHOW PAGE MyModule.OrderDetail (Order = $NewOrder);\nCLOSE PAGE;", SeeAlso: []string{"page"}, }) @@ -592,8 +592,8 @@ func init() { "validation", "feedback", "validation feedback", "error message", "field error", "form validation", }, - Syntax: "VALIDATION FEEDBACK $Obj/Attr MESSAGE 'error text';\nVALIDATION FEEDBACK $Obj/Attr MESSAGE '{1} is invalid'\n OBJECTS [$Value];", - Example: "VALIDATION FEEDBACK $Order/Quantity MESSAGE 'Quantity must be positive';\nVALIDATION FEEDBACK $Customer/Email MESSAGE '{1} is not valid'\n OBJECTS [$Customer/Email];", + Syntax: "VALIDATION FEEDBACK $Obj/Attr MESSAGE 'error text';\nVALIDATION FEEDBACK $Obj/Attr MESSAGE '{1} is invalid'\n WITH ({1} = $Value);", + Example: "VALIDATION FEEDBACK $Order/Quantity MESSAGE 'Quantity must be positive';\nVALIDATION FEEDBACK $Customer/Email MESSAGE '{1} is not valid'\n WITH ({1} = $Customer/Email);", SeeAlso: []string{"microflow.error-handling"}, }) diff --git a/cmd/mxcli/syntax/features_page.go b/cmd/mxcli/syntax/features_page.go index c8952bf9a9..f9bf6d9ca3 100644 --- a/cmd/mxcli/syntax/features_page.go +++ b/cmd/mxcli/syntax/features_page.go @@ -244,7 +244,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "datasource", "data source", "database", "microflow", "selection", "variable", "binding", "binds", "association", "data from context", }, - Syntax: "DataSource: $Variable -- Parameter/variable binding\nDataSource: DATABASE Module.Entity -- Database query\nDataSource: DATABASE Module.Entity WHERE [Attr != ''] SORT BY Attr ASC\n -- ...optionally constrained and sorted\nDataSource: DATABASE Module.Entity SORT BY Module.Assoc/Attr ASC\n -- ...sorted over an association. Name the\n -- hop when two reach the same entity —\n -- the wrong one builds cleanly and sorts\n -- by the wrong thing.\nDataSource: DATABASE Module.Entity SEARCH BY Attr, Attr2\n -- LIST VIEW only: the attributes its\n -- search bar filters on. Mirrors SORT BY,\n -- but takes no direction.\nDataSource: MICROFLOW Module.MF -- Microflow datasource, no parameters\nDataSource: MICROFLOW Module.MF(Param: $P) -- ...one argument per PARAMETER, required,\n -- and NAMED: the positional form\n -- MF($P) is a parse error. Mendix does\n -- NOT auto-map an object in scope, not\n -- even one of the exact type, so a\n -- missing argument is CE1571\nDataSource: SELECTION widgetName -- Selection from another widget\nDataSource: $currentObject/Module.Assoc -- Over an association (\"data from context\")\n -- list widget → to-many collection\n -- nested DATAVIEW → the to-one referenced object\nAttribute: AttributeName -- Attribute binding (inputs)", + Syntax: "DataSource: $Variable -- Parameter/variable binding\nDataSource: DATABASE Module.Entity -- Database query\nDataSource: DATABASE Module.Entity WHERE [Attr != ''] SORT BY Attr ASC\n -- ...optionally constrained and sorted\nDataSource: DATABASE Module.Entity SORT BY Module.Assoc/Attr ASC\n -- ...sorted over an association. Name the\n -- hop when two reach the same entity —\n -- the wrong one builds cleanly and sorts\n -- by the wrong thing.\nDataSource: DATABASE Module.Entity SEARCH BY Attr, Attr2\n -- LIST VIEW only: the attributes its\n -- search bar filters on. Mirrors SORT BY,\n -- but takes no direction.\nDataSource: MICROFLOW Module.MF -- Microflow datasource, no parameters\nDataSource: MICROFLOW Module.MF(Param = $P) -- ...one argument per PARAMETER, required,\n -- and NAMED: the positional form\n -- MF($P) is a parse error. Mendix does\n -- NOT auto-map an object in scope, not\n -- even one of the exact type, so a\n -- missing argument is CE1571\nDataSource: SELECTION widgetName -- Selection from another widget\nDataSource: $currentObject/Module.Assoc -- Over an association (\"data from context\")\n -- list widget → to-many collection\n -- nested DATAVIEW → the to-one referenced object\nAttribute: AttributeName -- Attribute binding (inputs)", Example: "-- Database datasource with grid\nDATAGRID grid (DataSource: DATABASE Module.Customer) {\n COLUMN colName (Attribute: Name, Caption: 'Name')\n}\n\n-- Microflow datasource\nDATAVIEW dv (DataSource: MICROFLOW Module.GetData) {\n TEXTBOX txtName (Label: 'Name', Attribute: Name)\n}\n\n-- Over an association: a nested DataView shows the referenced (to-one) object\nDATAVIEW dvOrder (DataSource: $Order) {\n DATAVIEW dvCustomer (DataSource: $currentObject/Order_Customer) {\n TEXTBOX txtCustName (Label: 'Name', Attribute: Name)\n }\n}\n\n-- Over an association: a list widget shows the (to-many) collection\nLISTVIEW lvLines (DataSource: $currentObject/Order_OrderLine) {\n DYNAMICTEXT dtLine (Content: 'Line')\n}", SeeAlso: []string{"page.widgets", "page.create"}, }) @@ -259,8 +259,8 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "icon", "linkbutton", "link button", "nothing", "no action", "inert", "dead button", }, - Syntax: "Action: NOTHING -- deliberately no action (Forms$NoAction)\nAction: SAVE_CHANGES\nAction: SAVE_CHANGES CLOSE_PAGE -- save, then close the pop-up\nAction: CANCEL_CHANGES\nAction: CANCEL_CHANGES CLOSE_PAGE\nAction: CLOSE_PAGE\nAction: DELETE\nAction: DELETE CLOSE_PAGE\nAction: DELETE_OBJECT\nAction: NANOFLOW Module.NF\nAction: NANOFLOW Module.NF(Param: $val)\nAction: OPEN_LINK 'https://example.com'\nAction: OPEN_LINK $currentObject/URL -- address read from an attribute\nAction: SIGN_OUT\nAction: COMPLETE_TASK 'OutcomeName'\nAction: SHOW_PAGE Module.Page\nAction: SHOW_PAGE Module.Page(Param: $currentObject)\nAction: MICROFLOW Module.MF\nAction: MICROFLOW Module.MF(Param: $val)\nAction: CREATE_OBJECT Module.Entity THEN SHOW_PAGE Module.Page\n\nThe list above is exhaustive. Anything else in an action slot is an\nERROR (MDL-WIDGET28), including a real keyword short its argument --\n`Action: OPEN_LINK` without a URL, `Action: SHOW_PAGE` without a page.\nSuch a widget used to be written with NO action at all and rendered as a\ndead control, with check, exec and mxbuild all clean, because a\nno-action widget is legal Mendix (mendixlabs/mxcli#1062). Write NOTHING\nwhen a control really is meant to be inert.\n\nThe same forms serve `OnClick:` (an alias of `Action:`) and `OnChange:`.\n\nA microflow or nanoflow action is a CALL: it needs an argument for every\nparameter the flow declares, or Mendix rejects the page with CE1571. The\nargument list is the same on every widget that takes an action -- a\nCONTAINER (which is clickable) as much as an ACTIONBUTTON. An enclosing\ndata container of the right type supplies it without an argument; a data\ngrid's CONTROL BAR does not, because it is not row-scoped -- pass the\ngrid's selection there (`$dgOrders`).\n\nA SHOW_PAGE argument must be the enclosing widget's context object --\neither $currentObject or the name of the variable the enclosing data\nwidget is bound to. Mendix infers it from that widget, so naming any\nother variable is refused (MDL-PAGEARG01); call a microflow instead.\nOutside any data widget there is no context object to infer, so such a\nbutton takes NO argument at all -- not a page parameter, not\n$currentObject, not a literal. mxcli used to drop it in silence and\nmxbuild then reported CE1571 per parameter of the target page\n(mendixlabs/mxcli#1029). Route that navigation through a microflow.\n\nOPEN_LINK takes a web address, stored as a Forms$StaticOrDynamicString:\neither a literal, or $currentObject/Attr to read it from an attribute of\nthe enclosing data container's object at runtime (Studio Pro's \"Address:\nattribute\"). An address over an association path is not supported yet.\n\nButton styles: Default, Primary, Success, Info, Warning, Danger\n\nMendix has THREE icon elements and the keyword picks which one:\n\nIcon: 'Atlas_Core.Atlas_Filled.pencil' -- an icon collection\nIcon: image MyModule.Images.logo -- an IMAGE collection\nIcon: glyph 57377 -- a font code point\n\nThe bare form is the icon-collection icon and any collection in the\nproject works, third-party ones included. The image form points into a\ndifferent document, and is spelled the same way apart from the keyword\n-- write it without `image` and mxcli stores a custom-icon reference,\nwhich fails the build with CE1613 (mendixlabs/mxcli#1059).\n`mxcli check -p … --references` resolves each kind against its own\ncollection and names the remedy when the kind is wrong.\n\nA glyph carries a code and no name. Codes are sparse, and an undefined\none fails only at `mxbuild --target=deploy`, naming the PAGE rather\nthan the icon -- so MDL078 checks it against the font's own table.\nList them with `show glyphs`.\n\nA name may be quoted or bare; a hyphenated segment is double-quoted on\nits own: Atlas_Core.Atlas.\"align-center\".\n\nUse `linkbutton` instead of `actionbutton` for link render mode (same properties).", - Example: "ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)\nACTIONBUTTON btnEdit (Caption: 'Edit',\n Action: SHOW_PAGE Module.EditPage(Item: $currentObject))\nLINKBUTTON btnDelete (Caption: 'Delete', Action: DELETE,\n Icon: 'Atlas_Core.Atlas_Filled.pencil')\n\n-- A clickable CONTAINER in a data grid's control bar, calling a nanoflow\n-- with the grid's selection as its argument.\nDATAGRID dgOrders (DataSource: DATABASE FROM Sales.Order, Selection: Single) {\n COLUMN colNr (Attribute: Number, Caption: 'Order #')\n CONTROLBAR cb {\n CONTAINER cShip (Class: 'command',\n Action: NANOFLOW Sales.ACT_Ship($Order = $dgOrders)) {\n ACTIONBUTTON btnShip (Caption: 'Ship')\n }\n }\n}", + Syntax: "Action: NOTHING -- deliberately no action (Forms$NoAction)\nAction: SAVE_CHANGES\nAction: SAVE_CHANGES CLOSE_PAGE -- save, then close the pop-up\nAction: CANCEL_CHANGES\nAction: CANCEL_CHANGES CLOSE_PAGE\nAction: CLOSE_PAGE\nAction: DELETE\nAction: DELETE CLOSE_PAGE\nAction: DELETE_OBJECT\nAction: NANOFLOW Module.NF\nAction: NANOFLOW Module.NF(Param = $val)\nAction: OPEN_LINK 'https://example.com'\nAction: OPEN_LINK $currentObject/URL -- address read from an attribute\nAction: SIGN_OUT\nAction: COMPLETE_TASK 'OutcomeName'\nAction: SHOW_PAGE Module.Page\nAction: SHOW_PAGE Module.Page(Param = $currentObject)\nAction: MICROFLOW Module.MF\nAction: MICROFLOW Module.MF(Param = $val)\nAction: CREATE_OBJECT Module.Entity THEN SHOW_PAGE Module.Page\n\nThe list above is exhaustive. Anything else in an action slot is an\nERROR (MDL-WIDGET28), including a real keyword short its argument --\n`Action: OPEN_LINK` without a URL, `Action: SHOW_PAGE` without a page.\nSuch a widget used to be written with NO action at all and rendered as a\ndead control, with check, exec and mxbuild all clean, because a\nno-action widget is legal Mendix (mendixlabs/mxcli#1062). Write NOTHING\nwhen a control really is meant to be inert.\n\nThe same forms serve `OnClick:` (an alias of `Action:`) and `OnChange:`.\n\nA microflow or nanoflow action is a CALL: it needs an argument for every\nparameter the flow declares, or Mendix rejects the page with CE1571. The\nargument list is the same on every widget that takes an action -- a\nCONTAINER (which is clickable) as much as an ACTIONBUTTON. An enclosing\ndata container of the right type supplies it without an argument; a data\ngrid's CONTROL BAR does not, because it is not row-scoped -- pass the\ngrid's selection there (`$dgOrders`).\n\nA SHOW_PAGE argument must be the enclosing widget's context object --\neither $currentObject or the name of the variable the enclosing data\nwidget is bound to. Mendix infers it from that widget, so naming any\nother variable is refused (MDL-PAGEARG01); call a microflow instead.\nOutside any data widget there is no context object to infer, so such a\nbutton takes NO argument at all -- not a page parameter, not\n$currentObject, not a literal. mxcli used to drop it in silence and\nmxbuild then reported CE1571 per parameter of the target page\n(mendixlabs/mxcli#1029). Route that navigation through a microflow.\n\nOPEN_LINK takes a web address, stored as a Forms$StaticOrDynamicString:\neither a literal, or $currentObject/Attr to read it from an attribute of\nthe enclosing data container's object at runtime (Studio Pro's \"Address:\nattribute\"). An address over an association path is not supported yet.\n\nButton styles: Default, Primary, Success, Info, Warning, Danger\n\nMendix has THREE icon elements and the keyword picks which one:\n\nIcon: 'Atlas_Core.Atlas_Filled.pencil' -- an icon collection\nIcon: image MyModule.Images.logo -- an IMAGE collection\nIcon: glyph 57377 -- a font code point\n\nThe bare form is the icon-collection icon and any collection in the\nproject works, third-party ones included. The image form points into a\ndifferent document, and is spelled the same way apart from the keyword\n-- write it without `image` and mxcli stores a custom-icon reference,\nwhich fails the build with CE1613 (mendixlabs/mxcli#1059).\n`mxcli check -p … --references` resolves each kind against its own\ncollection and names the remedy when the kind is wrong.\n\nA glyph carries a code and no name. Codes are sparse, and an undefined\none fails only at `mxbuild --target=deploy`, naming the PAGE rather\nthan the icon -- so MDL078 checks it against the font's own table.\nList them with `show glyphs`.\n\nA name may be quoted or bare; a hyphenated segment is double-quoted on\nits own: Atlas_Core.Atlas.\"align-center\".\n\nUse `linkbutton` instead of `actionbutton` for link render mode (same properties).", + Example: "ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)\nACTIONBUTTON btnEdit (Caption: 'Edit',\n Action: SHOW_PAGE Module.EditPage(Item = $currentObject))\nLINKBUTTON btnDelete (Caption: 'Delete', Action: DELETE,\n Icon: 'Atlas_Core.Atlas_Filled.pencil')\n\n-- A clickable CONTAINER in a data grid's control bar, calling a nanoflow\n-- with the grid's selection as its argument.\nDATAGRID dgOrders (DataSource: DATABASE FROM Sales.Order, Selection: Single) {\n COLUMN colNr (Attribute: Number, Caption: 'Order #')\n CONTROLBAR cb {\n CONTAINER cShip (Class: 'command',\n Action: NANOFLOW Sales.ACT_Ship(Order = $dgOrders)) {\n ACTIONBUTTON btnShip (Caption: 'Ship')\n }\n }\n}", SeeAlso: []string{"page.widgets"}, }) diff --git a/cmd/mxcli/syntax/features_workflow.go b/cmd/mxcli/syntax/features_workflow.go index 14223800cc..d14dbb25a0 100644 --- a/cmd/mxcli/syntax/features_workflow.go +++ b/cmd/mxcli/syntax/features_workflow.go @@ -262,11 +262,14 @@ func init() { "call microflow", "microflow task", "automated step", "system task", }, - // The WITH values are QUOTED — the grammar takes a string literal there, - // not a bare variable. Omitting the clause from this entry is how an - // author ends up writing the unquoted form (ako/mxcli#1023). - Syntax: "CALL MICROFLOW Module.MF [AS ] [COMMENT '']\n [WITH ( = '', ...)]\n [OUTCOMES '' -> { } ...];", - Example: "CALL MICROFLOW HR.SendNotification\n COMMENT 'Notify manager';\n\n-- Parameter values are quoted, and named by their BARE parameter name:\nCALL MICROFLOW HR.Escalate AS callMicroflow1\n WITH (Request = '$WorkflowContext');", + // R4 (ako/mxcli#751): arguments are bound as at every call site, right + // after the callee, the expression bare. The older `WITH (P = '')` + // after the comment is the deprecated alias MDL-DEPR008. + Syntax: "CALL MICROFLOW Module.MF[( = , ...)] [AS ] [COMMENT '']\n" + + " [OUTCOMES '' -> { } ...];\n\n" + + "-- WITH ( = ''), the expression in a string, is the\n" + + "-- deprecated spelling of the argument list (MDL-DEPR008).", + Example: "CALL MICROFLOW HR.SendNotification\n COMMENT 'Notify manager';\n\n-- Arguments are named by their BARE parameter name:\nCALL MICROFLOW HR.Escalate(Request = $WorkflowContext) AS callMicroflow1;", SeeAlso: []string{"workflow.create", "workflow.call-workflow", "workflow.ai-agent-task"}, }) @@ -280,8 +283,7 @@ func init() { // Measured on mxbuild 11.13.0 against the same activity written as a call // microflow: the only difference in what builds is CE1590 for a microflow // with no parameters. - Syntax: "CALL AGENT MICROFLOW Module.MF [AS ] [COMMENT '']\n" + - " [WITH ( = '', ...)]\n" + + Syntax: "CALL AGENT MICROFLOW Module.MF[( = , ...)] [AS ] [COMMENT '']\n" + " [OUTCOMES -> { } ...]\n" + " [BOUNDARY EVENT ...];\n\n" + "-- The same statement as CALL MICROFLOW, stored as an AI agent task. The microflow\n" + @@ -290,8 +292,8 @@ func init() { "-- branch on the agent's answer with OUTCOMES. Needs Mendix 11.9+.", Example: "CREATE MICROFLOW HR.ACT_ClassifyRequest ($Request: HR.LeaveRequest)\n" + "RETURNS Boolean AS $Urgent\nBEGIN\n -- call the agent here\n RETURN false;\nEND;\n\n" + - "CALL AGENT MICROFLOW HR.ACT_ClassifyRequest AS aiAgentTask1 COMMENT 'Classify the request'\n" + - " WITH (Request = '$WorkflowContext')\n" + + "CALL AGENT MICROFLOW HR.ACT_ClassifyRequest(Request = $WorkflowContext) AS aiAgentTask1\n" + + " COMMENT 'Classify the request'\n" + " OUTCOMES true -> { USER TASK Expedite 'Expedite' PAGE HR.TaskPage OUTCOMES 'Done' { }; }\n" + " false -> { };", SeeAlso: []string{"workflow.call-microflow", "agents"}, @@ -303,8 +305,8 @@ func init() { Keywords: []string{ "call workflow", "sub-workflow", "nested workflow", }, - Syntax: "CALL WORKFLOW Module.WF [AS ] [COMMENT '']\n [WITH ( = '', ...)];", - Example: "CALL WORKFLOW HR.SubApproval COMMENT 'Delegate to sub-process';\n\n-- Parameter values are quoted:\nCALL WORKFLOW HR.SubApproval AS callWf1\n WITH (Request = '$WorkflowContext');", + Syntax: "CALL WORKFLOW Module.WF[( = , ...)] [AS ] [COMMENT ''];", + Example: "CALL WORKFLOW HR.SubApproval COMMENT 'Delegate to sub-process';\n\nCALL WORKFLOW HR.SubApproval(Request = $WorkflowContext) AS callWf1;", SeeAlso: []string{"workflow.create", "workflow.call-microflow"}, }) diff --git a/docs-site/src/appendixes/quick-reference.md b/docs-site/src/appendixes/quick-reference.md index 5747f21e2c..27a6994389 100644 --- a/docs-site/src/appendixes/quick-reference.md +++ b/docs-site/src/appendixes/quick-reference.md @@ -156,7 +156,7 @@ AUTHENTICATION Basic, Session | Retrieve (Assoc) | `RETRIEVE $List FROM $Parent/Module.AssocName;` | Retrieve by association | | Call microflow | `$Result = CALL MICROFLOW Module.Name (Param = $value);` | | | Call nanoflow | `$Result = CALL NANOFLOW Module.Name (Param = $value);` | | -| Show page | `LIST PAGE Module.PageName ($Param = $value);` | Also accepts `(Param: $value)` | +| Show page | `SHOW PAGE Module.PageName (Param = $value);` | `($Param = …)` and `(Param: …)` are deprecated | | Close page | `CLOSE PAGE;` | | | Validation | `VALIDATION FEEDBACK $Entity/Attribute MESSAGE 'message';` | Requires attribute path + MESSAGE | | Log | `LOG INFO\|WARNING\|ERROR [NODE 'name'] 'message';` | | @@ -354,7 +354,7 @@ MDL uses explicit property declarations for pages: | Attribute binding | `Attribute: AttrName` | `TEXTBOX txt (Label: 'Name', Attribute: Name)` | | Variable binding | `DataSource: $Var` | `DATAVIEW dv (DataSource: $Product) { ... }` | | Action binding | `Action: TYPE` | `ACTIONBUTTON btn (Caption: 'Save', Action: SAVE_CHANGES)` | -| Microflow action | `Action: MICROFLOW Name(Param: val)` | `Action: MICROFLOW Mod.ACT_Process(Order: $Order)` | +| Microflow action | `Action: MICROFLOW Name(Param = val)` | `Action: MICROFLOW Mod.ACT_Process(Order = $Order)` | | Database source | `DataSource: DATABASE Entity` | `DATAGRID dg (DataSource: DATABASE Module.Entity)` | | Selection binding | `DataSource: SELECTION widget` | `DATAVIEW dv (DataSource: SELECTION galleryList)` | | CSS class | `Class: 'classes'` | `CONTAINER c (Class: 'card mx-spacing-top-large')` | diff --git a/docs-site/src/examples/crm-module.md b/docs-site/src/examples/crm-module.md index ca5980c00a..00a1c1cdee 100644 --- a/docs-site/src/examples/crm-module.md +++ b/docs-site/src/examples/crm-module.md @@ -96,7 +96,7 @@ END; CREATE MICROFLOW CRM.ACT_Customer_Save ($Customer: CRM.Customer) RETURNS Boolean AS $IsValid BEGIN - $IsValid = CALL MICROFLOW CRM.VAL_Customer($param = $Customer); + $IsValid = CALL MICROFLOW CRM.VAL_Customer(param = $Customer); IF $IsValid THEN COMMIT $Customer; diff --git a/docs-site/src/examples/validation.md b/docs-site/src/examples/validation.md index 91f952aa0c..779e188227 100644 --- a/docs-site/src/examples/validation.md +++ b/docs-site/src/examples/validation.md @@ -54,7 +54,7 @@ The action microflow calls validation, and only saves if it passes: CREATE MICROFLOW Sales.ACT_Order_Save ($Order: Sales.Order) RETURNS Boolean AS $IsValid BEGIN - $IsValid = CALL MICROFLOW Sales.VAL_Order($param = $Order); + $IsValid = CALL MICROFLOW Sales.VAL_Order(param = $Order); IF $IsValid THEN COMMIT $Order; diff --git a/docs-site/src/language/activity-types.md b/docs-site/src/language/activity-types.md index 9ea237b1d7..73cc4695b3 100644 --- a/docs-site/src/language/activity-types.md +++ b/docs-site/src/language/activity-types.md @@ -171,23 +171,20 @@ Java action parameters follow the same `Name = value` syntax. Opens a page, passing parameters: ```sql -SHOW PAGE Sales.Order_Edit ($Order = $Order); +SHOW PAGE Sales.Order_Edit (Order = $Order); ``` -The parameter syntax uses `$PageParam = $MicroflowVar`. Multiple parameters are comma-separated: +The parameter syntax is `PageParam = $MicroflowVar`, with no `$` on the page's parameter name. Multiple parameters are comma-separated: ```sql SHOW PAGE Sales.OrderDetail ( - $Order = $Order, - $Customer = $Customer + Order = $Order, + Customer = $Customer ); ``` -An alternate syntax using colon notation is also supported: - -```sql -SHOW PAGE Sales.Order_Edit (Order: $Order); -``` +The older spellings `($Order = $Order)` and `(Order: $Order)` still parse but are +deprecated (MDL-DEPR006, MDL-DEPR007); `mxcli fmt --upgrade` rewrites them. ### CLOSE PAGE @@ -246,8 +243,7 @@ $Result = EXECUTE DATABASE QUERY MyModule.MyConn.SearchQuery DYNAMIC 'SELECT * FROM customers WHERE name LIKE ?'; -- With parameters -$Result = EXECUTE DATABASE QUERY MyModule.MyConn.GetByEmail - PARAMETERS ($EmailParam = $Email); +$Result = EXECUTE DATABASE QUERY MyModule.MyConn.GetByEmail (EmailParam = $Email); -- With runtime connection override $Result = EXECUTE DATABASE QUERY MyModule.MyConn.GetData @@ -272,7 +268,7 @@ The query name follows a three-part naming convention: `Module.ConnectionName.Qu | Call microflow | `$Var = CALL MICROFLOW Module.Name (Param = $val);` | Any type | | Call nanoflow | `$Var = CALL NANOFLOW Module.Name (Param = $val);` | Any type | | Call Java action | `$Var = CALL JAVA ACTION Module.Name (Param = val);` | Any type | -| Show page | `SHOW PAGE Module.Page ($Param = $val);` | -- | +| Show page | `SHOW PAGE Module.Page (Param = $val);` | -- | | Close page | `CLOSE PAGE;` | -- | | Validation | `VALIDATION FEEDBACK $Var/Attr MESSAGE 'msg';` | -- | | Log | `LOG INFO\|WARNING\|ERROR [NODE 'name'] 'msg';` | -- | diff --git a/docs-site/src/language/data-binding.md b/docs-site/src/language/data-binding.md index cb24095636..2200aaa89a 100644 --- a/docs-site/src/language/data-binding.md +++ b/docs-site/src/language/data-binding.md @@ -144,7 +144,7 @@ Action buttons can pass the current data context to microflows and pages: DATAVIEW dvOrder (DataSource: $Order) { ACTIONBUTTON btnProcess ( Caption: 'Process Order', - Action: MICROFLOW Sales.ACT_ProcessOrder(Order: $Order), + Action: MICROFLOW Sales.ACT_ProcessOrder(Order = $Order), ButtonStyle: Primary ) diff --git a/docs-site/src/language/microflow-patterns.md b/docs-site/src/language/microflow-patterns.md index 469041b489..26b2fccb92 100644 --- a/docs-site/src/language/microflow-patterns.md +++ b/docs-site/src/language/microflow-patterns.md @@ -22,7 +22,7 @@ BEGIN ); COMMIT $Customer; - SHOW PAGE Sales.Customer_Edit ($Customer = $Customer); + SHOW PAGE Sales.Customer_Edit (Customer = $Customer); RETURN $Customer; END; ``` diff --git a/docs-site/src/language/microflows.md b/docs-site/src/language/microflows.md index 8e516f9b3b..347dacb028 100644 --- a/docs-site/src/language/microflows.md +++ b/docs-site/src/language/microflows.md @@ -171,7 +171,7 @@ BEGIN Status = 'Draft' ); COMMIT $Order; - SHOW PAGE Sales.Order_Edit ($Order = $Order); + SHOW PAGE Sales.Order_Edit (Order = $Order); RETURN $Order; END; ``` diff --git a/docs-site/src/language/nanoflows.md b/docs-site/src/language/nanoflows.md index 672e88fa18..4dfa53c9ad 100644 --- a/docs-site/src/language/nanoflows.md +++ b/docs-site/src/language/nanoflows.md @@ -79,7 +79,7 @@ $HasNetwork = CALL JAVASCRIPT ACTION NanoflowCommons.HasConnectivity(); ```sql -- Show a page -SHOW PAGE Sales.CartDetail ($Cart = $Cart); +SHOW PAGE Sales.CartDetail (Cart = $Cart); -- Close the current page CLOSE PAGE; @@ -104,7 +104,7 @@ IF $Cart/ItemCount = 0 THEN VALIDATION FEEDBACK $Cart/ItemCount MESSAGE 'Cart is empty'; RETURN false; ELSE - SHOW PAGE Sales.Checkout ($Cart = $Cart); + SHOW PAGE Sales.Checkout (Cart = $Cart); RETURN true; END IF; ``` @@ -184,7 +184,7 @@ CREATE NANOFLOW Sales.NAV_GoToOrderDetail BEGIN DECLARE $Order Sales.Order; - SHOW PAGE Sales.Order_Detail ($Order = $Order); + SHOW PAGE Sales.Order_Detail (Order = $Order); END; ``` diff --git a/docs-site/src/language/page-patterns.md b/docs-site/src/language/page-patterns.md index 89be951553..639c768e77 100644 --- a/docs-site/src/language/page-patterns.md +++ b/docs-site/src/language/page-patterns.md @@ -41,7 +41,7 @@ BEGIN $Customer = CREATE MyModule.Customer ( IsActive = true ); - SHOW PAGE MyModule.Customer_Edit ($Customer = $Customer); + SHOW PAGE MyModule.Customer_Edit (Customer = $Customer); RETURN $Customer; END; ``` @@ -273,7 +273,7 @@ BEGIN $Employee = CREATE HR.Employee ( HireDate = [%CurrentDateTime%] ); - SHOW PAGE HR.Employee_Edit ($Employee = $Employee); + SHOW PAGE HR.Employee_Edit (Employee = $Employee); RETURN $Employee; END; ``` diff --git a/docs-site/src/language/qualified-names.md b/docs-site/src/language/qualified-names.md index 326f2fe56c..29f633749e 100644 --- a/docs-site/src/language/qualified-names.md +++ b/docs-site/src/language/qualified-names.md @@ -83,7 +83,7 @@ CREATE ASSOCIATION Sales.Order_Customer Status: Enumeration(Sales.OrderStatus) DEFAULT 'Active' -- Microflow call -$Result = CALL MICROFLOW Sales.ACT_ProcessOrder ($Order = $Order); +$Result = CALL MICROFLOW Sales.ACT_ProcessOrder (Order = $Order); ``` ## See Also diff --git a/docs-site/src/language/widget-types.md b/docs-site/src/language/widget-types.md index 8e13a2248e..354e84cd02 100644 --- a/docs-site/src/language/widget-types.md +++ b/docs-site/src/language/widget-types.md @@ -364,7 +364,7 @@ ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger) ```sql ACTIONBUTTON btnProcess ( Caption: 'Process', - Action: MICROFLOW Sales.ACT_ProcessOrder(Order: $Order), + Action: MICROFLOW Sales.ACT_ProcessOrder(Order = $Order), ButtonStyle: Primary ) ``` diff --git a/docs-site/src/language/workflow-activities.md b/docs-site/src/language/workflow-activities.md index bc5b3f30f8..bc2e58e63b 100644 --- a/docs-site/src/language/workflow-activities.md +++ b/docs-site/src/language/workflow-activities.md @@ -83,7 +83,7 @@ builds and never completes. Execute a microflow as part of the workflow. Optionally specify a comment and outcomes: ```sql -CALL MICROFLOW . [COMMENT ''] +CALL MICROFLOW . [( = , ...)] [COMMENT ''] [OUTCOMES '' { } ...]; ``` @@ -91,8 +91,15 @@ Example: ```sql CALL MICROFLOW HR.ACT_SendNotification COMMENT 'Notify the applicant'; +CALL MICROFLOW HR.ACT_Escalate(Request = $WorkflowContext) COMMENT 'Escalate'; ``` +Arguments are bound like every other call in MDL: `Param = expression` right +after the callee, the expression written bare. The older spelling after the +comment, `WITH (Param = '')` with the expression inside a string, +still parses with the same meaning but is deprecated (MDL-DEPR008); +`mxcli fmt --upgrade` rewrites it. + ## AI Agent Task A step that runs an AI agent (Mendix 11.9 or later). It is written like `CALL @@ -101,16 +108,15 @@ mappings, outcomes and boundary events. The microflow is where the agent is invoked — build agents with the Studio Pro Agent Editor, or `CREATE AGENT`. ```sql -CALL AGENT MICROFLOW . [AS ] [COMMENT ''] - [WITH ( = '', ...)] +CALL AGENT MICROFLOW . [( = , ...)] [AS ] [COMMENT ''] [OUTCOMES -> { } ...]; ``` Example — branch on the agent's answer: ```sql -CALL AGENT MICROFLOW HR.ACT_ClassifyRequest AS aiAgentTask1 COMMENT 'Classify the request' - WITH (Request = '$WorkflowContext') +CALL AGENT MICROFLOW HR.ACT_ClassifyRequest(Request = $WorkflowContext) AS aiAgentTask1 + COMMENT 'Classify the request' OUTCOMES true -> { USER TASK Expedite 'Expedite the request' PAGE HR.TaskPage OUTCOMES 'Done' { }; } false -> { }; @@ -125,7 +131,7 @@ branch with `OUTCOMES`; return nothing for a single path. Start a sub-workflow: ```sql -CALL WORKFLOW . [COMMENT '']; +CALL WORKFLOW . [( = , ...)] [COMMENT '']; ``` Example: diff --git a/docs-site/src/migration/validation.md b/docs-site/src/migration/validation.md index 0c173d2ac8..efeddfae87 100644 --- a/docs-site/src/migration/validation.md +++ b/docs-site/src/migration/validation.md @@ -65,7 +65,7 @@ $Line = CREATE Sales.OrderLine (Price = 10.00, Quantity = 3); CHANGE $Line (Sales.OrderLine_Order = $Order); COMMIT $Line; -CALL MICROFLOW Sales.ACT_Order_CalculateTotal ($Order = $Order); +CALL MICROFLOW Sales.ACT_Order_CalculateTotal (Order = $Order); -- @assert: $Order/TotalAmount = 30.00 ``` diff --git a/docs-site/src/reference/microflow/create-microflow.md b/docs-site/src/reference/microflow/create-microflow.md index 24d4e4be43..ec6a2c1dff 100644 --- a/docs-site/src/reference/microflow/create-microflow.md +++ b/docs-site/src/reference/microflow/create-microflow.md @@ -86,7 +86,7 @@ Call another microflow, nanoflow, or Java action. Parameters are passed by name. **UI Actions** ```sql -SHOW PAGE Module.PageName ( $Param = $value ); +SHOW PAGE Module.PageName ( Param = $value ); CLOSE PAGE; ``` @@ -199,7 +199,7 @@ BEGIN Status = 'Draft' ); COMMIT $Order; - SHOW PAGE Sales.Order_Edit ($Order = $Order); + SHOW PAGE Sales.Order_Edit (Order = $Order); RETURN $Order; END; ``` diff --git a/docs-site/src/reference/microflow/create-nanoflow.md b/docs-site/src/reference/microflow/create-nanoflow.md index 99d5666fc9..e92d8a0333 100644 --- a/docs-site/src/reference/microflow/create-nanoflow.md +++ b/docs-site/src/reference/microflow/create-nanoflow.md @@ -101,7 +101,7 @@ CREATE NANOFLOW Sales.NAV_SubmitOrder BEGIN CHANGE $Order (Status = 'Submitted'); $Result = CALL MICROFLOW Sales.ACT_ProcessOrder (Order = $Order); - SHOW PAGE Sales.Order_Confirmation ($Order = $Order); + SHOW PAGE Sales.Order_Confirmation (Order = $Order); END; ``` diff --git a/docs-site/src/reference/page/create-page.md b/docs-site/src/reference/page/create-page.md index 6c27500945..406d048627 100644 --- a/docs-site/src/reference/page/create-page.md +++ b/docs-site/src/reference/page/create-page.md @@ -121,8 +121,8 @@ values, including the argument list. |--------|--------|-------------| | Save | `Action: SAVE_CHANGES` | Commits and closes | | Cancel | `Action: CANCEL_CHANGES` | Rolls back and closes | -| Microflow | `Action: MICROFLOW Module.Name(Param: val)` | Calls a microflow | -| Nanoflow | `Action: NANOFLOW Module.Name(Param: val)` | Calls a nanoflow | +| Microflow | `Action: MICROFLOW Module.Name(Param = val)` | Calls a microflow | +| Nanoflow | `Action: NANOFLOW Module.Name(Param = val)` | Calls a nanoflow | | Page | `Action: PAGE Module.PageName` | Opens a page | | Close | `Action: CLOSE_PAGE` | Closes the current page | | Delete | `Action: DELETE` | Deletes the context object | @@ -150,7 +150,7 @@ DATAGRID dgOrders (DataSource: DATABASE FROM Sales.Order, Selection: Single) { COLUMN colNr (Attribute: Number, Caption: 'Order #') CONTROLBAR cb { CONTAINER cShip (Class: 'command', - Action: NANOFLOW Sales.ACT_Ship($Order = $dgOrders)) { + Action: NANOFLOW Sales.ACT_Ship(Order = $dgOrders)) { ACTIONBUTTON btnShip (Caption: 'Ship') } } diff --git a/docs-site/src/reference/query/describe-microflow.md b/docs-site/src/reference/query/describe-microflow.md index 955e7c663c..75b72d2e61 100644 --- a/docs-site/src/reference/query/describe-microflow.md +++ b/docs-site/src/reference/query/describe-microflow.md @@ -37,7 +37,7 @@ BEGIN Status = 'Draft' ); COMMIT $Order; - SHOW PAGE Sales.Order_Edit ($Order = $Order); + SHOW PAGE Sales.Order_Edit (Order = $Order); RETURN $Order; END; ``` diff --git a/docs-site/src/tutorial/create-page.md b/docs-site/src/tutorial/create-page.md index ced0ca6078..8c5d6cd352 100644 --- a/docs-site/src/tutorial/create-page.md +++ b/docs-site/src/tutorial/create-page.md @@ -133,7 +133,7 @@ DYNAMICTEXT dynName (Attribute: Name) ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger) -ACTIONBUTTON btnProcess (Caption: 'Process', Action: MICROFLOW MyModule.ACT_ProcessProduct(Product: $Product)) +ACTIONBUTTON btnProcess (Caption: 'Process', Action: MICROFLOW MyModule.ACT_ProcessProduct(Product = $Product)) ``` ### Layout widgets diff --git a/docs/01-project/MDL_QUICK_REFERENCE.md b/docs/01-project/MDL_QUICK_REFERENCE.md index f9add0af03..5172417989 100644 --- a/docs/01-project/MDL_QUICK_REFERENCE.md +++ b/docs/01-project/MDL_QUICK_REFERENCE.md @@ -556,13 +556,13 @@ it is for pages. | REST call (mapping single) | `$Var = rest call get '' returns mapping Module.IMM as Module.Entity;` | Single object — Studio Pro emits `ForceSingleOccurrence=true` | | REST call (mapping list) | `$Var = rest call get '' returns mapping Module.IMM as list of Module.Entity;` | List result | | REST call (none) | `rest call get '' returns nothing;` | Discard response | -| Show page | `show page Module.PageName ($Param = $value);` | Also accepts `(Param: $value)` | +| Show page | `show page Module.PageName (Param = $value);` | `Param = expression`, as at every call site. `($Param = …)` and `(Param: …)` are deprecated (MDL-DEPR006/007) | | Close page | `close page;` | | | Download file | `download file $FileDocument [show in browser];` | Streams a `System.FileDocument` | -| Show message | `show message 'text' [type Information\|Warning\|Error] [objects [$a, $b]] [blocking];` | `blocking` halts the client until the user dismisses it — Studio Pro's checkbox. It goes after `objects` and before `on error`. Without it, a describe → exec round trip turned a blocking message into a non-blocking one (16 microflows measured) | +| Show message | `show message 'text' [type Information\|Warning\|Error] [with ({1} = $a, {2} = $b)] [blocking];` | `blocking` halts the client until the user dismisses it — Studio Pro's checkbox. It goes after the `with` list and before `on error`. `with ({1} = $a, {2} = $b)` is the deprecated spelling of the list (MDL-DEPR009). Without it, a describe → exec round trip turned a blocking message into a non-blocking one (16 microflows measured) | | Database connection credentials | `connection string @Mod.Const`, `username @Mod.Const`, `password @Mod.Const` | Constant **references** only. A literal writes an unopenable project — MDL058 | | Synchronize (nanoflow only) | `synchronize all;` / `synchronize unsynchronized;` / `synchronize $Obj, $List;` | Offline sync. `unsynchronized` needs Mendix 9.4+. In a microflow this is MDL057 / CE0009 | -| Validation | `validation feedback $entity/attribute message 'message';` | Requires attribute path + MESSAGE | +| Validation | `validation feedback $entity/attribute message 'message {1}' [with ({1} = $a)];` | Requires attribute path + MESSAGE | | Log | `log info\|warning\|error [node 'name'] 'message';` | | | Apply entity access | `@applyentityaccess` / `@applyentityaccess(false)` before `create microflow` or `create rule` | Runs the flow under the **current user's** entity access rules instead of with full access. A **security** setting and only ever narrowing, so an ABSENT annotation **preserves** what is stored rather than clearing it — the same rule as `@excluded`. Not available on a nanoflow: it runs in the client and Mendix stores no such property | | Position | `@position(x, y)` | Canvas position (before activity) | @@ -714,9 +714,9 @@ mandatory and a misplaced clause failed with a token error - A **targeting microflow** takes exactly `System.Workflow` + the context entity (or a generalization of it), in either order — anything else is CE6677. Users targeting returns a list of `System.User`, groups a list of `System.WorkflowGroup`. - An **on-created microflow** takes exactly `System.WorkflowUserTask` + the context entity, in either order (CE6683), and returns nothing (CE5012). - `check --references` reports these before anything is written; `exec` refuses the workflow statement itself (Mendix 11+). -- `call microflow Mod.MF [as ] [comment ''] [with ( = '', ...)] [outcomes '' -> { } ...];` +- `call microflow Mod.MF[( = , ...)] [as ] [comment ''] [outcomes '' -> { } ...];` - `call agent microflow Mod.MF [as ] [comment ''] [with ( = '', ...)] [outcomes … -> { } ...];` — an **AI agent task** (Mendix 11.9+): the call-microflow statement stored as `Workflows$AIAgentTaskActivity`. Its microflow must take at least one parameter (CE1590). -- `call workflow Mod.WF [as ] [comment ''] [with ( = '', ...)];` +- `call workflow Mod.WF[( = , ...)] [as ] [comment ''];` - `decision [] [''] outcomes -> { } ...;` - `parallel split [] path 1 { } path 2 { };` - `jump to ;` @@ -766,8 +766,10 @@ the build fails `CE6686`). Anything shorter is refused as `MDL-WF03`, and by module — is not a build error but a `StorageLoadException` that leaves the project unopenable in Studio Pro and mxbuild. -**Parameter values in `with (...)` are quoted strings**, not bare variables: -`call microflow Mod.MF with (Request = '$WorkflowContext')`. +**Arguments go right after the callee**, bound as at every call site with a bare +expression: `call microflow Mod.MF(Request = $WorkflowContext)`. The older +`with (Request = '$WorkflowContext')`, the expression in a string, is a deprecated +alias with the same meaning (MDL-DEPR008). **An enumeration decision also needs an empty outcome.** Mendix generates one outcome per enumeration value **plus one for the empty value**, and MxBuild @@ -1455,11 +1457,11 @@ MDL uses explicit property declarations for pages: | Variable binding | `datasource: $Var` | `dataview dv (datasource: $Product) { ... }` | | Action binding | `action: type` | `actionbutton btn (caption: 'Save', action: save_changes)` — the forms are a closed set (`mxcli syntax page.action`); anything else is **MDL-WIDGET28** | | No action | `action: nothing` | `actionbutton btn (caption: 'Decorative', action: nothing)` — an explicitly inert control. Write it deliberately: an action keyword **short its argument** (`action: open_link` with no URL) is now an error rather than a widget silently written with no action at all | -| Microflow action | `action: microflow Name(Param: val)` | `action: microflow Mod.ACT_Process(Order: $Order)` | +| Microflow action | `action: microflow Name(Param = val)` | `action: microflow Mod.ACT_Process(Order = $Order)` | | Button icon | `icon: 'Module.IconCollection.IconName'` | `linkbutton btn (caption: 'Edit', action: nothing, icon: 'Atlas_Core.Atlas_Filled.pencil')` — the **icon-collection** icon; MxBuild rejects an unknown name (CE1613) | | Image icon | `icon: image Module.ImageCollection.Name` | `actionbutton btn (caption: 'Logo', action: nothing, icon: image MyMod.Images.logo)` — an **image** collection is a different document from an icon collection, and the names are spelled the same, so the keyword is what separates them. Written without `image` it is stored as a custom-icon reference and the build fails **CE1613** | | Glyph icon | `icon: glyph ` | `actionbutton btn (caption: 'Home', action: nothing, icon: glyph 57377)` — a font code point with no name. Codes are sparse; an undefined one fails only at `mxbuild --target=deploy`, naming the **page**, so **MDL078** checks it. Browse with `show glyphs` | -| Clickable container | `onclick: action` (alias of `action:`) | `container card (onclick: microflow Mod.ACT_Open) { ... }` — takes an argument list like a button: `action: nanoflow Mod.ACT_Ship($Order = $dgOrders)` | +| Clickable container | `onclick: action` (alias of `action:`) | `container card (onclick: microflow Mod.ACT_Open) { ... }` — takes an argument list like a button: `action: nanoflow Mod.ACT_Ship(Order = $dgOrders)` | | Action arguments | every parameter needs one | A flow action with an unfilled parameter is **CE1571**. An enclosing data container of its type supplies it; a data grid's **control bar** does not (not row-scoped) — pass the grid's selection, `$dgOrders` | | Database source | `datasource: database entity` | `datagrid dg (datasource: database Module.Entity)` | | Database source, constrained and sorted | `datasource: database entity where [...] sort by Attr asc` | `listview lv (datasource: database from Mod.Vehicle where [Brand != ''] sort by Brand asc)` | diff --git a/mdl/deprecation/deprecation.go b/mdl/deprecation/deprecation.go index 7ae080b8bd..992d405cf7 100644 --- a/mdl/deprecation/deprecation.go +++ b/mdl/deprecation/deprecation.go @@ -99,6 +99,20 @@ const ( // name for: a layout grid's rows and columns, a data grid's columns and // control bar, a gallery's template and filter (R12, ako/mxcli#749). UnstoredWidgetName = "MDL-DEPR005" + // DollarArgumentName is `$Param = expr` at a call site: the parameter + // named with the `$` of a variable (R4, ako/mxcli#751). + DollarArgumentName = "MDL-DEPR006" + // ColonArgument is `Param: expr` at a call site (`show page`, a page + // action or data source): `:` sets a model property, `=` binds a value + // (R3/R4, ako/mxcli#751). + ColonArgument = "MDL-DEPR007" + // WorkflowStringArgument is a workflow call's `with (Param = '')`: + // the argument expression written inside a string (R4, ako/mxcli#751). + WorkflowStringArgument = "MDL-DEPR008" + // PositionalTemplateArguments is `objects [a, b]` / `parameters [a, b]` + // on a text template: the placeholders bound by position (R4, + // ako/mxcli#751). + PositionalTemplateArguments = "MDL-DEPR009" ) // entries is the registry. Append only: a code is never reused or renumbered, @@ -168,6 +182,55 @@ var entries = []Entry{ Example: "create page M.P (Title: 'P', Layout: Atlas_Core.Atlas_Default) { datagrid dg (DataSource: database from M.E) { column Name (Attribute: Name) } };", CanonicalExample: "create page M.P (Title: 'P', Layout: Atlas_Core.Atlas_Default) { datagrid dg (DataSource: database from M.E) { column (Attribute: Name) } };", }, + { + Code: DollarArgumentName, + Old: "call microflow M.F($Param = expr)", + Canonical: "call microflow M.F(Param = expr)", + Rewrite: Rewrite{Structural: "parameter name without its `$` (quoted when it is not an identifier or keyword)"}, + RemovedIn: 2, + Note: "Every call site binds an argument as `Param = expression` (R4): call microflow, nanoflow, java " + + "action, javascript action, external action, web service operation, execute database query, " + + "send rest request, show page, and page/button actions and data sources.", + Example: "create microflow M.F ($O: M.E) begin call microflow M.G($Order = $O); end;", + CanonicalExample: "create microflow M.F ($O: M.E) begin call microflow M.G(Order = $O); end;", + }, + { + Code: ColonArgument, + Old: "show page M.P(Param: expr)", + Canonical: "show page M.P(Param = expr)", + Rewrite: Rewrite{Structural: "colon as `=`: `Param: expr` -> `Param = expr`"}, + RemovedIn: 2, + Note: "`:` sets a model property and `=` binds a runtime value (R3). An argument binds a value, so " + + "it takes `=` wherever the call appears: show page, and page/button actions and data sources " + + "(`Action: microflow M.F(Param = expr)`).", + Example: "create microflow M.F ($O: M.E) begin show page M.P(Order: $O); end;", + CanonicalExample: "create microflow M.F ($O: M.E) begin show page M.P(Order = $O); end;", + }, + { + Code: WorkflowStringArgument, + Old: "call microflow M.F with (Param = '')", + Canonical: "call microflow M.F(Param = )", + Rewrite: Rewrite{Structural: "string list as a list after the callee, each string's content written as the bare expression"}, + RemovedIn: 2, + Note: "In a workflow. The string form keeps its meaning — its content is the expression — so it is an alias, not a " + + "change of meaning. A string whose content does not parse as an MDL expression is left in place " + + "and reported by fmt --upgrade.", + Example: "create workflow M.W parameter $WorkflowContext: M.E begin " + + "call microflow M.F with (Order = '$WorkflowContext'); end workflow;", + CanonicalExample: "create workflow M.W parameter $WorkflowContext: M.E begin " + + "call microflow M.F(Order = $WorkflowContext); end workflow;", + }, + { + Code: PositionalTemplateArguments, + Old: "objects [$a, $b] / parameters ['a', 'b']", + Canonical: "with ({1} = $a, {2} = $b)", + Rewrite: Rewrite{Structural: "positional list as numbered placeholders: `objects [a, b]` -> `with ({1} = a, {2} = b)`"}, + RemovedIn: 2, + Note: "One text-template form everywhere: show message, validation feedback, log, and REST " + + "url and body templates.", + Example: "create microflow M.F ($N: String) begin show message 'Hi {1}' type Information objects [$N]; end;", + CanonicalExample: "create microflow M.F ($N: String) begin show message 'Hi {1}' type Information with ({1} = $N); end;", + }, } // All returns every registered entry, in code order. diff --git a/mdl/executor/cmd_microflows_format_action.go b/mdl/executor/cmd_microflows_format_action.go index b7600d7e35..74d7efd38b 100644 --- a/mdl/executor/cmd_microflows_format_action.go +++ b/mdl/executor/cmd_microflows_format_action.go @@ -777,7 +777,7 @@ func formatAction( // Extract just the parameter name from the qualified name parts := strings.Split(pm.Parameter, ".") paramName := parts[len(parts)-1] - params = append(params, fmt.Sprintf("$%s = %s", paramName, describeExpr(pm.Argument))) + params = append(params, fmt.Sprintf("%s = %s", paramName, describeExpr(pm.Argument))) } // Build the statement @@ -807,11 +807,7 @@ func formatAction( } result := fmt.Sprintf("show message %s type %s", message, msgType) if len(a.TemplateParameters) > 0 { - objs := make([]string, len(a.TemplateParameters)) - for i, p := range a.TemplateParameters { - objs[i] = describeExpr(p) - } - result += " objects [" + strings.Join(objs, ", ") + "]" + result += templateArgsClause(a.TemplateParameters) } // Without this, a describe -> exec round trip turned a BLOCKING message // box into a non-blocking one. The model carried Blocking on both @@ -856,7 +852,7 @@ func formatAction( } else if a.AssociationName != "" { attrPath = varName + "/" + a.AssociationName } - return fmt.Sprintf("validation feedback %s message %s;", attrPath, msgText) + return fmt.Sprintf("validation feedback %s message %s%s;", attrPath, msgText, templateArgsClause(a.TemplateParameters)) case *microflows.RestCallAction: return formatRestCallAction(ctx, a) @@ -1466,7 +1462,6 @@ func formatRestOperationCallAction(ctx *ExecContext, a *microflows.RestOperation if i > 0 { sb.WriteString(", ") } - sb.WriteString("$") sb.WriteString(p.name) sb.WriteString(" = ") sb.WriteString(p.value) @@ -2061,3 +2056,17 @@ func mdlAggregateKeyword(fn microflows.AggregateFunction) (string, bool) { return "", false } } + +// templateArgsClause renders a text template's arguments as ` with ({1} = a, +// {2} = b)` — R4's one text-template form (ako/mxcli#751) — or "" when there +// are none. +func templateArgsClause(args []string) string { + if len(args) == 0 { + return "" + } + parts := make([]string, len(args)) + for i, a := range args { + parts[i] = fmt.Sprintf("{%d} = %s", i+1, describeExpr(a)) + } + return " with (" + strings.Join(parts, ", ") + ")" +} diff --git a/mdl/executor/cmd_microflows_format_action_test.go b/mdl/executor/cmd_microflows_format_action_test.go index 792db3f71c..7a4d457718 100644 --- a/mdl/executor/cmd_microflows_format_action_test.go +++ b/mdl/executor/cmd_microflows_format_action_test.go @@ -525,7 +525,7 @@ func TestFormatAction_ShowPage_WithParams(t *testing.T) { }, } got := e.formatAction(action, nil, nil) - want := "show page MyModule.OrderDetail($Order = $Order);" + want := "show page MyModule.OrderDetail(Order = $Order);" if got != want { t.Errorf("got %q, want %q", got, want) } diff --git a/mdl/executor/cmd_misc.go b/mdl/executor/cmd_misc.go index 5b0cf3e152..1a201654ab 100644 --- a/mdl/executor/cmd_misc.go +++ b/mdl/executor/cmd_misc.go @@ -146,8 +146,8 @@ Microflows: change $Object (attr = value); commit $Object [with events] [refresh]; retrieve $List from Module.Entity where condition; - $Var = call microflow Module.Name($param = value); - $Var = call java action Module.Name($param = value); + $Var = call microflow Module.Name(param = value); + $Var = call java action Module.Name(param = value); validation feedback $Var/Attr message 'Error'; -- Show validation error close page [n]; -- Close page(s) log info|warning|error [node 'name'] 'message'; diff --git a/mdl/executor/cmd_pages_describe_datasource.go b/mdl/executor/cmd_pages_describe_datasource.go index 63bb53f33b..a80c95339d 100644 --- a/mdl/executor/cmd_pages_describe_datasource.go +++ b/mdl/executor/cmd_pages_describe_datasource.go @@ -302,7 +302,7 @@ func dataSourceExpr(ds *rawDataSource) string { if len(ds.Args) > 0 { parts := make([]string, 0, len(ds.Args)) for _, arg := range ds.Args { - parts = append(parts, arg.Name+": "+arg.Value) + parts = append(parts, visitor.ParameterNameSpelling(arg.Name)+" = "+arg.Value) } expr += "(" + strings.Join(parts, ", ") + ")" } diff --git a/mdl/executor/cmd_pages_describe_datasource_test.go b/mdl/executor/cmd_pages_describe_datasource_test.go index bc0f7cdab4..99f83a2d50 100644 --- a/mdl/executor/cmd_pages_describe_datasource_test.go +++ b/mdl/executor/cmd_pages_describe_datasource_test.go @@ -299,7 +299,7 @@ func TestDataSourceArgsRoundTrip(t *testing.T) { t.Errorf("second arg = %+v, want {Limit 10}", got.Args[1]) } - want := "microflow Mod.DS_Filtered(Term: $Term, Limit: 10)" + want := "microflow Mod.DS_Filtered(Term = $Term, Limit = 10)" if expr := dataSourceExpr(got); expr != want { t.Errorf("rendered %q, want %q", expr, want) } @@ -345,7 +345,7 @@ func TestDataSourceArgsNanoflowFlat(t *testing.T) { if got == nil { t.Fatal("nanoflow datasource not read") } - want := "nanoflow Mod.NF_Rows(Ctx: $currentObject)" + want := "nanoflow Mod.NF_Rows(Ctx = $currentObject)" if expr := dataSourceExpr(got); expr != want { t.Errorf("rendered %q, want %q", expr, want) } @@ -369,7 +369,7 @@ func TestDataSourceArgsNanoflow(t *testing.T) { if got == nil { t.Fatal("nanoflow datasource not read") } - want := "nanoflow Mod.NF_Rows(Ctx: $currentObject)" + want := "nanoflow Mod.NF_Rows(Ctx = $currentObject)" if expr := dataSourceExpr(got); expr != want { t.Errorf("rendered %q, want %q", expr, want) } diff --git a/mdl/executor/cmd_pages_describe_flow_args_test.go b/mdl/executor/cmd_pages_describe_flow_args_test.go index ec18170e04..015508847e 100644 --- a/mdl/executor/cmd_pages_describe_flow_args_test.go +++ b/mdl/executor/cmd_pages_describe_flow_args_test.go @@ -85,17 +85,17 @@ func TestDataSourceArgsReadPageVariableBinding(t *testing.T) { "Parameter": "Mod.DS.Order", "Variable": map[string]any{"$Type": "Forms$PageVariable", "PageParameter": "Order"}, }, - "microflow Mod.DS(Order: $Order)", + "microflow Mod.DS(Order = $Order)", }, { "expression binding still read", map[string]any{"Parameter": "Mod.DS.Order", "Expression": "$Order"}, - "microflow Mod.DS(Order: $Order)", + "microflow Mod.DS(Order = $Order)", }, { "bare string in Variable still read", map[string]any{"Parameter": "Mod.DS.Order", "Variable": "$Order"}, - "microflow Mod.DS(Order: $Order)", + "microflow Mod.DS(Order = $Order)", }, } { t.Run(tc.name, func(t *testing.T) { diff --git a/mdl/executor/cmd_pages_describe_output.go b/mdl/executor/cmd_pages_describe_output.go index 7d3da40143..4330fc32c2 100644 --- a/mdl/executor/cmd_pages_describe_output.go +++ b/mdl/executor/cmd_pages_describe_output.go @@ -5,6 +5,7 @@ package executor import ( "context" "fmt" + "github.com/mendixlabs/mxcli/mdl/visitor" "io" "regexp" "strconv" @@ -1546,7 +1547,7 @@ func pageActionParameters(ctx *ExecContext, settings map[string]any, pageName st // bound to the row object the enclosing widget supplies. var params []string for _, name := range targetPageParameterNames(ctx, pageName) { - params = append(params, mdlIdent(name)+": $currentObject") + params = append(params, visitor.ParameterNameSpelling(name)+" = $currentObject") } return strings.Join(params, ", ") } @@ -1586,7 +1587,7 @@ func targetPageParameterNames(ctx *ExecContext, qualifiedName string) []string { } // extractPageParameters extracts page parameter mappings from a FormSettings/PageSettings object. -// Returns formatted string like "Product: $currentObject" or empty string if no params. +// Returns formatted string like "Product = $currentObject" or empty string if no params. func extractPageParameters(ctx *ExecContext, settings map[string]any) string { mappings := getBsonArrayElements(settings["ParameterMappings"]) if len(mappings) == 0 { @@ -1636,7 +1637,7 @@ func extractPageParameters(ctx *ExecContext, settings map[string]any) string { } if value != "" { - params = append(params, mdlIdent(paramName)+": "+value) + params = append(params, visitor.ParameterNameSpelling(paramName)+" = "+value) } } @@ -1695,7 +1696,7 @@ func extractMicroflowParameters(ctx *ExecContext, settings map[string]any) strin if value != "" { // Canonical microflowArgV3 form is `Param: $value` (IDENTIFIER COLON expr); // emitting `Param = $value` is IDENTIFIER EQUALS, which doesn't re-parse (#640). - params = append(params, mdlIdent(paramName)+": "+value) + params = append(params, visitor.ParameterNameSpelling(paramName)+" = "+value) } } @@ -1754,7 +1755,7 @@ func extractNanoflowParameters(ctx *ExecContext, action map[string]any) string { if value != "" { // Canonical microflowArgV3 form is `Param: $value` (IDENTIFIER COLON expr); // emitting `Param = $value` is IDENTIFIER EQUALS, which doesn't re-parse (#640). - params = append(params, mdlIdent(paramName)+": "+value) + params = append(params, visitor.ParameterNameSpelling(paramName)+" = "+value) } } diff --git a/mdl/executor/cmd_pages_describe_pageparams_test.go b/mdl/executor/cmd_pages_describe_pageparams_test.go index e9c38459dc..b127ee95dd 100644 --- a/mdl/executor/cmd_pages_describe_pageparams_test.go +++ b/mdl/executor/cmd_pages_describe_pageparams_test.go @@ -56,7 +56,7 @@ func showPageAction(page string, mappings []any) map[string]any { } } -// mxcli-formula1 §39: `SHOW_PAGE P(Race: $currentObject)` described back as +// mxcli-formula1 §39: `SHOW_PAGE P(Race = $currentObject)` described back as // `show_page P`. The mapping was never in the model — mxcli stores it implicitly // on purpose — but DESCRIBE had no compensating recovery, so its output read as // a diagnosis ("the mapping was dropped, that is why the page gets an empty @@ -66,7 +66,7 @@ func TestRenderShowPageAction_RecoversTheImplicitParameter(t *testing.T) { ctx := pageParamFixture(t, "Race") got := renderClientActionMDL(ctx, showPageAction("Formula1Frontend.Race_Weekend", nil)) - want := "show_page Formula1Frontend.Race_Weekend(Race: $currentObject)" + want := "show_page Formula1Frontend.Race_Weekend(Race = $currentObject)" if got != want { t.Errorf("DESCRIBE lost the page parameter:\n got: %s\nwant: %s", got, want) } @@ -77,7 +77,7 @@ func TestRenderShowPageAction_RecoversEveryParameter(t *testing.T) { ctx := pageParamFixture(t, "Race", "Season") got := renderClientActionMDL(ctx, showPageAction("Formula1Frontend.Race_Weekend", nil)) - want := "show_page Formula1Frontend.Race_Weekend(Race: $currentObject, Season: $currentObject)" + want := "show_page Formula1Frontend.Race_Weekend(Race = $currentObject, Season = $currentObject)" if got != want { t.Errorf("got: %s\nwant: %s", got, want) } @@ -119,7 +119,7 @@ func TestRenderShowPageAction_ExplicitMappingWins(t *testing.T) { }, } got := renderClientActionMDL(ctx, showPageAction("Formula1Frontend.Race_Weekend", mappings)) - want := "show_page Formula1Frontend.Race_Weekend(Race: $SelectedRace)" + want := "show_page Formula1Frontend.Race_Weekend(Race = $SelectedRace)" if got != want { t.Errorf("an explicit mapping was overwritten:\n got: %s\nwant: %s", got, want) } diff --git a/mdl/executor/cmd_workflows.go b/mdl/executor/cmd_workflows.go index 80b18d9f8f..3e8a91d6ec 100644 --- a/mdl/executor/cmd_workflows.go +++ b/mdl/executor/cmd_workflows.go @@ -10,6 +10,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/visitor" "github.com/mendixlabs/mxcli/sdk/workflows" ) @@ -789,20 +790,8 @@ func formatCallMicroflowTask(a *workflows.CallMicroflowTask, indent string) []st asAndComment += " comment " + mdlQuoted(a.Caption) trailing = "" } - if len(a.ParameterMappings) > 0 { - var params []string - for _, pm := range a.ParameterMappings { - paramName := pm.Parameter - if idx := strings.LastIndex(paramName, "."); idx >= 0 { - paramName = paramName[idx+1:] - } - params = append(params, fmt.Sprintf("%s = %s", paramName, mdlQuoted(pm.Expression))) - } - lines = append(lines, fmt.Sprintf("%s%s %s%s with (%s)%s", indent, verb, mf, - asAndComment, strings.Join(params, ", "), trailing)) - } else { - lines = append(lines, fmt.Sprintf("%s%s %s%s%s", indent, verb, mf, asAndComment, trailing)) - } + args, legacy := workflowCallArguments(a.ParameterMappings) + lines = append(lines, fmt.Sprintf("%s%s %s%s%s%s%s", indent, verb, mf, args, asAndComment, legacy, trailing)) // Outcomes, then boundary events — the order the grammar requires // (workflowCallMicroflowStmt: … OUTCOMES? BOUNDARY EVENT?). Emitting them @@ -860,21 +849,9 @@ func formatCallWorkflowActivity(a *workflows.CallWorkflowActivity, indent string wf = "?" } - if len(a.ParameterMappings) > 0 { - var params []string - for _, pm := range a.ParameterMappings { - paramName := pm.Parameter - if idx := strings.LastIndex(paramName, "."); idx >= 0 { - paramName = paramName[idx+1:] - } - params = append(params, fmt.Sprintf("%s = %s", paramName, mdlQuoted(pm.Expression))) - } - lines = append(lines, fmt.Sprintf("%scall workflow %s%s comment %s with (%s)", indent, wf, - workflowActivityAsClause(a.Name, shortDocName(wf)), mdlQuoted(caption), strings.Join(params, ", "))) - } else { - lines = append(lines, fmt.Sprintf("%scall workflow %s%s comment %s", indent, wf, - workflowActivityAsClause(a.Name, shortDocName(wf)), mdlQuoted(caption))) - } + args, legacy := workflowCallArguments(a.ParameterMappings) + lines = append(lines, fmt.Sprintf("%scall workflow %s%s%s comment %s%s", indent, wf, args, + workflowActivityAsClause(a.Name, shortDocName(wf)), mdlQuoted(caption), legacy)) // BoundaryEvents lines = append(lines, formatBoundaryEvents(a.BoundaryEvents, indent+" ")...) @@ -1005,3 +982,42 @@ func formatConditionOutcomes(outcomes []workflows.ConditionOutcome, indent strin return lines } + +// workflowCallArguments renders a workflow call's parameter mappings in R4's +// argument form, `(Param = expression)` right after the callee +// (ako/mxcli#751). +// +// A stored expression that does not read back as itself when written bare — +// one with surrounding whitespace, or text the MDL expression grammar cannot +// parse — is rendered in the deprecated string form instead, returned as +// legacy (` with (Param = '')`, written after the comment): the +// string carries it byte for byte, and describe must never alter a stored +// expression. All mappings take the same form, since a call has one list. +func workflowCallArguments(mappings []*workflows.ParameterMapping) (args, legacy string) { + if len(mappings) == 0 { + return "", "" + } + bare := true + for _, pm := range mappings { + if !visitor.BareExpression(pm.Expression) { + bare = false + break + } + } + parts := make([]string, 0, len(mappings)) + for _, pm := range mappings { + name := pm.Parameter + if idx := strings.LastIndex(name, "."); idx >= 0 { + name = name[idx+1:] + } + if bare { + parts = append(parts, visitor.ParameterNameSpelling(name)+" = "+pm.Expression) + } else { + parts = append(parts, name+" = "+mdlQuoted(pm.Expression)) + } + } + if bare { + return "(" + strings.Join(parts, ", ") + ")", "" + } + return "", " with (" + strings.Join(parts, ", ") + ")" +} diff --git a/mdl/executor/describe_argument_binding_test.go b/mdl/executor/describe_argument_binding_test.go new file mode 100644 index 0000000000..233097c261 --- /dev/null +++ b/mdl/executor/describe_argument_binding_test.go @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/visitor" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/microflows" + "github.com/mendixlabs/mxcli/sdk/workflows" +) + +// ako/mxcli#751 (R4): describe binds every argument as `Param = expression` +// and every text-template parameter as `with ({n} = expression)`. Each line +// must also parse back with no deprecation recorded. + +func assertCanonicalMicroflowLine(t *testing.T, line string) { + t.Helper() + prog, errs := visitor.Build("create microflow M.F ($O: M.E, $N: String) begin " + line + " end;") + if len(errs) > 0 { + t.Fatalf("%q does not parse: %v", line, errs[0]) + } + if len(prog.Deprecations) > 0 { + t.Errorf("%q records %s", line, prog.Deprecations[0].Code) + } +} + +func TestDescribe_ShowMessageTemplateArguments(t *testing.T) { + e := newTestExecutor() + action := µflows.ShowMessageAction{ + Type: microflows.MessageTypeWarning, + Template: &model.Text{Translations: map[string]string{"en_US": "Hi {1} {2}"}}, + TemplateParameters: []string{"$N", "$O/Name"}, + Blocking: true, + } + got := e.formatAction(action, nil, nil) + want := "show message 'Hi {1} {2}' type Warning with ({1} = $N, {2} = $O/Name) blocking;" + if got != want { + t.Errorf("got %q, want %q", got, want) + } + assertCanonicalMicroflowLine(t, got) +} + +// Describe used to drop a validation feedback's template arguments altogether, +// so describe -> exec lost them. +func TestDescribe_ValidationFeedbackTemplateArguments(t *testing.T) { + e := newTestExecutor() + action := µflows.ValidationFeedbackAction{ + ObjectVariable: "O", + AttributeName: "M.E.Name", + Template: &model.Text{Translations: map[string]string{"en_US": "{1} is wrong"}}, + TemplateParameters: []string{"$N"}, + } + got := e.formatAction(action, nil, nil) + want := "validation feedback $O/Name message '{1} is wrong' with ({1} = $N);" + if got != want { + t.Errorf("got %q, want %q", got, want) + } + assertCanonicalMicroflowLine(t, got) +} + +func TestDescribe_SendRestRequestArguments(t *testing.T) { + action := µflows.RestOperationCallAction{ + Operation: "M.C.Get", + ParameterMappings: []*microflows.RestParameterMapping{{Parameter: "M.C.Get.id", Value: "$N"}}, + } + got := formatRestOperationCallAction(nil, action) + if !strings.Contains(got, "with (id = $N)") { + t.Errorf("got %q, want the argument as `id = $N`", got) + } + assertCanonicalMicroflowLine(t, strings.ReplaceAll(got, "\n", " ")) +} + +func TestDescribe_WorkflowCallArguments(t *testing.T) { + bare := &workflows.CallMicroflowTask{ + Microflow: "M.G", + ParameterMappings: []*workflows.ParameterMapping{{Parameter: "M.G.Order", Expression: "$WorkflowContext"}}, + } + bare.Name = "act1" + bare.Caption = "Go" + got := strings.Join(formatSingleActivity(bare, " "), "\n") + if !strings.Contains(got, "call microflow M.G(Order = $WorkflowContext) as act1 comment 'Go'") { + t.Errorf("describe = %q", got) + } + assertCanonicalWorkflowLine(t, got, false) + + // A stored expression the MDL grammar cannot write bare keeps the string + // form, which carries it byte for byte. + odd := &workflows.CallWorkflowActivity{ + Workflow: "M.Sub", + ParameterMappings: []*workflows.ParameterMapping{{Parameter: "M.Sub.Order", Expression: "$WorkflowContext\n"}}, + } + odd.Name = "callWorkflow1" + odd.Caption = "Sub" + got = strings.Join(formatSingleActivity(odd, " "), "\n") + if !strings.Contains(got, "comment 'Sub' with (Order = '") { + t.Errorf("describe = %q", got) + } + prog := assertCanonicalWorkflowLine(t, got, true) + cw := prog.Statements[0].(*ast.CreateWorkflowStmt).Activities[0].(*ast.WorkflowCallWorkflowNode) + if len(cw.ParameterMappings) != 1 || cw.ParameterMappings[0].Expression != "$WorkflowContext\n" { + t.Errorf("re-parsed mappings = %+v, want the stored expression unchanged", cw.ParameterMappings) + } +} + +func assertCanonicalWorkflowLine(t *testing.T, line string, legacy bool) *ast.Program { + t.Helper() + prog, errs := visitor.Build("create workflow M.W parameter $WorkflowContext: M.E begin\n" + line + "\nend workflow;") + if len(errs) > 0 { + t.Fatalf("%q does not parse: %v", line, errs[0]) + } + if got := len(prog.Deprecations) > 0; got != legacy { + t.Errorf("%q: records a deprecation = %v, want %v", line, got, legacy) + } + return prog +} diff --git a/mdl/executor/member_path_after_operator_test.go b/mdl/executor/member_path_after_operator_test.go new file mode 100644 index 0000000000..abd190d6ea --- /dev/null +++ b/mdl/executor/member_path_after_operator_test.go @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/visitor" + "github.com/mendixlabs/mxcli/sdk/microflows" +) + +// A member path after a multiplicative operator (`$a/X * $b/Y`) must be stored +// exactly as written. `/` sits at the same precedence as `*` in the MDL grammar, +// so the left-associative chain used to become `($a/X * $b) / Y`: the serializer +// then printed `$b / Y` as division, and a filter's iterator qualification read +// the stranded `Y` as a bare attribute and wrote `$b / $currentObject/Y`. The +// build and mx check stayed green; Mendix evaluates a different expression. +func TestMemberPathAfterMultiplicativeOperatorIsStoredAsWritten(t *testing.T) { + const src = `create microflow MyFirstModule.Agg0 ($Feedbacks: List of FeedbackModule.Feedback, $One: FeedbackModule.Feedback) +returns Integer +begin + $Area = sum($Feedbacks, $currentObject/ScreenWidth * $currentObject/ScreenHeight); + $W = filter($Feedbacks, $currentObject/ScreenWidth * $currentObject/ScreenHeight > 3); + declare $X Integer = $One/ScreenWidth * $One/ScreenHeight; + declare $D Decimal = $One/ScreenWidth div $One/ScreenHeight * $One/Rating; + declare $N Integer = 2 * -$One/ScreenWidth; + return $Area; +end;` + got := storedExpressions(t, src) + + for _, want := range []string{ + "sum: $currentObject/ScreenWidth * $currentObject/ScreenHeight", + "filter: $currentObject/ScreenWidth * $currentObject/ScreenHeight > 3", + "declare X: $One/ScreenWidth * $One/ScreenHeight", + // div binds like *; the member path after it is navigation too. + "declare D: $One/ScreenWidth div $One/ScreenHeight * $One/Rating", + // A unary minus in front of the path keeps the path whole (already + // correct before the fix; guards against a regression). + "declare N: 2 * -$One/ScreenWidth", + } { + if !containsString(got, want) { + t.Errorf("stored expression %q missing; stored: %q", want, got) + } + } +} + +// Control: division by a parenthesised expression or a variable is not member +// navigation and must not be re-associated. +func TestSlashAfterNonPathIsNotMadeAMemberPath(t *testing.T) { + const src = `create microflow MyFirstModule.Agg1 ($One: FeedbackModule.Feedback) +returns Integer +begin + declare $P Decimal = $One/ScreenWidth * ($One/ScreenHeight) div 2; + return 1; +end;` + got := storedExpressions(t, src) + want := "declare P: $One/ScreenWidth * ($One/ScreenHeight) div 2" + if !containsString(got, want) { + t.Errorf("stored expression %q missing; stored: %q", want, got) + } +} + +// storedExpressions builds each statement of a microflow through the flow +// builder and returns the expression strings the actions would store. +func storedExpressions(t *testing.T, src string) []string { + t.Helper() + prog, errs := visitor.Build(src) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + mf, ok := prog.Statements[0].(*ast.CreateMicroflowStmt) + if !ok { + t.Fatalf("expected a CreateMicroflowStmt, got %T", prog.Statements[0]) + } + fb := &flowBuilder{ + posX: 100, posY: 100, spacing: HorizontalSpacing, + varTypes: map[string]string{"Feedbacks": "List of FeedbackModule.Feedback", "One": "FeedbackModule.Feedback"}, + declaredVars: map[string]string{}, + measurer: &layoutMeasurer{varTypes: map[string]string{"Feedbacks": "List of FeedbackModule.Feedback"}}, + } + for _, stmt := range mf.Body { + fb.addStatement(stmt) + } + var got []string + for _, obj := range fb.objects { + act, ok := obj.(*microflows.ActionActivity) + if !ok { + continue + } + switch a := act.Action.(type) { + case *microflows.AggregateListAction: + got = append(got, "sum: "+a.Expression) + case *microflows.ListOperationAction: + if f, ok := a.Operation.(*microflows.FilterOperation); ok { + got = append(got, "filter: "+f.Expression) + } + case *microflows.CreateVariableAction: + got = append(got, "declare "+a.VariableName+": "+a.InitialValue) + } + } + return got +} diff --git a/mdl/executor/roundtrip_page_test.go b/mdl/executor/roundtrip_page_test.go index 720f68c853..601b785f87 100644 --- a/mdl/executor/roundtrip_page_test.go +++ b/mdl/executor/roundtrip_page_test.go @@ -597,7 +597,7 @@ func TestRoundtripPage_MicroflowButtonWithParams(t *testing.T) { ) { dataview dv (DataSource: $Product) { textbox txt (Label: 'Name', Attribute: Name) - actionbutton btnProcess (Caption: 'Process', Action: microflow ` + mfName + `(Product: $Product)) + actionbutton btnProcess (Caption: 'Process', Action: microflow ` + mfName + `(Product = $Product)) } }` @@ -619,8 +619,8 @@ func TestRoundtripPage_MicroflowButtonWithParams(t *testing.T) { if !strings.Contains(output, mfName) { t.Errorf("Expected microflow name '%s' in describe output.\nOutput:\n%s", mfName, output) } - if !strings.Contains(output, "Product: $Product") { - t.Errorf("Expected 'Product: $Product' parameter mapping in describe output.\nOutput:\n%s", output) + if !strings.Contains(output, "Product = $Product") { + t.Errorf("Expected 'Product = $Product' parameter mapping in describe output.\nOutput:\n%s", output) } t.Logf("Microflow button with params roundtrip successful:\n%s", output) @@ -670,7 +670,7 @@ func TestRoundtripPage_MicroflowButtonWithCurrentObject(t *testing.T) { datagrid dg (DataSource: database ` + entityName + `) { column colName (Attribute: Name, Caption: 'Name') column colActions (Attribute: Name, Caption: 'Actions', ShowContentAs: customContent) { - actionbutton btnProcess (Caption: 'Process', Action: microflow ` + mfName + `(Target: $currentObject)) + actionbutton btnProcess (Caption: 'Process', Action: microflow ` + mfName + `(Target = $currentObject)) } } }` @@ -702,9 +702,9 @@ func TestRoundtripPage_MicroflowButtonWithCurrentObject(t *testing.T) { // Both forms re-parse here (TARGET is non-reserved, so the unquoted spelling // this test's own input uses is still accepted); what the assertion is for is // the MAPPING surviving the roundtrip, which is independent of the quoting. - if !strings.Contains(output, "Target: $currentObject") && - !strings.Contains(output, `"Target": $currentObject`) { - t.Errorf("Expected a 'Target: $currentObject' parameter mapping (quoted or not) "+ + if !strings.Contains(output, "Target = $currentObject") && + !strings.Contains(output, `"Target" = $currentObject`) { + t.Errorf("Expected a 'Target = $currentObject' parameter mapping (quoted or not) "+ "in describe output.\nOutput:\n%s", output) } diff --git a/mdl/executor/roundtrip_workflow_test.go b/mdl/executor/roundtrip_workflow_test.go index 85e54ff595..7805430388 100644 --- a/mdl/executor/roundtrip_workflow_test.go +++ b/mdl/executor/roundtrip_workflow_test.go @@ -189,7 +189,7 @@ end workflow;` {"boundary interrupting", "boundary event interrupting timer '${PT24H}'"}, {"boundary non interrupting", "boundary event non interrupting timer '${PT1H}'"}, {"multi user task", "multi user task MultiReviewTask"}, - {"call microflow with", "call microflow " + mod + ".ScoreCalc with (Score ="}, + {"call microflow args", "call microflow " + mod + ".ScoreCalc(Score = $WorkflowContext/Score)"}, {"outcomes true", "true ->"}, {"outcomes false", "false ->"}, {"decision", "decision '$WorkflowContext/IsApproved'"}, @@ -420,7 +420,7 @@ end workflow;` t.Fatalf("Failed to describe workflow: %v", err) } - if !strings.Contains(output, "with (") { - t.Errorf("Expected describe output to contain 'with (', got:\n%s", output) + if !strings.Contains(output, "SomeMicroflow(Amount = $WorkflowContext/Amount)") { + t.Errorf("Expected describe output to bind the argument in R4's form, got:\n%s", output) } } diff --git a/mdl/executor/validate_page_button_context.go b/mdl/executor/validate_page_button_context.go index 2c302a3fb7..5e1df31e16 100644 --- a/mdl/executor/validate_page_button_context.go +++ b/mdl/executor/validate_page_button_context.go @@ -94,7 +94,7 @@ func controlBarSuggestion(controlBarOf string) string { return "Move the button into a grid column (row-scoped) so it has a current row, or pass a page parameter instead of $currentObject." } return fmt.Sprintf( - "Pass the selection of `%s` instead — `Action: microflow M.F($Param = $%s)`, with `Selection:` set on the widget. "+ + "Pass the selection of `%s` instead — `Action: microflow M.F(Param = $%s)`, with `Selection:` set on the widget. "+ "Or move the button into a grid column (row-scoped) so it has a current row, or pass a page parameter.", controlBarOf, controlBarOf) } diff --git a/mdl/executor/validate_workflow_agent_test.go b/mdl/executor/validate_workflow_agent_test.go index 29212425eb..4dd88be41f 100644 --- a/mdl/executor/validate_workflow_agent_test.go +++ b/mdl/executor/validate_workflow_agent_test.go @@ -77,7 +77,7 @@ func TestWorkflowAgentTask_DescribeRoundTrips(t *testing.T) { out := strings.Join(formatSingleActivity(task, " "), "\n") // The caption is authored (not the microflow's name), so it is described as a // comment clause, which re-executes into the caption. - if !strings.Contains(out, "call agent microflow workflow.InvokeAgent as aiAgentTask1 comment 'AI Agent Task' with (workfow1context = '$WorkflowContext')") { + if !strings.Contains(out, "call agent microflow workflow.InvokeAgent(workfow1context = $WorkflowContext) as aiAgentTask1 comment 'AI Agent Task'") { t.Fatalf("describe = %q", out) } stmt := parseWorkflowStmt(t, "create workflow M.W\nbegin\n"+out+"\nend workflow;") diff --git a/mdl/exprcheck/hints/registry.go b/mdl/exprcheck/hints/registry.go index 9f4ff6ece9..00639e7654 100644 --- a/mdl/exprcheck/hints/registry.go +++ b/mdl/exprcheck/hints/registry.go @@ -60,8 +60,8 @@ var Registry = ®istry{byCode: map[string]Entry{ Note: "IF condition", }, { - Wrong: "CALL Mf($Status = 'Validated')", - Right: "CALL Mf($Status = FraudDetection.AlertStatus.Validated)", + Wrong: "CALL Mf(Status = 'Validated')", + Right: "CALL Mf(Status = FraudDetection.AlertStatus.Validated)", Note: "CALL parameter", }, }, diff --git a/mdl/grammar/domains/MDLMicroflow.g4 b/mdl/grammar/domains/MDLMicroflow.g4 index ff5a0c63a7..480c44066b 100644 --- a/mdl/grammar/domains/MDLMicroflow.g4 +++ b/mdl/grammar/domains/MDLMicroflow.g4 @@ -526,7 +526,7 @@ logLevel // Template parameters: WITH ({1} = expr, {2} = expr) or PARAMETERS [expr, expr] templateParams : WITH LPAREN templateParam (COMMA templateParam)* RPAREN // WITH ({1} = $var) - | PARAMETERS arrayLiteral // PARAMETERS ['val'] (deprecated) + | PARAMETERS /* @alias MDL-DEPR009 */ arrayLiteral // PARAMETERS ['val'] (deprecated) ; templateParam @@ -686,9 +686,11 @@ callArgumentList : callArgument (COMMA callArgument)* ; -// Named arguments: $FirstName = 'Hello' or Level = 'INFO' or OqlStatement = '...' +// Named arguments: FirstName = 'Hello' or Level = 'INFO' or OqlStatement = '...' +// (R4: `Param = expression`, no `$` on the parameter name). `$FirstName = …` is +// the deprecated spelling of the same argument. callArgument - : (VARIABLE | parameterName) EQUALS expression + : (VARIABLE /* @alias MDL-DEPR006 */ | parameterName) EQUALS expression ; showPageStatement @@ -699,9 +701,12 @@ showPageArgList : showPageArg (COMMA showPageArg)* ; +// R4: `Param = expression`, the argument form of every call site. `$Param = …` +// and `Param: …` are deprecated spellings of the same argument. showPageArg - : VARIABLE EQUALS (VARIABLE | expression) // $Param = $value (canonical) - | identifierOrKeyword COLON expression // Param: $value (widget-style, also accepted) + : parameterName EQUALS expression // Param = $value (canonical) + | VARIABLE /* @alias MDL-DEPR006 */ EQUALS (VARIABLE | expression) // $Param = $value + | identifierOrKeyword COLON /* @alias MDL-DEPR007 */ expression // Param: $value ; closePageStatement @@ -712,9 +717,12 @@ showHomePageStatement : SHOW HOME PAGE ; -// SHOW MESSAGE 'Hello {1}' TYPE Information OBJECTS [$Name]; +// SHOW MESSAGE 'Hello {1}' TYPE Information WITH ({1} = $Name); +// `OBJECTS [$Name]` is the deprecated positional spelling of the same list. showMessageStatement - : SHOW MESSAGE expression (TYPE identifierOrKeyword)? (OBJECTS LBRACKET expressionList RBRACKET)? BLOCKING? onErrorClause? + : SHOW MESSAGE expression (TYPE identifierOrKeyword)? + (OBJECTS /* @alias MDL-DEPR009 */ LBRACKET expressionList RBRACKET | templateParams)? + BLOCKING? onErrorClause? ; // SYNCHRONIZE ALL; @@ -740,7 +748,8 @@ throwStatement // VALIDATION FEEDBACK $Product/Code MESSAGE 'Product code cannot be empty'; validationFeedbackStatement - : VALIDATION FEEDBACK (attributePath | VARIABLE) MESSAGE expression (OBJECTS LBRACKET expressionList RBRACKET)? onErrorClause? + : VALIDATION FEEDBACK (attributePath | VARIABLE) MESSAGE expression + (OBJECTS /* @alias MDL-DEPR009 */ LBRACKET expressionList RBRACKET | templateParams)? onErrorClause? ; // ============================================================================= @@ -828,7 +837,7 @@ sendRestRequestWithClause ; sendRestRequestParam - : VARIABLE EQUALS expression + : (VARIABLE /* @alias MDL-DEPR006 */ | parameterName) EQUALS expression ; sendRestRequestBodyClause diff --git a/mdl/grammar/domains/MDLPage.g4 b/mdl/grammar/domains/MDLPage.g4 index 3052d578b9..2e651c26bc 100644 --- a/mdl/grammar/domains/MDLPage.g4 +++ b/mdl/grammar/domains/MDLPage.g4 @@ -667,16 +667,18 @@ actionExprV3 | COMPLETE_TASK STRING_LITERAL // COMPLETE_TASK 'OutcomeName' ; -// V3 Microflow arguments: (Param: value, ...) +// V3 Microflow arguments: (Param = value, ...) — R4, the argument form of every +// call site. `Param: value` and `$Param = value` are deprecated spellings. microflowArgsV3 : LPAREN microflowArgV3 (COMMA microflowArgV3)* RPAREN ; microflowArgV3 - : identifierOrKeyword COLON expression // Param: $value (identifierOrKeyword so a param - // named after a keyword — View/Source/Item/Page/ - // Entity — works unquoted, matching callArgument) - | VARIABLE EQUALS expression // $Param = $value (microflow-style, also accepted) + : parameterName EQUALS expression // Param = $value (parameterName so a param named + // after a keyword — View/Source/Item/Page/Entity — + // works unquoted, matching callArgument) + | identifierOrKeyword COLON /* @alias MDL-DEPR007 */ expression // Param: $value + | VARIABLE /* @alias MDL-DEPR006 */ EQUALS expression // $Param = $value ; // A value in `Visible: Attr in (…)`: an enumeration value name, true/false, diff --git a/mdl/grammar/domains/MDLWorkflow.g4 b/mdl/grammar/domains/MDLWorkflow.g4 index a03f3e8336..26706d8060 100644 --- a/mdl/grammar/domains/MDLWorkflow.g4 +++ b/mdl/grammar/domains/MDLWorkflow.g4 @@ -237,19 +237,31 @@ workflowUserTaskOutcome * invoked. */ workflowCallMicroflowStmt - : CALL AGENT? MICROFLOW qualifiedName (AS workflowActivityName)? (COMMENT STRING_LITERAL)? - (WITH LPAREN workflowParameterMapping (COMMA workflowParameterMapping)* RPAREN)? + : CALL AGENT? MICROFLOW qualifiedName workflowCallArguments? (AS workflowActivityName)? (COMMENT STRING_LITERAL)? + (WITH /* @alias MDL-DEPR008 */ LPAREN workflowParameterMapping (COMMA workflowParameterMapping)* RPAREN)? (OUTCOMES workflowConditionOutcome+)? (BOUNDARY EVENT workflowBoundaryEventClause ((BOUNDARY EVENT)? workflowBoundaryEventClause)*)? ; +// R4: a workflow call binds its arguments like every other call site, +// `(Param = expression)` right after the callee, the expression bare. +// `with (Param = '')`, the expression in a string, is the +// deprecated spelling of the same mapping. +workflowCallArguments + : LPAREN (workflowCallArgument (COMMA workflowCallArgument)*)? RPAREN + ; + +workflowCallArgument + : parameterName EQUALS expression + ; + workflowParameterMapping : qualifiedName EQUALS STRING_LITERAL ; workflowCallWorkflowStmt - : CALL WORKFLOW qualifiedName (AS workflowActivityName)? (COMMENT STRING_LITERAL)? - (WITH LPAREN workflowParameterMapping (COMMA workflowParameterMapping)* RPAREN)? + : CALL WORKFLOW qualifiedName workflowCallArguments? (AS workflowActivityName)? (COMMENT STRING_LITERAL)? + (WITH /* @alias MDL-DEPR008 */ LPAREN workflowParameterMapping (COMMA workflowParameterMapping)* RPAREN)? ; workflowDecisionStmt diff --git a/mdl/upgrade/argument_binding_test.go b/mdl/upgrade/argument_binding_test.go new file mode 100644 index 0000000000..3a35e6dbf0 --- /dev/null +++ b/mdl/upgrade/argument_binding_test.go @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 + +package upgrade + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/deprecation" +) + +// ako/mxcli#751 (R4): fmt --upgrade rewrites every argument to `Param = +// expression` and every positional text-template list to `with ({n} = …)`, +// touching nothing else — comments, layout, keyword case and the arguments' +// own text survive. +func TestUpgrade_ArgumentBinding(t *testing.T) { + src := `create microflow M.F ($O: M.E, $N: String) begin + call microflow M.G($Order = $O, Force = false); -- keep me + show page M.P(Order: $O); + SHOW MESSAGE 'Hi {1} {2}' TYPE Warning OBJECTS [$N, $O/Name] BLOCKING; + validation feedback $O/Name message '{1}' objects [ $N ]; + log info 'x {1}' parameters ['a']; + $R = send rest request M.C.Get with ($id = $N); +end; +create page M.Q (Title: 'Q', Layout: Atlas_Core.Atlas_Default) { + dataview dv (DataSource: $O) { + actionbutton b (Caption: 'Go', Action: microflow M.G(Order:$currentObject)) + } +}; +create workflow M.W parameter $WorkflowContext: M.E begin + call microflow M.G as act1 comment 'Go' + with (M.G.Order = '$WorkflowContext'); +end workflow; +` + want := `create microflow M.F ($O: M.E, $N: String) begin + call microflow M.G(Order = $O, Force = false); -- keep me + show page M.P(Order = $O); + SHOW MESSAGE 'Hi {1} {2}' TYPE Warning WITH ({1} = $N, {2} = $O/Name) BLOCKING; + validation feedback $O/Name message '{1}' with ( {1} = $N ); + log info 'x {1}' with ({1} = 'a'); + $R = send rest request M.C.Get with (id = $N); +end; +create page M.Q (Title: 'Q', Layout: Atlas_Core.Atlas_Default) { + dataview dv (DataSource: $O) { + actionbutton b (Caption: 'Go', Action: microflow M.G(Order = $currentObject)) + } +}; +create workflow M.W parameter $WorkflowContext: M.E begin + call microflow M.G(Order = $WorkflowContext) as act1 comment 'Go'; +end workflow; +` + res := mustUpgrade(t, src, Options{}) + if res.Source != want { + t.Fatalf("got:\n%s\nwant:\n%s", res.Source, want) + } + for code, n := range map[string]int{ + deprecation.DollarArgumentName: 2, + deprecation.ColonArgument: 2, + deprecation.WorkflowStringArgument: 1, + deprecation.PositionalTemplateArguments: 3, + } { + if res.Rewritten[code] != n { + t.Errorf("Rewritten[%s] = %d, want %d (all: %v)", code, res.Rewritten[code], n, res.Rewritten) + } + } + if again := mustUpgrade(t, res.Source, Options{}); again.Changed() { + t.Errorf("a second upgrade changed the script again:\n%s", again.Source) + } +} + +// A workflow string argument that does not read back as the same bare +// expression is left as it is and reported, never guessed at. +func TestUpgrade_WorkflowStringArgumentWithoutBareForm(t *testing.T) { + src := "create workflow M.W parameter $WorkflowContext: M.E begin\n" + + " call microflow M.G with (Order = ' $WorkflowContext');\nend workflow;\n" + res := mustUpgrade(t, src, Options{}) + if res.Source != src { + t.Errorf("the script changed:\n%s", res.Source) + } + if len(res.Unrewritten) != 1 || res.Unrewritten[0].Code != deprecation.WorkflowStringArgument { + t.Errorf("Unrewritten = %+v, want the one MDL-DEPR008 use", res.Unrewritten) + } +} diff --git a/mdl/visitor/visitor_argument_binding.go b/mdl/visitor/visitor_argument_binding.go new file mode 100644 index 0000000000..1b74d0f83d --- /dev/null +++ b/mdl/visitor/visitor_argument_binding.go @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: Apache-2.0 + +package visitor + +import ( + "fmt" + "sort" + "strings" + + "github.com/antlr4-go/antlr/v4" + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/deprecation" + "github.com/mendixlabs/mxcli/mdl/grammar/parser" +) + +// R4 (PROPOSAL_mdl_beta_syntax_freeze.md §3; ako/mxcli#751): every call site +// binds an argument as `Param = expression`, and every text template as +// `with ({1} = expression)`. The spellings below build exactly what the +// canonical form builds, so each is a registered alias (mdl/deprecation) whose +// use is recorded here, where its tokens are still at hand, together with the +// rewrite `fmt --upgrade` applies: +// +// - MDL-DEPR006 `$Param = e` — the `$` of a variable on a parameter name; +// - MDL-DEPR007 `Param: e` — `:` sets a model property, `=` binds a value; +// - MDL-DEPR008 a workflow call's `with (Param = '')`; +// - MDL-DEPR009 `objects [a, b]` / `parameters [a, b]`. + +// ExitCallArgument records `$Param = e` in call microflow/nanoflow/java +// action/javascript action/external action/web service/execute database query. +func (b *Builder) ExitCallArgument(ctx *parser.CallArgumentContext) { + b.recordDollarArgument(ctx.VARIABLE()) +} + +// ExitSendRestRequestParam records `$Param = e` in send rest request. +func (b *Builder) ExitSendRestRequestParam(ctx *parser.SendRestRequestParamContext) { + b.recordDollarArgument(ctx.VARIABLE()) +} + +// ExitShowPageArg records the two deprecated argument spellings of show page. +func (b *Builder) ExitShowPageArg(ctx *parser.ShowPageArgContext) { + if ctx.ParameterName() != nil { + return + } + if iok := ctx.IdentifierOrKeyword(); iok != nil { + b.recordColonArgument(iok, ctx.COLON(), ctx.Expression()) + return + } + if vars := ctx.AllVARIABLE(); len(vars) > 0 { + b.recordDollarArgument(vars[0]) + } +} + +// ExitMicroflowArgV3 records the two deprecated argument spellings of a page +// action or a microflow/nanoflow data source. +func (b *Builder) ExitMicroflowArgV3(ctx *parser.MicroflowArgV3Context) { + if ctx.ParameterName() != nil { + return + } + if iok := ctx.IdentifierOrKeyword(); iok != nil { + b.recordColonArgument(iok, ctx.COLON(), ctx.Expression()) + return + } + b.recordDollarArgument(ctx.VARIABLE()) +} + +// recordDollarArgument records MDL-DEPR006 for the parameter-name token v, with +// the rewrite that drops its `$`. +func (b *Builder) recordDollarArgument(v antlr.TerminalNode) { + if v == nil { + return + } + tok := v.GetSymbol() + name := strings.TrimPrefix(tok.GetText(), "$") + b.recordDeprecation(deprecation.DollarArgumentName, tok, "argument") + edit := ast.TextEdit{Start: tok.GetStart(), Stop: tok.GetStop() + 1, Text: ParameterNameSpelling(name)} + b.fixLastDeprecation(deprecation.DollarArgumentName, &ast.Fix{Edits: []ast.TextEdit{edit}}, "") +} + +// recordColonArgument records MDL-DEPR007 for `Param: e`, with the rewrite +// `Param = e`: the colon and the space around it become ` = `. +func (b *Builder) recordColonArgument(name parser.IIdentifierOrKeywordContext, colon antlr.TerminalNode, expr parser.IExpressionContext) { + if colon == nil { + return + } + b.recordDeprecation(deprecation.ColonArgument, colon.GetSymbol(), "argument") + nameStop := name.GetStop() + if nameStop == nil || expr == nil || expr.GetStart() == nil { + b.fixLastDeprecation(deprecation.ColonArgument, nil, "the argument is incomplete") + return + } + edit := replaceGap(nameStop.GetStop(), expr.GetStart().GetStart(), " = ") + b.fixLastDeprecation(deprecation.ColonArgument, &ast.Fix{Edits: []ast.TextEdit{edit}}, "") +} + +// ParameterNameSpelling spells a parameter name as parameterName accepts it: +// bare when it parses as one (an identifier or a keyword), quoted otherwise. +func ParameterNameSpelling(name string) string { + if parsesFully(name, func(p *parser.MDLParser) antlr.ParserRuleContext { return p.ParameterName() }) { + return name + } + return `"` + name + `"` +} + +// parsesFully reports whether text is exactly one rule, as rule parses it, +// with no syntax error and nothing left over. +func parsesFully(text string, rule func(*parser.MDLParser) antlr.ParserRuleContext) bool { + _, ok := parseRule(text, rule) + return ok +} + +func parseRule(text string, rule func(*parser.MDLParser) antlr.ParserRuleContext) (antlr.ParserRuleContext, bool) { + errs := newErrorListener() + lexer := parser.NewMDLLexer(newScriptStream(text)) + lexer.RemoveErrorListeners() + lexer.AddErrorListener(errs) + stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel) + p := parser.NewMDLParser(stream) + p.RemoveErrorListeners() + p.AddErrorListener(errs) + ctx := rule(p) + if len(errs.errors) > 0 || ctx == nil || stream.LA(1) != antlr.TokenEOF { + return nil, false + } + return ctx, true +} + +// BareExpression reports whether a stored expression can be written as a bare +// MDL expression that the visitor reads back as exactly that text. describe +// asks it before writing a workflow argument without quotes; fmt --upgrade +// asks it before unquoting one. +func BareExpression(expr string) bool { + if strings.TrimSpace(expr) != expr || expr == "" { + return false + } + ctx, ok := parseRule(expr, func(p *parser.MDLParser) antlr.ParserRuleContext { return p.Expression() }) + if !ok { + return false + } + return bareArgumentText(ctx.(parser.IExpressionContext)) == expr +} + +// bareArgumentText is the expression a bare workflow argument stores: its +// source text, as the Mendix expression editor would hold it. +func bareArgumentText(expr parser.IExpressionContext) string { + if se, ok := buildSourceExpression(expr).(*ast.SourceExpr); ok { + return se.Source + } + return stripExpressionIdentifierQuotes(expressionSourceText(expr)) +} + +// buildWorkflowCallArguments builds the canonical `(Param = expression)` list +// of a workflow call. +func buildWorkflowCallArguments(ctx parser.IWorkflowCallArgumentsContext) []ast.WorkflowParameterMappingNode { + if ctx == nil { + return nil + } + var out []ast.WorkflowParameterMappingNode + for _, a := range ctx.(*parser.WorkflowCallArgumentsContext).AllWorkflowCallArgument() { + arg := a.(*parser.WorkflowCallArgumentContext) + if arg.ParameterName() == nil || arg.Expression() == nil { + continue + } + out = append(out, ast.WorkflowParameterMappingNode{ + Parameter: parameterNameText(arg.ParameterName()), + Expression: bareArgumentText(arg.Expression()), + }) + } + return out +} + +// workflowCallCtx is what the two workflow call statements have in common. +type workflowCallCtx interface { + antlr.ParserRuleContext + QualifiedName() parser.IQualifiedNameContext + WorkflowCallArguments() parser.IWorkflowCallArgumentsContext + WITH() antlr.TerminalNode + RPAREN() antlr.TerminalNode + AllWorkflowParameterMapping() []parser.IWorkflowParameterMappingContext +} + +// ExitWorkflowCallMicroflowStmt records MDL-DEPR008 on a workflow's call +// microflow activity. +func (b *Builder) ExitWorkflowCallMicroflowStmt(ctx *parser.WorkflowCallMicroflowStmtContext) { + b.recordWorkflowStringArguments(ctx) +} + +// ExitWorkflowCallWorkflowStmt records MDL-DEPR008 on a workflow's call +// workflow activity. +func (b *Builder) ExitWorkflowCallWorkflowStmt(ctx *parser.WorkflowCallWorkflowStmtContext) { + b.recordWorkflowStringArguments(ctx) +} + +func (b *Builder) recordWorkflowStringArguments(ctx workflowCallCtx) { + with := ctx.WITH() + if with == nil { + return + } + if ctx.WorkflowCallArguments() != nil { + b.addError(fmt.Errorf("line %d: a workflow call has two argument lists; write the arguments once, "+ + "as `(Param = expression)` after the callee", with.GetSymbol().GetLine())) + return + } + b.recordDeprecation(deprecation.WorkflowStringArgument, with.GetSymbol(), "workflow call") + fix, why := workflowStringArgumentsFix(ctx) + b.fixLastDeprecation(deprecation.WorkflowStringArgument, fix, why) +} + +// workflowStringArgumentsFix moves `with (P = 'e', …)` to `(P = e, …)` after +// the callee. A string whose content is not a bare expression that reads back +// as itself has no rewrite: unquoting it would change the stored expression. +func workflowStringArgumentsFix(ctx workflowCallCtx) (*ast.Fix, string) { + qn, rparen := ctx.QualifiedName(), ctx.RPAREN() + if qn == nil || qn.GetStop() == nil || rparen == nil { + return nil, "the argument list is incomplete" + } + var args []string + for _, m := range ctx.AllWorkflowParameterMapping() { + pm := m.(*parser.WorkflowParameterMappingContext) + if pm.QualifiedName() == nil || pm.STRING_LITERAL() == nil { + return nil, "the argument list is incomplete" + } + expr := unquoteStringLit(pm.STRING_LITERAL()) + if !BareExpression(expr) { + return nil, fmt.Sprintf("the string %s does not read back as the same bare expression; "+ + "write the argument as `(Param = expression)` by hand", pm.STRING_LITERAL().GetText()) + } + name := bareWorkflowParameterName(pm.QualifiedName().GetText()) + args = append(args, ParameterNameSpelling(name)+" = "+expr) + } + // Delete ` with (…)` from the end of the token before WITH, and write the + // list after the callee. + withTok := ctx.WITH().GetSymbol() + prevStop := -1 + for i := 0; i < ctx.GetChildCount(); i++ { + if t, ok := ctx.GetChild(i).(antlr.TerminalNode); ok && t.GetSymbol() == withTok { + break + } + if _, stop := nodeSpan(ctx.GetChild(i)); stop >= 0 { + prevStop = stop + } + } + if prevStop < 0 { + return nil, "the argument list is incomplete" + } + return &ast.Fix{Edits: []ast.TextEdit{ + insertAt(qn.GetStop().GetStop()+1, "("+strings.Join(args, ", ")+")"), + {Start: prevStop + 1, Stop: rparen.GetSymbol().GetStop() + 1, Text: ""}, + }}, "" +} + +// ExitShowMessageStatement records MDL-DEPR009 for `objects [..]` and checks +// the numbering of `with ({n} = …)`. +func (b *Builder) ExitShowMessageStatement(ctx *parser.ShowMessageStatementContext) { + b.recordObjectsList(ctx.OBJECTS(), ctx.LBRACKET(), ctx.ExpressionList(), ctx.RBRACKET()) + b.checkTemplateNumbering(ctx.TemplateParams()) +} + +// ExitValidationFeedbackStatement is ExitShowMessageStatement for validation +// feedback. +func (b *Builder) ExitValidationFeedbackStatement(ctx *parser.ValidationFeedbackStatementContext) { + b.recordObjectsList(ctx.OBJECTS(), ctx.LBRACKET(), ctx.ExpressionList(), ctx.RBRACKET()) + b.checkTemplateNumbering(ctx.TemplateParams()) +} + +func (b *Builder) recordObjectsList(objects, lbracket antlr.TerminalNode, list parser.IExpressionListContext, rbracket antlr.TerminalNode) { + if objects == nil { + return + } + b.recordDeprecation(deprecation.PositionalTemplateArguments, objects.GetSymbol(), "text template") + if lbracket == nil || rbracket == nil || list == nil { + b.fixLastDeprecation(deprecation.PositionalTemplateArguments, nil, "the list is incomplete") + return + } + var items []antlr.ParserRuleContext + for _, e := range list.(*parser.ExpressionListContext).AllExpression() { + items = append(items, e) + } + b.fixLastDeprecation(deprecation.PositionalTemplateArguments, + numberedListFix(objects.GetSymbol(), lbracket.GetSymbol(), items, rbracket.GetSymbol()), "") +} + +// numberedListFix rewrites `kw [a, b]` as `with ({1} = a, {2} = b)`: the +// keyword and bracket become `with (`, each item gets its number, and the +// closing bracket becomes `)`. The items themselves are not touched. +func numberedListFix(kw, lbracket antlr.Token, items []antlr.ParserRuleContext, rbracket antlr.Token) *ast.Fix { + edits := []ast.TextEdit{replaceSpan(kw, lbracket, keywordLike(kw.GetText(), "with")+" (")} + for i, it := range items { + if it.GetStart() == nil { + return nil + } + edits = append(edits, insertAt(it.GetStart().GetStart(), fmt.Sprintf("{%d} = ", i+1))) + } + edits = append(edits, replaceSpan(rbracket, rbracket, ")")) + return &ast.Fix{Edits: edits} +} + +// ExitTemplateParams records MDL-DEPR009 for `parameters [..]`. +func (b *Builder) ExitTemplateParams(ctx *parser.TemplateParamsContext) { + kw := ctx.PARAMETERS() + if kw == nil { + return + } + b.recordDeprecation(deprecation.PositionalTemplateArguments, kw.GetSymbol(), "text template") + arr, ok := ctx.ArrayLiteral().(*parser.ArrayLiteralContext) + if !ok || arr == nil || arr.LBRACKET() == nil || arr.RBRACKET() == nil { + b.fixLastDeprecation(deprecation.PositionalTemplateArguments, nil, "the list is incomplete") + return + } + var items []antlr.ParserRuleContext + for _, l := range arr.AllLiteral() { + items = append(items, l) + } + if len(items) == 0 { + // `parameters []` binds nothing: the clause goes, with the space before it. + if prev := previousTokenStop(ctx); prev >= 0 { + edit := ast.TextEdit{Start: prev + 1, Stop: arr.RBRACKET().GetSymbol().GetStop() + 1} + b.fixLastDeprecation(deprecation.PositionalTemplateArguments, &ast.Fix{Edits: []ast.TextEdit{edit}}, "") + } + return + } + b.fixLastDeprecation(deprecation.PositionalTemplateArguments, + numberedListFix(kw.GetSymbol(), arr.LBRACKET().GetSymbol(), items, arr.RBRACKET().GetSymbol()), "") +} + +// previousTokenStop is the rune offset of the last rune before ctx's first +// token that is not hidden, or -1. +func previousTokenStop(ctx antlr.ParserRuleContext) int { + start := ctx.GetStart() + if start == nil || start.GetInputStream() == nil { + return -1 + } + return start.GetStart() - 1 - leadingSpace(start) +} + +// leadingSpace counts the whitespace runes right before tok. +func leadingSpace(tok antlr.Token) int { + in := tok.GetInputStream() + n := 0 + for i := tok.GetStart() - 1; i >= 0; i-- { + r := in.GetText(i, i) + if r != " " && r != "\t" && r != "\n" && r != "\r" { + break + } + n++ + } + return n +} + +// checkTemplateNumbering refuses a `with ({n} = …)` list on a show message or +// validation feedback whose numbers are not 1..N, each once: the model holds +// the arguments as a list, so `{3}` with no `{2}` has no place to go. +func (b *Builder) checkTemplateNumbering(ctx parser.ITemplateParamsContext) { + if ctx == nil { + return + } + tp := ctx.(*parser.TemplateParamsContext) + if tp.WITH() == nil { + return + } + var nums []int + for _, p := range tp.AllTemplateParam() { + if n := p.(*parser.TemplateParamContext).NUMBER_LITERAL(); n != nil { + var v int + fmt.Sscanf(n.GetText(), "%d", &v) + nums = append(nums, v) + } + } + sort.Ints(nums) + for i, n := range nums { + if n != i+1 { + b.addError(fmt.Errorf("line %d: the placeholders of a text template are numbered {1} to {%d}, "+ + "each once; got %v", tp.GetStart().GetLine(), len(nums), nums)) + return + } + } +} + +// templateArgsByNumber orders a `with ({n} = …)` list by its numbers, the +// positional list a show message or validation feedback stores. +func templateArgsByNumber(params []ast.TemplateParam) []ast.Expression { + sorted := append([]ast.TemplateParam(nil), params...) + sort.SliceStable(sorted, func(i, j int) bool { return sorted[i].Index < sorted[j].Index }) + out := make([]ast.Expression, 0, len(sorted)) + for _, p := range sorted { + out = append(out, p.Value) + } + return out +} diff --git a/mdl/visitor/visitor_argument_binding_test.go b/mdl/visitor/visitor_argument_binding_test.go new file mode 100644 index 0000000000..47f22a9185 --- /dev/null +++ b/mdl/visitor/visitor_argument_binding_test.go @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: Apache-2.0 + +package visitor + +import ( + "reflect" + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/deprecation" +) + +// R4 (ako/mxcli#751): every call site binds an argument as `Param = +// expression`, and every text template as `with ({1} = expression)`. Each old +// spelling must record its code, and build exactly the statement its canonical +// form builds — the proof that it is an alias and not a change of meaning. +func TestArgumentBindingAliases(t *testing.T) { + const page = "create page M.P (Title: 'P', Layout: Atlas_Core.Atlas_Default) { dataview dv (DataSource: $O) { %s } };" + mf := func(body string) string { + return "create microflow M.F ($O: M.E, $N: String) begin " + body + " end;" + } + wf := func(body string) string { + return "create workflow M.W parameter $WorkflowContext: M.E begin " + body + " end workflow;" + } + pg := func(widget string) string { return strings.Replace(page, "%s", widget, 1) } + cases := []struct { + name, old, canonical string + codes []string + }{ + {"call microflow", mf("call microflow M.G($Order = $O, Force = false);"), + mf("call microflow M.G(Order = $O, Force = false);"), []string{deprecation.DollarArgumentName}}, + {"call nanoflow", mf("$R = call nanoflow M.G($Order = $O);"), + mf("$R = call nanoflow M.G(Order = $O);"), []string{deprecation.DollarArgumentName}}, + {"call java action", mf("$R = call java action M.J($Amount = 1);"), + mf("$R = call java action M.J(Amount = 1);"), []string{deprecation.DollarArgumentName}}, + {"keyword-named parameter is quoted only when it must be", mf("call microflow M.G($Page = $O);"), + mf("call microflow M.G(Page = $O);"), []string{deprecation.DollarArgumentName}}, + {"send rest request", mf("$R = send rest request M.C.Get with ($id = $N);"), + mf("$R = send rest request M.C.Get with (id = $N);"), []string{deprecation.DollarArgumentName}}, + {"show page dollar", mf("show page M.P($Order = $O);"), + mf("show page M.P(Order = $O);"), []string{deprecation.DollarArgumentName}}, + {"show page colon", mf("show page M.P(Order: $O);"), + mf("show page M.P(Order = $O);"), []string{deprecation.ColonArgument}}, + {"button action colon", pg("actionbutton b (Caption: 'Go', Action: microflow M.G(Order: $currentObject))"), + pg("actionbutton b (Caption: 'Go', Action: microflow M.G(Order = $currentObject))"), []string{deprecation.ColonArgument}}, + {"button action dollar", pg("actionbutton b (Caption: 'Go', Action: nanoflow M.G($Order = $currentObject))"), + pg("actionbutton b (Caption: 'Go', Action: nanoflow M.G(Order = $currentObject))"), []string{deprecation.DollarArgumentName}}, + {"show_page action colon", pg("actionbutton b (Caption: 'Go', Action: show_page M.Q(Order: $currentObject))"), + pg("actionbutton b (Caption: 'Go', Action: show_page M.Q(Order = $currentObject))"), []string{deprecation.ColonArgument}}, + {"data source colon", pg("listview lv (DataSource: microflow M.DS(Order: $O, Limit: 10)) { }"), + pg("listview lv (DataSource: microflow M.DS(Order = $O, Limit = 10)) { }"), + []string{deprecation.ColonArgument, deprecation.ColonArgument}}, + {"workflow call microflow", wf("call microflow M.G as act1 comment 'Go' with (M.G.Order = '$WorkflowContext');"), + wf("call microflow M.G(Order = $WorkflowContext) as act1 comment 'Go';"), []string{deprecation.WorkflowStringArgument}}, + {"workflow call workflow", wf("call workflow M.Sub comment 'Sub' with (Order = '$WorkflowContext/M.E_Other');"), + wf("call workflow M.Sub(Order = $WorkflowContext/M.E_Other) comment 'Sub';"), []string{deprecation.WorkflowStringArgument}}, + {"workflow expression with a string inside", wf("call microflow M.G with (Label = 'if $WorkflowContext/Name = ''x'' then ''a'' else ''b''');"), + wf("call microflow M.G(Label = if $WorkflowContext/Name = 'x' then 'a' else 'b');"), []string{deprecation.WorkflowStringArgument}}, + {"show message objects", mf("show message 'Hi {1} {2}' type Warning objects [$N, $O/Name] blocking;"), + mf("show message 'Hi {1} {2}' type Warning with ({1} = $N, {2} = $O/Name) blocking;"), []string{deprecation.PositionalTemplateArguments}}, + {"validation feedback objects", mf("validation feedback $O/Name message '{1} is wrong' objects [$N];"), + mf("validation feedback $O/Name message '{1} is wrong' with ({1} = $N);"), []string{deprecation.PositionalTemplateArguments}}, + {"log parameters", mf("log info 'a {1} {2}' parameters ['x', 2];"), + mf("log info 'a {1} {2}' with ({1} = 'x', {2} = 2);"), []string{deprecation.PositionalTemplateArguments}}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + old := mustBuild(t, c.old) + if got := deprecationCodes(old); !reflect.DeepEqual(got, c.codes) { + t.Errorf("old form recorded %v, want %v", got, c.codes) + } + for _, d := range old.Deprecations { + if d.Fix == nil { + t.Errorf("%s at line %d has no rewrite: %s", d.Code, d.Line, d.NoFix) + } + } + canon := mustBuild(t, c.canonical) + if got := deprecationCodes(canon); len(got) != 0 { + t.Errorf("canonical form recorded %v, want none", got) + } + if !reflect.DeepEqual(old.Statements, canon.Statements) { + t.Errorf("the two forms build different statements:\n old: %#v\n canon: %#v", old.Statements, canon.Statements) + } + }) + } +} + +// The canonical workflow argument stores the bare expression's text, exactly +// what the string form stored. +func TestWorkflowCallArgumentStoresTheExpression(t *testing.T) { + prog := mustBuild(t, "create workflow M.W parameter $WorkflowContext: M.E begin "+ + "call microflow M.G(Order = $WorkflowContext, Count = 1 + 2); end workflow;") + wf := prog.Statements[0].(*ast.CreateWorkflowStmt) + cm := wf.Activities[0].(*ast.WorkflowCallMicroflowNode) + want := []ast.WorkflowParameterMappingNode{ + {Parameter: "Order", Expression: "$WorkflowContext"}, + {Parameter: "Count", Expression: "1 + 2"}, + } + if !reflect.DeepEqual(cm.ParameterMappings, want) { + t.Errorf("ParameterMappings = %+v, want %+v", cm.ParameterMappings, want) + } +} + +// A workflow string argument whose content is not an expression that reads +// back as itself keeps its string: the upgrade reports it rather than +// changing the stored expression. +func TestWorkflowStringArgumentWithoutBareFormHasNoRewrite(t *testing.T) { + for _, arg := range []string{"' $WorkflowContext'", "''", "'$WorkflowContext +'"} { + prog := mustBuild(t, "create workflow M.W parameter $WorkflowContext: M.E begin "+ + "call microflow M.G with (Order = "+arg+"); end workflow;") + if len(prog.Deprecations) != 1 || prog.Deprecations[0].Fix != nil || prog.Deprecations[0].NoFix == "" { + t.Errorf("%s: deprecations = %+v, want one with no rewrite and a reason", arg, prog.Deprecations) + } + } +} + +func TestWorkflowCallWithBothArgumentListsIsAnError(t *testing.T) { + _, errs := Build("create workflow M.W parameter $WorkflowContext: M.E begin " + + "call microflow M.G(Order = $WorkflowContext) with (Order = '$WorkflowContext'); end workflow;") + if len(errs) == 0 || !strings.Contains(errs[0].Error(), "two argument lists") { + t.Errorf("errs = %v, want the two-lists error", errs) + } +} + +// with ({n} = …) on a show message is stored by number, and the numbers must +// be 1..N: the model holds a list, so a gap has nowhere to go. +func TestTemplateArgumentsByNumber(t *testing.T) { + prog := mustBuild(t, "create microflow M.F ($A: String, $B: String) begin "+ + "show message '{1} {2}' with ({2} = $B, {1} = $A); end;") + sm := prog.Statements[0].(*ast.CreateMicroflowStmt).Body[0].(*ast.ShowMessageStmt) + if len(sm.TemplateArgs) != 2 || !strings.Contains(fmtExpr(sm.TemplateArgs[0]), "A") { + t.Errorf("TemplateArgs = %#v, want $A then $B", sm.TemplateArgs) + } + for _, bad := range []string{"with ({1} = $A, {3} = $B)", "with ({1} = $A, {1} = $B)", "with ({2} = $A)"} { + _, errs := Build("create microflow M.F ($A: String, $B: String) begin show message 'x' " + bad + "; end;") + if len(errs) == 0 || !strings.Contains(errs[0].Error(), "numbered {1}") { + t.Errorf("%s: errs = %v, want the numbering error", bad, errs) + } + } +} + +func fmtExpr(e ast.Expression) string { + if v, ok := e.(*ast.VariableExpr); ok { + return v.Name + } + return reflect.ValueOf(e).String() +} + +func TestBareExpression(t *testing.T) { + for expr, want := range map[string]bool{ + "$WorkflowContext": true, + "$WorkflowContext/M.A/Name": true, + "1 + 2": true, + "'a''b'": true, + "": false, + " $WorkflowContext": false, + "$WorkflowContext +": false, + "$WorkflowContext; drop x": false, + } { + if got := BareExpression(expr); got != want { + t.Errorf("BareExpression(%q) = %v, want %v", expr, got, want) + } + } +} + +func TestParameterNameSpelling(t *testing.T) { + for name, want := range map[string]string{ + "Order": "Order", + "Page": "Page", + "with": "with", + "a b": `"a b"`, + } { + if got := ParameterNameSpelling(name); got != want { + t.Errorf("ParameterNameSpelling(%q) = %q, want %q", name, got, want) + } + } +} diff --git a/mdl/visitor/visitor_microflow_actions.go b/mdl/visitor/visitor_microflow_actions.go index 6c3559f464..7b3cc39ea0 100644 --- a/mdl/visitor/visitor_microflow_actions.go +++ b/mdl/visitor/visitor_microflow_actions.go @@ -1241,14 +1241,20 @@ func buildShowPageArgList(ctx parser.IShowPageArgListContext) []ast.ShowPageArg arg := argCtx.(*parser.ShowPageArgContext) spa := ast.ShowPageArg{} - if iok := arg.IdentifierOrKeyword(); iok != nil { - // Widget-style: Param: $value + if pn := arg.ParameterName(); pn != nil { + // Canonical (R4): Param = $value + spa.ParamName = parameterNameText(pn) + if expr := arg.Expression(); expr != nil { + spa.Value = buildSourceExpression(expr) + } + } else if iok := arg.IdentifierOrKeyword(); iok != nil { + // Deprecated (MDL-DEPR007): Param: $value spa.ParamName = identifierOrKeywordText(iok) if expr := arg.Expression(); expr != nil { spa.Value = buildSourceExpression(expr) } } else { - // Canonical: $Param = $value + // Deprecated (MDL-DEPR006): $Param = $value vars := arg.AllVARIABLE() if len(vars) >= 1 { spa.ParamName = strings.TrimPrefix(vars[0].GetText(), "$") @@ -1286,7 +1292,11 @@ func buildShowMessageStatement(ctx parser.IShowMessageStatementContext) *ast.Sho stmt.Type = id.GetText() } - // Build template arguments (optional) + // Build template arguments (optional): `with ({1} = e)`, or the deprecated + // positional `objects [e]` (MDL-DEPR009). + if tp := smCtx.TemplateParams(); tp != nil { + stmt.TemplateArgs = templateArgsByNumber(buildTemplateParams(tp)) + } if exprList := smCtx.ExpressionList(); exprList != nil { listCtx := exprList.(*parser.ExpressionListContext) allExprs := listCtx.AllExpression() @@ -1380,7 +1390,11 @@ func buildValidationFeedbackStatement(ctx parser.IValidationFeedbackStatementCon stmt.Message = buildSourceExpression(msgExpr) } - // Build template arguments (optional) + // Build template arguments (optional): `with ({1} = e)`, or the deprecated + // positional `objects [e]` (MDL-DEPR009). + if tp := vfCtx.TemplateParams(); tp != nil { + stmt.TemplateArgs = templateArgsByNumber(buildTemplateParams(tp)) + } if exprList := vfCtx.ExpressionList(); exprList != nil { listCtx := exprList.(*parser.ExpressionListContext) allExprs := listCtx.AllExpression() @@ -1678,6 +1692,8 @@ func buildSendRestRequestStatement(ctx parser.ISendRestRequestStatementContext) param := ast.SendRestParamDef{} if v := pc.VARIABLE(); v != nil { param.Name = strings.TrimPrefix(v.GetText(), "$") + } else if pn := pc.ParameterName(); pn != nil { + param.Name = parameterNameText(pn) } if expr := pc.Expression(); expr != nil { param.Expression = expressionSourceText(expr) diff --git a/mdl/visitor/visitor_microflow_expression.go b/mdl/visitor/visitor_microflow_expression.go index 3d32c4efde..b032c8a38a 100644 --- a/mdl/visitor/visitor_microflow_expression.go +++ b/mdl/visitor/visitor_microflow_expression.go @@ -256,6 +256,9 @@ func buildMultiplicativeExpression(ctx parser.IMultiplicativeExpressionContext) result = pathExpr continue } + if attachPathToLastOperand(result, right) { + continue + } } result = &ast.BinaryExpr{ @@ -268,6 +271,32 @@ func buildMultiplicativeExpression(ctx parser.IMultiplicativeExpressionContext) return result } +// attachPathToLastOperand makes `/ Member` a member step of the operand it +// belongs to when a multiplicative operator came first. `/` shares a precedence +// level with `*`, `div` and `mod`, so `$a/X * $b/Y` is parsed left to right as +// `(($a/X) * $b) / Y`: by the time the `/` is seen, `$b` is already the right +// operand of the product, and tryBuildAttributePath (which looks at the whole left +// side) cannot reach it. Mendix has no `/` division — it is always navigation +// when a member name follows — so the step moves onto `$b`, giving +// `$a/X * ($b/Y)`, the tree the source means. Reports whether the step was +// attached. +func attachPathToLastOperand(left ast.Expression, right ast.Expression) bool { + bin, ok := left.(*ast.BinaryExpr) + if !ok { + return false + } + switch bin.Operator { + case "*", "div", "mod", "%", ":": + default: + return false + } + if pathExpr := tryBuildAttributePath(bin.Right, right); pathExpr != nil { + bin.Right = pathExpr + return true + } + return false +} + // tryBuildAttributePath attempts to build an AttributePathExpr from a left expression // and a right identifier. Returns nil if not an XPath-style path. func tryBuildAttributePath(left ast.Expression, right ast.Expression) *ast.AttributePathExpr { diff --git a/mdl/visitor/visitor_page_v3.go b/mdl/visitor/visitor_page_v3.go index 99815096e2..2b3336413f 100644 --- a/mdl/visitor/visitor_page_v3.go +++ b/mdl/visitor/visitor_page_v3.go @@ -1192,11 +1192,14 @@ func buildMicroflowArgV3(ctx parser.IMicroflowArgV3Context) ast.FlowArgV3 { argCtx := ctx.(*parser.MicroflowArgV3Context) arg := ast.FlowArgV3{} - if v := argCtx.VARIABLE(); v != nil { - // Microflow-style: $Param = $value + if pn := argCtx.ParameterName(); pn != nil { + // Canonical (R4): Param = $value + arg.Name = parameterNameText(pn) + } else if v := argCtx.VARIABLE(); v != nil { + // Deprecated (MDL-DEPR006): $Param = $value arg.Name = strings.TrimPrefix(v.GetText(), "$") } else if iok := argCtx.IdentifierOrKeyword(); iok != nil { - // Widget-style: Param: $value. identifierOrKeyword accepts a bare + // Deprecated (MDL-DEPR007): Param: $value. identifierOrKeyword accepts a bare // keyword (View/Source/Item/Page/Entity) or a "quoted" name; // identifierOrKeywordText unquotes as needed. arg.Name = identifierOrKeywordText(iok) diff --git a/mdl/visitor/visitor_workflow.go b/mdl/visitor/visitor_workflow.go index f236c3397e..595c4ca161 100644 --- a/mdl/visitor/visitor_workflow.go +++ b/mdl/visitor/visitor_workflow.go @@ -677,7 +677,10 @@ func buildWorkflowCallMicroflow(ctx parser.IWorkflowCallMicroflowStmtContext) *a } // Parameter mappings (Issue #10) - node.ParameterMappings = buildWorkflowParameterMappings(cmCtx.AllWorkflowParameterMapping()) + node.ParameterMappings = buildWorkflowCallArguments(cmCtx.WorkflowCallArguments()) + if len(node.ParameterMappings) == 0 { + node.ParameterMappings = buildWorkflowParameterMappings(cmCtx.AllWorkflowParameterMapping()) + } // BoundaryEvents (Issue #7) for _, beCtx := range cmCtx.AllWorkflowBoundaryEventClause() { @@ -743,7 +746,10 @@ func buildWorkflowCallWorkflow(ctx parser.IWorkflowCallWorkflowStmtContext) *ast } // Parameter mappings - node.ParameterMappings = buildWorkflowParameterMappings(cwCtx.AllWorkflowParameterMapping()) + node.ParameterMappings = buildWorkflowCallArguments(cwCtx.WorkflowCallArguments()) + if len(node.ParameterMappings) == 0 { + node.ParameterMappings = buildWorkflowParameterMappings(cwCtx.AllWorkflowParameterMapping()) + } return node }