diff --git a/.claude/skills/fix-issue/findings/mdl-executor/2026-10-25-protected-module-hidden-entity-retrieve-create-ce1406.json b/.claude/skills/fix-issue/findings/mdl-executor/2026-10-25-protected-module-hidden-entity-retrieve-create-ce1406.json new file mode 100644 index 000000000..644f3750f --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-executor/2026-10-25-protected-module-hidden-entity-retrieve-create-ce1406.json @@ -0,0 +1 @@ +{"area": "mdl/executor", "date": "2026-10-25", "symptom": "mendixlabs/mxcli#1652: `check -p --references` passed a microflow in an ordinary module that did `retrieve $l from AddOn.Internal` or `$i = create AddOn.Internal` on a Hidden entity of a Protected (add-on) module; mx check refused it with CE1406 (Retrieve object(s) activity / Create object activity … is not allowed to use entity)", "cause": "checkFlow checked an entity only in a flow's parameter and return types, and checkFlowMembers checked the members a CreateObjectStmt assigns and a RetrieveStmt's XPath names, but not s.EntityType / s.Source. A bare create or a retrieve without where reached no check", "file": "mdl/executor/validate_protected_module_members.go (checkFlowMembers)", "insight": "CE1406 is raised per element a flow names, not per member: every place a flow body names an entity (create, database retrieve) needs the same entityRef call the signature gets. entityRef already folds in the script-stated, stored and created-by-script levels, so reuse it rather than a new lookup. An association retrieve names an association, not an entity, and stays with the member check.", "refs": ["mendixlabs/mxcli#1652", "mendixlabs/mxcli#1498"], "ce": ["CE1406"], "rules": []} diff --git a/CHANGELOG.md b/CHANGELOG.md index b291e68f2..5bdc2cd4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- **`check -p --references` reports CE1406 for a database retrieve or a create of a Hidden entity of a Protected module** (mendixlabs/mxcli#1652) — `retrieve $l from AddOn.Internal` and `$i = create AddOn.Internal` in an ordinary module passed `check` and `exec`, and mxbuild refused the build with CE1406 ("Retrieve object(s) activity … is not allowed to use entity", "Create object activity … is not allowed to use entity"). The flow-body check looked at the members a create or retrieve names, never at the entity itself, so a bare create or a retrieve without `where` reached no check. Both entities are now resolved like a parameter type: a level the script states, else the stored one; a Usable entity, an ordinary module and the add-on module itself stay clean. - **A pluggable widget's text-template parameter across an association builds, and describes so it re-executes** (mendixlabs/mxcli#1494) — `fieldset fs (legend: '{1}', legendParams: ({1} = M.Parent_Child/Label))` passed `check` and `exec` and failed the build with `[CE1365] "Attribute 'M.Child.Label' is not an attribute of entity 'M.Parent'."`: the parameter was written with its attribute but without the association hop, so mxbuild read the attribute against the widget's own entity. The same writer serves an object-list item's template (an Accordion group's `headerText`) and a Data grid 2 column's dynamic text, and all three failed; on Mendix 11.12.4 they now build with 0 errors. `describe page` printed such a parameter as the bare attribute (`{1} = Label`), which re-executed into CE1613 "The selected attribute 'M.Parent.Label' no longer exists"; it now prints `M.Parent_Child/Label`. The companion report, an expression parameter (`{1} = if 1 = 1 then 'A' else 'B'`) stored as an attribute name (mendixlabs/mxcli#1495), was fixed in v0.25.0 (#823) and is now pinned by a test. - **`alter entity … add event handler` adds a second handler for the same event** (mendixlabs/mxcli#1540) — `add event handler on before commit call M.Second($currentObject)` failed with "event handler already exists for Before Commit" when the entity already had one, though Studio Pro allows several handlers for one event and `create or modify entity` with two `on before commit` clauses stores both (measured on 11.12.4: two Before Commit handlers are 0 errors, a void-returning one is CE7247, so the check does look at them). A handler is now identified by its moment, event and microflow: another microflow adds a second handler, the same microflow again is still a duplicate. The same key had made `add event handler if not exists … call M.New` skip — reporting "already exists" — whenever any handler for the event existed, and `drop event handler on before commit` remove whichever handler was stored first. `drop event handler` now takes `call M.Mf` to say which one; without it, a drop that matches several handlers is refused and names them. diff --git a/mdl/executor/validate_protected_module_members.go b/mdl/executor/validate_protected_module_members.go index 97f0fa5b6..5792139bd 100644 --- a/mdl/executor/validate_protected_module_members.go +++ b/mdl/executor/validate_protected_module_members.go @@ -24,6 +24,11 @@ import ( // retrieve … where Payload = … Retrieve … attribute (and an association step) // return $r/Payload, $i/AddOn.Assoc End event … attribute / association // +// The entity of a create and of a database retrieve is checked here too: a +// Hidden entity is refused by both (Create object activity / Retrieve object(s) +// activity … is not allowed to use entity), even with no member named +// (mendixlabs/mxcli#1652). +// // A member is reported only when it is established: its variable's entity is // known, the member resolves on it (an association hop through // associationTargetFrom), and its level is known — a level the script states, @@ -55,6 +60,7 @@ func (r *protectedRefResolver) checkFlowMembers(name ast.QualifiedName, params [ switch s := st.(type) { case *ast.CreateObjectStmt: who := "create " + s.EntityType.String() + out = appendNonEmpty(out, r.entityRef(who, s.EntityType, "app modules")) for _, ch := range s.Changes { m, ok := r.changeMember(s.EntityType.String(), ch.Attribute) report(who, m, ok) @@ -71,7 +77,12 @@ func (r *protectedRefResolver) checkFlowMembers(name ast.QualifiedName, params [ who := "retrieve $" + s.Variable if s.StartVariable != "" { report(who, protectedMember{kind: "association", qn: s.Source.String()}, s.Source.Module != "") - } else if s.Where != nil { + break + } + // A database retrieve names the entity itself, so a Hidden one is + // refused with or without a where clause. + out = appendNonEmpty(out, r.entityRef(who, s.Source, "app modules")) + if s.Where != nil { for _, steps := range xpathMemberPaths(s.Where) { for _, m := range r.pathMembers(s.Source.String(), steps) { report(who, m, true) diff --git a/mdl/executor/validate_protected_module_refs_test.go b/mdl/executor/validate_protected_module_refs_test.go index bf5decf23..6a8a2e7ce 100644 --- a/mdl/executor/validate_protected_module_refs_test.go +++ b/mdl/executor/validate_protected_module_refs_test.go @@ -269,6 +269,28 @@ func TestValidateProtectedModuleRefs(t *testing.T) { {"open module hidden attribute", "create microflow MyFirstModule.C ($e: Open.E) begin change $e (Inner = 'x'); end;", nil}, {"untyped variable is silent", "create microflow MyFirstModule.C () begin change $nobody (Inner = 'x'); end;", nil}, + // --- CE1406 on the entity of a create or a database retrieve (#1652) --- + {"retrieve of hidden entity", + "create microflow MyFirstModule.R () begin retrieve $l from MyAddOn.Request; end;", + []string{"CE1406", "retrieve $l is not allowed to use entity 'MyAddOn.Request'", "app modules cannot reference hidden elements"}}, + {"retrieve of hidden entity with where", + "create microflow MyFirstModule.R () begin retrieve $l from MyAddOn.Request where Payload = 'x'; end;", + []string{"CE1406", "entity 'MyAddOn.Request'"}}, + {"retrieve of usable entity", "create microflow MyFirstModule.R () begin retrieve $l from MyAddOn.Pub; end;", nil}, + {"bare create of hidden entity", + "create microflow MyFirstModule.C () begin $r = create MyAddOn.Request; end;", + []string{"CE1406", "create MyAddOn.Request is not allowed to use entity 'MyAddOn.Request'"}}, + {"create of usable entity", "create microflow MyFirstModule.C () begin $p = create MyAddOn.Pub (Shown = 'x'); end;", nil}, + {"retrieve of hidden entity made usable by the script", + "alter entity MyAddOn.Request set export level api;\ncreate microflow MyFirstModule.R () begin retrieve $l from MyAddOn.Request; end;", nil}, + {"retrieve of hidden entity in a nanoflow", + "create nanoflow MyFirstModule.NF () begin retrieve $l from MyAddOn.Request; end;", + []string{"CE1406", "entity 'MyAddOn.Request'"}}, + {"add-on module retrieves its own hidden entity", + "create microflow MyAddOn.Own () begin retrieve $l from MyAddOn.Request; $r = create MyAddOn.Request; end;", nil}, + {"open module retrieve and create of hidden entity", + "create microflow MyFirstModule.C () begin retrieve $l from Open.E; $e = create Open.E; end;", nil}, + // --- controls: an ordinary module hides nothing --- {"open module page", "create or modify navigation Responsive home page Open.P;", nil}, {"open module hidden microflow", "create microflow MyFirstModule.C () begin call microflow Open.MF(); end;", nil},