Skip to content

fix: references to members/associations that do not exist pass check (#1690, #1646, #1697) - #1738

Merged
ako merged 4 commits into
mainfrom
fix/1690-member-references
Oct 27, 2026
Merged

ako merged 4 commits into
mainfrom
fix/1690-member-references

Conversation

@ako

@ako ako commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Closes #1690. Closes #1646. Closes #1697 (mirror of mendixlabs#1650).

References to a member or association that does not exist passed check and exec; only mx check noticed — and in #1690 the model was written broken. One commit per issue on one shared core.

Shared core: memberVerdictOver (new member_verdict.go) is the single walk over an entity's generalization chain. Exec's builder reads each link from the backend (backendMemberModel), check from the script with the backend as fallback (scriptMemberModel) — the overlay changes how a link is read, never the rule. The existing CREATE/CHANGE member check (resolveMemberOnEntity, #1048) now delegates to it. Inherited members, System members and members of modules that are not loaded are not refused.

Measurement trap worth knowing: while any CE1613 exists in an app, mx check reports no CE0117 and no CE0226. Every control was re-measured in an otherwise clean app — one earlier "clean" result was masked (an aggregate over createdDate is CE0226, now refused).

#1690 — a bare association in a retrieve was stored with an empty module

retrieve $C from $O/Ord_Cust; passed check and exec and was stored as .Ord_Cust (CE1613; describe printed $O/.Ord_Cust, which does not parse). Cause: the builder built s.Source.Module + "." + s.Source.Name; check never looked at a bare retrieve association, and MDL-RETRIEVE03 guessed the start entity's module. Fix: a bare name resolves when exactly one association of that name has an end on the start entity's generalization chain (either end; specializations inherit) and is stored qualified; no match or several are refused by check and exec with the qualified spelling to write, and exec refuses a bare name it cannot resolve rather than writing .Name. Describe's speller and MDL-RETRIEVE03 use the same resolver, so a re-run is Unchanged.
Verified: PedApp tests (check and exec must agree): stored bytes, describe, re-run; owner side, TO side, specialization, inherited cross-module association, Administration.Account/UserRoles → System.UserRoles; script-declared association; missing, not-an-end, ambiguous, unknown start entity. Reverted: every case stores .Ord_Cust. 11.13.0: before CE1613 and AssociationId ".Ord_Cust"; after 0 errors, "Shop.Ord_Cust", describe $O/Shop.Ord_Cust.

#1646 — find / filter / sort / aggregate by a member the entity lacks

Cause: the builder fabricated <entity>.<name> for an unresolved member (resolveMemberChange fallback via resolveListOperationMember, sort, aggregate). Fix: the builder refuses a member established as missing with check's own message (listMemberRefusal, asserted identical); check's listMember uses the same verdict. Sibling fixed: the aggregate never walked the generalization chain — sum $sl by Total with Total inherited was stored as Shop.SubOrd.Total (CE1613) although the member exists; it now qualifies with the declaring entity. A sort may read createdDate/changedDate where the root stores them (clean); an aggregate cannot (CE0226, measured) — refused, naming CE0226.
Verified: refusals for the four reported shapes, maximum by a missing member, bare owner (hint System.owner), aggregate over createdDate; controls (own/inherited attributes, bare and inherited cross-module associations #1637, System.owner #1629, sorts on stored system dates, an untyped call-result list, a member the script adds with ALTER first). Reverted: refusals accepted, aggregate stores SubOrd.Total. 11.13.0: before 4× CE1613; after refused; an 11-statement control microflow builds at 0 errors.

#1697 — $Var/<member> in an expression for a member the entity lacks (incl. id)

Cause: nothing resolved an expression path's member steps (the type pass infers an unknown attribute as unknown, silently). Fix (check side, in the reference pass, which exec's preflight also runs): validate_flow_member_refs.go follows each $Var/... path through the same verdict across association hops and reports a missing attribute, a bare association, an association that does not exist or does not start at the entity, and id (hint: compare the objects, or CommunityCommons.getGUID). Paths are collected by reflecting over each statement's fields, so new statement kinds are covered. The builder still writes expressions verbatim, so exec --no-check does not refuse these.
Verified: the 12 shapes measured as CE0117 on 11.13.0 are reported; 11 measured-clean shapes are not (incl. $S/createdDate on a specialization, a bare hop target). Reverted: every report case fails. 11.13.0: before 2× CE0117; after refused by check and exec.

All

Noticed, not fixed

  1. In an expression, $O/System.owner/Name is CE0117 (the builder does not insert the System.User step).
  2. walkTypedFlow does not descend into enum-split / inheritance-split case bodies, so every member check is silent inside case bodies.
  3. The Protected-module (fix: check misses CE1406 for Hidden entities of Protected modules and on call results (#1699, #1701) #1734) check skips a bare retrieve association; now that bare names resolve, a bare name to a Hidden association of a Protected module is not checked for CE1406.
  4. walkTypedFlow does not type a bare-association retrieve's result.
  5. Under exec --no-check, CREATE/CHANGE with a missing member and a bad expression member are still written as-is.
  6. A bare association inside an expression is reported, not auto-qualified as a retrieve now is.

🤖 Generated with Claude Code

ako and others added 4 commits October 26, 2026 22:53
…1690)

`retrieve $C from $O/Ord_Cust;` - an association path written with the
association's short name - passed `check -p --references` and exec and
was stored as the association ".Ord_Cust": mx check CE1613 "The selected
association '.Ord_Cust' no longer exists.", and describe printed
`$O/.Ord_Cust`, which does not parse.

Cause: addRetrieveAction joined the AST's empty Module to the name, and
nothing on the check side looked at a bare retrieve association.
MDL-RETRIEVE03's qualifyAssoc guessed the start entity's module - a
second answer to the same question.

Fix: one resolver, resolveBareAssociation (bare_association.go), a
function of plain data - the name, the start entity's generalization
chain, and an association catalog with both ends of every association.
A bare name resolves when exactly one association of that name has an
end on the chain (either end; a specialization inherits) and is stored
qualified; none or several is refused, with the qualified spelling to
write. The builder feeds it the project as the earlier statements left
it, check --references the project plus the associations declared
before the flow (validate_flow_member_refs.go), so the two agree; exec
also refuses a bare name it cannot resolve rather than writing ".Name".
The describe speller and MDL-RETRIEVE03 ask the same resolver, so a
re-run of the script reports "Unchanged microflow".

Verification: retrieve_bare_association_1690_test.go (PedApp, check and
exec agree; stored unit bytes; describe; re-run unchanged; refusals;
speller). Reverting the fix fails every case with ".Ord_Cust" stored and
check/exec accepting. Real 11.13.0 app: the reported repro went from
CE1613 to 0 errors with "Shop.Ord_Cust" stored and described; owner
side, TO side, specialization, cross-module inherited and
Account/UserRoles -> System.UserRoles all resolve and build at 0 errors;
a missing and an ambiguous name are refused by check, exec and
exec --no-check. No doctype or bug-test script changes verdict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heck and exec (#1646)

`find $l by Numbr = '42'`, and filter / sort / sum by a member the
list's entity does not have, passed `check -p` and exec; mx check then
reported CE1613 "The selected attribute 'Shop.Order.Numbr' no longer
exists." once per activity. An aggregate by an INHERITED attribute was
broken the same way: `sum $sl by Total` was written as
"Shop.SubOrd.Total" (CE1613).

Cause: the builder's member resolution (resolveMemberChange via
resolveListOperationMember, the sort branch, the aggregate) fell back to
writing `<entity>.<name>` when the name resolved to nothing, and the
aggregate never walked the generalization chain. check --references
judged CREATE/CHANGE members and retrieve constraints, but no list
operation member.

Fix: one chain walk, memberVerdictOver (member_verdict.go), over a
per-link interface - read from the backend by the builder, from the
script over the backend by check - so the script overlay changes how a
link is read, never the rule. The builder refuses a member established
as missing with listMemberRefusal, the message check reports, so exec
without the preflight says what check says; resolveMemberOnEntity now
delegates to the same walk. The aggregate qualifies an attribute with
the entity that declares it. A sort may read createdDate/changedDate
where the root stores them; an aggregate may not (CE0226, measured).

Verification: list_member_refs_1646_test.go (PedApp, check and exec
agree): the four reported shapes plus maximum, bare `owner` and an
aggregate over createdDate are refused; own/inherited attributes, bare
and inherited cross-module associations, System.owner, sorts on a
stored system date, an untyped list and a member the script adds first
stay accepted; the inherited aggregate stores the declaring entity.
Reverting the fix fails the refusal tests with check and exec accepting
and the aggregate storing B1646.SubOrd.Total. Real 11.13.0 app: the
repro went from 4 x CE1613 to refused by check and exec; the controls
build at 0 errors. No doctype or bug-test script changes verdict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…id (#1697)

`toString($User/id)`, `$A/id = $B/id` and `$User/Bogus` passed
`check -p --references` - the type pass said "Expression types OK" -
and exec wrote them; mx check reported CE0117 "Error(s) in expression."
(mirror of mendixlabs#1650).

Cause: nothing resolved the member steps of an expression's `$Var/...`
path against the variable's entity. The type pass infers an unknown
attribute as KindUnknown and stays silent, and it is not part of exec's
preflight anyway.

Fix: the reference pass - which check --references and exec's preflight
both run - follows every `$Var/...` path in a flow statement's
expressions through the same member verdict as #1646, across
association hops: an attribute the entity (or anything it extends) does
not have, a bare association, an association that does not exist or
does not start at the entity are reported, and `id` gets the
compare-the-objects / CommunityCommons.getGUID hint. The hop's target
entity may follow the association qualified or bare (the builder
qualifies a bare one, #829). Paths are collected by reflecting over each
statement's fields, so a statement kind added later is covered; nested
bodies are left to the typed walk.

Verification: expression_member_refs_1697_test.go (PedApp): the twelve
shapes measured as CE0117 on 11.13.0 are reported, the ten measured
clean are not (plus the bare target entity step), the System.User repro
and a loop/IF body. Reverting
the fix fails every report case. Real 11.13.0 app: the repro went from
2 x CE0117 to refused by check and exec; the controls build at 0
errors. Every CE0117 shape was measured in an app with no other error -
a CE1613 elsewhere suppresses them all. No doctype or bug-test script
changes verdict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ences

# Conflicts:
#	mdl/executor/validate_member_refs.go
@ako
ako merged commit 60da2e5 into main Oct 27, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant