# 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", }) 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")