Skip to content
Open
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
2 changes: 2 additions & 0 deletions .claude/skills/fix-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ to the symptom table below, so the next similar issue costs fewer reads.
| An unrecognized property on a **built-in** (non-pluggable) widget — a typo like `Contnet`, or a genuinely unsupported key — passes `mxcli check`+`exec` with no error and is silently dropped on write (pluggable widgets already got MDL-WIDGET01) | `validateStaticWidget` had no unknown-property check; core widgets have no single property registry — builders read keys imperatively, and `describe` even emits keys (`WidthUnit`) the native builder doesn't consume — so a hard reject would false-positive on valid MDL and break the describe→create roundtrip | `mdl/executor/validate_widgets.go` (`staticWidgetKnownProps`, `validateStaticWidgetUnknownProps`, wired in `validateWidgetTree` gated on `lookupWidgetDef==nil`) | Add **MDL-WIDGET07** as a **WARNING** (never an error — the core-widget vocabulary can't be proven complete): flag any `Properties` key not in `staticWidgetKnownProps` (the union of grammar keyword props + builder-consumed keys + the full `describe`-emit vocabulary, harvested by grep), with a `nearestKey` "did you mean" hint. Runs only for non-pluggable widgets. Guard the allow-list against describe-vocabulary drift with `TestStaticWidgetKnownPropsCoverDescribe`, and sweep `mdl-examples/**` for zero false positives after any change. Bug-test `mdl-examples/bug-tests/widget-unknown-property.mdl`. |
| A **quoted** attribute/association name inside an *expression* — a microflow decision/`if`/`while`/`return` (`if $Expense/"Justification" != empty`), a widget `contentparams` entry (`contentparams: [{1} = "Amount"]`), a `visible:`/`editable:` expression (`visible: [ "Amount" > 1000 ]`), or an **XPath database datasource WHERE** (`datasource: database from M.E where [ "Status" = 'Submitted' ]`) — passes `mxcli check` (and `--references`). The first three fail MxBuild "Error(s) in expression"; the **datasource WHERE is the worst variant** — it *also* passes MxBuild, then **silently returns zero rows at runtime** because Mendix reads the quoted `"Status"` as the string literal `'Status'`, making the constraint `'Status' = 'Submitted'` always false. The unquoted form is fine. Sibling of the SET/Change-**target** row above, but for expression contexts (the "always quote identifiers" guidance is parser-safe, not platform-safe here) | Two sub-causes. (a) Expression contexts stored the **raw source text verbatim** — `SourceExpr.Source` from `extractOriginalText` (microflow), `expr.GetText()` (contentparams), the serialized visibility string. (b) The **inline-bracket XPath** path built member names via `buildXPathQualifiedName` (`words[i].GetText()`, no unquote) → an `IdentifierExpr{Name:"\"Status\""}` that `xpathExprToString` re-emits verbatim. Either way the quotes leaked, whereas binding contexts route each identifier through `unquoteIdentifier`. The general (non-XPath) expression AST was already unquoted | `mdl/visitor/visitor_helpers.go` (`stripExpressionIdentifierQuotes`) + `mdl/visitor/visitor_microflow_statements.go` (`buildSourceExpression`, `buildXPathSourceExpression`, `buildRetrieveWhereExpression`) + `mdl/visitor/visitor_page_v3.go` (`buildConditionalExpression`, `buildParamAssignmentV3`) + `mdl/visitor/visitor_xpath.go` (`buildXPathQualifiedName`) | Strip double-quote/backtick identifier quotes at the source. For raw-text slots: `stripExpressionIdentifierQuotes` (tracks single-quote string state incl. `''` escapes, so `'he said "hi"'` is untouched — safe because in Mendix expr/XPath only identifiers are double-quoted). For the inline-bracket XPath AST: `unquoteIdentifier` each `xpathWord` in `buildXPathQualifiedName` (root fix, feeds datasource WHERE + inline retrieve + conditional visibility); guard the `empty`/`true`/`false` special-value switch to fire only on the **unquoted** form so a quoted `"empty"` stays a member name. `dbSource.XPathConstraint = ds.Where` verbatim, so the visitor value is the persisted BSON constraint. Not caught by the expression **type** checker (`PROPOSAL_expression_type_checking.md`) — it works on the clean AST, not the raw string, and is microflow-scoped. Tests: `mdl/visitor/visitor_expr_quoting_test.go`; bug-test `mdl-examples/bug-tests/expr-quoted-identifiers.mdl` |
| A `dynamictext`/`datagrid` **contentparams**/**captionparams** entry that navigates an association (`[{1} = Module.Assoc/Attr]`, e.g. `Expense_Employee/Name`) passes `mxcli check --references` but fails MxBuild "Value of text template parameter {1} … No value specified." A **direct** attribute (`[{1} = Title]`) persists fine — the drop is specific to association navigation | The value string (`Module.Assoc/Attr`) reached the AttributeRef serializer, which rejects any path with `< 2` dots (the association hop has 1 dot; `/Attr` is navigation, not counted) → **null AttributeRef** → CE0402. Nothing built the association-step structure Mendix needs. Verified against a Studio-Pro-authored page: the binding is a `DomainModels$AttributeRef` whose `Attribute` is the FINAL attr (`Module.Employee.Name`) and whose `EntityRef` is a `DomainModels$IndirectEntityRef` of `DomainModels$EntityRefStep{Association, DestinationEntity}` hops | `sdk/pages/pages_widgets_display.go` (`ClientTemplateParameter.AttributeRefSteps`, `AttributeRefStep`) + `mdl/executor/cmd_pages_builder_v3.go` (`resolveTemplateAssociationPath`, `associationEndpoints`) + `mdl/backend/modelsdk/widget_write.go` (`attributeRefWithStepsToGen`) + `mdl/executor/cmd_pages_describe_output.go` (`associationTemplateParamPath`) | **modelsdk engine only.** Resolver splits the path into association hop(s) + final attribute, resolves each association's destination entity from the domain model (ParentID=FROM, ChildID=TO; direction from the context entity), and stores `AttributeRef`=final attr QN + `AttributeRefSteps`. The writer emits `AttributeRef.EntityRef` via `entityRefToGen` (reused from microflows — identical `IndirectEntityRef`/`EntityRefStep` storage names); the `< 2 dots` guard no longer trips since the final attr is 2-dot. DESCRIBE reconstructs `Assoc/Attr` from `EntityRef.Steps`. Legacy engine still drops it (out of scope). Generated BSON is byte-structurally identical to Studio Pro. **Non-String final attribute needs NO `toString()`** — verified against Studio Pro, an association-navigated param uses `AttributeRef` + `FormattingInfo` (DecimalPrecision/DateFormat/EnumFormat) for any type; the `toString()` wrapping is only the *direct*-attribute path's behavior. Do not "fix" the assoc path to wrap non-String — that diverges from Studio Pro. Tests: `mdl/backend/modelsdk/widget_template_assoc_test.go`, `mdl/executor/cmd_pages_describe_assoc_test.go`; bug-test `mdl-examples/bug-tests/dynamictext-contentparam-association.mdl` |
| Under the **default (modelsdk)** engine, any page widget with an **association datasource** (`datasource: $currentObject/Module.Assoc`, the master-detail idiom) fails at exec: `CreatePage: DataView source *pages.AssociationSource not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy`. Same for a nested `listview` | `customWidgetDataSourceToGen` (pluggable widgets) already handled `*pages.AssociationSource`, but `dataViewSourceToGen`/`listViewSourceToGen` had no case → hit the "not yet supported" default. (Legacy doesn't error but writes a **broken** `Forms$AssociationSource{EntityRef: nil}` in `serializeListViewDataSource`, so legacy is not a valid reference — tracked as **L1** in [`docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md`](../../docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md); do **not** fix legacy, it will be removed when modelsdk matures) | `mdl/backend/modelsdk/widget_write.go` (`associationSourceToGen`, wired into `dataViewSourceToGen`+`listViewSourceToGen`) + `mdl/executor/cmd_pages_describe.go` (`associationSourcePath`) + `cmd_pages_describe_parse.go` (DataView/ListView datasource cases) + `cmd_pages_describe_output.go` (`associationDataSourceExpr`) | **modelsdk engine only.** Extract the proven `AssociationSource` block from `customWidgetDataSourceToGen` into a shared `associationSourceToGen` (Forms$AssociationSource → `IndirectEntityRef` of `EntityRefStep{Association, DestinationEntity}` + optional page-param `SourceVariable`; `$currentObject` → nil source var) and add a `case *pages.AssociationSource` to the DataView and ListView source builders. Matches the canonical legacy `serializeAssociationSource` (writer_widgets.go) and the Studio-Pro-verified `EntityRefStep` structure — NOT the broken `serializeListViewDataSource` stub. DESCRIBE reconstructs `$currentObject/Module.Assoc` from `EntityRef.Steps`. **An association source is valid only on LIST-producing widgets (listview/datagrid/gallery/templategrid), NOT a plain DataView** — Studio Pro rejects a DataView association source ("cannot have a data source of type association"); the widget still exec-creates (valid BSON), so `check` flags it with **MDL-WIDGET08** (`validateStaticWidget`). Tests: `mdl/backend/modelsdk/widget_assoc_source_test.go`, `mdl/executor/cmd_pages_describe_assoc_source_test.go`, `validate_widgets_test.go`; bug-tests `mdl-examples/bug-tests/assoc-datasource-modelsdk.mdl` (valid listview) + `dataview-association-source-rejected.fail.mdl` (MDL-WIDGET08) |
| `IF … ELSIF … ELSE … END IF` silently drops every ELSIF arm on write — DESCRIBE round-trips `if … else …` with the middle arms gone (read path unchanged by the fix ⇒ the arm was absent from the stored model — dropped on write); no error from `check`/`exec` (both engines) | `buildIfStatement` read only `exprs[0]`/`bodies[0]` and the trailing ELSE body; the grammar's `(ELSIF expression THEN microflowBody)*` pairs were never visited | `mdl/visitor/visitor_microflow_statements.go` (`buildIfStatement`) | Lower each ELSIF arm into a nested `IfStmt` in the ELSE branch of the arm before it (built innermost-first) — Mendix has no native elsif, nested exclusive splits are the canonical shape. Guarded by visitor tests (chain, no-else, plain-if regression) + `mdl-examples/bug-tests/745-elsif-arms-dropped.mdl`. Issue #745 |
| Under the **default (modelsdk)** engine, any page widget with an **association datasource** (`datasource: $currentObject/Module.Assoc`, the master-detail idiom) fails at exec: `CreatePage: DataView source *pages.AssociationSource not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy`. Same for a nested `listview` | `customWidgetDataSourceToGen` (pluggable widgets) already handled `*pages.AssociationSource`, but `dataViewSourceToGen`/`listViewSourceToGen` had no case → hit the "not yet supported" default. (Legacy doesn't error but writes a **broken** `Forms$AssociationSource{EntityRef: nil}` in `serializeListViewDataSource`, so legacy is not a valid reference — tracked as **L1** in [`docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md`](../../docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md); do **not** fix legacy, it will be removed when modelsdk matures) | `mdl/backend/modelsdk/widget_write.go` (`associationSourceToGen`, wired into `dataViewSourceToGen`+`listViewSourceToGen`) + `mdl/executor/cmd_pages_describe.go` (`associationSourcePath`) + `cmd_pages_describe_parse.go` (DataView/ListView datasource cases) + `cmd_pages_describe_output.go` (`associationDataSourceExpr`) | **modelsdk engine only.** Extract the proven `AssociationSource` block from `customWidgetDataSourceToGen` into a shared `associationSourceToGen` (Forms$AssociationSource → `IndirectEntityRef` of `EntityRefStep{Association, DestinationEntity}` + optional page-param `SourceVariable`; `$currentObject` → nil source var) and add a `case *pages.AssociationSource` to the DataView and ListView source builders. Matches the canonical legacy `serializeAssociationSource` (writer_widgets.go) and the Studio-Pro-verified `EntityRefStep` structure — NOT the broken `serializeListViewDataSource` stub. DESCRIBE reconstructs `$currentObject/Module.Assoc` from `EntityRef.Steps`. A `Forms$AssociationSource` is valid on **LIST-producing** widgets (listview/datagrid/gallery/templategrid). A plain **DataView** cannot use `Forms$AssociationSource` (MxBuild **CE6705**) — but the DataView *association* case is still valid via a **different** source type; see the "data from context over association" row below (an earlier fix wrongly rejected it with MDL-WIDGET08, now removed). Tests: `mdl/backend/modelsdk/widget_assoc_source_test.go`, `mdl/executor/cmd_pages_describe_assoc_source_test.go`; bug-test `mdl-examples/bug-tests/assoc-datasource-modelsdk.mdl` (valid listview) |
| A **DataView bound to a to-one referenced object over an association** ("data from context", e.g. an inner DataView showing the referenced Employee's `Name`/`Email` inside an `Expense` DataView) can't be authored/round-tripped: `dataview dvEmp (datasource: $currentObject/Module.Assoc)` was rejected (MDL-WIDGET08), a bare nested `dataview { textbox (attribute: Name) }` bound children to the **parent** entity (CE1613), and `describe` dropped the source → describe/exec **silently strips** the association DataView (round-trip corruption) | A DataView over an association is **not** a `Forms$AssociationSource` (that's list-widgets-only; CE6705 on a DataView). It is a `Forms$DataViewSource` whose `EntityRef` is a `DomainModels$IndirectEntityRef` navigating the association (a DataViewSource *is* an `EntityPathSource`, so its EntityRef may be indirect — `generated/metamodel/types.go:558`). mxcli generated the wrong source type, then (Bug 5 v1) wrongly rejected the whole case | `mdl/backend/modelsdk/widget_write.go` (`dataViewContextAssociationSourceToGen`, wired into `dataViewSourceToGen`'s `*pages.AssociationSource` case) + `mdl/executor/cmd_pages_builder_v3_widgets.go` (removed the `buildDataViewV3` association guard) + `mdl/executor/validate_widgets.go` (removed MDL-WIDGET08) + `mdl/executor/cmd_pages_describe_parse.go` (`Forms$DataViewSource` reads `EntityRef.Steps` via `associationSourcePath`) | **modelsdk engine only.** For a DataView association datasource, emit `Forms$DataViewSource` with `EntityRef = IndirectEntityRef{EntityRefStep{Association, DestinationEntity}}` (+ optional page-param `SourceVariable`; `$currentObject` → none) — mirrors `associationSourceToGen` but wraps the IndirectEntityRef in a DataViewSource. Children then bind to the destination entity. DESCRIBE reconstructs `$currentObject/Module.Assoc`. Removes the earlier MDL-WIDGET08 over-rejection. **When two widget kinds share a navigation but MxBuild accepts it on only one, the distinction is the source `$Type` wrapper, not the EntityRef inside it** — check `generated/metamodel/types.go` for the widget's allowed `DataSource` subtypes. mxbuild-validated (`mxcli docker check --no-update-widgets` = 0 errors) + full describe→re-exec round-trip. Tests: `mdl/backend/modelsdk/widget_assoc_source_test.go` (`TestDataViewAssociationSource_Serialized`); bug-test `mdl-examples/bug-tests/dataview-context-association-source.mdl`. Bug 5 (reclassified) |
| **DataGrid2** (`datagrid`) columns fail MxBuild **CE0463** "widget definition changed" on the **default (modelsdk)** engine — reported for custom-content columns, but actually **every** column (attribute too). Legacy is fine | The column WidgetObject's `Properties` were serialized in **alphabetical** order (`alignment, allowEventPropagation, attribute, …`) instead of the template's `PropertyTypes` order (`showContentAs, attribute, content, dynamicText, …`). Studio Pro hashes the object structure against the type and flags CE0463 on any order mismatch. Cause: the object-list item builder (`mdl/backend/widgetobj/builder.go:274`) orders by `NestedKeyOrder`, falling back to alphabetical when empty; the **modelsdk** registry loader never captured it (`types.PropertyTypeIDEntry` had no such field), while the MPR/legacy loader did → legacy correct, modelsdk broken. Top-level widget props were unaffected (ordered by a different path), which is why only the nested column object-list broke | `mdl/types/widget_property_type.go` (`PropertyTypeIDEntry.NestedKeyOrder`) + `modelsdk/widgets/loader.go` (thread `nestedKeyOrder` through `jsonValueToBSONWithNestedObjectType`→`extractNestedObjectType`→`extractNestedPropertyTypes`, append in template array order) + `mdl/backend/modelsdk/widget_pluggable_write.go` (`convertPropTypeIDs` copies `NestedKeyOrder`) | Mirror the `sdk/widgets` loader's existing `nestedKeyOrder` capture in the parallel `modelsdk/widgets` loader (dedup on first occurrence, append in PropertyTypes array order), add the field to `types.PropertyTypeIDEntry`, and copy it in the modelsdk `convertPropTypeIDs`. **Diagnose column-order CE0463 by mapping each column WidgetProperty's `TypePointer` → the type's `WidgetPropertyType.PropertyKey` and comparing the sequence to the template's PropertyTypes order.** Verified: modelsdk column order now equals the template (and legacy) order for both attribute and custom-content columns, and **`mxcli docker check --no-update-widgets` (raw output, no widget normalization) = 0 errors** for the report's exact pattern (attribute column + custom-content dynamictext-over-association + custom-content actionbutton with `show_page`). The custom-content child-widget subtree needed no separate fix — ordering was the whole cause. Tests: `modelsdk/widgets/nested_key_order_test.go`; bug-test `mdl-examples/bug-tests/datagrid2-custom-content-column.mdl`. **To validate CE-class MxBuild errors locally: `mxcli setup mxbuild -p app.mpr --force` then `mxcli docker check -p app.mpr --no-update-widgets`** (the `--no-update-widgets` is essential — the default runs `mx update-widgets` which auto-normalizes pluggable widgets and masks a real CE0463) |
Expand Down
50 changes: 50 additions & 0 deletions mdl-examples/bug-tests/745-elsif-arms-dropped.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
-- ============================================================================
-- Bug #745: IF/ELSIF/ELSE silently drops every ELSIF arm on write (both engines)
-- ============================================================================
--
-- Symptom (before fix):
-- A microflow authored with IF … ELSIF … ELSE … END IF lost every ELSIF
-- arm when written to the .mpr. Only the first IF condition/body and the
-- trailing ELSE body reached the model — no error, no warning. DESCRIBE
-- round-tripped `if … else …` with the elsif arm gone. The read path was
-- untouched by the fix, so the missing arm was absent from the stored
-- model itself (dropped on write).
--
-- Root cause:
-- buildIfStatement (mdl/visitor/visitor_microflow_statements.go) read only
-- exprs[0]/bodies[0] and the trailing ELSE body; the grammar's
-- (ELSIF expression THEN microflowBody)* pairs were never visited.
--
-- After fix:
-- Each ELSIF arm is lowered into a nested IfStmt in the ELSE branch of the
-- arm before it (Mendix has no native elsif; nested exclusive splits are
-- the canonical shape). DESCRIBE now round-trips the chain as nested
-- if/else, and every arm's content is present in the model.
--
-- Usage:
-- mxcli exec mdl-examples/bug-tests/745-elsif-arms-dropped.mdl -p app.mpr
-- mxcli -p app.mpr -c "DESCRIBE MICROFLOW BugTestElsif.MF_Grade"
-- Expect: the 'two' and 'three' branches are present (nested if/else).
-- ============================================================================

create module BugTestElsif;

create microflow BugTestElsif.MF_Grade (
$In: integer
)
returns string as $Out
begin
declare $Out string = 'none';

if $In = 1 then
set $Out = 'one';
elsif $In = 2 then
set $Out = 'two';
elsif $In = 3 then
set $Out = 'three';
else
set $Out = 'other';
end if;

return $Out;
end;
Loading
Loading