use std::collections::BTreeSet;
use std::fs::read_to_string;
use std::path::{Path, PathBuf};
use ast_grep_core::tree_sitter::LanguageExt;
use ast_grep_core::{Doc, Node};
use ast_grep_language::SupportLang;
use super::syntax::{replace, source_of};
use super::{FixEnv, RawEdit};
fn module_dir(file: &Path) -> Option<PathBuf> {
let parent = file.parent()?;
let name = file.file_name()?.to_str()?;
if matches!(name, "lib.rs" | "main.rs" | "mod.rs") {
Some(parent.to_path_buf())
} else {
Some(parent.join(file.file_stem()?))
}
}
#[derive(Clone)]
struct ModuleLoc {
text: String,
inline: Vec<String>,
dir: Option<PathBuf>,
}
fn visibility<D: Doc>(item: &Node<'_, D>) -> Option<String> {
item.children()
.find(|c| c.kind() == "visibility_modifier")
.map(|v| v.text().split_whitespace().collect())
}
fn child_module<'r, D: Doc>(scope: &Node<'r, D>, name: &str) -> Option<Node<'r, D>> {
let mut found = scope
.children()
.filter(|c| c.kind() == "mod_item" && c.field("name").is_some_and(|n| n.text() == name));
let module = found.next()?;
found.next().is_none().then_some(module)
}
fn descend<'r, D: Doc>(root: Node<'r, D>, path: &[String]) -> Option<Node<'r, D>> {
path.iter().try_fold(root, |scope, name| {
child_module(&scope, name)?.field("body")
})
}
fn has_path_attribute<D: Doc>(module: &Node<'_, D>) -> bool {
let mut current = module.prev();
while let Some(prev) = current {
if prev.kind() != "attribute_item" {
break;
}
if prev.text().replace(' ', "").starts_with("#[path") {
return true;
}
current = prev.prev();
}
false
}
fn resolve(mut from: ModuleLoc, segments: &[String]) -> Option<ModuleLoc> {
for segment in segments {
let ast = SupportLang::Rust.ast_grep(&from.text);
let scope = descend(ast.root(), &from.inline)?;
let module = child_module(&scope, segment)?;
if has_path_attribute(&module) {
return None;
}
let dir = from.dir.as_ref().map(|d| d.join(segment));
if module.field("body").is_some() {
from.inline.push(segment.clone());
from.dir = dir;
continue;
}
let parent = from.dir.as_ref()?;
let text = [
parent.join(format!("{segment}.rs")),
parent.join(segment).join("mod.rs"),
]
.iter()
.find_map(|file| read_to_string(file).ok())?;
from = ModuleLoc {
text,
inline: Vec::new(),
dir,
};
}
Some(from)
}
fn use_names<D: Doc>(argument: &Node<'_, D>, out: &mut BTreeSet<String>) -> Option<()> {
match &*argument.kind() {
"identifier" => {
out.insert(argument.text().to_string());
}
"scoped_identifier" => {
out.insert(argument.field("name")?.text().to_string());
}
"use_as_clause" => {
let alias = argument.field("alias")?.text().to_string();
if alias == "_" {
return None;
}
out.insert(alias);
}
"scoped_use_list" | "use_list" => {
let list = if argument.kind() == "use_list" {
argument.clone()
} else {
argument.field("list")?
};
for item in list.named_children() {
if item.kind() == "self" {
let path = argument.field("path")?;
let last = path.text().rsplit("::").next()?.to_string();
out.insert(last);
} else {
use_names(&item, out)?;
}
}
}
_ => return None,
}
Some(())
}
const NAMED_ITEMS: &[&str] = &[
"function_item",
"struct_item",
"enum_item",
"union_item",
"trait_item",
"type_item",
"const_item",
"static_item",
"mod_item",
];
#[derive(Clone, Copy, PartialEq)]
enum Reading {
Exact,
Superset,
}
fn exported_names<D: Doc>(scope: &Node<'_, D>, reading: Reading) -> Option<BTreeSet<String>> {
let mut names = BTreeSet::new();
for item in scope.named_children() {
let visible = match visibility(&item).as_deref() {
None => false,
Some("pub") => true,
Some(_) if reading == Reading::Superset => true,
Some(_) => return None,
};
let kind = item.kind();
match &*kind {
"line_comment"
| "block_comment"
| "inner_attribute_item"
| "impl_item"
| "macro_definition"
| "empty_statement" => {}
"attribute_item" => {
let is_cfg = item.text().replace(' ', "").starts_with("#[cfg(");
if is_cfg && reading == Reading::Exact {
return None;
}
}
"use_declaration" => {
if visible {
use_names(&item.field("argument")?, &mut names)?;
}
}
kind if NAMED_ITEMS.contains(&kind) => {
if visible {
names.insert(item.field("name")?.text().to_string());
}
}
_ => return None,
}
}
Some(names)
}
fn local_names<D: Doc>(scope: &Node<'_, D>) -> BTreeSet<String> {
let mut names = BTreeSet::new();
for item in scope.named_children() {
if item.kind() == "use_declaration" {
if let Some(argument) = item.field("argument") {
let mut bound = BTreeSet::new();
if use_names(&argument, &mut bound).is_some() {
names.extend(bound);
}
}
} else if let Some(name) = item.field("name") {
names.insert(name.text().to_string());
}
}
names
}
fn glob_segments<D: Doc>(wildcard: &Node<'_, D>) -> Option<Vec<String>> {
let path = wildcard.named_children().next()?;
let segments: Vec<String> = path
.text()
.split("::")
.map(|s| s.trim().to_string())
.skip_while(|s| s == "self")
.collect();
let crosses_up = segments.iter().any(|s| s == "crate" || s == "super");
(!segments.is_empty() && !crosses_up).then_some(segments)
}
pub(super) fn fix<D: Doc>(use_declaration: &Node<'_, D>, env: &FixEnv<'_>) -> Option<Vec<RawEdit>> {
if visibility(use_declaration).as_deref() != Some("pub") {
return None;
}
let wildcard = use_declaration.field("argument")?;
if wildcard.kind() != "use_wildcard" {
return None;
}
let scope = use_declaration.parent()?;
let mut inline: Vec<String> = use_declaration
.ancestors()
.filter(|a| a.kind() == "mod_item")
.filter_map(|m| m.field("name").map(|n| n.text().to_string()))
.collect();
inline.reverse();
let dir = env
.path
.and_then(module_dir)
.map(|dir| inline.iter().fold(dir, |d, name| d.join(name)));
let here = ModuleLoc {
text: source_of(use_declaration).to_string(),
inline,
dir,
};
let names_of = |glob: &Node<'_, D>, reading: Reading| -> Option<BTreeSet<String>> {
let target = resolve(here.clone(), &glob_segments(glob)?)?;
let ast = SupportLang::Rust.ast_grep(&target.text);
exported_names(&descend(ast.root(), &target.inline)?, reading)
};
let names = names_of(&wildcard, Reading::Exact)?;
if !local_names(&scope).is_disjoint(&names) {
return None;
}
for other in scope.children().filter(|c| c.kind() == "use_declaration") {
let Some(other_glob) = other
.field("argument")
.filter(|a| a.kind() == "use_wildcard")
else {
continue;
};
if other_glob.range() == wildcard.range() {
continue;
}
if !names_of(&other_glob, Reading::Superset)?.is_disjoint(&names) {
return None;
}
}
if names.is_empty() {
return None;
}
let path = wildcard.named_children().next()?.text().to_string();
let list: Vec<_> = names.into_iter().collect();
let expanded = match list.as_slice() {
[single] => format!("{path}::{single}"),
many => format!("{path}::{{{}}}", many.join(", ")),
};
let range = wildcard.range();
Some(vec![replace(range.start, range.end, expanded)])
}