mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 02:17:04 +00:00
784e9c833e
Cross-file name resolution folded case for every language, so `from pathlib import Path` resolved to a shell script's `export PATH=...` node — one variable becoming the corpus's #1 god-node (266 false incoming edges on a real repo), polluting god-node rankings, affected blast-radius, and clustering. Reported with a precise diagnosis by @sheik-hiiobd. Case is semantic in Python/Rust/Go/Java/C#/Kotlin/Swift/Ruby/C/C++/JS/TS: `Path` (class), `PATH` (env var), `path` (variable) are distinct. Fix gates folding by language at the two resolution sites the repro exercised: - global cross-file CALL resolver: index by exact case; a folded index is built only for case-insensitive-language nodes (PHP/SQL/Nim) and consulted only when the calling file is such a language. - type-reference STUB rewire (_rewire_unique_stub_nodes): match stubs to real defs by exact case, with a folded fallback restricted to case-insensitive- language definitions — so a case-sensitive `PATH` can never absorb a `Path`. For case-sensitive languages this only ever removes false edges. Concept/doc dedup (dedup.py, guarded to non-code nodes) is intentionally left folding. Regression tests: Python `Path` no longer hits shell `PATH`; a case-differing cross-file ref doesn't resolve; exact-case resolution still works; PHP fold preserved. Full suite 2777. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
88 lines
3.3 KiB
Python
88 lines
3.3 KiB
Python
"""Cross-file name resolution respects case in case-sensitive languages (#1581).
|
|
|
|
Case is semantic in most languages: `Path` (a class), `PATH` (an env var), and
|
|
`path` (a variable) are distinct. Cross-file resolution used to fold case for every
|
|
language, so `from pathlib import Path` (ubiquitous) resolved to a shell script's
|
|
`export PATH=...` node — turning one shell variable into the corpus's #1 god-node.
|
|
|
|
These tests pin: case-sensitive languages match by exact case (removing that false
|
|
edge), while genuinely case-insensitive languages (PHP) still fold.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from graphify.extract import extract
|
|
|
|
|
|
def _extract(tmp_path, files: dict[str, str]):
|
|
for name, body in files.items():
|
|
(tmp_path / name).write_text(body)
|
|
old = os.getcwd()
|
|
try:
|
|
os.chdir(tmp_path)
|
|
r = extract([Path(n) for n in files], cache_root=tmp_path)
|
|
finally:
|
|
os.chdir(old)
|
|
return r
|
|
|
|
|
|
def _labels(r):
|
|
return {n["id"]: n["label"] for n in r["nodes"]}
|
|
|
|
|
|
def test_python_Path_does_not_resolve_to_shell_PATH(tmp_path):
|
|
r = _extract(tmp_path, {
|
|
"run.sh": "export PATH=/usr/local/bin:$PATH\n",
|
|
"mod.py": (
|
|
"from pathlib import Path\n"
|
|
"def load(p: Path) -> Path:\n return Path(p)\n"
|
|
"def other():\n return load(Path('x'))\n"
|
|
),
|
|
})
|
|
lbl = _labels(r)
|
|
path_nid = next((n["id"] for n in r["nodes"] if n["label"] == "PATH"), None)
|
|
assert path_nid is not None
|
|
# No edge from the Python functions should land on the shell PATH node
|
|
false_edges = [
|
|
e for e in r["edges"]
|
|
if e["target"] == path_nid and lbl.get(e["source"], "").startswith(("load", "other"))
|
|
]
|
|
assert not false_edges, f"Python Path leaked onto shell PATH: {false_edges}"
|
|
# PATH keeps only its own `defines` edge (from run.sh), not a false super-hub
|
|
assert sum(1 for e in r["edges"] if e["target"] == path_nid) <= 1
|
|
|
|
|
|
def test_case_sensitive_cross_file_ref_respects_case(tmp_path):
|
|
r = _extract(tmp_path, {
|
|
"consts.rs": 'pub const PATH: &str = "/x";\n',
|
|
"use.rs": "struct Wrap(Path);\n", # `Path` — no such node in the corpus
|
|
})
|
|
lbl = _labels(r)
|
|
path_nid = next((n["id"] for n in r["nodes"] if n["label"] == "PATH"), None)
|
|
xref = [e for e in r["edges"] if e["target"] == path_nid and lbl.get(e["source"]) == "Wrap"]
|
|
assert not xref, "a `Path` reference must not resolve to a case-differing `PATH`"
|
|
|
|
|
|
def test_exact_case_cross_file_still_resolves(tmp_path):
|
|
r = _extract(tmp_path, {
|
|
"h.py": "def helper():\n return 1\n",
|
|
"m.py": "from h import helper\ndef go():\n return helper()\n",
|
|
})
|
|
lbl = _labels(r)
|
|
calls = {(lbl.get(e["source"]), lbl.get(e["target"]))
|
|
for e in r["edges"] if e["relation"] == "calls"}
|
|
assert ("go()", "helper()") in calls
|
|
|
|
|
|
def test_php_case_insensitive_resolution_preserved(tmp_path):
|
|
r = _extract(tmp_path, {
|
|
"lib.php": "<?php\nfunction Greet() { return 1; }\n",
|
|
"main.php": "<?php\nfunction run() { return greet(); }\n",
|
|
})
|
|
lbl = _labels(r)
|
|
calls = {(lbl.get(e["source"]), lbl.get(e["target"]))
|
|
for e in r["edges"] if e["relation"] == "calls"}
|
|
assert ("run()", "Greet()") in calls, "PHP identifiers are case-insensitive; fold must still apply"
|