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 @@ -702,4 +702,5 @@
{"area": "mdl/executor", "date": "2026-09-25", "symptom": "`container c1 (dynamicclasses: [ if $currentObject/Featured then 'a' else 'b' ])` - the first-class spelling mendixlabs/mxcli#750 proposes - checked clean and `exec` reported `Created page`, but the widget was stored with no DynamicClasses at all. `alter page ... set DynamicClasses = [ ... ] on w` reported `Altered page` and changed nothing; a column's `DynamicCellClass` on ALTER stored `[if$x/Ythen'a'else'b']` as its expression", "cause": "`[ ... ]` parses as propertyValueV3's array alternative, so the value reaches the AST as a []string. The create writers read only a string (GetStringProp for DynamicClasses, `v.(string)` for columnClass); the mutator's dynamicclasses case returned nil when the type check failed; setColumnPropertyMut formatted the list with %v, after the visitor's GetText() had already fused its tokens", "file": "`mdl/executor/validate_widget_expression_list.go` (MDL-WIDGET32), `mdl/backend/pagemutator/mutator.go` (`errExpressionNotAString`)", "insight": "**A proposal's example syntax is worth running through `check` before designing around it** - here the proposed spelling already parsed, and the parse was the bug. Same class as #999 / MDL-WIDGET27 (empty `[]`): a value shape no writer claims. Fix at both ends and they stay in step: a no-project check rule for CREATE, and an error (not a nil return) from the ALTER setter, which `check -p` reports for free because validateAlterSetProperties dry-runs the setter. Key the rule on the property, never on the brackets - `visible: [cond]` is valid MDL. Measured with pre-fix and fixed binaries on copies of ako/TestApp: pre-fix `describe` shows the container without DynamicClasses and the quoted control intact. Tests `validate_widget_expression_list_test.go`, `pagemutator/expression_list_value_test.go`", "refs": ["mendixlabs/mxcli#750", "#999"], "rules": ["MDL-WIDGET32"]}
{"area": "mdl/executor", "symptom": "describe → exec of Administration.Account_Edit takes the page's 8 Enumerations$Condition entries to 0: every widget with Studio Pro's \"Visible: based on attribute value\" becomes ALWAYS visible; check, exec and mx check all report success", "cause": "MDL had no spelling for attribute-based conditional visibility. extractConditionalSettings read only Expression, conditionalVisibilityToGen wrote only Expression, and the settings' list markers were the default [3] where Studio Pro stores Conditions [2] and ModuleRoles [1]", "file": "`mdl/grammar/domains/MDLPage.g4` (`VISIBLE COLON attributePathV3 IN (…)`), `mdl/executor/cmd_pages_builder_visible_when.go` (`applyVisibleWhen`), `mdl/backend/modelsdk/widget_write.go` (`conditionalVisibilityToGen`, TypeDefaults), `mdl/executor/cmd_pages_describe_parse.go` / `_output.go` (`visibleWhenProp`)", "insight": "**A dropped visibility setting is invisible to every check**: the model stays valid, the widget just shows for everyone — count `Enumerations$Condition` in `bson dump --format ndsl` before and after a round trip, since mx check never will. Survey the corpus before designing syntax: all 12 settings in the stock project were attribute-based (booleans and one enum), none role-based or editability, which scoped the feature. Studio Pro stores EVERY value (enum values in declaration order plus \"(empty)\", or true/false) with a flag, so MDL lists only the shown values and the writer fills the rest from the domain model — and a byte-identical before/after diff of the settings block (markers included) on 3 pages is the proof. mdlIdent quotes `empty`/`true`/`false` as keywords; emit them bare in a value list. A new AST property key must be added to the known-property list (validate_widgets.go) or MDL-WIDGET07 falsely warns it is dropped", "refs": ["Administration.Account_Edit", "Administration.ScheduledEvents"], "rules": ["MDL-WIDGET07"], "date": "2026-09-25"}
{"area":"mdl/executor","date":"2026-09-25","symptom":"describe → exec of an EXCLUDED page (Feedback v4.0.2 FeedbackModule.ShareFeedback_Logo, 11.13.0) refused: `page '…' has reference errors: - nanoflow not found: FeedbackModule.DS_FeedbackForm …`, though the untouched project passes mx check at 0 errors (Mendix does not validate excluded documents). With --no-check the page builder refused the same names again (`failed to resolve nanoflow`).","cause":"Two refusals, not one: validate.go's CreatePageStmtV3/CreateSnippetStmtV3 cases ignored exclusion (microflow/nanoflow/rule had been exempt since #312, silently), and pageBuilder.resolveMicroflow/resolveNanoflowByName/resolvePageRef/resolveSnippetRef fail on a missing name though the writer only ever stores the qualified NAME (IDs are never serialized).","file":"mdl/executor/validate.go (relaxExcludedWidgetRefs, carriedExclusion, warnExcluded), mdl/executor/cmd_pages_builder.go (tolerateDanglingRefs/danglingRefOK), cmd/mxcli/cmd_exec.go + cmd_check.go (ValidateProgramWithWarnings)","insight":"Relaxing the check is NOT safe for a DATA SOURCE, and only a real run shows it: the source flow's return type is the entity in scope, describe prints the nested bindings as bare names (`Attribute: Subject`, `ImageUrlParams: [{1} = ImageB64]`), and writing them without the entity left a bare `ImageB64` AttributeRef that made mx unable to LOAD the project (ArgumentNullException setting 'Attribute') — excluded page or not, where the pre-fix refusal had been protecting it by accident. So dangling action targets/snippet calls are warnings, dangling data sources and entities still block with the reason. 'Excluded' must mean what exec WRITES — @excluded OR the #914 carry (every stored namesake excluded) — or check and exec disagree. A/B on 11.13.0: identical page with 3 dangling action targets, excluded → 0 errors; live → 3x CE1613. Follow-up not fixed: describe loses attribute qualification inside a container whose flow is unresolvable, so ShareFeedback_Logo itself still cannot round-trip.","refs":["mdl-examples/bug-tests/excluded-page-dangling-references.mdl","#312","#914"]}
{"area": "mdl/executor", "symptom": "describe → exec of Feedback v4.0.2's EXCLUDED FeedbackModule.ShareFeedback_Logo refused `nanoflow not found: FeedbackModule.DS_FeedbackForm (data source)`; forcing it through left a project `mx check` could not LOAD (ArgumentNullException setting 'Attribute')", "cause": "The data view's flow is not in the project, so DESCRIBE had no context entity and printed every binding inside it bare (`Attribute: Subject`, `{1} = ImageB64`, `Visible: _showEmail in (…)`); exec had nothing to qualify them against, and a bare DomainModels$AttributeRef makes Mendix's loader throw. The stored model always had the full names", "file": "`mdl/executor/cmd_pages_describe_flowcontext.go` (`withQualifiedAttrs`, `describeAttr`), `mdl/executor/validate.go` (`unscopedBindings`), `mdl/executor/cmd_pages_builder_v3.go` (dangling DS flow kept by name), `mdl/backend/modelsdk/page_bare_attributeref.go` (`refuseBareAttributeRefs`)", "insight": "**The information was never lost — DESCRIBE threw it away**: every AttributeRef inside the unresolvable container still stored Module.Entity.Attr; shortening to the bare name is only safe where the reader can re-derive the entity, so key the shortening on whether the context resolved, not on habit. Measure the loader's tolerance before adding a write guard: 72 of 72 Studio Pro AttributeRefs in the project are qualified, so refusing a bare one refuses only writes that were already fatal — and turns a load-time stack trace into a statement-level error naming the attribute. Pair a blanket AST-level refusal with the exact failing slots (Attribute, CaptionAttribute, Visible-in, *Params) so the refusal names the widget, and keep the writer guard as the net for slots the walk does not know. Forced-fault run: hand-edit one qualified binding back to bare and confirm the refusal names it", "refs": ["ako/mxcli#675", "FeedbackModule.ShareFeedback_Logo"], "date": "2026-09-25"}
{"area": "mdl/executor", "symptom": "describe → exec of Administration.Account_New fails `mx check` with [CE0642] \"Property 'Caption' is required.\" at Combo box 'comboBox2' (Account_Edit comboBox4 too); check and exec report success", "cause": "The ComboBox caption is an EXPRESSION (optionsSourceAssociationCaptionType=expression, optionsSourceAssociationCaptionExpression='$currentObject/Description'); describe read only the attribute caption, so the widget was rewritten with none. The write side already worked via the explicit-property pass, but MDL-WIDGET06 claimed both keys 'will be dropped'", "file": "`mdl/executor/cmd_pages_describe_parse.go` (combobox branch), `cmd_pages_describe_output.go`, `validate_widget_explicit_writable.go` (`persistedByExplicitPass`), `validate_widgets.go` (MDL-WIDGET06)", "insight": "**Resolve pluggable-widget properties by key before theorising**: a 20-line script mapping each Property's TypePointer to its PropertyKey through the widget's own Type.ObjectType settled the cause in one run, where the ndsl dump shows only anonymous values. Then TEST THE WRITE SIDE before building one: adding the two storage keys to the describe output by hand persisted both and built clean, which shrank the fix to describe + a false warning — no new syntax. A validator rule that asserts 'not persisted' must be tied to what the write path handles (here: the explicit pass writes Expression/TextTemplate/Attribute and scalar types), or it goes stale when the writer grows; the #643 test pinned the stale claim. A dedicated extractor for a 'known' pluggable widget silently drops every property it does not map — generic widgets emit them, known ones do not", "refs": ["ako/mxcli#664", "ako/mxcli#643"], "ce": ["CE0642"], "rules": ["MDL-WIDGET06"], "date": "2026-09-25"}
10 changes: 7 additions & 3 deletions .claude/skills/mendix/check-syntax/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,13 @@ does not validate excluded documents (Feedback v4.0.2 ships an excluded page bou
to nanoflows it lacks, and the project checks at 0 errors), so `check` and `exec`
print them as `Reference warning` lines for excluded microflows, nanoflows, rules,
and pages/snippets exec will write excluded (`@excluded`, or a stored namesake that
is). **A page's or snippet's missing data source (or entity) still blocks:** the
widgets inside bind against it, and written without it their bindings are bare
names — on 11.13.0 that left a project `mx` could not load.
is). **A missing data-source flow is a warning only when the bindings inside it are
qualified** (`Attribute: Module.Entity.Attr`, `{1} = Module.Entity.Attr`,
`Visible: Module.Entity.Attr in (…)`) — the form `describe` writes there. The
widgets inside bind against the entity that flow returns, so with the flow missing
a bare binding cannot be resolved; it is refused, naming the widget, because on
11.13.0 a bare attribute reference left a project `mx` could not load. A missing
entity still blocks.

### It also reports a name the PROJECT already has

Expand Down
8 changes: 5 additions & 3 deletions docs-site/src/tutorial/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ This is the check you should run before executing a script. It's fast (reads the
References inside an **excluded** document (`@excluded`, or a page or snippet
that stays excluded because its stored namesake is) are reported as
`Reference warnings` rather than errors, because Mendix does not validate
excluded documents. A page's or snippet's missing *data source* still fails the
check: the widgets inside it bind against that source's entity, and cannot be
written without it.
excluded documents. A missing *data-source flow* is a warning too, but only when
every binding inside that container is qualified (`Module.Entity.Attribute`) —
`describe page` writes them that way there. The widgets bind against the entity
the flow returns, so with the flow missing a bare binding cannot be resolved and
the check fails, naming the widget. A missing entity still fails the check.

### Name conflicts with the project

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
-- ============================================================================
-- An excluded page over a flow the project lacks: bindings kept qualified
-- ============================================================================
--
-- Symptom: describe → exec of FeedbackModule.ShareFeedback_Logo (Feedback
-- v4.0.2, Mendix 11.13.0), an EXCLUDED example page whose data view is sourced
-- by a nanoflow the module does not ship, was refused:
-- nanoflow not found: FeedbackModule.DS_FeedbackForm (data source)
-- and writing it anyway left a project `mx check` could not LOAD
-- (ArgumentNullException setting 'Attribute'), because DESCRIBE printed the
-- bindings inside that data view bare (`Attribute: Subject`, `{1} = ImageB64`)
-- and with the flow missing there is no entity to qualify them against.
--
-- Fix: DESCRIBE keeps the stored Module.Entity.Attr inside a data container
-- whose flow cannot be resolved (and `Visible: Mod.Entity.Attr in (…)` accepts
-- it); the missing flow is then a warning on an excluded page. A bare binding
-- there is still refused, naming the widget, and the page writer refuses any
-- bare attribute reference outright.
--
-- Verify: exec on a project WITHOUT MyFirstModule.DS_MissingForm → warning
-- only, "Created page"; `mxcli docker check` — 0 errors (the page stays
-- excluded). Replace one qualified binding with a bare name → refused.
-- ============================================================================

create entity MyFirstModule.FeedbackDraft (
Subject: String(200),
ShowEmail: Boolean default false,
Email: String(200)
);
/

@excluded
create or modify page MyFirstModule.FeedbackDraft_Example
( Title: 'Feedback (example)', Layout: Atlas_Core.Atlas_Default )
{
dataview dv (DataSource: nanoflow MyFirstModule.DS_MissingForm) {
textbox txtSubject (Label: 'Subject', Attribute: MyFirstModule.FeedbackDraft.Subject)
dynamictext txtEcho (Content: 'About: {1}', ContentParams: [{1} = MyFirstModule.FeedbackDraft.Subject])
textbox txtEmail (
Label: 'Email',
Attribute: MyFirstModule.FeedbackDraft.Email,
Visible: MyFirstModule.FeedbackDraft.ShowEmail in (true)
)
}
}
/
59 changes: 59 additions & 0 deletions mdl/backend/modelsdk/page_bare_attributeref.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: Apache-2.0

package modelsdkbackend

import (
"fmt"
"strings"

"go.mongodb.org/mongo-driver/bson"
)

// refuseBareAttributeRefs refuses a page or snippet whose encoded form holds a
// DomainModels$AttributeRef that is not Module.Entity.Attribute.
//
// Mendix rebuilds each stored reference into a typed identifier as it loads,
// and an attribute that does not parse as one takes the loader down before any
// validation runs: a bare `ImageB64` image parameter left `mx check` unable to
// load the project (ArgumentNullException setting 'Attribute', 11.13.0) — the
// page was excluded, which does not help, as loading is not validating. Studio
// Pro qualifies every one (72 of 72 across a stock project's pages, snippets
// and layouts). A bare name reaches here when nothing could qualify it — inside
// a data container whose flow the project lacks — so this is the last line
// under the check that refuses it first (checkUnscopedBindings).
func refuseBareAttributeRefs(contents []byte) error {
var bad []string
var walk func(v bson.RawValue, path string)
walk = func(v bson.RawValue, path string) {
switch v.Type {
case bson.TypeEmbeddedDocument:
doc := v.Document()
if t, ok := doc.Lookup("$Type").StringValueOK(); ok && t == "DomainModels$AttributeRef" {
if a, ok := doc.Lookup("Attribute").StringValueOK(); ok && a != "" && strings.Count(a, ".") < 2 {
bad = append(bad, fmt.Sprintf("%q at %s", a, path))
}
}
name, _ := doc.Lookup("Name").StringValueOK()
elems, _ := doc.Elements()
for _, e := range elems {
p := path + "/" + e.Key()
if name != "" {
p = path + "/" + name + "." + e.Key()
}
walk(e.Value(), p)
}
case bson.TypeArray:
vals, _ := v.Array().Values()
for _, x := range vals {
walk(x, path)
}
}
}
walk(bson.RawValue{Type: bson.TypeEmbeddedDocument, Value: contents}, "")
if len(bad) == 0 {
return nil
}
return fmt.Errorf("attribute reference not qualified as Module.Entity.Attribute — Mendix cannot load a "+
"project holding one, so it is not written: %s. Qualify it in the script; inside a data container "+
"whose flow the project lacks there is no entity to resolve a bare name against", strings.Join(bad, "; "))
}
47 changes: 47 additions & 0 deletions mdl/backend/modelsdk/page_bare_attributeref_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: Apache-2.0

package modelsdkbackend

import (
"strings"
"testing"

"go.mongodb.org/mongo-driver/bson"
)

// A DomainModels$AttributeRef whose Attribute is not Module.Entity.Attr makes
// the project unloadable: an image URL parameter written as a bare `ImageB64`
// (Feedback v4.0.2's ShareFeedback_Logo, under a data view whose flow the
// project lacks) left `mx check` unable to LOAD the project —
// ArgumentNullException setting 'Attribute', Mendix 11.13.0 — while the page
// was excluded. Studio Pro qualifies every one: 72 of 72 AttributeRefs across
// that project's pages, snippets and layouts. So the writer refuses the bare
// form, naming it, instead of storing it.
func TestRefuseBareAttributeRefs(t *testing.T) {
attrRef := func(a string) bson.D {
return bson.D{{Key: "$Type", Value: "DomainModels$AttributeRef"}, {Key: "Attribute", Value: a}, {Key: "EntityRef", Value: nil}}
}
doc := func(a string) []byte {
b, err := bson.Marshal(bson.D{
{Key: "$Type", Value: "Forms$Page"},
{Key: "Widgets", Value: bson.A{int32(2), bson.D{
{Key: "$Type", Value: "CustomWidgets$CustomWidget"},
{Key: "Name", Value: "image1"},
{Key: "Params", Value: bson.A{int32(2), bson.D{{Key: "AttributeRef", Value: attrRef(a)}}}},
}}},
})
if err != nil {
t.Fatal(err)
}
return b
}
err := refuseBareAttributeRefs(doc("ImageB64"))
if err == nil || !strings.Contains(err.Error(), "ImageB64") {
t.Fatalf("a bare attribute reference must be refused, naming it; got %v", err)
}
for _, ok := range []string{"FeedbackModule.Feedback.ImageB64", ""} {
if err := refuseBareAttributeRefs(doc(ok)); err != nil {
t.Errorf("%q must be accepted: %v", ok, err)
}
}
}
9 changes: 8 additions & 1 deletion mdl/backend/modelsdk/page_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,14 @@ func encodePage(page *pages.Page, pv *types.ProjectVersion, carry func(*genPg.Pa
if carry != nil {
carry(g)
}
return docEncoder("Forms$Page", pv).Encode(g)
contents, err := docEncoder("Forms$Page", pv).Encode(g)
if err != nil {
return nil, err
}
if err := refuseBareAttributeRefs(contents); err != nil {
return nil, fmt.Errorf("page %q: %w", page.Name, err)
}
return contents, nil
}

// pageToGen builds the full gen Page: header, layout call, the widget tree (under
Expand Down
9 changes: 8 additions & 1 deletion mdl/backend/modelsdk/snippet_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ func encodeSnippet(snippet *pages.Snippet, pv *types.ProjectVersion) ([]byte, er
return nil, err
}
g.SetID(element.ID(snippet.ID))
return docEncoder("Forms$Snippet", pv).Encode(g)
contents, err := docEncoder("Forms$Snippet", pv).Encode(g)
if err != nil {
return nil, err
}
if err := refuseBareAttributeRefs(contents); err != nil { // see page_bare_attributeref.go
return nil, fmt.Errorf("snippet %q: %w", snippet.Name, err)
}
return contents, nil
}

// CreateSnippet inserts a new Forms$Snippet document — a reusable widget tree with
Expand Down
Loading
Loading