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.
This commit is contained in:
Deepjyoti Sarmah authored and safishamsi committed 2026-10-20 10:35:37 +01:00
1 parent f893a6e2da
commit 474d23b182
2 files changed
+22 -3

No files matched your search

+1 -1
View File
@@ -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
+21 -2
View File
@@ -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")