From 4a834dd24b41d60ab93f1d59bc6ac827ef2fb046 Mon Sep 17 00:00:00 2001 From: Dennis Yeung Date: Sun, 17 May 2026 20:39:19 +0800 Subject: [PATCH] fix(extract): filter language built-in globals from call edges (#726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevents `String()`, `Number()`, `Boolean()`, `fetch()`, and other language built-ins from becoming god-nodes that accumulate spurious edges from every call site across the codebase. ## Root cause Tree-sitter classifies `String(value)`, `Number(value)`, etc. as `call_expression` with the callee identifier as "String". With no local declaration to match in `label_to_nid`, the callee goes to `raw_calls` for cross-file resolution. The cross-file resolver then matches any node whose label happens to be "String" — typically the first inline call expression the AST encountered — and creates INFERRED edges from EVERY `String(...)` call in the corpus. ## Real-world impact Reproduced on a 3848-node Next.js + TypeScript codebase (revenue-proven, 1021 code files): | Metric | Before patch | After patch | |---|---|---| | `String()` god-node degree | 134 edges | 1 edge (just file containment) | | `String()` cross-community edges | 30+ communities | 0 | | Total graph edges | 5232 | 4946 (−286 spurious) | | Top god-node | `String()` (134) | `withCache()` (49) | The "Surprising Connections" report previously listed entries like `parseMinImpressions() --calls--> String()` linking `docker/opencli/` to `src/components/data-table/` — those are gone. ## Fix New `_LANGUAGE_BUILTIN_GLOBALS` frozenset of 89 known JS/TS/Python language built-ins. Filtered at two call-resolution sites: 1. Same-file resolution (4 sites across Python/JS/TS/Go/Java/PHP): `if callee_name and callee_name not in _LANGUAGE_BUILTIN_GLOBALS` 2. Cross-file resolution (1 site in `_resolve_cross_file_calls`): `if callee in _LANGUAGE_BUILTIN_GLOBALS: continue` Builtins blocked include: - JS/TS: `String`, `Number`, `Boolean`, `Object`, `Array`, `Symbol`, `BigInt`, `Date`, `RegExp`, `Error`, `Promise`, `Map`, `Set`, `JSON`, `Math`, `parseInt`, `parseFloat`, etc. - Browser/Node: `fetch`, `URL`, `URLSearchParams`, `FormData`, `Blob`, `Headers`, `Request`, `Response`, `AbortController`, `TextEncoder`, `TextDecoder`, `console` - Python: `str`, `int`, `float`, `list`, `dict`, `set`, `len`, `range`, `enumerate`, `zip`, `map`, `filter`, `print`, `isinstance`, etc. ## Related to #726, similar pattern to #908 Builds on the precedent set by PR #908 (filter Rust scoped_identifier and trait-method names) — same general approach of name-blocklist filtering for ambiguous cross-corpus names. Partial fix for #726: addresses the JS/TS `String` god-node pattern specifically. Issue #726 also mentions broader "different repository" filtering which this patch does not implement. ## Tests All 122 existing tests pass (test_extract.py 53, test_analyze.py 24, test_build.py 19, test_cluster.py 26). No new tests added — the existing test corpus does not include calls to language builtins, so the filter is a no-op there. Real-world validation was done on the 3848-node Next.js codebase cited above. Co-authored-by: Claude Opus 4.7 (1M context) --- graphify/extract.py | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index b086feec8..460762ddc 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -14,6 +14,36 @@ _RECURSION_LIMIT = 10_000 + +# Language built-in globals that AST may classify as call targets when used as +# constructors or coercion functions (e.g. String(x), Number(x), Boolean(x)). +# Without this filter they become god-nodes accumulating spurious edges from +# every call site across the codebase, polluting "God Nodes", "Surprising +# Connections", and "Suggested Questions" sections of the report. +# +# Filter applied at BOTH same-file resolution and cross-file resolution. +# See issue #726 and related discussion. +_LANGUAGE_BUILTIN_GLOBALS = frozenset({ + # JavaScript / TypeScript ECMAScript built-ins + "String", "Number", "Boolean", "Object", "Array", "Symbol", "BigInt", + "Date", "RegExp", "Error", "TypeError", "RangeError", "SyntaxError", + "ReferenceError", "EvalError", "URIError", + "Promise", "Map", "Set", "WeakMap", "WeakSet", "JSON", "Math", + "Reflect", "Proxy", "Intl", + "parseInt", "parseFloat", "isNaN", "isFinite", + "encodeURIComponent", "decodeURIComponent", "encodeURI", "decodeURI", + # Browser / Node common globals + "fetch", "URL", "URLSearchParams", "FormData", "Blob", "File", + "Headers", "Request", "Response", "AbortController", "AbortSignal", + "TextEncoder", "TextDecoder", "console", + # Python built-in callables that get confused with user code + "str", "int", "float", "bool", "list", "dict", "set", "tuple", "bytes", + "len", "range", "enumerate", "zip", "map", "filter", "sum", "min", "max", + "print", "open", "isinstance", "type", "super", "sorted", "reversed", + "any", "all", "abs", "round", "next", "iter", "hash", "id", "repr", + "callable", "getattr", "setattr", "hasattr", "delattr", "vars", "dir", +}) + def _raise_recursion_limit() -> None: if sys.getrecursionlimit() < _RECURSION_LIMIT: sys.setrecursionlimit(_RECURSION_LIMIT) @@ -1721,7 +1751,7 @@ def walk_calls(node, caller_nid: str) -> None: # Try reading the node directly (e.g. Java name field is the callee) callee_name = _read_text(func_node, source) - if callee_name: + if callee_name and callee_name not in _LANGUAGE_BUILTIN_GLOBALS: tgt_nid = label_to_nid.get(callee_name.lower()) if tgt_nid and tgt_nid != caller_nid: pair = (caller_nid, tgt_nid) @@ -3567,7 +3597,7 @@ def walk_calls(node, caller_nid: str) -> None: is_member_call = receiver_name not in go_imported_pkgs if field: callee_name = _read_text(field, source) - if callee_name: + if callee_name and callee_name not in _LANGUAGE_BUILTIN_GLOBALS: tgt_nid = label_to_nid.get(callee_name.lower()) if tgt_nid and tgt_nid != caller_nid: pair = (caller_nid, tgt_nid) @@ -3768,7 +3798,7 @@ def walk_calls(node, caller_nid: str) -> None: name = func_node.child_by_field_name("name") if name: callee_name = _read_text(name, source) - if callee_name: + if callee_name and callee_name not in _LANGUAGE_BUILTIN_GLOBALS: tgt_nid = label_to_nid.get(callee_name.lower()) if tgt_nid and tgt_nid != caller_nid: pair = (caller_nid, tgt_nid) @@ -4747,7 +4777,7 @@ def walk_calls(node, caller_nid: str) -> None: if child.type == "identifier": callee_name = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace") break - if callee_name: + if callee_name and callee_name not in _LANGUAGE_BUILTIN_GLOBALS: tgt_nid = label_to_nid.get(callee_name.lower()) if tgt_nid and tgt_nid != caller_nid: pair = (caller_nid, tgt_nid) @@ -6486,6 +6516,11 @@ def extract( callee = rc.get("callee", "") if not callee: continue + # Skip language built-in globals (String, Number, Boolean, etc.) — + # they accumulate spurious edges from every call site, creating + # god-nodes and cross-language false positives in the report. + if callee in _LANGUAGE_BUILTIN_GLOBALS: + continue # Skip member-call callees: obj.log() → "log" has no import evidence # and collides with any top-level function named "log" in the corpus. if rc.get("is_member_call"):