Skip to content

fix(extract): don't bind TS/JS builtin static calls (Date.now(), …) to same-named user symbols (#1726)#1727

Closed
2loch-ness6 wants to merge 1 commit into
Graphify-Labs:v8from
2loch-ness6:fix/ts-builtin-static-call-god-node-1726
Closed

fix(extract): don't bind TS/JS builtin static calls (Date.now(), …) to same-named user symbols (#1726)#1727
2loch-ness6 wants to merge 1 commit into
Graphify-Labs:v8from
2loch-ness6:fix/ts-builtin-static-call-god-node-1726

Conversation

@2loch-ness6

@2loch-ness6 2loch-ness6 commented Jul 8, 2026

Copy link
Copy Markdown

What

Fixes #1726 — TS/JS static calls on ECMAScript built-in globals (Date.now(), Object.keys(), Math.max(), …) bind to a same-spelled user symbol, creating a false god-node.

Root cause

_resolve_typescript_member_calls treats a capitalized member-call receiver as a type name and matches it through _key(), which case-folds:

if receiver[:1].isupper():
    type_name = receiver            # "Date"
...
type_defs = type_def_nids.get(_key(type_name), [])   # _key("Date") == "date"

If the corpus has a lone user symbol whose name case-folds to the same key — e.g. a module-local const DATE (_key("DATE") == "date") — it is the unique match. Since the const has no now method, the resolver falls to relation = "references" and emits a references/call edge onto it. Date.now() (and friends) appear in nearly every file, so the one user symbol accumulates hundreds of cross-file edges.

Observed on a real 3,368-file repo: a non-exported const DATE became the #1 god-node — 472 edges, betweenness 0.435, bridging ~130 communities. Every source was a method calling new Date() / Date.now() in unrelated packages.

The call-edge pass (if callee in _LANGUAGE_BUILTIN_GLOBALS: continue) and the raw-call pass already guard against exactly this. The member-call resolver was the one path that didn't.

Fix

One guard, mirroring the existing ones — skip the receiver when it is a language builtin:

if type_name in _LANGUAGE_BUILTIN_GLOBALS:
    continue

Date is already in _LANGUAGE_BUILTIN_GLOBALS (alongside Object, Array, Math, JSON, Promise, …), so no list changes are needed.

Scope / safety

  • Legitimate receiver-typed member calls are unaffected — the resolver's real job (constructor-injection type tables, this.repo.findById()UserRepo.findById) still resolves. Covered by an added test.
  • A user type genuinely named like a builtin (class Date) matched case-sensitively would have resolved before this branch anyway; a builtin receiver never legitimately targets an unrelated same-folded user symbol.

Tests

tests/test_case_sensitive_resolution.py:

  • test_ts_builtin_static_call_does_not_resolve_to_user_symbol — reproduces the leak (fails without the guard, passes with it).
  • test_ts_receiver_typed_member_call_still_resolves — pins that legitimate cross-file member-call resolution is preserved.

Full suite green locally (2848 passed; the only failures are pre-existing tree_sitter_hcl not installed Terraform tests, unrelated to this change).

…r symbols

`_resolve_typescript_member_calls` treats a capitalized member-call receiver
(`Date.now()`, `Object.keys()`, `Math.max()`) as a type name and matches it
case-folded via `_key()`. A lone user symbol whose name case-folds to the same
key — e.g. a module-local `const DATE` — then absorbs a `references` edge from
every such call across the corpus, manufacturing a false Graphify-Labs#1 god-node (observed:
472 edges, betweenness 0.435, bridging ~130 communities in a 3.4k-file repo).

Guard the receiver against `_LANGUAGE_BUILTIN_GLOBALS` before resolving, exactly
as the call-edge and raw-call passes already do. Legitimate receiver-typed member
calls (constructor-injection type tables, `this.repo.findById()`) are unaffected.

Adds a regression test that reproduces the leak without the guard and pins that
the resolver's real cross-file resolution still works.

Fixes Graphify-Labs#1726

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014zDzgs1rz5aEdJeRJ1yuPx
safishamsi added a commit that referenced this pull request Jul 8, 2026
Adds a regression test for the Date.now()/static-call shape (credit PR #1727 /
@2loch-ness6, who independently found the same fix): a capitalized builtin
receiver must not bind cross-file to a same-spelled user const/class. The same
guard added in 67f4f83 already covers it (verified); this locks the static-call
shape in, while allowing the legitimate same-file const reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@safishamsi

Copy link
Copy Markdown
Collaborator

Thanks for this — you independently arrived at the exact same fix. This shipped in 0.9.10 (67f4f83, #1726): the identical guard if type_name in _LANGUAGE_BUILTIN_GLOBALS: continue at the same spot in _resolve_typescript_member_calls, so Date.now() / Object.keys() / Math.max() etc. no longer bind cross-file to a same-spelled user symbol.

I verified your Date.now() static-call shape (typed param arming the type table) against current v8: the cross-file phantom is gone, while a legitimate same-file reference to a real const DATE is preserved. I also folded your static-call test shape into the regression suite (crediting you). Closing as already implemented — appreciate the independent confirmation and the extra test case.

@safishamsi safishamsi closed this Jul 8, 2026
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.

TS/JS new/type references to builtin globals (e.g. Date) collapse onto same-named user symbols → false god node

2 participants