From 474d23b1829eaa81cb23f4ddef9f343921dd84fa Mon Sep 17 00:00:00 2001 From: Deepjyoti Sarmah Date: Mon, 19 Oct 2026 20:32:54 +0530 Subject: [PATCH] fix: a Rust use rooted at a child mod reaches its declaration (#4564) A `use` whose first path segment is a `mod` declared in the same scope, such as `mod item; pub use item::Invoice;`, named no node for the stub rewire. When nothing else lent the rewire a node, the import edge dangled, build minted an external stub `invoice`, and `affected` and `explain` tied on it, the same symptom as #4439 for paths rooted at `crate`, `self` or `super`. The extractor now also names a `use` whose first segment is a `mod` declared beside it. The scope is the use's own parent, so an inline `mod tests { .. }` does not see the file's top-level mods, and a path that starts with `::` is never named, because it always means a crate. `use serde::Serialize` therefore still never lands on a same-named type in the corpus. The AST cache schema goes from 17 to 18 so cached Rust files are extracted again instead of keeping the dangling edge. --- graphify/cache.py | 2 +- graphify/extractors/rust.py | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/graphify/cache.py b/graphify/cache.py index 0ffe13e1c..df39c6a35 100644 --- a/graphify/cache.py +++ b/graphify/cache.py @@ -37,7 +37,7 @@ except Exception: _EXTRACTOR_VERSION = "unknown" # Bump when AST cache-key semantics change independently of the package version. -_AST_CACHE_SCHEMA = 17 # Batch: Python pytest-fixture markers (`_py_fixture`/`_python_fixture_param`, #4332) and module-level class-id collision salting (#2810); TS `this.field` typed from any method incl. static (#4473); Solidity callables carry `metadata.arity` + inherited-call/`super`/modifier raw calls (#4379); Go type-decl and Rust file-item case-distinct node ids (#4444); prior (16): Rust `use` edges carry the imported name as written (`_rust_use_name`, #4439); (15): Go generic receiver types normalized (`Box[T]` -> `Box`) so methods attach to the type node; prior (14): C# receiver resolution for nameless/`new X()`/`foreach` receivers (defer + receiver-chain markers) and `_`/case-distinct node ids (underscore-salted); prior (13): Kotlin/Scala/C++/Swift generic & template type params no longer mint sourceless stubs; Objective-C NS_ENUM/NS_OPTIONS extraction and `_callable`/`_callable_class` markers; Go and PHP receiver-typed member calls park cross-repo (`go_type_table`/`php_type_table`, receiver fields); prior (12): Java inherited-supertype (`receiver_unbound`) and chained/new/cast receiver typing (`java_receiver_chain`, `_java_ret`); C/C++ quoted cross-directory include provenance (`include_kind`); JS external imports anchored to the package, not the path leaf; prior (11): Python property-accessor decorators / `Self`, Swift `Self`, Go generic type params, Python forward type-hint file node, PowerShell dot-source, C# verbatim-interpolated string recovery; (10): C# partial types carry `type_path`; (9): Python property-read raw calls and `_py_property` nodes; (8): Rust `use`-group per-binding edges; (7): Python opaque-base/super(args) markers + nested-class enclosing ids. +_AST_CACHE_SCHEMA = 18 # Rust `use` rooted at a `mod` declared in the same scope carries `_rust_use_name` (#4564); prior (17): Python pytest-fixture markers (`_py_fixture`/`_python_fixture_param`, #4332) and module-level class-id collision salting (#2810); TS `this.field` typed from any method incl. static (#4473); Solidity callables carry `metadata.arity` + inherited-call/`super`/modifier raw calls (#4379); Go type-decl and Rust file-item case-distinct node ids (#4444); prior (16): Rust `use` edges carry the imported name as written (`_rust_use_name`, #4439); (15): Go generic receiver types normalized (`Box[T]` -> `Box`) so methods attach to the type node; prior (14): C# receiver resolution for nameless/`new X()`/`foreach` receivers (defer + receiver-chain markers) and `_`/case-distinct node ids (underscore-salted); prior (13): Kotlin/Scala/C++/Swift generic & template type params no longer mint sourceless stubs; Objective-C NS_ENUM/NS_OPTIONS extraction and `_callable`/`_callable_class` markers; Go and PHP receiver-typed member calls park cross-repo (`go_type_table`/`php_type_table`, receiver fields); prior (12): Java inherited-supertype (`receiver_unbound`) and chained/new/cast receiver typing (`java_receiver_chain`, `_java_ret`); C/C++ quoted cross-directory include provenance (`include_kind`); JS external imports anchored to the package, not the path leaf; prior (11): Python property-accessor decorators / `Self`, Swift `Self`, Go generic type params, Python forward type-hint file node, PowerShell dot-source, C# verbatim-interpolated string recovery; (10): C# partial types carry `type_path`; (9): Python property-read raw calls and `_py_property` nodes; (8): Rust `use`-group per-binding edges; (7): Python opaque-base/super(args) markers + nested-class enclosing ids. # Per-process memo: distributions() scans site-packages metadata, which is diff --git a/graphify/extractors/rust.py b/graphify/extractors/rust.py index bc79d64c2..0c327abfc 100644 --- a/graphify/extractors/rust.py +++ b/graphify/extractors/rust.py @@ -746,11 +746,30 @@ def extract_rust(path: Path) -> dict: if t == "use_declaration": arg = node.child_by_field_name("argument") if arg: + # The mods declared beside this `use`: an inline `mod` block is a scope of + # its own, so a `use serde::..` inside `mod tests { .. }` sees none of the + # file's top-level mods (#4564). + scope_mods = { + _read_text(name_node, source) + for sibling in (node.parent.children if node.parent is not None else ()) + if sibling.type == "mod_item" + and (name_node := sibling.child_by_field_name("name")) is not None + } + def name_use(written: str, path: str) -> None: # The name as written, for the corpus rewire (#4439): the id folds its # case. Only for a path rooted in this crate: `serde::Serialize` or - # `std::fs::File` is never the corpus's own `Serialize` or `File`. - if path.lstrip(": ").split("::")[0].strip() in ("crate", "self", "super"): + # `std::fs::File` is never the corpus's own `Serialize` or `File`. A + # first segment that is a `mod` declared in this scope is this crate's + # too (`mod item; pub use item::Invoice;`), unless the path starts with + # `::`, which always names a crate (#4564). + first = path.lstrip(": ").split("::")[0].strip() + ident = re.match(r"[A-Za-z_][A-Za-z0-9_]*", first) # `item as items` -> `item` + if first in ("crate", "self", "super") or ( + ident is not None + and ident.group() in scope_mods + and not path.lstrip().startswith("::") + ): edges[-1]["_rust_use_name"] = written.split(" as ")[0].strip() module_name = _rust_use_module(_read_text(arg, source)) grouped = arg.type in ("use_list", "scoped_use_list")