_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>
86 lines
4.2 KiB
Python
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")
|