Skip to content

Commit fa0dc4f

Browse files
committed
fix(check): catch five script shapes that passed check --references
Each of these passed `mxcli check -p app.mpr --references` and then failed at exec or at mxbuild (reported from the marketplace-rnd project): - XPath `[Module.Assoc != empty]` / `= empty` (CE0161): MDL047 now covers associations, with a path-form hint. - retrieve over a Reference from its TO end (owner not Both) whose result is used as an object (CE0117): reported with a head()/limit 1 hint. - page layout that does not exist: resolved against the project and the script's CREATE LAYOUT statements. - page/snippet widget action naming a microflow created later in the script: MDL-ORDER01 now walks widget actions. - CALL MICROFLOW/NANOFLOW with unmapped parameters (CE0115) or a string literal for an enumeration parameter (CE0117). The microflow type builders now set ExplicitEnum for `enum M.X` / `Enumeration(M.X)`, which they never did. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 57259e8 commit fa0dc4f

20 files changed

Lines changed: 984 additions & 28 deletions

‎.claude/skills/fix-issue/findings/mdl-executor.jsonl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,3 +680,4 @@
680680
{"area": "mdl/executor", "date": "2026-09-23", "symptom": "`describe page` on a page with several galleries emits MDL that mxcli's own `check --references` rejects: `duplicate widget name 'template1' (used 4 times) — Mendix requires unique widget names per page (CE0495)` (and `filter1` when the galleries have filters). The row/col half of the same report was already fixed; this third name survived it", "cause": "`template`/`filter` inside a gallery are CHILD SLOTS of its definition (gallery.def.json childSlots). `applyChildSlots` builds only the block's children into the slot property and discards the block's name, so DESCRIBE has to synthesise `template1` for every gallery. `checkDuplicateWidgetNames` already exempted the parent's object-list containers but not its child-slot containers", "file": "`mdl/executor/validate_page_context.go` (`unstoredContainerKinds`, was `objectListContainerKinds`; `checkDuplicateWidgetNames`); test `mdl/executor/validate_page_slot_containers_test.go`", "insight": "**When a multi-symptom report is marked fixed, re-run every symptom in it, not the first** — the earlier fix covered `row1`/`col1`/`FullName` and its test never mentioned `template1`, so the issue stayed open with a third name nobody had reproduced. Cheapest proof a name is not stored: author a distinctive one (`template contentZebra`), `grep -rl` it in `mprcontents/` (0 files) and DESCRIBE it back (`template1`). Don't exempt the keyword `template` globally — outside a slot-declaring widget, `template` is `buildTemplateV3`, a `Forms$DivContainer` that DOES store its name; resolve it from the enclosing widget's def (both `ChildSlots` and every mode's), and cover the `container <slotName>` spelling that `applyChildSlots` routes by name. Measured on 11.12.2: four galleries each `template template1` → `mx check` 0 errors", "refs": ["mendixlabs/mxcli#978"], "ce": ["CE0495"]}
681681
{"area":"mdl/executor","date":"2026-09-23","symptom":"\"List views are written with `Editable: false` by mxcli, so every input inside a list view renders disabled — even with `editable: Always` on the text box, and even inside a nested data view.\" Valid BSON, `mxcli check` clean, build succeeds; only the rendered app shows it","cause":"Not a writer bug: false IS Mendix's default for Pages$ListView.editable. The 2026-09-06 fix made an explicit `editable: true` reach the document, but nothing told an author who never wrote it that the list view's read-only context overrides the inputs' own `editable:`","file":"`mdl/executor/validate_listview_editable_inputs.go` (MDL-WIDGET31, hooked in `validate_widgets.go`); test `validate_listview_editable_inputs_test.go`; example `mdl-examples/bug-tests/631-listview-inputs-not-editable.mdl`","insight":"**Measure the default before changing it — and the measurement is cheap.** The issue parked \"default to true\" on a Studio Pro comparison it could not run; `npm pack mendixmodelsdk` and reading the class in `src/gen/pages.js` settles it in a minute: `new PrimitiveProperty(ListView, this, \"editable\", false, …)` and `_initializeDefaultProperties` never sets it, so flipping the default would have made mxcli disagree with every Studio Pro-authored list view. A Studio Pro project confirms it (ako/TestApp, 11.14.0): 30 of 32 list views stored false, none holding an input, and the one with inputs set to true by its author; `describe` of that page checks clean, deleting `Editable: true` from it fires MDL-WIDGET31, and describe -> exec preserves it. The fix is therefore a check for the COMBINATION (not-editable list view + input not marked `editable: Never`). Read editability with the SAME accessor the builder uses (`GetBoolProp`): a quoted `editable: 'true'` parses as a string, the builder writes false, and a rule that read it leniently would stay quiet on a page that renders disabled. Stop the descent at a nested list view — its own Editable governs its inputs and it is visited on its own — but NOT at a nested data view, which the reporter measured does not escape the context","refs":["ako/mxcli#631"],"rules":["MDL-WIDGET31"]}
682682
{"area": "mdl/executor", "date": "2026-09-23", "symptom": "`captionparams` on an action button is accepted by `mxcli check` but not written; a container with `onclick` and a dynamic text does the job. Measured: `actionbutton b (caption: 'Save {1}', captionparams: [{1} = Title])` in a dataview wrote the LITERAL 'Title' (describe: `ContentParams: [{1} = 'Title']`), check --references and mx check both clean; and describe -> exec of any button with params dropped them all -> 4x CE0720 'Place holder index 1 is greater than 0' at mx check.", "cause": "buildButtonV3 carried its own copy of the template-parameter resolver (bare name without `.`/`$` -> quoted literal) instead of the shared buildClientTemplateParams that dynamictext/datagrid columns use; and DESCRIBE emitted a button's params as `ContentParams:`, a key the button builder never read. MDL-WIDGET04's orphan-placeholder check covered dynamictext only, so the round-tripped `{1}` with no parameter reached the build.", "file": "`mdl/executor/cmd_pages_builder_v3_widgets.go` (buildButtonV3 -> buildClientTemplateParams, ContentParams fallback), `mdl/executor/cmd_pages_describe_output.go` (ActionButton emits CaptionParams), `mdl/executor/validate_widgets.go` (validateButtonCaptionPlaceholders)", "insight": "**The report's workaround is the diagnosis**: 'a dynamic text does the job' means the SAME parameter text works on one widget and not another, so diff the two builders before touching grammar or the writer -- the writer was fine (clientTemplateToGen is shared). Two duplicate resolvers had drifted: the shared one gained SourceVariable, toString, association steps and format blocks over a year of fixes; the button copy got none. Do not trust 'check passed' for the literal case -- a literal holding the attribute's name is a valid model, so nothing short of describe (or looking at the rendered button) shows it. The describe->exec round trip is the cheap detector: run it once and the ContentParams/CaptionParams mismatch drops every param and mx check reports CE0720. Keep reading `ContentParams:` on buttons -- scripts dumped before this fix use it. Remaining gap, NOT this bug: `check --references` does not flag an unknown BARE attribute in contentparams/captionparams on either widget (`[{1} = Titel]` passes).", "refs": ["ako/mxcli#632"], "ce": ["CE0720"], "rules": ["MDL-WIDGET04"]}
683+
{"area": "mdl/executor", "date": "2026-09-23", "symptom": "`mxcli check script.mdl -p app.mpr --references` printed \"Check passed!\" for five scripts that then failed at exec or at mxbuild (marketplace-rnd): (1) XPath `[UserGroups.Group_Guests != empty]` gives CE0161; (2) `retrieve $G from $App/UserGroups.GuestGroup_App`, which starts at the TO end of a Reference, gives a list, and using `$G/UUID` as an object gives CE0117; (3) `Layout: Atlas_Core.Popup_Content`, a layout that does not exist, makes exec fail with 'layout not found'; (4) a page button whose `Action: MICROFLOW` names a microflow created later in the script makes exec fail midway; (5) a string literal passed to an enum parameter gives CE0117, and unmapped call parameters give CE0115", "cause": "Five holes in the check tiers, none of them a writer bug. The XPath hint table covered `= empty` on attributes but not on associations. No rule compared a retrieve's start entity with the association's ends, because the flow builder silently types the output from how it is used. CreatePageStmtV3 validation resolved widgets but never the layout. MDL-ORDER01's eager-reference walk covered flow bodies but not widget actions. validateFlowArguments checked data-source calls only, never CALL MICROFLOW/NANOFLOW activities, and flowParam did not record enum types. A sixth, found only on a real parse: the microflow-context type builders in `mdl/visitor/visitor_microflow.go` built `enum M.X` / `Enumeration(M.X)` without ExplicitEnum, so any check keyed on a certain enum was silent for microflow parameters", "file": "`mdl/executor/validate_microflow.go` and `validate_widgets.go` (xpathAssocEmptyMatch, MDL047); `validate_reverse_reference_retrieve.go`; `validate.go` (pageLayoutResolves); `validate_definition_order.go` (widgetFlowDefRefs); `validate_flow_call_args.go` plus flowParam.Enum in `validate_datasource_args.go`. Examples: `mdl-examples/bug-tests/check-gap-*.mdl`; `mdl/visitor/visitor_microflow.go` (ExplicitEnum on both enum branches, test `visitor_microflow_enum_param_test.go`)", "insight": "**Run the reporter's failing script through the released `check --references` before writing any rule.** v0.23 passed all five, which proved each was a check gap rather than a bad install. Three of the five cannot be judged without the model: the association's ends and owner, the layout list, and the callee's signature. Those rules therefore live in the `--references` tier, and their bug-test scripts are plain `.mdl` that fail only with `-p`. Naming them `.fail.mdl` would make check-mdl report a working rule as regressed (#891/#892). For the reverse-retrieve rule, owner Both means one-to-one, so the TO-side result is an object and must not be flagged. `collectObjectInputVariables` does not look at DECLARE initial values, which is exactly where the report used the variable, so the rule adds that scan itself. For the enum-literal rule, only an explicit `enum Module.Name` parameter is trusted, because a bare `Module.Name` parses as TypeEnumeration whether it is an entity or an enum. Known skips: associations the script creates itself (case 2), and string variables passed to enum parameters, since their type is unknown at that point. **Test the rule through `visitor.Build`, not only on hand-built AST.** The enum-literal unit test passed on an AST with ExplicitEnum set by hand, but the real script never produced that flag, so only the end-to-end run against the project exposed it", "refs": ["marketplace-rnd"], "ce": ["CE0161", "CE0117", "CE0115"], "rules": ["MDL047", "MDL-ORDER01"]}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-- check --references gap (marketplace-rnd): a CALL MICROFLOW that passes a
2+
-- string literal for an enumeration parameter and leaves parameters unmapped.
3+
--
4+
-- `exec` writes the call as given. mxbuild then reports CE0117 ("The expression
5+
-- is of type String but should be of type Enumeration") for Kind = 'Removed',
6+
-- and CE0115 ("the arguments do not match the expected parameters") for the
7+
-- missing $App and $Note.
8+
--
9+
-- The rule reads the callee's signature from the project or from this script,
10+
-- so it runs only with a project: this passes a project-less `check` and fails
11+
-- `mxcli check <this file> -p app.mpr --references`.
12+
-- Fix: `call microflow GapCall.SUB_Register (Kind = GapCall.Kinds.Removed,
13+
-- App = $App, Note = '')`.
14+
15+
create or modify module GapCall;
16+
17+
create enumeration GapCall.Kinds (Added 'Added', Removed 'Removed');
18+
19+
create microflow GapCall.SUB_Register ($Kind: enum GapCall.Kinds, $App: System.User, $Note: String)
20+
begin
21+
end;
22+
/
23+
24+
create microflow GapCall.MF_Caller ($App: System.User)
25+
begin
26+
call microflow GapCall.SUB_Register (Kind = 'Removed');
27+
end;
28+
/
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- check --references gap (marketplace-rnd): a page button's action names a
2+
-- microflow the script creates only further down.
3+
--
4+
-- A page is written as soon as its statement runs, and resolving its button's
5+
-- microflow happens then, so `exec` stops with "microflow not found" before the
6+
-- microflow exists. Before the fix, `check` passed it. The rule (MDL-ORDER01)
7+
-- now covers widget actions on pages and snippets, as it already did for
8+
-- microflow calls. Create the microflow first.
9+
10+
create or modify module GapOrder;
11+
12+
create page GapOrder.P_Home (Title: 'Home', Layout: Atlas_Core.Atlas_Default) {
13+
actionbutton btnGo (Caption: 'Go', Action: MICROFLOW GapOrder.ACT_Go)
14+
};
15+
16+
create microflow GapOrder.ACT_Go () begin end;
17+
/
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-- check --references gap (marketplace-rnd): a page whose layout does not exist.
2+
--
3+
-- Atlas_Core has no Popup_Content layout (the popup layout is
4+
-- Atlas_Core.PopupLayout). `check --references` used to pass this and `exec`
5+
-- then failed with "layout not found". The layout is now resolved against the
6+
-- project and the script's own CREATE LAYOUT statements.
7+
--
8+
-- Needs a project: passes a project-less `check` and fails
9+
-- `mxcli check <this file> -p app.mpr --references`.
10+
11+
create or modify module GapLayout;
12+
13+
create page GapLayout.P_Popup (Title: 'Popup', Layout: Atlas_Core.Popup_Content) {
14+
dynamictext dt (content: 'x')
15+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
-- check --references gap (marketplace-rnd): a retrieve over a Reference from its
2+
-- TO end, used as an object.
3+
--
4+
-- System.User_Language runs from User to Language (a Reference, owner Default).
5+
-- Retrieving it from a Language starts at the end that does not hold the
6+
-- reference, so Mendix returns every User pointing at that language: a LIST.
7+
-- `exec` types the output from how it is used and writes the microflow, and
8+
-- mxbuild then rejects `$User/Name` as CE0117.
9+
--
10+
-- The rule needs the association's ends from the model, so this passes a
11+
-- project-less `check` and fails
12+
-- `mxcli check <this file> -p app.mpr --references`. Associations the script
13+
-- creates itself are not resolved by the rule.
14+
-- Fix: `$One = head($User)`, or
15+
-- `retrieve $User from System.User where [System.User_Language = $Lang] limit 1`.
16+
17+
create or modify module GapReverse;
18+
19+
create microflow GapReverse.MF_FirstUserName ($Lang: System.Language) returns String as $Name
20+
begin
21+
retrieve $User from $Lang/System.User_Language;
22+
declare $Name String = $User/Name;
23+
return $Name;
24+
end;
25+
/
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- check --references gap (marketplace-rnd): an association compared to empty in
2+
-- an XPath constraint.
3+
--
4+
-- `[Mod.Assoc != empty]` is not valid XPath in Mendix: mxbuild rejects it as
5+
-- CE0161. Before the fix, `mxcli check` and `exec` both accepted it. Write the
6+
-- path to the associated entity instead: `[Mod.Assoc/Mod.Target]`, or
7+
-- `[not(Mod.Assoc/Mod.Target)]` for the "= empty" form.
8+
9+
create or modify module GapXPath;
10+
11+
create or modify persistent entity GapXPath.Team (Name: String(100));
12+
create or modify persistent entity GapXPath.Member (Name: String(100));
13+
create association GapXPath.Member_Team from GapXPath.Member to GapXPath.Team;
14+
15+
create microflow GapXPath.MF_MembersInATeam () returns list of GapXPath.Member as $L
16+
begin
17+
retrieve $L from GapXPath.Member where [GapXPath.Member_Team != empty];
18+
return $L;
19+
end;
20+
/

‎mdl/executor/validate.go‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,19 @@ func validateWithContext(ctx *ExecContext, stmt ast.Statement, sc *scriptContext
564564
return mdlerrors.NewValidationf("microflow '%s' has reference errors:\n - %s",
565565
s.Name.String(), strings.Join(refErrors, "\n - "))
566566
}
567+
if !s.Excluded {
568+
// Call arguments against the called flow's signature: missing
569+
// parameters (CE0115) and string literals for enumeration
570+
// parameters (CE0117) pass exec and fail only at build.
571+
if argErrors := validateFlowCallArguments(ctx, s.Body, sc); len(argErrors) > 0 {
572+
return mdlerrors.NewValidationf("microflow '%s' has call argument errors:\n - %s",
573+
s.Name.String(), strings.Join(argErrors, "\n - "))
574+
}
575+
if assocErrors := validateReverseReferenceRetrieves(ctx, s, sc); len(assocErrors) > 0 {
576+
return mdlerrors.NewValidationf("microflow '%s' has retrieve errors:\n - %s",
577+
s.Name.String(), strings.Join(assocErrors, "\n - "))
578+
}
579+
}
567580
case *ast.CreateRuleStmt:
568581
if s.Name.Module != "" && !sc.modules[s.Name.Module] {
569582
if _, err := findModule(ctx, s.Name.Module); err != nil {
@@ -602,13 +615,26 @@ func validateWithContext(ctx *ExecContext, stmt ast.Statement, sc *scriptContext
602615
return mdlerrors.NewValidationf("nanoflow '%s' has reference errors:\n - %s",
603616
s.Name.String(), strings.Join(refErrors, "\n - "))
604617
}
618+
if argErrors := validateFlowCallArguments(ctx, s.Body, sc); len(argErrors) > 0 {
619+
return mdlerrors.NewValidationf("nanoflow '%s' has call argument errors:\n - %s",
620+
s.Name.String(), strings.Join(argErrors, "\n - "))
621+
}
605622
}
606623
case *ast.CreatePageStmtV3:
607624
if s.Name.Module != "" && !sc.modules[s.Name.Module] {
608625
if _, err := findModule(ctx, s.Name.Module); err != nil {
609626
return mdlerrors.NewNotFound("module", s.Name.Module)
610627
}
611628
}
629+
// The layout is resolved at write time: exec refuses a page with widgets
630+
// whose layout does not exist, and a page without them is written with no
631+
// layout call at all (CE1613 at build). Resolve it here the same way
632+
// resolveLayout does, so `check --references` reports it first.
633+
if s.Layout != "" && !pageLayoutResolves(ctx, s.Layout, sc.layouts) {
634+
return mdlerrors.NewValidationf(
635+
"page '%s' references layout '%s', which does not exist in the project or this script. List the layouts with `show catalog table layouts` (e.g. Atlas_Core.Atlas_Default, Atlas_Core.PopupLayout).",
636+
s.Name.String(), s.Layout)
637+
}
612638
// Every widget-bearing field, not just the bare body — see pageWidgets.
613639
pageWidgets := allPageWidgets(s)
614640
// Validate widget references (DataSource, Action, Snippet)
@@ -1354,6 +1380,56 @@ func (sc *scriptContext) recordFlowParams(qualifiedName string, params []ast.Mic
13541380
// neighbourhood: validateIconRefs (mendixlabs/mxcli#1008) and forEachWidget
13551381
// both had to grow the placeholder arm separately. Collecting the roots once,
13561382
// here, is what stops the fourth.
1383+
// pageLayoutResolves resolves a page's layout against the project and script.
1384+
// When the project's layouts cannot be listed — or the listing is empty, which
1385+
// no real project is (every app carries Atlas_Core's) — it answers true: "could
1386+
// not look" is not evidence the layout is missing, and a check that failed
1387+
// every page on a listing error would be noise.
1388+
func pageLayoutResolves(ctx *ExecContext, ref string, script map[string]bool) bool {
1389+
if ctx == nil || ctx.Backend == nil {
1390+
return true
1391+
}
1392+
h, err := getHierarchy(ctx)
1393+
if err != nil {
1394+
return true
1395+
}
1396+
layouts, err := ctx.Backend.ListLayouts()
1397+
if err != nil || len(layouts) == 0 {
1398+
return true
1399+
}
1400+
project := make(map[string]bool, len(layouts))
1401+
for _, l := range layouts {
1402+
if l != nil {
1403+
project[h.GetQualifiedName(l.ContainerID, l.Name)] = true
1404+
}
1405+
}
1406+
return layoutResolves(ref, project, script)
1407+
}
1408+
1409+
// layoutResolves reports whether a page's `Layout:` value names a layout the
1410+
// project or the script defines, matching pageBuilder.resolveLayout: a
1411+
// qualified name must match module and name, a bare name matches in any module.
1412+
func layoutResolves(ref string, project, script map[string]bool) bool {
1413+
ref = strings.TrimSpace(ref)
1414+
if project[ref] || script[ref] {
1415+
return true
1416+
}
1417+
parts := strings.Split(ref, ".")
1418+
mod, name := "", parts[len(parts)-1]
1419+
if len(parts) >= 2 {
1420+
mod = parts[0]
1421+
}
1422+
for _, set := range []map[string]bool{project, script} {
1423+
for qn := range set {
1424+
qp := strings.Split(qn, ".")
1425+
if qp[len(qp)-1] == name && (mod == "" || qp[0] == mod) {
1426+
return true
1427+
}
1428+
}
1429+
}
1430+
return false
1431+
}
1432+
13571433
func allPageWidgets(s *ast.CreatePageStmtV3) []*ast.WidgetV3 {
13581434
if len(s.Placeholders) == 0 {
13591435
return s.Widgets

0 commit comments

Comments
 (0)