Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions graphify/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"):
Expand Down