diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index 66acdf3a20..25fbe97b9e 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -868,6 +868,7 @@ {"area": "mdl/executor", "date": "2026-10-05", "symptom": "On Mendix 11.15, `TestMxCheck_DoctypeScripts/40-message-definition-examples.mdl` fails with CE0270 \"No root element could be found in the schema\" at MsgTest.IMM_Order. On a converted or Studio Pro 11.15 project, `list message definition collections` finds nothing, and `describe import mapping` prints no source", "cause": "Mendix 11.15 replaced `MessageDefinitions$MessageDefinitionCollection` with one `MessageDefinitions$MessageDefinition2` document per definition. `mx convert` turns the collection into a `Projects$Folder` with the same unit ID and name, and the ExposedEntity tree is byte-identical apart from $IDs. mxcli read and wrote only collections, so on 11.15 there was nothing to resolve a mapping against", "file": "`mdl/backend/modelsdk/messagedefinition_document.go` (read/write, reusing the collection's exposedNodeFromGen/messageNodeToGen), `mdl/executor/cmd_messagedefinition_documents.go` (create/describe/drop/list + version refusals), `mdl/executor/mapping_messagedefinition.go` (`findMessageDefinition(ctx, ref)` returns the stored reference), `cmd_messagedefinitions.go` (alter target resolution, association-drop guard), grammar `createMessageDefinitionStatement` / `DROP|DESCRIBE MESSAGE DEFINITION` / `LIST MESSAGE DEFINITIONS`", "insight": "**`mx convert` is the oracle for a storage change.** Converting a known 11.14 project gives a Studio Pro-authored 11.15 reference for the new shape and for how references move, so no guessing is needed. **Under the revert, 11.15 fails earlier than CE0270.** A two-part name written into the old `MessageDefinition` key makes mxbuild refuse to load the project (`StorageLoadException: ... is not a valid OldMessageDefinitionIdentifier`). The old key is typed as a three-part identifier even though 11.15 no longer reads it. The doctype script uses `-- @version: ..11.14` / `11.15+` sections, and the 11.15 mapping needs a different name from the older section's, because a plain `check` sees both sections and reports MDL-DUPDEF", "refs": ["ako/mxcli#987"]} {"area": "mdl/executor", "date": "2026-10-06", "symptom": "v0.25.0: `describe microflow` 4-8x slower than v0.24.0 on a flow arranged by hand in Studio Pro, and the cost grows faster than the flow (60 if/else blocks: 4.7 s -> 35.6 s); flows laid out by mxcli barely change", "cause": "The canonical describe's layout derivation (#748) re-renders the description every round, and a hand-laid flow takes gatedLayoutRounds+2 = 8 rounds. Each render re-ran the stored flow's split/merge analysis (findSplitMergePoints, labelCrossedMerges) and the body warnings (postDominators via microflowgraph.Analyze) - superlinear, and independent of which annotations the round keeps", "file": "`mdl/executor/cmd_microflows_derived_layout.go` (flowAnalysisMemo, installed by useDerivedFlowLayout, rebuildOnly view for the rounds), `mdl/executor/cmd_microflows_show.go` (findSplitMergePoints / labels / microflowBodyWarnings go through it)", "insight": "**Profile before believing the issue's theory.** The report blamed the round count, but rounds were already capped at 8; the rebuild itself was cheap. The CPU profile showed the cost was the *render* inside each round - the stored flow's graph analysis, redone per round. Memoising it per describe (keyed on the stored collection pointer) and leaving the `-- WARNING` comment lines out of rounds (the rebuild never reads them) gave 14.2 s -> 1.7 s at 60 blocks, with the printed description byte-identical to the unfixed build. A wall-clock assertion would be flaky; the test counts split/merge analyses per describe (18 over 8 rounds before, 2 after), with the round count as its control", "refs": ["mendixlabs/mxcli#1301"]} {"area":"mdl-executor","date":"2026-10-05","symptom":"A view entity selecting a **non-localized** DateTime column (Studio Pro's \"Localize\" unticked — the normal choice for a calendar date) passes `mxcli check --references` and exec, then `mx check` fails CE6770 \"View Entity is out of sync with the OQL Query.\" The view attribute was always written LocalizeDate = true, and MDL has no spelling for the flag, so describe -> exec re-broke it on every run","cause":"execCreateViewEntity built every view attribute with convertDataType, whose DateTime is Mendix's default LocalizeDate = true; nothing looked at the attribute the column reads","file":"`mdl/executor/oql_view_localize_date.go` (viewDateTimeLocalize), `mdl/executor/cmd_entities.go` (execCreateViewEntity); test `mdl/executor/view_entity_localize_date_test.go`; bug-test `mdl-examples/bug-tests/1297-view-entity-non-localized-datetime.mdl`","insight":"**Derive, don't add syntax**: on a view entity the column's localization is a property of the query, so the fix reads it from the source attribute and describe -> exec round-trips (`Unchanged`) with nothing new to spell — same reasoning as the view association (the column is the declaration). **Measure the shapes before choosing the scope**: one project, one view per shape, mxbuild 11.12.5 — pass-through, `s/Attr`, MIN, MAX and CASE over a non-localized source are ALL CE6770 when written localized and 0 errors when patched to false, so a pass-through-only rule (the obvious one, mirroring the string-length rule) would have fixed the report and left MAX(date) broken. Sources that disagree (coalesce of a localized and a non-localized column) are unmeasured and keep the default. **Repro needs a Studio Pro-authored flag**: mxcli writes every persistent DateTime localized, so the bug is invisible from MDL alone — the reporter's pymongo patch flipping Sale.SaleDate is the cheapest stand-in. Control: stubbing the assignment fails 5 of 6 cases with `LocalizeDate = true`; pre-fix binary on the same project gives 5 × CE6770, fixed gives 0","refs":["mendixlabs/mxcli#1297"]} +{"area": "mdl-executor", "date": "2026-10-07", "refs": ["mendixlabs/mxcli#1324"], "symptom": "A button directly inside data view `dvGate` passing `$dvGate` (its own container, by widget name) passes `check --references` and `exec`, then `mx check` reports `[error] [CE0117] \"Error(s) in expression.\" at Action button 'btnOwn'`. The same `$dvGate` from a button in a NESTED data view builds clean", "cause": "No check modelled which data-container names are in scope. A container's widget-name variable exists only for the data containers nested below it; in its own context the object is $currentObject", "file": "`mdl/executor/validate_page_button_context.go` (`checkButtonContextTree` carries `nearest`; `checkOwnContainerName`, `exprReadsVariable`, MDL-BUTTON02)", "insight": "**Measure the neighbours before writing the rule — the report's shape is one of six.** One probe page with ten buttons through `mx check` 11.14.0 settled the whole rule in one build: own data view, own data view through a plain container, a control bar of a grid inside it, `$dv/Attr`, a list view's own name from its item, a grid's own name from its column and a gallery's from its template are all CE0117; the enclosing name from a grid column or list item is clean, and so is a grid's own name from its control bar (the selection). So the rule is 'the NEAREST data container's name', and it moves exactly like the existing MDL-BUTTON01 context (a control bar takes its context from above its grid) — which is why it went into the same walk rather than a new one. A data-view-only fix, the literal reading of the issue, would have missed four of the six. **Unmeasured, deliberately not flagged:** the same name in a nested widget's DATA SOURCE arguments and in widget expressions (visibility, dynamic text). Control: stubbing the call fails all three tests with `got []`; end to end the fixed binary refuses the issue's script ('Nothing was written') and the suggested `$currentObject` rewrite builds at 0 errors. Repro `mdl-examples/bug-tests/1324-own-data-container-name.fail.mdl`; tests `validate_page_own_container_name_test.go`"} {"area": "mdl-executor", "date": "2026-10-07", "symptom": "`retrieve … where starts-with(Name, 'MS-' + $Key)` passes `check --references` and `exec`, then mx check CE0161 \"Error(s) in XPath constraint.\" — the same `+` at the constraint's top level (`Name = 'X-' + $Key`) builds clean", "cause": "MDL091 only matched expression-only function NAMES (startsWith/endsWith/getKey) by regex over the rendered XPath string; an operator inside a function ARGUMENT is a different expression construct the string regex never looked for", "file": "`mdl/executor/validate_microflow.go` (`xpathFunctionArgumentOperators`, `checkXPathFunctionArgumentOperators`, MDL091)", "insight": "Walk the `RetrieveStmt.Where` AST, not the XPath string: a call's argument that IS a `+`/`-` BinaryExpr is CE0161 (after unwrapping ParenExpr AND `SourceExpr` — the retrieve where-clause is wrapped in a SourceExpr, and a walker that misses it silently matches nothing, which is how the first cut passed nothing while compiling clean). Only the unbracketed form reaches the writer: the bracketed `[…]` grammar already refuses `+` in a function argument as a parse error. Measured on mxbuild 11.14.0 with a stubbed-out check to force the fault in: `+` and `-` in a string argument CE0161 (also nested in `not()`), `year-from-dateTime(Due) = $N - 1` clean. `*`/`div`/`mod` deliberately NOT flagged — the only argument they fit is numeric, and the control `contains(Name, $N)` is already CE0161 with no operator, so no measurement isolates the operator; MDL091 is exec-enforced, so an unmeasured operator would be a write barrier on a guess. Tests `TestValidateMicroflow_XPathFunctionArgumentOperator`, `TestCheckExecAgree_RetrieveConstraintFunctionArgumentOperator`; repro `mdl-examples/bug-tests/1326-xpath-function-argument-operator.fail.mdl`", "refs": ["mendixlabs/mxcli#1326", "mendixlabs/mxcli#1213"]} {"area": "mdl/executor", "date": "2026-10-07", "symptom": "`set $Cursor = $Next;` where both are OBJECT variables (Reference retrieves followed from the FROM entity) passes `check --references` and `exec` (\"Created microflow\"), is written as a Microflows$ChangeVariableAction, and mx check reports [CE7247] \"Variable 'Cursor' does not have a primitive type.\" Natural to write when walking a parent chain in a `while` loop", "cause": "`set` on a plain variable had two outcomes: Change list Replace for a known list (ako/mxcli#949) and Change variable for everything else, so an object fell into the primitive-only action. Mendix has NO action that reassigns an object variable. Check could not have caught it even with a guard: the check-time validator (validateFlowBody) typed every association retrieve as `List of `, because it had no association multiplicity, so the forward-Reference object read as a list", "file": "`mdl/executor/cmd_microflows_builder_actions.go` (`objectVariableType`, `refuseSetOnObject`), called from `cmd_microflows_builder_graph.go` (exec) and `cmd_microflows_builder_validate.go` (check); `validate.go` passes `checkAssociationShapes(ctx, sc)` into `validateFlowBody` so `forwardReferenceTarget` types a forward Reference retrieve as an object", "insight": "A refusal is only as good as the variable typing under it, and check and exec type variables differently: exec asks the backend for the association, check guessed `List of`. A guard added to the check validator alone passes the reported repro because the object it should refuse is, to check, a list. Write the check-path test against the REPORTED shape (association retrieve), not an object parameter, or it goes green while the issue stays open. Reuse `checkAssociationShapes` (project + script associations, built for MDL-ASSOCDS01) rather than a new lookup. Type only the unambiguous shape (Reference followed from FROM, not a self-association): Mendix types a self-association retrieve as a list, and that `set` is a valid Change list Replace. Measured on 11.14.0: faulted build exec -> CE7247; the self-association and recursive sub-microflow forms -> 0 errors. Plain `check` with no -p does not run validateFlowBody at all, so a `.fail.mdl` cannot pin this", "refs": ["mendixlabs/mxcli#1323", "ako/mxcli#949"], "ce": ["CE7247"]} {"area": "mdl/executor", "date": "2026-10-07", "symptom": "Plain `mxcli check` (no -p) passed `set $X = $Y;` on an OBJECT variable (entity parameter, create, retrieve first, loop iterator, head) even after check --references and exec refused it; the build then failed with CE7247. The bug-test could not be a .fail.mdl because check-mdl runs plain check", "cause": "Plain check runs only the MDL0xx rule set (ValidateProgram -> ValidateMicroflow/ValidateNanoflow); validateFlowBody, where the first #1323 refusal lived, runs only under --references (validate.go) and in exec. A refusal added to one validator is invisible to the other path", "file": "`mdl/executor/validate_microflow_set_object.go` (`checkSetOnObjectVariable`, MDL-SET01), wired from `validate_microflow.go` (validate) and `validate_nanoflow.go`; `cmd_microflows_builder_validate.go` now reports only `assocObjectVars`", "insight": "Know which validator each entry point runs before adding a refusal: plain check = ValidateMicroflow; --references = that PLUS validateFlowBody; exec = the builder (plus execEnforcedMicroflowRules). Putting the rule in both check validators made --references print it twice; split by what each can see — the rule takes every object visible in the text, validateFlowBody only the association-typed ones that need the project. Measure every producer a rule judges, with the faulted build: all five plus a nanoflow gave CE7247 on 11.14.0, but a PARAMETER gives different wording, \"Parameter 'A' cannot be changed.\" — and that wording also fires for a PRIMITIVE parameter (`set $N = 1` on `$N: Integer`), a separate unrefused gap the object-only rule does not cover. Leave `find` out of the object producers: it is also the String function", "refs": ["mendixlabs/mxcli#1323", "ako/mxcli#1011"], "ce": ["CE7247"], "rules": ["MDL-SET01"]} diff --git a/.claude/skills/mendix/cheatsheet-errors/SKILL.md b/.claude/skills/mendix/cheatsheet-errors/SKILL.md index 578f4695f8..002af66d67 100644 --- a/.claude/skills/mendix/cheatsheet-errors/SKILL.md +++ b/.claude/skills/mendix/cheatsheet-errors/SKILL.md @@ -265,6 +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 | +| CE0117 | …on a button inside a data container | The button passes the container it sits in by its widget name (`$dvGate` inside `dvGate`). That name is a variable only for containers nested below it; use `$currentObject` there. MDL-BUTTON02 | | 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: call 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 | diff --git a/.claude/skills/mendix/create-page/reference/widgets.md b/.claude/skills/mendix/create-page/reference/widgets.md index e6e1154cc9..24d650928d 100644 --- a/.claude/skills/mendix/create-page/reference/widgets.md +++ b/.claude/skills/mendix/create-page/reference/widgets.md @@ -1068,6 +1068,15 @@ A **container** takes an argument list exactly like an `actionbutton` does — t two share one action grammar. Reaching for a button because a container "cannot pass parameters" changes the rendering for no reason (mendixlabs/mxcli#1082). +**A data container's name is a variable only *below* it.** `$dvOrder` reads data +view `dvOrder`'s object from a data view, list or grid nested inside it. In the +container's own context — directly inside it, through plain containers, or in the +control bar of a grid inside it — the object is `$currentObject`, and +`$dvOrder` is **CE0117** "Error(s) in expression." (`mxcli check` reports +MDL-BUTTON02). The same goes for a list view, gallery or grid read by its own +name from its item or row. A grid's own name *is* valid from its control bar — +that is the selection, above (mendixlabs/mxcli#1324). + ### Charts (Charts.mpk — ColumnChart / BarChart / AreaChart / PieChart) Charts are pluggable widgets whose data lives in one or more `series` object-list diff --git a/CHANGELOG.md b/CHANGELOG.md index 61357ebe0d..1bc075cf26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- **`check` refuses a button that passes its own data container by widget name** (mendixlabs/mxcli#1324) — `actionbutton btnOwn (Action: call microflow M.F(Gate = $dvGate))` directly inside data view `dvGate` passed `check --references` and `exec`, then `mx check` reported `[CE0117] "Error(s) in expression." at Action button 'btnOwn'`. A data container's name is a variable only for the containers nested below it; in its own context the object is `$currentObject`. Reported as **MDL-BUTTON02** (error, so `exec` refuses it), for data views, list views, galleries and data grids alike, including attribute paths (`$dvGate/Name`) and a control bar inside the container. A grid's own name from its control bar (the selection) and an enclosing container's name from a nested one are not flagged. The flagged set matches mxbuild 11.14.0's CE0117s widget for widget on a ten-button probe page. - **`set $Obj = …` on an object variable is refused** (mendixlabs/mxcli#1323) — with both variables single objects (e.g. a Reference retrieved from its FROM entity), `set $Cursor = $Next;` passed `check --references` and `exec` and was written as a Change variable action, which mxbuild refuses with CE7247 "Variable 'Cursor' does not have a primitive type". Mendix has no action that reassigns an object variable, so `check` (MDL-SET01, for the objects it can see without a project), `check --references` and `exec` now refuse it and name the alternatives (a sub-microflow that returns the next object, `change $Obj (…)`). `check --references` now types a Reference retrieve from its FROM entity as the object it is. `set` on a list variable stays a Change list Replace (ako/mxcli#949). - **`RENAME JAVA ACTION` renames the Java class, not just the file** — renaming `M.JA_Old` to `JA_New` moved the source to `JA_New.java` but left `public class JA_Old`, its constructor and its `toString` inside it, which javac rejects (`class JA_Old is public, should be declared in a file named JA_Old.java`). A full build hid it, because mxbuild regenerates the stub in its own copy; `run --local --watch` hot reload and IDEs compile the file on disk. Those three places now follow the rename exactly as mxbuild rewrites them; user code and extra code are left as written. Applies to `mxcli rename java-action` too. - **`run --local --watch` starts on Mendix 10.24 and 11.6** — it waited the full web-client timeout (5 minutes) for a bundle that had finished in seconds, then failed with `web client watcher timed out`. The rollup runner those versions ship reports its status as `{"code":"SUCCESS"}` rather than the modern-web-bundler protocol mxcli was reading; both are now understood, including that runner's error reports. diff --git a/mdl-examples/bug-tests/1324-own-data-container-name.fail.mdl b/mdl-examples/bug-tests/1324-own-data-container-name.fail.mdl new file mode 100644 index 0000000000..eefd5f6326 --- /dev/null +++ b/mdl-examples/bug-tests/1324-own-data-container-name.fail.mdl @@ -0,0 +1,33 @@ +-- mendixlabs/mxcli#1324: a button passing its OWN data container by widget name +-- +-- NEGATIVE TEST (.fail.mdl) — EXPECTED to fail `mxcli check`. +-- `make check-mdl` inverts the exit code: an unexpected pass is a regression of +-- the MDL-BUTTON02 rule. +-- +-- Symptom: `check --references` and `exec` were clean, then `mx check` reported +-- [error] [CE0117] "Error(s) in expression." at Action button 'btnOwn' +-- A data container's widget name ($dvGate) is a variable only for the data +-- containers nested below it. Directly inside dvGate (a plain container is +-- transparent) the object is $currentObject. The same holds for a list view, +-- gallery or data grid read by its own name from its item or row, and for a +-- control bar, which takes its context from above its grid. +-- +-- Fix: `mxcli check` flags it as MDL-BUTTON02 (error), so exec refuses it. +-- btnCur and btnOuter build clean (measured, Mendix 11.14.0) and are not flagged. + +create module G52; +create persistent entity G52.Gate (Name: String(50)); +create microflow G52.ACT_Open ($Gate: G52.Gate) +begin + log info node 'G52' 'open'; +end; + +create page G52.GatePage (Title: 'Gate', Layout: Atlas_Core.PopupLayout, Params: ( $Gate: G52.Gate )) { + dataview dvGate (DataSource: $Gate) { + actionbutton btnOwn (Caption: 'Own data view name', Action: call microflow G52.ACT_Open(Gate = $dvGate)) + actionbutton btnCur (Caption: 'Current object', Action: call microflow G52.ACT_Open(Gate = $currentObject)) + dataview dvInner (DataSource: $Gate) { + actionbutton btnOuter (Caption: 'Enclosing data view name', Action: call microflow G52.ACT_Open(Gate = $dvGate)) + } + } +}; diff --git a/mdl/executor/validate_page_button_context.go b/mdl/executor/validate_page_button_context.go index fe9071b091..d8ea51bc60 100644 --- a/mdl/executor/validate_page_button_context.go +++ b/mdl/executor/validate_page_button_context.go @@ -25,7 +25,7 @@ func ValidatePageButtonContext(prog *ast.Program) []linter.Violation { var out []linter.Violation for _, stmt := range prog.Statements { if label, widgets, ok := documentWidgets(stmt); ok { - out = append(out, checkButtonContextTree(widgets, "", false, label)...) + out = append(out, checkButtonContextTree(widgets, "", false, "", label)...) } } return out @@ -46,29 +46,102 @@ func ValidatePageButtonContext(prog *ast.Program) []linter.Violation { // context, and its control bar's $currentObject is that object — mxbuild builds // it clean. The grid's OWN data source never scopes its control bar, so the // control bar inherits the context from above the grid, not from the grid. -func checkButtonContextTree(widgets []*ast.WidgetV3, controlBarOf string, inContext bool, locationPrefix string) []linter.Violation { +// +// nearest is the name of the data container whose object the widget sits in +// ("" at the top), for MDL-BUTTON02 (mendixlabs/mxcli#1324). It moves with the +// same rule as inContext: a control bar keeps the context from above its grid. +func checkButtonContextTree(widgets []*ast.WidgetV3, controlBarOf string, inContext bool, nearest, locationPrefix string) []linter.Violation { var out []linter.Violation for _, w := range widgets { if w == nil { continue } - if controlBarOf != "" && !inContext { - if a := w.GetAction(); a != nil { + if a := w.GetAction(); a != nil { + if controlBarOf != "" && !inContext { out = append(out, checkControlBarAction(a, w.Name, controlBarOf, locationPrefix)...) } + if nearest != "" { + out = append(out, checkOwnContainerName(a, w.Name, nearest, locationPrefix)...) + } } childContext := inContext || isObjectContextContainer(w) + childNearest := nearest + if isObjectContextContainer(w) && w.Name != "" { + childNearest = w.Name + } for _, c := range w.Children { - childOf, ctx := controlBarOf, childContext + childOf, ctx, near := controlBarOf, childContext, childNearest if c != nil && strings.EqualFold(c.Type, "controlbar") { - childOf, ctx = w.Name, inContext + childOf, ctx, near = w.Name, inContext, nearest } - out = append(out, checkButtonContextTree([]*ast.WidgetV3{c}, childOf, ctx, locationPrefix)...) + out = append(out, checkButtonContextTree([]*ast.WidgetV3{c}, childOf, ctx, near, locationPrefix)...) } } return out } +// checkOwnContainerName flags an action argument (and its chained THEN action) +// that reads the nearest data container by its widget name — `$dvGate` from a +// button directly inside dvGate. A container's name is a variable only for the +// containers nested BELOW it; in its own context the object is +// $currentObject, and mxbuild reports the name as CE0117 "Error(s) in +// expression." (mendixlabs/mxcli#1324, measured on 11.14.0 for data views, +// list views, galleries and data grids alike). +func checkOwnContainerName(a *ast.ActionV3, widgetName, nearest, locationPrefix string) []linter.Violation { + var out []linter.Violation + for a != nil { + for _, arg := range a.Args { + s, ok := arg.Value.(string) + if !ok || !exprReadsVariable(s, nearest) { + continue + } + out = append(out, linter.Violation{ + RuleID: "MDL-BUTTON02", + Severity: linter.SeverityError, + Message: fmt.Sprintf( + "%s: `%s` passes `%s` to its %s action, but `%s` is the data container the widget sits in directly — its name is a variable only inside a data container nested below it (CE0117)", + locationPrefix, widgetName, s, a.Type, nearest), + Suggestion: fmt.Sprintf( + "Use $currentObject for the object of `%s` here (`$currentObject/Attr` for an attribute); `$%s` works from a data view, list or grid nested inside it.", + nearest, nearest), + }) + } + a = a.ThenAction + } + return out +} + +// exprReadsVariable reports whether expression source reads $name as a +// variable: a `$name` token outside a string literal, ending at a +// non-identifier character. Case-insensitive, as widget names are unique +// case-insensitively on a page. +func exprReadsVariable(expr, name string) bool { + inString := false + for i := 0; i < len(expr); i++ { + c := expr[i] + if c == '\'' { + inString = !inString // '' inside a literal toggles twice + continue + } + if inString || c != '$' { + continue + } + j := i + 1 + for j < len(expr) && isExprIdentByte(expr[j]) { + j++ + } + if strings.EqualFold(expr[i+1:j], name) { + return true + } + i = j - 1 + } + return false +} + +func isExprIdentByte(c byte) bool { + return c == '_' || c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' +} + // isObjectContextContainer reports whether w gives its (non-control-bar) // children a current object: a data view's object, a list view or gallery // item, a grid row (column content). diff --git a/mdl/executor/validate_page_own_container_name_test.go b/mdl/executor/validate_page_own_container_name_test.go new file mode 100644 index 0000000000..d2f0629570 --- /dev/null +++ b/mdl/executor/validate_page_own_container_name_test.go @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "sort" + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/visitor" +) + +// ownContainerFlagged returns the widgets MDL-BUTTON02 names, sorted. +func ownContainerFlagged(t *testing.T, src string) []string { + t.Helper() + prog, errs := visitor.Build(src) + if len(errs) > 0 { + t.Fatalf("parse errors: %v", errs) + } + var names []string + for _, v := range ValidatePageButtonContext(prog) { + if v.RuleID != "MDL-BUTTON02" { + continue + } + start := strings.Index(v.Message, "`") + end := strings.Index(v.Message[start+1:], "`") + names = append(names, v.Message[start+1:start+1+end]) + } + sort.Strings(names) + return names +} + +// mendixlabs/mxcli#1324: a data container's widget-name variable ($dvGate) is +// only in scope for widgets nested in ANOTHER data container below it. From the +// container's own direct context it is not a variable at all, and mxbuild +// 11.14.0 reports `[CE0117] "Error(s) in expression." at Action button 'btnOwn'` +// while check and exec were clean. This is the issue's reproduction verbatim. +func TestValidatePageButtonContext_OwnDataViewName_Issue1324(t *testing.T) { + src := `create page "G52"."GatePage" (Title: 'Gate', Layout: Atlas_Core.PopupLayout, Params: { $Gate: "G52"."Gate" }) { + dataview dvGate (DataSource: $Gate) { + actionbutton btnOwn (Caption: 'Own data view name', Action: microflow "G52"."ACT_Open"(Gate: $dvGate)) + actionbutton btnCur (Caption: 'Current object', Action: microflow "G52"."ACT_Open"(Gate: $currentObject)) + dataview dvInner (DataSource: $Gate) { + actionbutton btnOuter (Caption: 'Enclosing data view name', Action: microflow "G52"."ACT_Open"(Gate: $dvGate)) + } + } +};` + got := ownContainerFlagged(t, src) + if strings.Join(got, ",") != "btnOwn" { + t.Fatalf("want only btnOwn flagged (btnCur and btnOuter build clean), got %v", got) + } +} + +// Every verdict below was measured with `mx check` on Mendix 11.14.0: the +// flagged set is exactly the set mxbuild reported CE0117 for. The nearest +// data container decides — a plain container is transparent, a control bar +// takes the context from ABOVE its grid (so a grid's own selection stays +// spellable there), and a row or item of a nested list is a new context. +func TestValidatePageButtonContext_OwnContainerName_MeasuredShapes(t *testing.T) { + src := `create page G52.Probe (Title: 'Probe', Layout: Atlas_Core.PopupLayout, Params: ( $Gate: G52.Gate )) { + dataview dvP (DataSource: $Gate) { + container cWrap { + actionbutton btnInContainer (Caption: 'c', Action: microflow G52.ACT_Open(Gate = $dvP)) + } + actionbutton btnOwnPath (Caption: 'p', Action: microflow G52.ACT_Str(S = $dvP/Name)) + actionbutton btnLiteral (Caption: 'l', Action: microflow G52.ACT_Str(S = '$dvP')) + datagrid dgIn (DataSource: database from G52.Gate) { + controlbar cb1 { + actionbutton btnCtlBar (Caption: 'cb', Action: microflow G52.ACT_Open(Gate = $dvP)) + } + column Name (Attribute: Name) { + actionbutton btnInColumn (Caption: 'col', Action: microflow G52.ACT_Open(Gate = $dvP)) + } + } + listview lvIn (DataSource: database from G52.Gate) { + actionbutton btnInList (Caption: 'li', Action: microflow G52.ACT_Open(Gate = $dvP)) + actionbutton btnLvOwn (Caption: 'lo', Action: microflow G52.ACT_Open(Gate = $lvIn)) + } + } + datagrid dgSelf (DataSource: database from G52.Gate, Selection: Single) { + controlbar cb2 { + actionbutton btnSelCtl (Caption: 'sel', Action: microflow G52.ACT_Open(Gate = $dgSelf)) + } + column Name (Attribute: Name) { + actionbutton btnDgOwnCol (Caption: 'dg', Action: microflow G52.ACT_Open(Gate = $dgSelf)) + } + } + gallery gaSelf (DataSource: database from G52.Gate) { + template t1 { + actionbutton btnGaOwn (Caption: 'ga', Action: microflow G52.ACT_Open(Gate = $gaSelf)) + } + } +};` + got := strings.Join(ownContainerFlagged(t, src), ",") + want := "btnCtlBar,btnDgOwnCol,btnGaOwn,btnInContainer,btnLvOwn,btnOwnPath" + if got != want { + t.Fatalf("flagged %q, want %q (mx check 11.14.0's CE0117 set)", got, want) + } +} + +// The suggestion is the spelling that works where the button is. +func TestValidatePageButtonContext_OwnContainerName_Suggestion(t *testing.T) { + prog, errs := visitor.Build(`create page P.X (Title: 'x', Layout: Atlas_Core.PopupLayout, Params: ( $O: P.O )) { + dataview dvO (DataSource: $O) { + actionbutton b1 (Caption: 'b', Action: microflow P.F(O = $dvO)) + } +};`) + if len(errs) > 0 { + t.Fatalf("parse errors: %v", errs) + } + vs := ValidatePageButtonContext(prog) + if len(vs) != 1 || vs[0].RuleID != "MDL-BUTTON02" { + t.Fatalf("want one MDL-BUTTON02, got %+v", vs) + } + if !strings.Contains(vs[0].Message, "CE0117") || !strings.Contains(vs[0].Suggestion, "$currentObject") { + t.Errorf("message should name CE0117 and suggest $currentObject: %+v", vs[0]) + } +}