fix(extract): drop Python Self type annotation from references

A `-> Self` / `: Self` / `typing.Self` annotation (PEP 673) names the
enclosing class, not a corpus symbol, but the Python type-ref pass treated
the bare identifier `Self` as a type reference and ran it through the
sourceless-stub path. That fabricated a node with the bare id `self` that
every `Self`-annotated method in the corpus collapses onto — a god node,
the same keyword-type pitfall Rust's `Self` was fixed for.

`Self` now sits with the other typing special forms in the type-container
set, so it is skipped the way `None`/`Ellipsis`/`ClassVar` already are;
this also covers the `typing.Self` attribute spelling. Dropping it matches
how the `-> "ClassName"` self forward-ref is already left unresolved. A
genuine user return type is unaffected.
This commit is contained in:
rajatnagda45 committed 2026-10-12 22:41:18 +05:30
1 parent 7536dc0665
commit b67f05408e
2 files changed
+85

No files matched your search

+7
View File
@@ -64,6 +64,13 @@ _PYTHON_TYPE_CONTAINERS = frozenset({
"Generator", "AsyncGenerator", "ContextManager", "AsyncContextManager",
"Annotated", "ClassVar", "Final", "Literal", "Concatenate", "ParamSpec", "TypeVar",
"None", "Ellipsis",
# PEP 673: `Self` names the enclosing class, not a corpus symbol. Emitting a
# ref mints a sourceless stub with the bare id `self` that every `-> Self`
# method in the corpus collapses onto (a god node), the same keyword-type
# pitfall as Rust's `Self`. Dropping it matches how the `-> "ClassName"`
# self forward-ref is already left unresolved. `typing.Self` resolves to the
# same tail and is covered by the attribute branch below.
"Self",
})
_PYTHON_ANNOTATION_NOISE = frozenset({
+78
View File
@@ -0,0 +1,78 @@
"""PEP 673 `Self` must not mint a sourceless god node.
A `-> Self` (or `: Self`, or `typing.Self`) annotation names the enclosing
class, not a corpus symbol. The Python type-ref pass treated the bare
identifier `Self` as a type reference and ran it through the sourceless-stub
path, fabricating a node with the bare id `self` that every `Self`-annotated
method in the corpus collapses onto — the same keyword-type god node Rust's
`Self` was fixed for. It is now dropped, matching how the `-> "ClassName"`
self forward-ref is already left unresolved.
"""
from pathlib import Path
from graphify.extract import extract
def _sourceless(r):
return [(n["id"], n.get("label")) for n in r["nodes"] if not n.get("source_file")]
def test_self_return_annotation_mints_no_stub(tmp_path):
(tmp_path / "b.py").write_text(
"from typing import Self\n"
"class Builder:\n"
" def set(self, x) -> Self:\n"
" return self\n"
)
r = extract([tmp_path / "b.py"], cache_root=tmp_path, parallel=False)
assert _sourceless(r) == []
assert not any(n["id"] == "self" for n in r["nodes"])
def test_typing_qualified_self_mints_no_stub(tmp_path):
(tmp_path / "b.py").write_text(
"import typing\n"
"class Node:\n"
" def clone(self) -> typing.Self:\n"
" return self\n"
" def link(self, other: typing.Self) -> None:\n"
" pass\n"
)
r = extract([tmp_path / "b.py"], cache_root=tmp_path, parallel=False)
assert _sourceless(r) == []
def test_self_is_not_a_cross_file_god_node(tmp_path):
(tmp_path / "a.py").write_text(
"from typing import Self\n"
"class A:\n"
" def f(self) -> Self:\n"
" return self\n"
)
(tmp_path / "b.py").write_text(
"from typing import Self\n"
"class B:\n"
" def g(self) -> Self:\n"
" return self\n"
)
r = extract(sorted(tmp_path.glob("*.py")), cache_root=tmp_path, parallel=False)
assert not any(n["id"] == "self" for n in r["nodes"])
assert not any(e.get("target") == "self" for e in r["edges"])
def test_real_return_type_still_referenced(tmp_path):
# The guard must not suppress a genuine user return type.
(tmp_path / "m.py").write_text(
"class Foo:\n"
" pass\n"
"class B:\n"
" def make(self) -> Foo:\n"
" return Foo()\n"
)
r = extract([tmp_path / "m.py"], cache_root=tmp_path, parallel=False)
foo_ids = [n["id"] for n in r["nodes"] if n.get("label") == "Foo"]
assert foo_ids
assert any(
e.get("relation") == "references" and e.get("target") in foo_ids
for e in r["edges"]
)