use std::collections::HashMap;
use std::path::Path;
use crate::utils::codebase_scan::{self, ExtensionFilter, ScanConfig};
use crate::utils::grammar::{self, Grammar};
#[derive(Debug, Clone)]
pub struct ImportRef {
pub file: String,
pub line: usize,
pub module_path: String,
pub imported_names: Vec<String>,
pub original_text: String,
}
#[derive(Debug, Clone)]
pub struct CallerRef {
pub file: String,
pub import: Option<ImportRef>,
pub has_call_site: bool,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ImportRewrite {
pub file: String,
pub line: usize,
pub original: String,
pub replacement: String,
}
#[derive(Debug, Clone)]
pub struct RewriteResult {
pub rewrites: Vec<ImportRewrite>,
pub unresolved_callers: Vec<String>,
pub applied: bool,
}
pub fn module_path_from_file(file_path: &str) -> String {
let p = file_path.strip_prefix("src/").unwrap_or(file_path);
let p = p.strip_suffix(".rs").unwrap_or(p);
let p = p.strip_suffix("/mod").unwrap_or(p);
p.replace('/', "::")
}
pub fn parse_imports(content: &str, grammar: &Grammar, relative_path: &str) -> Vec<ImportRef> {
let symbols = grammar::extract(content, grammar);
let lines: Vec<&str> = content.lines().collect();
let language_id = grammar.language.id.as_str();
symbols
.iter()
.filter(|s| s.concept == "import")
.filter_map(|s| {
let raw_path = s.get("path")?;
let line_text = lines.get(s.line.saturating_sub(1)).unwrap_or(&"");
let (module_path, imported_names) = match language_id {
"rust" => parse_rust_import_path(raw_path),
"php" | "wordpress" => parse_php_import_path(raw_path),
_ => (raw_path.to_string(), vec![]),
};
Some(ImportRef {
file: relative_path.to_string(),
line: s.line,
module_path,
imported_names,
original_text: line_text.to_string(),
})
})
.collect()
}
fn parse_rust_import_path(raw: &str) -> (String, Vec<String>) {
if let Some(brace_start) = raw.find("::{") {
let module = &raw[..brace_start];
let inner = raw[brace_start + 3..]
.trim_end_matches('}')
.split(',')
.map(|s| {
let s = s.trim();
if let Some(pos) = s.find(" as ") {
s[..pos].trim().to_string()
} else {
s.to_string()
}
})
.filter(|s| !s.is_empty() && s != "self")
.collect();
(module.to_string(), inner)
} else {
if let Some(last_sep) = raw.rfind("::") {
let module = &raw[..last_sep];
let name = &raw[last_sep + 2..];
if name == "self" || name == "*" {
(raw.to_string(), vec![])
} else {
(module.to_string(), vec![name.to_string()])
}
} else {
(raw.to_string(), vec![])
}
}
}
fn parse_php_import_path(raw: &str) -> (String, Vec<String>) {
if let Some(last_sep) = raw.rfind('\\') {
let module = &raw[..last_sep];
let name = &raw[last_sep + 1..];
(module.to_string(), vec![name.to_string()])
} else {
(raw.to_string(), vec![])
}
}
pub fn trace_symbol_callers(
symbol_name: &str,
source_module: &str,
root: &Path,
file_extensions: &[&str],
) -> Vec<CallerRef> {
let config = ScanConfig {
extensions: ExtensionFilter::Only(file_extensions.iter().map(|e| e.to_string()).collect()),
skip_hidden: true,
..Default::default()
};
let files = codebase_scan::walk_files(root, &config);
let mut callers = Vec::new();
for file_path in &files {
let rel_path = file_path
.strip_prefix(root)
.unwrap_or(file_path)
.to_string_lossy()
.to_string();
let content = match std::fs::read_to_string(file_path) {
Ok(c) => c,
Err(_) => continue,
};
if !content.contains(symbol_name) {
continue;
}
let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
let matching_import = if let Some(grammar) = load_grammar_for_ext(ext) {
let imports = parse_imports(&content, &grammar, &rel_path);
imports.into_iter().find(|imp| {
imp.imported_names.contains(&symbol_name.to_string())
&& import_matches_module(&imp.module_path, source_module)
})
} else {
None
};
let has_import = matching_import.is_some();
let has_call_site = content.contains(&format!("{}(", symbol_name))
|| content.contains(&format!("{}::", symbol_name))
|| content.contains(&format!(".{}", symbol_name));
if has_import || has_call_site {
callers.push(CallerRef {
file: rel_path,
import: matching_import,
has_call_site,
});
}
}
callers
}
fn import_matches_module(import_module: &str, source_module: &str) -> bool {
if import_module == source_module {
return true;
}
let with_crate = format!("crate::{}", source_module);
if import_module == with_crate {
return true;
}
let without_crate = source_module
.strip_prefix("crate::")
.unwrap_or(source_module);
if import_module == without_crate {
return true;
}
let import_without = import_module
.strip_prefix("crate::")
.unwrap_or(import_module);
import_without == source_module || import_without == without_crate
}
pub fn rewrite_imports(
symbol_name: &str,
old_module: &str,
new_module: &str,
root: &Path,
file_extensions: &[&str],
write: bool,
) -> RewriteResult {
let callers = trace_symbol_callers(symbol_name, old_module, root, file_extensions);
let mut rewrites = Vec::new();
let mut unresolved = Vec::new();
for caller in &callers {
if let Some(ref import) = caller.import {
if let Some(rewrite) = compute_import_rewrite(import, symbol_name, new_module) {
rewrites.push(rewrite);
}
} else if caller.has_call_site {
unresolved.push(caller.file.clone());
}
}
if write {
apply_rewrites(&rewrites, root);
}
RewriteResult {
rewrites,
unresolved_callers: unresolved,
applied: write,
}
}
fn compute_import_rewrite(
import: &ImportRef,
symbol_name: &str,
new_module: &str,
) -> Option<ImportRewrite> {
let original = &import.original_text;
let indent = &original[..original.len() - original.trim_start().len()];
if import.imported_names.len() == 1 {
let new_module_with_crate = if new_module.starts_with("crate::") {
new_module.to_string()
} else {
format!("crate::{}", new_module)
};
let replacement = format!("{}use {}::{};", indent, new_module_with_crate, symbol_name);
Some(ImportRewrite {
file: import.file.clone(),
line: import.line,
original: original.to_string(),
replacement,
})
} else if import.imported_names.len() > 1 {
let remaining: Vec<&String> = import
.imported_names
.iter()
.filter(|n| n.as_str() != symbol_name)
.collect();
if remaining.is_empty() {
let new_module_with_crate = if new_module.starts_with("crate::") {
new_module.to_string()
} else {
format!("crate::{}", new_module)
};
let replacement = format!("{}use {}::{};", indent, new_module_with_crate, symbol_name);
Some(ImportRewrite {
file: import.file.clone(),
line: import.line,
original: original.to_string(),
replacement,
})
} else {
let old_module_with_crate = if import.module_path.starts_with("crate::") {
import.module_path.clone()
} else {
format!("crate::{}", import.module_path)
};
let new_module_with_crate = if new_module.starts_with("crate::") {
new_module.to_string()
} else {
format!("crate::{}", new_module)
};
let remaining_str = if remaining.len() == 1 {
format!("{}use {}::{};", indent, old_module_with_crate, remaining[0])
} else {
format!(
"{}use {}::{{{}}};",
indent,
old_module_with_crate,
remaining
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
)
};
let replacement = format!(
"{}\n{}use {}::{};",
remaining_str, indent, new_module_with_crate, symbol_name
);
Some(ImportRewrite {
file: import.file.clone(),
line: import.line,
original: original.to_string(),
replacement,
})
}
} else {
None
}
}
fn apply_rewrites(rewrites: &[ImportRewrite], root: &Path) {
let mut by_file: HashMap<&str, Vec<&ImportRewrite>> = HashMap::new();
for rewrite in rewrites {
by_file
.entry(rewrite.file.as_str())
.or_default()
.push(rewrite);
}
for (file, file_rewrites) in &by_file {
let abs_path = root.join(file);
let Ok(content) = std::fs::read_to_string(&abs_path) else {
continue;
};
let mut lines: Vec<String> = content.lines().map(String::from).collect();
let mut sorted_rewrites: Vec<&&ImportRewrite> = file_rewrites.iter().collect();
sorted_rewrites.sort_by(|a, b| b.line.cmp(&a.line));
for rewrite in sorted_rewrites {
let idx = rewrite.line.saturating_sub(1);
if idx < lines.len() {
let replacement_lines: Vec<&str> = rewrite.replacement.lines().collect();
lines.splice(idx..=idx, replacement_lines.iter().map(|s| s.to_string()));
}
}
let mut modified = lines.join("\n");
if content.ends_with('\n') && !modified.ends_with('\n') {
modified.push('\n');
}
let _ = std::fs::write(&abs_path, &modified);
}
}
fn load_grammar_for_ext(ext: &str) -> Option<Grammar> {
let matched = crate::extension::find_extension_for_file_ext(ext, "fingerprint")?;
let extension_path = matched.extension_path.as_deref()?;
let grammar_path = Path::new(extension_path).join("grammar.toml");
if grammar_path.exists() {
return grammar::load_grammar(&grammar_path).ok();
}
let grammar_json_path = Path::new(extension_path).join("grammar.json");
if grammar_json_path.exists() {
return grammar::load_grammar_json(&grammar_json_path).ok();
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn module_path_from_file_strips_src_and_extension() {
assert_eq!(
module_path_from_file("src/core/code_audit/conventions.rs"),
"core::code_audit::conventions"
);
}
#[test]
fn module_path_from_file_handles_mod_rs() {
assert_eq!(
module_path_from_file("src/core/code_audit/mod.rs"),
"core::code_audit"
);
}
#[test]
fn module_path_from_file_no_src_prefix() {
assert_eq!(module_path_from_file("lib/utils.rs"), "lib::utils");
}
#[test]
fn parse_rust_import_simple() {
let (module, names) = parse_rust_import_path("crate::core::fixer::module_path_from_file");
assert_eq!(module, "crate::core::fixer");
assert_eq!(names, vec!["module_path_from_file"]);
}
#[test]
fn parse_rust_import_grouped() {
let (module, names) = parse_rust_import_path("crate::core::fixer::{insertion, Fix}");
assert_eq!(module, "crate::core::fixer");
assert_eq!(names, vec!["insertion", "Fix"]);
}
#[test]
fn parse_rust_import_self() {
let (module, names) = parse_rust_import_path("crate::core::fixer::self");
assert_eq!(module, "crate::core::fixer::self");
assert!(names.is_empty());
}
#[test]
fn parse_rust_import_wildcard() {
let (module, names) = parse_rust_import_path("super::*");
assert_eq!(module, "super::*");
assert!(names.is_empty());
}
#[test]
fn parse_rust_import_alias() {
let (module, names) = parse_rust_import_path("crate::mod::{Foo as Bar, Baz}");
assert_eq!(module, "crate::mod");
assert_eq!(names, vec!["Foo", "Baz"]);
}
#[test]
fn parse_php_import() {
let (module, names) = parse_php_import_path("App\\Models\\User");
assert_eq!(module, "App\\Models");
assert_eq!(names, vec!["User"]);
}
#[test]
fn import_matches_module_variants() {
assert!(import_matches_module("core::fixer", "core::fixer"));
assert!(import_matches_module("crate::core::fixer", "core::fixer"));
assert!(import_matches_module("core::fixer", "crate::core::fixer"));
assert!(import_matches_module(
"crate::core::fixer",
"crate::core::fixer"
));
assert!(!import_matches_module("core::fixer", "core::other"));
}
#[test]
fn parse_imports_with_rust_grammar() {
let grammar_path =
std::path::Path::new("/root/.config/homeboy/extensions/rust/grammar.toml");
if !grammar_path.exists() {
return; }
let grammar = crate::utils::grammar::load_grammar(grammar_path).unwrap();
let content = r#"use std::path::Path;
use crate::core::fixer::{insertion, Fix};
use crate::utils::grammar;
pub fn hello() {}
"#;
let imports = parse_imports(content, &grammar, "src/example.rs");
assert_eq!(imports.len(), 3);
assert_eq!(imports[0].module_path, "std::path");
assert_eq!(imports[0].imported_names, vec!["Path"]);
assert_eq!(imports[0].line, 1);
assert_eq!(imports[1].module_path, "crate::core::fixer");
assert_eq!(imports[1].imported_names, vec!["insertion", "Fix"]);
assert_eq!(imports[2].module_path, "crate::utils");
assert_eq!(imports[2].imported_names, vec!["grammar"]);
}
#[test]
fn compute_rewrite_simple_import() {
let import = ImportRef {
file: "src/core/refactor/move_items.rs".to_string(),
line: 22,
module_path: "crate::core::refactor::move_items".to_string(),
imported_names: vec!["walk_source_files".to_string()],
original_text: "use crate::core::refactor::move_items::walk_source_files;".to_string(),
};
let rewrite =
compute_import_rewrite(&import, "walk_source_files", "core::refactor::transform")
.unwrap();
assert_eq!(
rewrite.replacement,
"use crate::core::refactor::transform::walk_source_files;"
);
}
#[test]
fn compute_rewrite_grouped_import() {
let import = ImportRef {
file: "src/example.rs".to_string(),
line: 5,
module_path: "crate::core::fixer".to_string(),
imported_names: vec![
"insertion".to_string(),
"module_path_from_file".to_string(),
"Fix".to_string(),
],
original_text: "use crate::core::fixer::{insertion, module_path_from_file, Fix};"
.to_string(),
};
let rewrite =
compute_import_rewrite(&import, "module_path_from_file", "core::symbol_graph").unwrap();
assert!(rewrite
.replacement
.contains("use crate::core::fixer::{insertion, Fix};"));
assert!(rewrite
.replacement
.contains("use crate::core::symbol_graph::module_path_from_file;"));
}
}