Files
PathGao 3868434942 fix(go): scope the predeclared-function filter to Go bare identifiers
Review feedback on #2296: the previous revision added Go's predeclared
names to _LANGUAGE_BUILTIN_GLOBALS, which ~11 languages consult through
engine.py and the cross-file pass, and whose check wraps the in-file
EXTRACTED branch as well as raw_calls. Two confirmed regressions:

  * Rust normalizes 'Widget::new(3)' to the bare token 'new', so every
    in-file 'Type::new()' edge disappeared. Rust keeps its own
    _RUST_TRAIT_METHOD_BLOCKLIST, deliberately on the cross-file branch
    only — this change follows that language-local pattern.
  * Go 'h.append(v)' is a selector_expression call to a real method and
    was dropped with it. On the 3x-ui corpus this cost a genuine
    'systemMetrics.append(...)' -> '(*metricHistory).append' edge.

The filter now lives in extractors/go.py as _GO_PREDECLARED_FUNCS and
fires only when the callee node is a bare identifier, so selector calls
('h.append(v)', 'pkg.Delete(x)') and every other language are untouched.
Go raw_calls now carry language="go" and the shared pass gates on it,
mirroring the bash gate, as a backstop for Go raw_calls minted
elsewhere. The set is the Go spec's predeclared list in full: being
Go-local and bare-identifier-only makes 'len'/'max'/'min'/'print' safe
to include, and a spec boundary beats a hand-picked subset.

Remeasured on the same 466-file Go corpus:

  upstream v8      16904 edges, 334 inbound to 'append'
  previous rev     16571 edges,   1 inbound  (332 phantom gone, but the
                                              genuine selector call too)
  this rev         16572 edges,   2 inbound  (332 phantom gone, genuine
                                              call restored)

Tests kept, plus three regressions: builtin 'append' must not bind
in-file (the branch a cross-file-only gate would miss), the Go selector
call must survive, and the in-file Rust 'Type::new()' edge must survive.
The last two fail on the previous revision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:13:48 +01:00

86 lines
4.2 KiB
Python

# DO NOT import from graphify.extract here — direction is extract.py → extractors/ only.
from __future__ import annotations
from pathlib import Path
from graphify.ids import make_id
# 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. Filter applied at same-file and cross-file resolution.
# See issue #726.
_LANGUAGE_BUILTIN_GLOBALS: frozenset[str] = 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
"URL", "URLSearchParams", "FormData", "Blob", "File",
"Headers", "Request", "Response", "AbortController", "AbortSignal",
"TextEncoder", "TextDecoder", "console",
# Python built-in callables
"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",
# Swift standard library / Foundation / SwiftUI (#2147). Value-type
# initializers (Data(x), Int(x), UUID()) and protocol conformance targets
# appear from virtually every file of a Swift codebase, exactly like the
# ECMAScript constructors above. String/Date/URL/Error are already listed.
"Int", "Int8", "Int16", "Int32", "Int64",
"UInt", "UInt8", "UInt16", "UInt32", "UInt64",
"Double", "Float", "Bool", "Character",
"Sendable", "Codable", "Decodable", "Encodable", "Equatable", "Hashable",
"Identifiable", "Comparable", "CaseIterable", "RawRepresentable",
"CustomStringConvertible", "CustomDebugStringConvertible", "AnyObject",
"LocalizedError",
"Data", "UUID", "Decimal", "Calendar", "Locale", "TimeZone", "Bundle",
"IndexPath", "IndexSet", "NotificationCenter", "UserDefaults",
"FileManager", "URLSession", "URLRequest", "URLComponents",
"JSONDecoder", "JSONEncoder", "DateFormatter", "NumberFormatter",
"ISO8601DateFormatter",
"NSObject", "NSString", "NSError", "NSLock", "NSAttributedString",
"DispatchQueue", "DispatchGroup", "OperationQueue", "RunLoop",
"View", "Color", "Font",
})
def _make_id(*parts: str) -> str:
return make_id(*parts)
def _file_stem(path: Path) -> str:
"""Stem used as the node-ID prefix for a file and its symbols.
The full path (extension dropped) is preserved as path segments; ``make_id``
later collapses the separators to underscores. Using every segment — not just
the immediate parent dir (#1504) — means same-named files in different
directories get distinct IDs instead of colliding into one
last-writer-wins node:
docs/v1/api/README.md -> docs/v1/api/README -> docs_v1_api_readme
docs/v2/api/README.md -> docs/v2/api/README -> docs_v2_api_readme
Top-level files keep a bare stem (``setup.py`` -> ``setup``). When passed an
absolute path the whole path is encoded; the extract() id-remap post-pass
re-derives the canonical repo-relative form from ``source_file`` so the on-disk
location can't leak into the persisted IDs (#502).
Returns "" for a path with no name (``Path('.')`` — a source_file that equals
the scan root, so it has no per-file stem). Guarding here keeps
``path.with_suffix("")`` from raising ``ValueError: '.' has an empty name`` and
protects every caller, not just ``_semantic_id_remap`` (#1618)."""
if not path.name:
return ""
return path.with_suffix("").as_posix()
def _read_text(node, source: bytes) -> str:
return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")