Files
graphify/graphify/extractors/base.py
T
MasterFede5andClaude Fable 5 33aa89c722 fix(extract,analyze): filter Swift/Foundation/SwiftUI builtins from resolution and god-node ranking (#2147)
_LANGUAGE_BUILTIN_GLOBALS and _BUILTIN_NOISE_LABELS covered only JS/TS and
Python, so on Swift codebases framework symbols (Foundation, NSLock, View,
Data, Sendable, ...) ranked as god nodes, and the Swift member-call resolver
could bind a builtin-typed receiver (let d: Data) to a same-named user symbol
in another file — the same phantom-edge shape #1726 fixed for TypeScript.

- extractors/base.py: add Swift stdlib value types, conformance protocols,
  Foundation types, and SwiftUI View/Color/Font to _LANGUAGE_BUILTIN_GLOBALS
- analyze.py: add the same set plus framework module names (Foundation,
  SwiftUI, UIKit, AppKit, Combine) to _BUILTIN_NOISE_LABELS
- extract.py: _resolve_swift_member_calls now skips builtin receiver types,
  matching the guard the TS/Python member-call resolvers already have (#1726)
- tests: god_nodes exclusion (parametrized) + Swift builtin-receiver
  no-bind regression + user-type-still-resolves guard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 15:44:50 +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")