Skip to content

lint: visibility-bare-identifier goes silent for an identifier that also appears inside a has() in the same predicate #16118

Description

@os-steve

@objectstack/lint@17.3.0. Measured on objectstack-ai/hotcrm at ff43321, three injections into ONE site in src/views/lead.view.ts (views[9] · formViews.status_update_modal), same run, same command (objectstack lint --json).

The reachability fact

visibility-bare-identifier reports a bare identifier in a view visibility predicate — until the SAME identifier also appears as the argument of a has() macro anywhere in that predicate. Then the rule goes silent for it entirely, including for its unwrapped occurrence.

predicate written at that site lint finding
status == "qualified" exit 1 visibility-bare-identifier at error — CONTROL
has(status) && status == "qualified" exit 0 none
has(status) exit 0 none

Baseline for all three: passed:true · errors:0 · warnings:17 · suggestions:12 — the two silent injections moved nothing at all.

The control that makes this negative worth reading

The site is definitely walked, and the walk definitely reports there. Two OTHER injections into the byte-identical anchor at the same site fired in the same harness:

  • has(record.status) && && record.status == "qualified" gives visibility-predicate-syntax at error, exit 1, naming views[9] · formViews.status_update_modal;
  • has(record.status) && notARealFn(record.status) == "qualified" gives visibility-predicate-unknown-function at error, exit 1, same site.

So this is not "the rule cannot see that view".

Why the middle row matters — it is the shape an author actually writes

has(x) && x == "…" is the guarded spelling the totality discipline pushes authors toward. An author who correctly adds the has() guard and incorrectly forgets the record. prefix on both halves lands exactly in the silent row. The predicate then never evaluates for any record (Unknown variable: status), and on a form surface an unevaluable visibleWhen fails OPEN — the field renders, carrying its required: true into the console's submit check. That is the #688 shape, and the rule that exists to catch it is quiet for it.

The plausible cause is that a has() argument is a legitimate select target and is excluded from the bare-identifier walk — but the exclusion appears to be keyed on the identifier NAME across the whole predicate rather than on the occurrence.

⛔ Not necessarily a defect in the exclusion's intent. What it is, definitely, is a reachability fact: on this input the rule reports nothing, and the guarded-but-unprefixed spelling is the one an author is most likely to produce.

How it was found

objectstack-ai/hotcrm#1625 retires local test assertions proven duplicate of platform rules. hotcrm's test/view-predicate-dialect.test.ts carries a sweep, references only namespaces the evaluator binds, which reds on all three rows above (it reads roots through collectCelRootIdentifiers). Because the platform covers only the first row, that assertion was kept rather than retired.

Dedupe ran before filing — #13594, #13652, #12665 and #14288 read in full; none covers this. Unassigned.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions