Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/fix-issue/findings/mdl-executor.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -709,5 +709,6 @@
{"area": "mdl/executor", "symptom": "`alter styling … set 'Spacing bottom' = 'Outer medium'` (an Atlas Core 4.1.3 old name) warned MDL-WIDGET11 \"which no widget type in this project's theme declares — mxbuild reports this as CE6083\"; mxbuild actually reports CE6087 \"Design properties have been renamed in your theme\"", "cause": "validateAlterStylingDesignProps checked only current property names across all widget groups; #679 taught the authoring paths the theme's oldNames but not ALTER STYLING, which then offered a spelling near-miss instead of the current property", "file": "`mdl/executor/validate_alter_styling.go` (`renamedAnywhere`, `renamedStylingSuggestion`)", "insight": "**Pick the real-run example by what the resolver can see**: ALTER STYLING knows only a widget NAME, so it asks the whole theme — and Atlas 4.1.3 declares a CURRENT 'Align content' on the Image widget while 'Align content' is an old name on DivContainer, so that example is (correctly) silent under the under-report policy; 'Spacing bottom' is declared nowhere under a current name and exercises the rename path. A renamed key whose replacement is a compound (Spacing side, multi-select option) cannot be written by ALTER STYLING's one flat value, so the suggestion must point at the inline DesignProperties form rather than a `set` it cannot execute. Real run: old name via exec → CE6087; the suggested inline form → 0 errors", "refs": ["ako/mxcli#679"], "rules": ["MDL-WIDGET11"], "date": "2026-09-25"}
{"area": "mdl/executor", "date": "2026-09-25", "symptom": "A `label` widget's DesignProperties were never checked: describe → check --references of FeedbackModule.ShareFeedback_Logo (Feedback v4.0.2, Atlas Core 4.1.3, Mendix 11.13.0) warned MDL-WIDGET11 'renamed' on the containers but not on label1's 'Spacing bottom': 'Outer none' (CE6087 in mxbuild). Same gap on the write side: `label l (DesignProperties: ['Style': '#ff0000'])` checked clean, exec'd, and failed mx check with [CE6085] \"Unknown option #ff0000 for design property Style.\" at Label", "cause": "The `label` keyword (PR #670, writes Forms$Label) was never added to mdlKeywordToDesignPropsKey, so resolveDesignPropsKey returned 'label' — no design-properties.json key. validateWidgetDesignProps skips a widget whose key is absent (meant for unknown pluggables), and the builder's GetPropertiesForWidget returned only the 'Widget' base group, so the Label's own ColorPicker 'Style' was unknown and a free colour fell to the Option default. bsonTypeToDesignPropsKey already had Forms$Label → Label; only the keyword half was missing", "file": "mdl/executor/theme_reader.go", "insight": "Adding a native widget keyword has a third registration nobody asks for: mdlKeywordToDesignPropsKey. Missing it is silent in both directions — the validator's 'no theme key → skip' rule turns an unmapped keyword into approval, and the builder still gets the Widget base group, so Spacing/Hide on write correctly and only the type-specific properties (Label's ColorPicker Style) mis-type. Test a type-specific property with an off-list value; a Widget-base property passes either way. Quick audit: a keyword should resolve to the same key as its stored $Type — groupbox (GroupBox, which has a ColorPicker Style), tabcontainer, navigationtree, menubar, simplemenubar, row/column (LayoutGridRow/Column) are still unmapped. Also seen: MDL-WIDGET12 warns on a ColorPicker free colour that the builder writes as Custom and mxbuild accepts (0 errors) — a pre-existing false positive, not changed here", "refs": ["#670", "#679"], "rules": ["MDL-WIDGET11", "MDL-WIDGET12"], "ce": ["CE6085", "CE6087"]}
{"area": "mdl/executor", "date": "2026-09-25", "symptom": "`mxcli check` passed a microflow with a commit inside a loop \u2014 one database round trip per iteration \u2014 that `mxcli lint` already flagged as CONV011. The defect surfaced only at project-wide lint time, long after the write.", "cause": "CONV011 reads the STORED model, so it cannot speak until `exec` has written the microflow; `check` reads the MDL and had no equivalent rule. The gap is temporal, not a missing capability on either side.", "file": "`mdl/executor/validate_commit_in_loop.go` (MDL-PERF01, hooked in `validate_microflow.go`), test `validate_commit_in_loop_test.go`, example `mdl-examples/bug-tests/1186-commit-in-loop.mdl`", "insight": "**When adding a check-time rule that anticipates an existing lint rule, pin the BOUNDARY to the lint rule's, not to the better one, and say why in the code.** A `while true` is built as an ExclusiveMerge back-edge rather than a LoopedActivity, so CONV011 (which walks LoopedActivity) does not flag a commit inside one. A commit there is arguably still N+1, and the tempting move is to be more correct \u2014 but two rules for one concept that disagree on what counts is precisely how a pair drifts, and this repo already has CONV010's three successive short allowlists as the worked example. If the case is worth reporting it is worth reporting in BOTH, and the stored-model rule is the one that sees the built flow. The test that pins this carries a control on the control: `while true` is exempt, `while <real condition>` is not, so the exemption cannot silently become 'never flag a while'. Name the sibling rule in the message (`lint reports this as CONV011`) so a reader hitting one recognises the other rather than filing it twice. Also worth reusing: `checkReturnInLoop` already had the depth-tracking walk over Loop/While/If/EnumSplit/InheritanceSplit \u2014 copying its shape got the nesting cases right for free.", "refs": ["ako/mxcli#681", "mendixlabs/mxcli#1186"], "rules": ["MDL-PERF01"]}
{"area": "mdl/executor", "date": "2026-09-25", "symptom": "`textbox t (Attribute: FullName)` at the top of a page (CREATE PAGE/SNIPPET, a plain container, or ALTER PAGE … INSERT at page level) passed plain `mxcli check`, `exec --no-check`/ALTER reported success, and `bson dump` showed `AttributeRef: null` — mxbuild 11.13.0: CE0544 \"This widget can only function inside a data context\" + CE7005 (textbox/textarea/datepicker/checkbox/radiobuttons/dropdown), CE0402 (dynamictext Attribute:), CE0642 (combobox). Qualified `Mod.Ent.Attr` there is stored and fails CE0544/CE2421/CE1365/CE7247 \"Move this widget into a data container\" + CE7006. `Attribute: $P/Attr` / `$currentObject/Attr` dropped even INSIDE a data view.", "cause": "resolveAttributePath returns the bare name when entityContext is \"\", and attributeRefToGen (and widgetobj setAttributeRefField) write nil for any path with < 2 dots, so the binding vanished between builder and writer; refuseBareAttributeRefs never sees it because no Attribute string is emitted. The only refusal (validatePageContextTree) runs in the --references phase for CREATE PAGE/SNIPPET, so plain check, --no-check and ALTER were unguarded. `$x/Attr` parses via the generic property rule as an *ast.DataSourceV3, so GetAttribute() returns \"\" and every builder skipped it.", "file": "mdl/executor/cmd_pages_input_binding_context.go (inputBindingProblem, checkInputBinding, validateInputBindingContext = MDL-WIDGET34), wired in cmd_pages_builder_v3_widgets.go (6 input builders + buildDynamicTextV3), widget_engine.go (primary Attribute mapping), validate_widgets.go (validateWidgetTreeIn); tests cmd_pages_input_binding_context_test.go; bug-tests input-binding-without-context{,.fail}.mdl", "insight": "Reuse the MDL-PAGEARG01 three-state context (pageArgContext known/present) rather than entityContext==\"\" as the 'outside a data container' signal: entityContext is also empty INSIDE a container whose flow cannot be resolved (excluded ShareFeedback_Logo), where DESCRIBE writes qualified names that must keep building — refusing qualified-on-empty-entity would have broken that round trip. So known-absent context refuses bare AND qualified; unknown context (ALTER) refuses only the bare name the writer provably nulls. Two existing unit tests (OnChangeSurvivesBuilder, DynamicTextV3_AttributeBinds) built inputs with NO entity and passed — the second asserted a bare `Title` AttributeRef counted as 'bound', i.e. it pinned the bug: when a fixture has no entity context, ask what the writer does with its output. The `$P/Attr` drop was found only by dumping the control page, not from the report — print the AST value type with a probe test before assuming a spelling reaches the builder. Evidence: 22 mxbuild errors before on the probe matrix; after, every case refused with nothing written, controls (dataview/listview/gallery/datagrid/snippet dataview/ALTER into dataview) 0 errors, 17/17 stock pages + 4/4 snippets describe→exec round trip.", "refs": ["MDL-WIDGET34"], "ce": ["CE0544", "CE7005", "CE0402", "CE0642", "CE2421", "CE1365", "CE7247", "CE7006"]}
{"area":"mdl/executor","date":"2026-09-25","symptom":"`alter page FeedbackModule.ShareFeedback_Logo { insert after textBox1 { image zzImg (ImageType: imageUrl, ImageUrl: '{1}', ImageUrlParams: [{1} = ImageB64]) } }` passed `check --references`; exec wrote a bare AttributeRef and `mx check` could not LOAD the project (ArgumentNullException setting 'Attribute'). Same at page top level outside any data container; a text box's `Attribute:` there is silently dropped (CE7005).","cause":"ALTER's entity context comes from the STORED document (nearest enclosing data source, or a flow source's return type via resolveDataSourceFlowEntity). With the flow missing (Feedback v4.0.2 ships no DS_FeedbackForm) or no container at all, entityContext is \"\" and resolveAttributePath returns the bare name. CREATE PAGE refused this at check time (relaxExcludedWidgetRefs/unscopedBindings, #678); ALTER's check never opened the document, so nothing could know the scope.","file":"mdl/executor/validate_alter_unscoped.go, mdl/executor/cmd_alter_page.go (alterEntityContext), mdl/executor/validate.go (bindingsWithoutScope)","insight":"For ALTER, scope is a property of the stored document, not the statement: a check-time question about it must open the document (OpenPageForMutation, never Save; validate_alter_set.go already does this) and ask through the SAME function exec uses, so the INSERT/REPLACE entity resolution was lifted into alterEntityContext rather than restated, and the binding walk lifted out of unscopedBindings (bindingsWithoutScope) rather than copied. Controls that keep it from blocking working scripts: skip a target the stored doc lacks (added earlier in the script), a flow the script declares with an entity return (sc.flowParams), DataGrid2 column and list-view-template paths, and documents the script creates. Reproduce with a Studio Pro-authored page whose flow is genuinely absent.","refs":["#678","#685"]}
{"area": "mdl/executor", "date": "2026-09-25", "symptom": "`describe page` on a File Uploader (files mode) emits `DataSource: association …`, and exec of that output fails: \"widget `upFiles` (fileuploader) exposes 2 datasources, so a generic `datasource:` clause is ambiguous — name the one you mean: associatedFiles, associatedImages\"", "cause": "DESCRIBE chose generic vs named by counting CONFIGURED datasources (namedCustomWidgetDataSources drops unset ones, so files mode = 1), while the builder's refuseAmbiguousGenericDataSource counts DECLARED datasource mappings (a generated def maps every top-level datasource = 2). Two sides of one round trip deciding the same question from different evidence.", "file": "mdl/executor/cmd_pages_describe_parse.go", "insight": "When describe and build each decide 'is this ambiguous?', they must count the same set. Fix read the DECLARED count from the stored schema (PropertyTypes with ValueType.Type=DataSource, excluding IsLinked) and excluded widgets with an embedded .def.json — those are hand-written and pick one datasource mapping per mode, so a database-mode ComboBox (two declared) must keep the generic clause. The #956 bug-test script itself authored the refused generic clause on a File Uploader: a bug-test that only runs `mxcli check` without a project can't see an exec-time refusal, so grep bug-tests for the old spelling whenever a builder starts refusing one.", "refs": ["mendixlabs/mxcli#1199", "mendixlabs/mxcli#956", "mendixlabs/mxcli#1109"], "rules": []}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Fixed

- **Keyword operators were fused in some stored expressions** — a page action's microflow argument `Flag: $a and $b` was stored as `$aand$b`, `if $x then 'a' else 'b'` as `if$xthen'a'else'b'`; the same in `contentparams` values, `send rest request … with (…)` parameters and a dynamic `execute database query`. Those four places took the expression's text without its whitespace; literals, `+` and `$currentObject` were unaffected, which is why it went unnoticed, and `check --references` passed. Measured by decoding the stored units on a Mendix 11.14.0 project. The expression is now stored as written, with MDL comments removed, as the microflow expression sites already did.
- **An input bound to an attribute outside any data container was written with no binding** — `textbox t (Attribute: FullName)` at the top of a page (or of a snippet, or inside a plain container) has no entity to qualify the name with, and the writer stored it as `AttributeRef: null`. Plain `check` passed, `exec --no-check` and `alter page … insert` at page level said success, and mxbuild 11.13.0 failed the page with CE0544 "This widget can only function inside a data context" + CE7005 (text box, text area, date picker, check box, radio buttons, drop-down), CE0402 (dynamic text `Attribute:`) or CE0642 (combo box). A qualified attribute there is stored and fails the same way (CE0544 / CE2421 / CE1365 / CE7247 "Move this widget into a data container"). `Attribute: $P/Name` and `Attribute: $currentObject/Name` never parsed as an attribute path and were dropped even inside a data view. `check` now reports all three as **MDL-WIDGET34** (no project needed), and the page builder refuses them with the widget named, so nothing is written; `alter page` refuses a bare name it has no entity for. Place the widget in a data view, list view, gallery or data grid and bind the attribute by name.
- **An expression property written in brackets was silently dropped** (mendixlabs/mxcli#750) — `dynamicclasses: [ if $currentObject/Featured then 'a' else 'b' ]`, the spelling #750 proposes, parsed as a list that no writer reads: `check` was clean, `exec` said `Created page`, and the widget was stored with no dynamic class. `alter page … set DynamicClasses = [ … ]` said `Altered page` and changed nothing, and a column's `DynamicCellClass` stored the list's text — tokens fused, `[if$x/Ythen'a'else'b']` — as its expression. Measured on a copy of a Mendix 11.14.0 project with the pre-fix binary. `mxcli check` now reports **MDL-WIDGET32** for `DynamicClasses` and `DynamicCellClass` written as a list (no project needed), and ALTER refuses it, so `check -p` reports that too. Write the expression quoted.
- **`describe odata client` lost a quote level on a literal credential** — Studio Pro stores a literal user name as the expression `'abc'`, quotes included. `describe` printed `HttpUsername: 'abc'`, and re-executing that output stored `abc`, an identifier. `ClientCertificate`, header keys, `Version`, `MetadataUrl` and `Folder` were printed unescaped and did not re-parse when they held a quote. Every value is now quoted so a re-exec stores exactly what was read; measured against a Studio Pro-authored client decoded before and after a round trip.
- **An OData client's proxy constant written `@Module.Const` was stored with the `@`** — `ProxyHost` / `ProxyPort` / `ProxyUsername` / `ProxyPassword` are by-name references to a constant, and Studio Pro stores the bare name (with `ProxyType: Override`). `"@Module.Const"` named no constant, so the proxy resolved to nothing. `create`, `create or modify` and `alter` now store the bare name for the bare, `@` and quoted-`@` spellings. The constant may be a String or an Integer.
Expand Down
36 changes: 36 additions & 0 deletions mdl-examples/bug-tests/input-binding-without-context.fail.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
-- An input widget bound to an attribute where nothing supplies an object.
--
-- `textbox t (Attribute: FullName)` at the top of a page — outside any data
-- view, list view, gallery or data grid — has no entity to qualify `FullName`
-- with, and the writer stores an unqualified name as `AttributeRef: null`. Plain
-- `mxcli check` said "Check passed!", `exec --no-check` (and ALTER PAGE … INSERT
-- at page level, which `check --references` never saw) reported success, and
-- mxbuild 11.13.0 then failed the page:
--
-- [CE0544] "This widget can only function inside a data context — like a data
-- view, list view, or a page with parameters or variables."
-- [CE7005] "No value selection has been made. Please select a value."
--
-- The same drop, per kind: textarea / datepicker / checkbox / radiobuttons /
-- dropdown (CE0544 + CE7005), dynamictext `Attribute:` (CE0402 "No value
-- specified."), combobox (CE0642 "Property 'Attribute' is required."). A
-- QUALIFIED attribute there is stored and fails all the same (CE0544 / CE2421 /
-- CE1365 / CE7247 "Move this widget into a data container", with CE7006), and
-- `Attribute: $P/Name` never parsed as an attribute path, so it was dropped even
-- inside a data view.
--
-- MDL-WIDGET34 now refuses each of these at check time, and the page builder
-- refuses them with the widget named, so nothing is written. This script must
-- FAIL check. The valid forms are in input-binding-without-context.mdl.
create module ProbeInBind;

create persistent entity ProbeInBind.Person (
FullName: String(100)
);

create page ProbeInBind.Edit (
title: 'Edit',
layout: Atlas_Core.Atlas_Default
) {
textbox t (Attribute: FullName)
};
49 changes: 49 additions & 0 deletions mdl-examples/bug-tests/input-binding-without-context.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-- What the MDL-WIDGET34 refusal must NOT touch: every binding below has an
-- object to bind to. Executed on a copy of a Mendix 11.13.0 project, the pages
-- build at 0 errors. This script must PASS check.
--
-- The refused forms are in input-binding-without-context.fail.mdl.
create module ProbeInBindOk;

create enumeration ProbeInBindOk.Color ( Red 'Red', Blue 'Blue' );

create persistent entity ProbeInBindOk.Person (
FullName: String(100),
Fav: Enumeration(ProbeInBindOk.Color)
);

create page ProbeInBindOk.Edit (
params: { $P: ProbeInBindOk.Person },
title: 'Edit',
layout: Atlas_Core.Atlas_Default
) {
layoutgrid lg {
row r1 {
column c1 (desktopwidth: autofill) {
-- The data view supplies the object; bare and qualified both bind.
dataview dv (DataSource: $P) {
textbox tName (Attribute: FullName)
textbox tQualified (Attribute: ProbeInBindOk.Person.FullName)
combobox cbFav (Attribute: Fav)
dynamictext dtName (Attribute: FullName)
}
}
}
}
-- A list widget's rows are the object.
listview lv (DataSource: database ProbeInBindOk.Person) {
dynamictext lvName (Attribute: FullName)
}
datagrid dg (DataSource: database ProbeInBindOk.Person) {
column cName (Attribute: FullName, Caption: 'Name')
}
};

-- A snippet binds through a data view over its parameter, exactly as a page does.
create snippet ProbeInBindOk.PersonFields (
params: { $P: ProbeInBindOk.Person }
) {
dataview sdv (DataSource: $P) {
textbox sName (Attribute: FullName)
}
};
3 changes: 3 additions & 0 deletions mdl/executor/cmd_pages_builder_onchange_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ func TestBuildWidgetV3_OnChangeSurvivesBuilder(t *testing.T) {
h := mkHierarchy(mod)
withContainer(h, mod.ID, mod.ID)
pb := newPageBuilder(&mock.MockBackend{}, h, "Mod")
// Inside a data container: with no entity in scope the binding
// has nothing to resolve against and the widget is refused.
pb.entityContext = "Mod.Ent"

w, err := pb.buildWidgetV3(mkOnChangeWidget(mdlType, "w1"))
if err != nil {
Expand Down
Loading
Loading