From fbc24c7d3f8a748838b1a08452d1ff63cd92066b Mon Sep 17 00:00:00 2001 From: safishamsi Date: Sat, 25 Jul 2026 22:53:43 +0100 Subject: [PATCH] fix(extract): suppress builtin/stdlib Python decorators from reference edges (follow-up to #2154) The decorator reference edges added in #2154 fabricated sourceless stub nodes for @property/@staticmethod/@dataclass/@functools.wraps and, via the unique-function rewire, could stamp a false edge onto a corpus's own def wraps(). Add _PYTHON_DECORATOR_NOISE (mirroring _PYTHON_ANNOTATION_NOISE) and skip those names, same accepted tradeoff as patch/Mock in annotations. Co-Authored-By: Claude Opus 4.8 (1M context) --- graphify/extractors/engine.py | 18 +++++++++- tests/test_python_decorators.py | 63 +++++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index aa1e5352..39c69879 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -65,6 +65,20 @@ _PYTHON_ANNOTATION_NOISE = frozenset({ "NonCallableMagicMock", "PropertyMock", "patch", "sentinel", }) +# Builtin/stdlib decorators (@property, @dataclass, @functools.wraps, …) are +# ambient vocabulary, not corpus symbols: emitting decorator edges for them +# fabricates sourceless stub nodes on nearly every class-heavy file, and the +# unique-function rewire can collapse them onto an unrelated local definition +# (a corpus defining its own `def wraps(...)` gets a false decorator edge). +# Same name-based tradeoff as `patch`/`Mock` in _PYTHON_ANNOTATION_NOISE. +_PYTHON_DECORATOR_NOISE = frozenset({ + "property", "staticmethod", "classmethod", "abstractmethod", + "abstractproperty", "cached_property", "wraps", "lru_cache", "cache", + "singledispatch", "singledispatchmethod", "total_ordering", + "contextmanager", "asynccontextmanager", "overload", "override", + "final", "no_type_check", "runtime_checkable", "dataclass", +}) + def _python_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: """Walk a Python type annotation; append (name, role) where role is 'type' or 'generic_arg'. @@ -3609,7 +3623,9 @@ def _extract_generic( if child.type != "decorator": continue deco_name = _python_decorator_name(child, source) - if not deco_name: + # Builtin/stdlib decorators are noise: no stub nodes, + # no false rewires onto same-named local definitions. + if not deco_name or deco_name in _PYTHON_DECORATOR_NOISE: continue deco_line = child.start_point[0] + 1 target = ensure_named_node(deco_name, deco_line) diff --git a/tests/test_python_decorators.py b/tests/test_python_decorators.py index 76307e8f..9e7747ae 100644 --- a/tests/test_python_decorators.py +++ b/tests/test_python_decorators.py @@ -150,14 +150,73 @@ def test_property_still_class_qualified(tmp_path): def test_decorated_class(tmp_path): f = _write(tmp_path / "pkg" / "model.py", + "from registry import register_model\n" + "\n" + "@register_model\n" + "class Point:\n" + " x: int\n") + r = extract([f], cache_root=tmp_path) + assert _make_id("register_model") in _deco_edges( + r, _class_nid("pkg/model.py", "Point")) + + +def test_stdlib_class_decorator_emits_no_edge(tmp_path): + # @dataclass is ambient stdlib vocabulary (_PYTHON_DECORATOR_NOISE): no + # decorator edge and no sourceless `dataclass` stub node. + f = _write(tmp_path / "pkg" / "dc.py", "from dataclasses import dataclass\n" "\n" "@dataclass\n" "class Point:\n" " x: int\n") r = extract([f], cache_root=tmp_path) - assert _make_id("dataclass") in _deco_edges( - r, _class_nid("pkg/model.py", "Point")) + assert _deco_edges(r, _class_nid("pkg/dc.py", "Point")) == set() + assert not any(n["id"] == _make_id("dataclass") for n in r["nodes"]) + + +def test_builtin_method_decorators_emit_no_edge_or_stub(tmp_path): + # @property / @staticmethod must not fabricate stub nodes or decorator + # edges — they would appear on nearly every class-heavy file. + f = _write(tmp_path / "pkg" / "builtins.py", + "class Config:\n" + " @property\n" + " def name(self):\n" + " return 1\n" + "\n" + " @staticmethod\n" + " def make():\n" + " return Config()\n") + r = extract([f], cache_root=tmp_path) + assert _deco_edges(r, _method_nid("pkg/builtins.py", "Config", "name")) == set() + assert _deco_edges(r, _method_nid("pkg/builtins.py", "Config", "make")) == set() + node_ids = {n["id"] for n in r["nodes"]} + assert _make_id("property") not in node_ids + assert _make_id("staticmethod") not in node_ids + + +def test_functools_wraps_does_not_rewire_onto_local_wraps(tmp_path): + # The demonstrated false positive: a corpus defining its own top-level + # `def wraps(...)` while another file uses `@functools.wraps` got a false + # decorator edge onto the local `wraps` via the unique-function rewire. + _write(tmp_path / "pkg" / "gift.py", + "def wraps(thing):\n" + " return thing\n") + f = _write(tmp_path / "pkg" / "util.py", + "import functools\n" + "\n" + "def logged(fn):\n" + " @functools.wraps(fn)\n" + " def inner(*args, **kwargs):\n" + " return fn(*args, **kwargs)\n" + " return inner\n") + r = extract([tmp_path / "pkg" / "gift.py", f], cache_root=tmp_path) + local_wraps = _func_nid("pkg/gift.py", "wraps") + assert not any( + e["target"] == local_wraps + and e["relation"] == "references" + and e.get("context") == "decorator" + for e in r["edges"] + ) def test_undecorated_function_emits_no_decorator_edge(tmp_path):