Context
Found during round-19 review of the #2088 implementation plan (docs/plans/issue-2088.md, PR #2612) — the receiver-correlated invoked-property-evidence design for object-literal value-refs (computeObjectLiteralSiteEscapes / allReferencesTracked in src/extractors/javascript.ts, and its Rust mirror).
The gap
isGlobalObjectQualifiedWrite (round 16, #2634) already accepts the classic-script premise for a globalThis/global/self/window-qualified write to a script-scope var — subtreeContainsReassignmentOf recognizes globalThis.name = ... as a reassignment of name, alongside the bare-identifier spelling.
The symmetric read is not modeled at all. In a classic (non-module) script, a top-level var T = {...} attaches to the global object, so globalThis.T.alpha() is exactly as reachable a reference to T as a bare T.alpha() is. allReferencesTracked's reference-matching walk only ever matches a bare identifier/shorthand_property_identifier node whose text equals the binding name (round 19 widens this filter to include shorthand_property_identifier, but a member_expression's property field is a property_identifier — a distinct, deliberately-excluded node kind, since an object-literal or member-expression key is never itself a value-producing reference to a binding of the same name). A globalThis.T read is therefore invisible to the walk in both directions: it is never counted as a reference at all, so a table read only this way is (wrongly) treated as if it has zero references beyond its own tracked ones — potentially reading as non-escaping (escapes = 0) when it should escape.
Verified runnable (approximating the classic-script premise via a function-scoped var accessed through the global object, the same mechanism isGlobalObjectQualifiedWrite's own fixtures already rely on):
function fnA() { return 1; }
var T = { alpha: fnA };
function sink() { return globalThis.T.alpha(); }
sink(); // -> 1, invoking fnA via T's own alpha
Direction
UNDER-escape (a site that reads escapes = false while a real invocation is invisible to T1 — a soundness gap, not a recall trade-off) — same direction as #2634's own write-side gap, the read-side counterpart of it.
Why not fixed inline
Symmetric to #2634, but scoped out of round 19 rather than fixed inline: modeling a globalThis-qualified read requires teaching allReferencesTracked's reference-matching walk to recognize a member_expression/subscript_expression whose object is globalThis/global/self/window (and whose property/subscript key matches the tracked name) as a genuine reference position — not merely reusing isGlobalObjectQualifiedWrite's existing write-side check, since the read side needs to feed into the reference-tracking walk (condition 3) rather than the reassignment-detection scan (condition 4's resolution chain), a different consumer with its own tracked/untracked classification via isTrackedReferencePosition. This is real design/implementation scope, not a one-line fix, and deserves its own round with its own review — the same reasoning already applied to #2625/#2627/#2631/#2633/#2635 for other recall-costing exclusions in this same design.
Suggested fix shape (for whoever picks this up)
- New disjunct (or shape) recognized by
isTrackedReferencePosition / allReferencesTracked's reference walk: a member_expression (globalThis.T) or subscript_expression (globalThis['T']) whose object (after unwrapParens) is an identifier in GLOBAL_OBJECT_NAMES, and whose property/subscript key (quote-stripped, $-free for the subscript case) equals the binding name — reusing the exact same normalization isGlobalObjectQualifiedWrite and isTrackedReferencePosition's own subscript branch already apply.
- Must be mirrored in
crates/codegraph-core/src/extractors/javascript.rs per this plan's dual-engine parity requirement (ADR-001).
- Needs its own WU-10 fixture(s): an escape-fallback case proving a
globalThis-qualified read must escape, and a correlation-shape guard proving an ordinary (non-global) object's own same-named property read is not mistaken for one.
Context
Found during round-19 review of the #2088 implementation plan (
docs/plans/issue-2088.md, PR #2612) — the receiver-correlated invoked-property-evidence design for object-literal value-refs (computeObjectLiteralSiteEscapes/allReferencesTrackedinsrc/extractors/javascript.ts, and its Rust mirror).The gap
isGlobalObjectQualifiedWrite(round 16, #2634) already accepts the classic-script premise for aglobalThis/global/self/window-qualified write to a script-scopevar—subtreeContainsReassignmentOfrecognizesglobalThis.name = ...as a reassignment ofname, alongside the bare-identifier spelling.The symmetric read is not modeled at all. In a classic (non-module) script, a top-level
var T = {...}attaches to the global object, soglobalThis.T.alpha()is exactly as reachable a reference toTas a bareT.alpha()is.allReferencesTracked's reference-matching walk only ever matches a bareidentifier/shorthand_property_identifiernode whose text equals the binding name (round 19 widens this filter to includeshorthand_property_identifier, but amember_expression'spropertyfield is aproperty_identifier— a distinct, deliberately-excluded node kind, since an object-literal or member-expression key is never itself a value-producing reference to a binding of the same name). AglobalThis.Tread is therefore invisible to the walk in both directions: it is never counted as a reference at all, so a table read only this way is (wrongly) treated as if it has zero references beyond its own tracked ones — potentially reading as non-escaping (escapes = 0) when it should escape.Verified runnable (approximating the classic-script premise via a function-scoped
varaccessed through the global object, the same mechanismisGlobalObjectQualifiedWrite's own fixtures already rely on):Direction
UNDER-escape (a site that reads
escapes = falsewhile a real invocation is invisible to T1 — a soundness gap, not a recall trade-off) — same direction as #2634's own write-side gap, the read-side counterpart of it.Why not fixed inline
Symmetric to #2634, but scoped out of round 19 rather than fixed inline: modeling a
globalThis-qualified read requires teachingallReferencesTracked's reference-matching walk to recognize amember_expression/subscript_expressionwhoseobjectisglobalThis/global/self/window(and whoseproperty/subscript key matches the tracked name) as a genuine reference position — not merely reusingisGlobalObjectQualifiedWrite's existing write-side check, since the read side needs to feed into the reference-tracking walk (condition 3) rather than the reassignment-detection scan (condition 4's resolution chain), a different consumer with its own tracked/untracked classification viaisTrackedReferencePosition. This is real design/implementation scope, not a one-line fix, and deserves its own round with its own review — the same reasoning already applied to #2625/#2627/#2631/#2633/#2635 for other recall-costing exclusions in this same design.Suggested fix shape (for whoever picks this up)
isTrackedReferencePosition/allReferencesTracked's reference walk: amember_expression(globalThis.T) orsubscript_expression(globalThis['T']) whoseobject(afterunwrapParens) is anidentifierinGLOBAL_OBJECT_NAMES, and whoseproperty/subscript key (quote-stripped,$-free for the subscript case) equals the binding name — reusing the exact same normalizationisGlobalObjectQualifiedWriteandisTrackedReferencePosition's own subscript branch already apply.crates/codegraph-core/src/extractors/javascript.rsper this plan's dual-engine parity requirement (ADR-001).globalThis-qualified read must escape, and a correlation-shape guard proving an ordinary (non-global) object's own same-named property read is not mistaken for one.