Skip to content

escape analysis: classic-script globalThis-qualified READS invisible to allReferencesTracked (#2088) #2640

Description

@carlos-alm

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 varsubtreeContainsReassignmentOf 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.

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