Skip to content

formula: firstUndeclaredReference returns null for every undeclared reference behind a first checker error of another class #16412

Description

@claude

@objectstack/formula packages/formula/src/cel-engine.ts:185. Measured on origin/main at de75e407e, in a worktree of this repo, with packages/formula built.

The contract and the reading that breaks it

firstUndeclaredReference(source, knownFields) documents itself as "the first such bare reference, or null". It gets that answer from cel-js's checker, which reports one error, and it acts only on a message matching Unknown variable: X:

const result = env.parse(source).check?.();
if (result && result.valid === false) {
  const m = /Unknown variable:\s*([A-Za-z_$][\w$]*)/.exec(result.error?.message ?? '');
  if (m) return m[1];
}
return null;

So when the checker's FIRST error is of any other class, every undeclared reference behind it in the same source is invisible, and the helper returns null — "every reference is rooted" — for a source where none of that is true.

Measured

Probe: the strict env this helper builds (unlistedVariablesAreDyn: false, stdlib registered, SCOPE_ROOTS plus current_user / page), the raw check() verdict beside the helper's answer.

source checker's first error firstUndeclaredReference
status == "qualified" Unknown variable: status "status"
has(status) && status == "qualified" has() invalid argument null
has(status) && other == "x" has() invalid argument null
other == "x" && has(status) Unknown variable: other "other"
has(record.status) && status == "qualified" Unknown variable: status "status"

Row 3 is the one that names the mechanism: the masked name is not the guarded one. The masking is positional — everything after the first non-Unknown variable error goes unjudged, whatever it is called — not name-keyed.

A bare has(x) is the trigger measured here because has() requires a select argument, but nothing in the mechanism is specific to has(): any first checker error of another class does the same.

Who reads this helper

  • packages/lint/src/validate-visibility-predicates.ts:780 — this consumer is repaired at its own call site by card 16118, by masking has(…) calls out of the source before the checker sees it. That repair is local to that rule; it does not touch this helper.
  • packages/lint/src/flow-variable-scope.ts:273 — loops over this helper to collect flow-variable references. Unmeasured against this shape.
  • packages/formula/src/validate.ts:688 and :713 — the record-scoped bare-ref error and the unknown-field check. Unmeasured against this shape.

⛔ Not measured here: whether either of those three surfaces can actually receive a source whose first checker error is of another class, and what an author writes to get there. That is the first thing a fix should establish, because it decides whether this is a live reachability gap on those surfaces or only a latent one.

Why it is not simply "fix it upstream"

The checker hands back one error. Reporting past it needs either a re-check loop over a neutralised source or a different checker entry — a design change to an oracle several rules share, and one that would change what those rules report. That is a decision, not a patch, which is why card 16118's repair stayed at its own call site and this is filed separately.

Surfaced by the work on #16118. Dedupe: all 678 open non-PR issues enumerated through the REST list endpoint and grepped locally for firstUndeclaredReference, cel-engine, flow-variable-scope, validate-expressions, Unknown variable, undeclared reference, masks / masking; nothing covers this. Unassigned.


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions