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:
safishamsiandClaude Opus 4.8 committed 2026-10-10 01:28:39 +01:00
1 parent 289aa41284
commit 162159dbd6
2 files changed
+80 -1

No files matched your search

+5 -1
View File
@@ -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}"
)