Skip to content

fix(rust-extractor): handle use ... as renames, renamed use-list specifiers, and single-segment wildcard imports#449

Open
tirth8205 wants to merge 2 commits into
Egonex-AI:mainfrom
tirth8205:fix/rust-extractor-use-renames-wildcard
Open

fix(rust-extractor): handle use ... as renames, renamed use-list specifiers, and single-segment wildcard imports#449
tirth8205 wants to merge 2 commits into
Egonex-AI:mainfrom
tirth8205:fix/rust-extractor-use-renames-wildcard

Conversation

@tirth8205

Copy link
Copy Markdown
Contributor

Problem

  • extractUseDeclaration has no case for use_as_clause, so a renaming import like use foo::Bar as Baz; (an extremely common Rust idiom, e.g. use std::io::Result as IoResult;) falls into the default fallback. The fallback uses argument.text for both source and specifier, producing source: "foo::Bar as Baz" and specifiers: ["foo::Bar as Baz"] — the whole textual clause, including the as keyword 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 shows use_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.
  • In the scoped_use_list case, the loop over the use-list children only collects children of type self, identifier, or scoped_identifier. A renamed member like use std::io::{Read as R, Write}; parses the Read as R member as a use_as_clause node, which matches none of those types and is silently dropped. I verified with the real extractor: use std::io::{Read as R, Write}; returns specifiers: ["Write"] — the Read/R import is lost entirely. Nested grouped lists (use a::{b::{C, D}}) parse the inner group as a scoped_use_list/use_list and are likewise skipped.
  • The use_wildcard case derives the import source via findChild(argument, "scoped_identifier"). That only matches multi-segment paths. For a single-segment path the path node is an identifier (use foo::*;) or a crate node (use crate::*;), neither of which is a scoped_identifier, so findChild returns null and source becomes the empty string. I verified with the real extractor: use foo::*; yields {"source":"","specifiers":["*"]} and use 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

  • Add a case before 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,…
  • Add an 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)); }
  • Read the first named child as the path rather than only matching scoped_identifier: const pathNode = argument.namedChild(0); const source = pathNode && pathNode.type !== "*" ? pathNode.text : ""; (or explicitly accept scoped_identifier, identifier, and crate node types).

Testing

Adds unit test(s) that fail before the change and pass after. The full core test suite, eslint, and tsc --noEmit all pass locally on this branch.

Found via a static correctness audit of the Rust extractor.

🤖 Generated with Claude Code

…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 thejesh23 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@tirth8205

Copy link
Copy Markdown
Contributor Author

All four points are addressed in 13f9bfb.

1. Nested grouped use-lists. Implemented rather than TODO'd. Verified against tree-sitter-rust that use a::{b::{C, D}}; parses the inner b::{C, D} as a nested scoped_use_list (with list = use_list) sitting inside the outer use_list. Added a collectUseListSpecifiers helper that recurses through scoped_use_list/use_list children so leaf specifiers surface at any depth. Tests: use a::{b::{C, D}};["C","D"], and use std::{io::{Read, Write}, fmt};["Read","Write","fmt"].

2. pub use re-exports. Implemented across all use arms, not just use_as_clause. The visibility_modifier is a child of the use_declaration node (not the argument), so isPublic(node) is checked once and each introduced binding is pushed to exports: the alias for use_as_clause, each specifier for list/scoped/identifier. self and glob * are intentionally not surfaced as named exports (a glob re-export has no statically-known binding name). Tests cover pub use foo::Bar as Baz;, pub use std::io::{Read, Write};, pub use crate::config::Settings;, and the negative cases (self, *, private use).

3. extern crate ... as. Routed extern_crate_declaration through a new extractExternCrate: crate name → source, as alias (or crate name) → specifier, and pub extern crate surfaces the binding as an export. Tests cover both extern crate serde; and extern crate serde as s;.

Nit (bare-prefix rename). Added pinning tests. For the cited use self::Foo as F; the path is a scoped_identifier(path: self, name: Foo), so extractScopedPath already returns {source: "self", specifiers: ["F"]}Foo is the path's name, not lost — and likewise use crate::Foo as F;{source: "crate", specifiers: ["F"]}. Both are now locked by tests; no conditional reshuffle was needed.

Full core suite is green (704 passed) and tsc --noEmit is clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants