Skip to content

Exempt member access through locally-bound names from the import-alias check - #99

Open
yenda wants to merge 1 commit into
juxt:mainfrom
yenda:fix/dotted-member-access-alias-false-positive
Open

yenda wants to merge 1 commit into
juxt:mainfrom
yenda:fix/dotted-member-access-alias-false-positive

Conversation

@yenda

@yenda yenda commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #97.

Bug

The reference checker rereads ident.UppercaseField member access as the legacy dotted import form alias.TypeName. The reread is a heuristic added for usage crediting (v3.2.3), where over-inclusion was harmless; since 3.5.3's unified alias validation it also feeds check_undefined_import_aliases, where over-inclusion is an error-severity false positive:

external entity Page {
    properties: Map<String, Any>
    current_status: String? = first_non_null(
        properties.Status.status.name,
        properties.Review.status.name
    )
}

errors twice with allium.reference.undefinedImportedAlias — "Reference 'properties/Status' uses unknown import alias 'properties'" — in a module with zero use declarations, naming an alias the author never wrote. The parser disagrees with the diagnosis: allium parse yields plain MemberAccess for this shape (a real p/Name is a distinct QualifiedName node), and the member's capitalisation alone flips valid spec text to an error (properties.status.name is silent). There is no alternative syntax for a capitalised Map key — properties["Status"] is not grammar.

Fix

QRefs minted by the dotted heuristic now carry a dotted: bool flag (documented in the same style as the collection flag's #82 exemption). The unknown-alias error stands down for a dotted reference whose qualifier is a locally-bound value name — an entity/external-entity/value field, a let, a given binding, a rule trigger parameter, a surface context/facing binding, a config parameter, a default declaration's name, a variant field, a surface name: item, a lambda or for binding — top-level invariant bodies included (collect_local_value_names, a deliberate module-wide over-approximation: its one consumer only suppresses an error, so over-inclusion fails safe, while under-inclusion is the false positive being fixed).

Everything else is unchanged:

Tests

New integration file crates/allium/tests/dotted_member_access.rs (harness mirrors cross_module_lifecycle.rs), 12 tests: the headline repro draws zero reference diagnostics; lowercase-member control; OptionalAccess (properties?.Status), trigger-param/let, top-level invariant-body, variant-field, default-name and surface-binding qualifiers all exempt; unbound dotted qualifier still errors; a dotted deferred path is NEVER exempt (a dangling legacy deferred reference keeps its error even when the module binds a value name spelled the same); slash-form unknown alias still errors (#87's case C); dotted-with-alias still name-checked. Every exemption test is red on main; the guards pass on both sides. Behaviour spec updated (three new rules + one added requires: on UndeclaredImportAliasIsDiagnosedAtEveryReferenceSite); CHANGELOG entry under Unreleased. Full workspace suite green; clippy warning set byte-identical to main; the repo's own docs/project/specs diagnostics are byte-identical before/after.

Real-world impact

Any spec that models externally-named data can hit this: the keys of a Map<String, Any> field frequently come from an outside system that capitalises them (JSON API fields, HTTP header names, the page properties of a SaaS tool). The natural read — properties.Status.status.name — currently draws an error-severity diagnostic naming an import alias the author never wrote, in a module that may declare no imports at all, and no spelling of the access avoids it (properties["Status"] is not grammar). The available workarounds both document the checker instead of the domain: mute an error-severity code line by line, or restructure the spec so the capitalised name only ever appears as a string literal (e.g. route the read through a deferred lookup function).

Supporting evidence from a private ~190-spec corpus we maintain: one such derived field reddened the whole-tree gate with two errors, and a full-corpus diagnostic diff with this fix shows exactly those two disappearing and nothing else changing at any severity. This change only removes diagnostics; no previously green gate turns red.


Note: this PR and the surface-offering one both touch analysis.rs, the check behaviour spec, and the CHANGELOG - whichever merges second needs a trivial union rebase; happy to rebase promptly.

…s check

The legacy dotted form alias.TypeName is a heuristic reread of
ident.UppercaseField member access, and it fired on every such access:
properties.Status on a properties: Map<String, Any> field drew an
error-severity undefinedImportedAlias in a module with zero use
declarations (juxt#97). QRefs minted by the dotted heuristic now carry a
dotted flag, and the unknown-alias error stands down when the qualifier
is a value name the module binds anywhere: block item names (entity /
external-entity / value fields, given bindings, config parameters,
surface projections), variant field declarations, default declaration
names, lets, trigger parameters, surface context bindings, lambda and
for bindings, top-level invariant bodies included. A dotted qualifier
bound nowhere still errors, slash-form references are never exempt, and
a deferred declaration's dotted path — a code location, not a value —
keeps its error even when the module binds a value name spelled the
same.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check: a Map/record field's PascalCase member access (properties.Status) is diagnosed as an import alias (allium.reference.undefinedImportedAlias)

1 participant