mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 07:29:33 +00:00
fix(python): stop a qualified external annotation binding to a private local class (#4269)
`def read_body(response: httpx.Response)` names an external type, but a private `class _Response` in a test file absorbed the reference, producing a fabricated EXTRACTED-confidence edge from production code to test code. The stub-rewire key (`_node_label_key`) stripped ALL non-alphanumeric characters, including underscores, so `_Response` and `Response` collapsed to the same key and the lone local `_Response` was chosen as the unique match. Underscores are significant identifier characters, so the key now preserves them (only `()`, `<>`, `.`, and whitespace are dropped); `Foo()` still matches `Foo`, but `_Response` no longer matches `Response`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
289aa41284
commit
162159dbd6
2 files changed
+80
-1
No files matched your search
+5
-1
@@ -3465,7 +3465,11 @@ _LANGUAGE_BUILTIN_BASE_CLASSES_CI: dict[str, frozenset[str]] = {
|
||||
|
||||
def _node_label_key(node: dict, fold: bool = False) -> str:
|
||||
label = str(node.get("label", "")).strip()
|
||||
key = re.sub(r"[^a-zA-Z0-9]+", "", label)
|
||||
# Keep underscores: they are significant identifier characters, so a private
|
||||
# `_Response` must not share a key with an external `Response` and absorb its
|
||||
# reference during stub rewiring (#4269). Only drop call/generic punctuation
|
||||
# (`()`, `<>`, `.`, whitespace) so `Foo()` and `Foo` still match.
|
||||
key = re.sub(r"[^a-zA-Z0-9_]+", "", label)
|
||||
return key.lower() if fold else key
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Regression for #4269: a qualified external annotation must not bind to an
|
||||
unrelated private local class through the stub-rewire underscore collision.
|
||||
|
||||
`def read_body(response: httpx.Response)` names an external type. A private
|
||||
`class _Response` defined in a test file used to absorb that reference because
|
||||
the stub-rewire key stripped the leading underscore, making `_Response` and
|
||||
`Response` compare equal. The result was a fabricated, EXTRACTED-confidence
|
||||
edge from production code to test code.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.build import build_from_json
|
||||
from graphify.extract import extract
|
||||
|
||||
|
||||
def _edge_set(G):
|
||||
return {
|
||||
(d.get("relation"), d.get("_src", u), d.get("_tgt", v))
|
||||
for u, v, d in G.edges(data=True)
|
||||
}
|
||||
|
||||
|
||||
def test_qualified_annotation_does_not_bind_to_private_local_class(tmp_path: Path) -> None:
|
||||
(tmp_path / "app").mkdir()
|
||||
(tmp_path / "tests").mkdir()
|
||||
(tmp_path / "app" / "client.py").write_text(
|
||||
"import httpx\n\n\n"
|
||||
"def read_body(response: httpx.Response) -> bytes:\n"
|
||||
" return response.content\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "tests" / "test_fake.py").write_text(
|
||||
"class _Response:\n"
|
||||
' """Stand-in for a urllib response."""\n\n'
|
||||
" def read(self):\n"
|
||||
' return b""\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
paths = [tmp_path / "app" / "client.py", tmp_path / "tests" / "test_fake.py"]
|
||||
res = extract(paths, root=tmp_path, parallel=False, cache_root=tmp_path / "graphify-cache")
|
||||
G = build_from_json(res, root=str(tmp_path), directed=True)
|
||||
edges = _edge_set(G)
|
||||
|
||||
# The fabricated production -> test edge must be gone.
|
||||
assert ("references", "app_client_read_body", "tests_test_fake_response") not in edges, (
|
||||
f"httpx.Response annotation falsely bound to the private local _Response: {edges}"
|
||||
)
|
||||
# The private class must still exist as its own node, untouched.
|
||||
assert "tests_test_fake_response" in G
|
||||
|
||||
|
||||
def test_bare_underscore_class_does_not_absorb_sibling_reference(tmp_path: Path) -> None:
|
||||
"""Minimal rewire control: a sourceless `Thing` reference must not rewire to a
|
||||
located `_Thing`; underscores are identifier-significant."""
|
||||
(tmp_path / "producer.py").write_text(
|
||||
"import external\n\n\n"
|
||||
"def build(x: external.Thing) -> None:\n"
|
||||
" return None\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "private.py").write_text(
|
||||
"class _Thing:\n"
|
||||
" pass\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
paths = [tmp_path / "producer.py", tmp_path / "private.py"]
|
||||
res = extract(paths, root=tmp_path, parallel=False, cache_root=tmp_path / "graphify-cache")
|
||||
G = build_from_json(res, root=str(tmp_path), directed=True)
|
||||
edges = _edge_set(G)
|
||||
assert ("references", "producer_build", "private_thing") not in edges, (
|
||||
f"`external.Thing` reference falsely absorbed by the private `_Thing`: {edges}"
|
||||
)
|
||||
Reference in new issue
Block a user