use std::collections::HashSet;
use std::path::{Path, PathBuf};
use petgraph::stable_graph::NodeIndex;
use crate::graph::{CodeGraph, node::GraphNode};
use crate::query::refs::find_refs;
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct RenameItem {
pub file_path: PathBuf,
pub line: usize,
pub old_text: String,
pub new_text: String,
pub note: Option<String>,
}
pub fn plan_rename(
graph: &CodeGraph,
symbol: &str,
new_name: &str,
root: &Path,
) -> Vec<RenameItem> {
let indices: Vec<NodeIndex> = match graph.symbol_index.get(symbol) {
Some(v) => v.clone(),
None => return Vec::new(),
};
let mut items: Vec<RenameItem> = Vec::new();
let mut seen: HashSet<(PathBuf, usize)> = HashSet::new();
for &sym_idx in &indices {
let info = match &graph.graph[sym_idx] {
GraphNode::Symbol(i) => i.clone(),
_ => continue,
};
let file_path = match find_containing_file_path(graph, sym_idx) {
Some(fp) => fp,
None => continue,
};
let key = (file_path.clone(), info.line);
if seen.insert(key) {
items.push(RenameItem {
file_path,
line: info.line,
old_text: symbol.to_string(),
new_text: new_name.to_string(),
note: None,
});
}
}
let refs = find_refs(graph, symbol, &indices, root);
for r in refs {
let line = r.line.unwrap_or(0);
let key = (r.file_path.clone(), line);
if seen.insert(key) {
let note = if line == 0 {
Some("import site — verify manually".to_string())
} else {
None
};
items.push(RenameItem {
file_path: r.file_path,
line,
old_text: symbol.to_string(),
new_text: new_name.to_string(),
note,
});
}
}
items.sort_by(|a, b| a.file_path.cmp(&b.file_path).then(a.line.cmp(&b.line)));
items
}
fn find_containing_file_path(graph: &CodeGraph, sym_idx: NodeIndex) -> Option<PathBuf> {
let file_idx = super::util::find_containing_file_idx(graph, sym_idx)?;
if let GraphNode::File(fi) = &graph.graph[file_idx] {
Some(fi.path.clone())
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use tempfile::TempDir;
use crate::graph::node::{SymbolInfo, SymbolKind};
fn root() -> PathBuf {
PathBuf::from("/proj")
}
#[test]
fn test_plan_rename_basic() {
let r = root();
let mut g = crate::graph::CodeGraph::new();
let def_file = g.add_file(r.join("src/foo.rs"), "rust");
let foo_sym = g.add_symbol(
def_file,
SymbolInfo {
name: "Foo".into(),
kind: SymbolKind::Struct,
line: 10,
..Default::default()
},
);
let caller1 = g.add_file(r.join("src/bar.rs"), "rust");
let bar_sym = g.add_symbol(
caller1,
SymbolInfo {
name: "bar".into(),
kind: SymbolKind::Function,
line: 5,
..Default::default()
},
);
g.add_calls_edge(bar_sym, foo_sym);
let caller2 = g.add_file(r.join("src/baz.rs"), "rust");
let baz_sym = g.add_symbol(
caller2,
SymbolInfo {
name: "baz".into(),
kind: SymbolKind::Function,
line: 7,
..Default::default()
},
);
g.add_calls_edge(baz_sym, foo_sym);
let items = plan_rename(&g, "Foo", "Bar", &r);
assert_eq!(items.len(), 3, "expected 3 rename items (1 def + 2 refs)");
for item in &items {
assert_eq!(item.old_text, "Foo");
assert_eq!(item.new_text, "Bar");
}
let def_item = items
.iter()
.find(|i| i.file_path.ends_with("foo.rs"))
.unwrap();
assert_eq!(def_item.line, 10);
}
#[test]
fn test_plan_rename_no_disk_writes() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().to_path_buf();
let mut g = crate::graph::CodeGraph::new();
let f = g.add_file(root.join("src/thing.rs"), "rust");
g.add_symbol(
f,
SymbolInfo {
name: "MyStruct".into(),
kind: SymbolKind::Struct,
line: 1,
..Default::default()
},
);
let files_before: Vec<_> = std::fs::read_dir(&root)
.map(|d| d.collect::<Vec<_>>())
.unwrap_or_default();
let _items = plan_rename(&g, "MyStruct", "RenamedStruct", &root);
let files_after: Vec<_> = std::fs::read_dir(&root)
.map(|d| d.collect::<Vec<_>>())
.unwrap_or_default();
assert_eq!(
files_before.len(),
files_after.len(),
"plan_rename must not create any files on disk"
);
}
#[test]
fn test_plan_rename_unknown_symbol() {
let g = crate::graph::CodeGraph::new();
let r = root();
let items = plan_rename(&g, "DoesNotExist", "NewName", &r);
assert!(
items.is_empty(),
"unknown symbol should return empty rename plan"
);
}
#[test]
fn test_plan_rename_import_refs() {
let r = root();
let mut g = crate::graph::CodeGraph::new();
let def_file = g.add_file(r.join("src/foo.ts"), "typescript");
g.add_symbol(
def_file,
SymbolInfo {
name: "Foo".into(),
kind: SymbolKind::Class,
line: 1,
is_exported: true,
..Default::default()
},
);
let importer = g.add_file(r.join("src/bar.ts"), "typescript");
g.add_resolved_import(importer, def_file, "./foo");
let items = plan_rename(&g, "Foo", "FooRenamed", &r);
let import_item = items
.iter()
.find(|i| i.file_path.ends_with("bar.ts"))
.expect("import ref for bar.ts expected");
assert_eq!(import_item.line, 0, "import site should have line=0");
assert!(
import_item
.note
.as_deref()
.unwrap_or("")
.contains("import site"),
"import site note expected"
);
}
}