fix(rust-extractor): handle use ... as renames, renamed use-list specifiers, and single-segment wildcard imports#449
Conversation
…ecifiers, and single-segment wildcard imports
Bug: extractUseDeclaration mishandled three common Rust `use` idioms.
`use foo::Bar as Baz;` fell into the default fallback, dumping the whole
`foo::Bar as Baz` clause into both source and specifiers. Renamed members
inside use-lists (`{Read as R, Write}`) were silently dropped because the
loop ignored `use_as_clause` children. Single-segment globs (`use crate::*;`,
`use foo::*;`) lost their source path because the wildcard path was only read
via a `scoped_identifier` lookup, which does not match `identifier`/`crate`.
Fix: add a `use_as_clause` case (path minus final segment as source, alias as
specifier), handle `use_as_clause` children in the scoped_use_list loop, and
read the wildcard path from the first named child rather than only matching
`scoped_identifier`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thejesh23
left a comment
There was a problem hiding this comment.
A few concerns before this lands.
1. Nested grouped use-lists are still dropped. The PR body calls this out (use a::{b::{C, D}} — the inner group parses as scoped_use_list/use_list) but the fix only adds use_as_clause to the scoped_use_list loop. Children of type scoped_use_list/use_list still match nothing and get silently skipped. Either recurse on grouped children or leave a TODO so the next reader doesn't think this is covered.
2. pub use re-exports aren't surfaced as exports. extractUseDeclaration is called from the use_declaration arm regardless of visibility, but pub use foo::Bar as Baz; is a public re-export and should also land in exports (alias Baz). The new use_as_clause case is the right place to check isPublic(node) and push to exports; otherwise renamed re-exports look identical to private imports in the structural view. Same shape will recur in Dart export 'x' show Y; for #435.
3. extern crate ... as remains unhandled. Out of scope for the PR title but adjacent: extern crate serde as s; parses as extern_crate_declaration, which isn't routed anywhere — it falls through walkTopLevel entirely. Worth a follow-up issue at minimum so the audit doesn't get re-done.
Nit: in the use_as_clause case, when pathNode is self/super/crate (e.g. use self::Foo as F;) the new code lands source: "self", specifiers: ["F"] — name is set from the prefix text but there's no actual Foo segment. Probably want to fall through to scoped_identifier only and treat bare-prefix renames separately, or add a test pinning the chosen behavior.
… route extern crate Address review on PR Egonex-AI#449: - Recurse into nested grouped use-lists (`use a::{b::{C, D}};`) via a collectUseListSpecifiers helper so inner-group members no longer drop. - Surface `pub use` re-exports (rename alias, list, and scoped specifiers) as exports; glob and `self` are not surfaced as named exports. - Route `extern_crate_declaration` (`extern crate serde as s;`) as an import, using the `as` alias as the binding specifier. - Pin bare-prefix rename behavior (`use self::Foo as F;`, `use crate::Foo as F;`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
All four points are addressed in 13f9bfb. 1. Nested grouped use-lists. Implemented rather than TODO'd. Verified against tree-sitter-rust that 2. 3. Nit (bare-prefix rename). Added pinning tests. For the cited Full core suite is green (704 passed) and |
Problem
extractUseDeclarationhas no case foruse_as_clause, so a renaming import likeuse foo::Bar as Baz;(an extremely common Rust idiom, e.g.use std::io::Result as IoResult;) falls into thedefaultfallback. The fallback usesargument.textfor both source and specifier, producingsource: "foo::Bar as Baz"andspecifiers: ["foo::Bar as Baz"]— the whole textual clause, including theaskeyword and alias, is dumped into both fields. I verified this by running the real RustExtractor:use foo::Bar as Baz;yields{"source":"foo::Bar as Baz","specifiers":["foo::Bar as Baz"]}. The grammar showsuse_as_clause => seq(field('path', $._path), 'as', field('alias', $.identifier)), so the import path and the bound alias are available as fields but are never read.scoped_use_listcase, the loop over the use-list children only collects children of typeself,identifier, orscoped_identifier. A renamed member likeuse std::io::{Read as R, Write};parses theRead as Rmember as ause_as_clausenode, which matches none of those types and is silently dropped. I verified with the real extractor:use std::io::{Read as R, Write};returnsspecifiers: ["Write"]— theRead/Rimport is lost entirely. Nested grouped lists (use a::{b::{C, D}}) parse the inner group as ascoped_use_list/use_listand are likewise skipped.use_wildcardcase derives the import source viafindChild(argument, "scoped_identifier"). That only matches multi-segment paths. For a single-segment path the path node is anidentifier(use foo::*;) or acratenode (use crate::*;), neither of which is ascoped_identifier, sofindChildreturns null andsourcebecomes the empty string. I verified with the real extractor:use foo::*;yields{"source":"","specifiers":["*"]}anduse crate::*;yields{"source":"","specifiers":["*"]}.use crate::*;is a very common glob-import idiom, so the source path is dropped in everyday code. The multi-segment case (use std::io::*;) still works.Fix
default: case "use_as_clause": { //use foo::Bar as Baz;const pathNode = argument.childForFieldName("path"); const aliasNode = argument.childForFieldName("alias"); const alias = aliasNode ? aliasNode.text : ""; // Source is the path minus its final segment; specifier is the alias. const { path, name } = pathNode && pathNode.type === "scoped_identifier" ? extractScopedPath(pathNode) : { path: "", name: pathNode ? pathNode.text : "" }; imports.push({ source: path || name,…else if (ch.type === "use_as_clause")branch that pushes the alias (or path text if no alias): } else if (ch.type === "use_as_clause") { const aliasNode = ch.childForFieldName("alias"); const pathNode = ch.childForFieldName("path"); specifiers.push(aliasNode ? aliasNode.text : (pathNode ? pathNode.text : ch.text)); }scoped_identifier: const pathNode = argument.namedChild(0); const source = pathNode && pathNode.type !== "*" ? pathNode.text : ""; (or explicitly acceptscoped_identifier,identifier, andcratenode types).Testing
Adds unit test(s) that fail before the change and pass after. The full core test suite,
eslint, andtsc --noEmitall pass locally on this branch.Found via a static correctness audit of the Rust extractor.
🤖 Generated with Claude Code