use std::collections::HashSet;
use std::path::Path;
use ast_grep_core::{AstGrep, Doc, Node};
use super::manifest::ManifestResolver;
use super::{first_line, FileImports, ImportRef, LanguageSupport};
const RUST_ALWAYS_RESOLVED: &[&str] = &[
"std",
"core",
"alloc",
"proc_macro",
"test",
"crate",
"self",
"super",
"Self",
];
const STD_GLOB_ROOTS: &[&str] = &["std", "core", "alloc"];
fn rust_use_root(arg_text: &str) -> Option<String> {
let trimmed = arg_text.trim().trim_start_matches("::").trim_start();
if trimmed.starts_with('{') {
return None;
}
let root: String = trimmed
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
(!root.is_empty()).then_some(root)
}
fn last_segment(path: &str) -> String {
path.rsplit("::").next().unwrap_or(path).trim().to_string()
}
fn bound_names<D: Doc>(tree: &Node<'_, D>, out: &mut Vec<String>) {
match &*tree.kind() {
"identifier" => out.push(tree.text().to_string()),
"scoped_identifier" => {
if let Some(name) = tree.field("name") {
out.push(name.text().to_string());
}
}
"use_as_clause" => {
if let Some(alias) = tree.field("alias") {
out.push(alias.text().to_string());
}
}
"scoped_use_list" => {
let parent = tree.field("path").map(|p| last_segment(&p.text()));
if let Some(list) = tree.field("list") {
for item in list.children() {
match (&*item.kind(), &parent) {
("self", Some(parent)) => out.push(parent.clone()),
_ => bound_names(&item, out),
}
}
}
}
"use_list" => {
for item in tree.children() {
bound_names(&item, out);
}
}
_ => {}
}
}
fn mods_in_macro_body(text: &str) -> Vec<String> {
let words: Vec<&str> = text
.split(|c: char| !(c.is_alphanumeric() || c == '_' || c == '$'))
.filter(|w| !w.is_empty())
.collect();
words
.windows(2)
.filter(|pair| pair[0] == "mod" && !pair[1].starts_with('$'))
.map(|pair| pair[1].to_string())
.collect()
}
fn module_scope<D: Doc>(node: &Node<'_, D>) -> Option<usize> {
node.ancestors()
.find(|a| a.kind() == "mod_item")
.map(|m| m.range().start)
}
fn names_an_item(root: &str) -> bool {
root.chars().next().is_some_and(char::is_uppercase)
}
pub struct Rust;
impl LanguageSupport for Rust {
fn extract_imports<D: Doc>(&self, root: &AstGrep<D>) -> FileImports {
let mut candidates: Vec<(ImportRef, Option<usize>)> = Vec::new();
let mut local_names = Vec::new();
let mut glob_scopes: HashSet<Option<usize>> = HashSet::new();
for node in root.root().dfs() {
match &*node.kind() {
"use_declaration" => {
let Some(arg) = node.field("argument") else {
continue;
};
let mut bound = Vec::new();
bound_names(&arg, &mut bound);
let Some(specifier) = rust_use_root(&arg.text()) else {
local_names.extend(bound);
continue;
};
local_names.extend(bound.into_iter().filter(|name| *name != specifier));
let scope = module_scope(&node);
let has_glob = arg.dfs().any(|n| n.kind() == "use_wildcard");
if has_glob && !STD_GLOB_ROOTS.contains(&specifier.as_str()) {
glob_scopes.insert(scope);
}
let start = node.start_pos();
let end = node.end_pos();
let import = ImportRef {
specifier,
line: start.line() + 1,
column: start.byte_point().1 + 1,
end_line: end.line() + 1,
end_column: end.byte_point().1 + 1,
matched_text: first_line(&node.text()),
};
candidates.push((import, scope));
}
"extern_crate_declaration" => {
let Some(name) = node.field("name") else {
continue;
};
if let Some(alias) = node.field("alias") {
local_names.push(alias.text().to_string());
}
let start = node.start_pos();
let end = node.end_pos();
let import = ImportRef {
specifier: name.text().to_string(),
line: start.line() + 1,
column: start.byte_point().1 + 1,
end_line: end.line() + 1,
end_column: end.byte_point().1 + 1,
matched_text: first_line(&node.text()),
};
candidates.push((import, module_scope(&node)));
}
"mod_item" => {
if let Some(name) = node.field("name") {
local_names.push(name.text().to_string());
}
}
"token_tree"
if node
.parent()
.is_some_and(|p| p.kind() == "macro_invocation") =>
{
local_names.extend(mods_in_macro_body(&node.text()));
}
_ => {}
}
}
local_names.sort();
local_names.dedup();
let imports = candidates
.into_iter()
.filter(|(_, scope)| !glob_scopes.contains(scope))
.map(|(import, _)| import)
.collect();
FileImports {
imports,
local_names,
}
}
fn resolves(
&self,
import: &ImportRef,
local_names: &[String],
file_dir: &Path,
manifests: &mut ManifestResolver,
) -> bool {
let spec = import.specifier.as_str();
if RUST_ALWAYS_RESOLVED.contains(&spec)
|| names_an_item(spec)
|| local_names.iter().any(|m| m == spec)
{
return true;
}
match manifests.cargo_deps(file_dir) {
Some(deps) => deps.contains(spec),
None => true,
}
}
}