Files
vulp/src/legacy/refactor/mod.rs
T
2026-06-19 01:10:19 +02:00

233 lines
9.3 KiB
Rust

pub mod ui;
use crate::cli::{RefactorAction, RefactorArgs};
use crate::config::AppConfig;
use anyhow::Result;
use std::path::Path;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct FindResult {
pub repo: String,
pub file: String,
pub line: usize,
pub text: String,
}
#[derive(Debug, Clone)]
pub struct CleanResult {
pub file: String,
pub kind: String,
pub line: usize,
pub detail: String,
}
// ── find ──────────────────────────────────────────────────────────────────────
pub fn find_in_dir(pattern: &str, root: &Path, repo_name: &str) -> Vec<FindResult> {
let mut results = Vec::new();
let rx = match regex_simple(pattern) {
Some(r) => r,
None => return results,
};
walk_files(root, &mut |file_path, rel| {
if let Ok(content) = std::fs::read_to_string(&file_path) {
for (i, line) in content.lines().enumerate() {
if rx(line) {
results.push(FindResult {
repo: repo_name.to_string(),
file: rel.clone(),
line: i + 1,
text: line.trim().chars().take(120).collect(),
});
}
}
}
});
results
}
// ── doc-files-only find (for TODO scanner — avoids self-referential hits in source) ──
pub fn find_in_doc_files(pattern: &str, root: &Path, repo_name: &str) -> Vec<FindResult> {
let mut results = Vec::new();
let rx = match regex_simple(pattern) {
Some(r) => r,
None => return results,
};
walk_doc_files(root, &mut |file_path, rel| {
if let Ok(content) = std::fs::read_to_string(&file_path) {
for (i, line) in content.lines().enumerate() {
if rx(line) {
results.push(FindResult {
repo: repo_name.to_string(),
file: rel.clone(),
line: i + 1,
text: line.trim().chars().take(120).collect(),
});
}
}
}
});
results
}
fn walk_doc_files<F: FnMut(std::path::PathBuf, String)>(root: &Path, cb: &mut F) {
let Ok(entries) = std::fs::read_dir(root) else { return };
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let name = path.file_name().unwrap_or_default().to_string_lossy();
if !matches!(name.as_ref(), ".git" | "target" | "node_modules" | ".next" | "dist" | "build" | ".venv" | "vendor") {
walk_doc_files(&path, cb);
}
} else {
let ext = path.extension().map(|e| e.to_string_lossy().to_lowercase());
let fname = path.file_name().unwrap_or_default().to_string_lossy().to_uppercase();
let is_doc = matches!(ext.as_deref(), Some("md") | Some("txt") | Some("todo") | Some("org") | Some("rst") | Some("taskpaper"))
|| matches!(fname.as_ref(), "TODO" | "TASKS" | "FIXME" | "NOTES" | "BUGS" | "ROADMAP");
if is_doc {
let rel = path.to_string_lossy().to_string();
cb(path, rel);
}
}
}
}
// ── rename ────────────────────────────────────────────────────────────────────
pub fn rename_in_dir(from: &str, to: &str, root: &Path, dry_run: bool) -> Vec<FindResult> {
let mut hits = Vec::new();
walk_files(root, &mut |file_path, rel| {
if let Ok(content) = std::fs::read_to_string(&file_path) {
if content.contains(from) {
let count = content.matches(from).count();
hits.push(FindResult {
repo: String::new(),
file: rel.clone(),
line: count,
text: format!("{} occurrence(s)", count),
});
if !dry_run {
let _ = std::fs::write(&file_path, content.replace(from, to));
}
}
}
});
hits
}
// ── clean ─────────────────────────────────────────────────────────────────────
pub fn find_unused(kind: &str, root: &Path) -> Vec<CleanResult> {
let mut results = Vec::new();
let pattern: &dyn Fn(&str) -> bool = match kind {
"unused-imports" => &|l: &str| l.trim_start().starts_with("use ") && l.contains("// unused"),
"unused-vars" => &|l: &str| l.contains("_unused") || l.contains("let _"),
"dead-code" => &|l: &str| l.contains("#[allow(dead_code)]"),
"unused-functions" => &|l: &str| l.contains("fn ") && l.contains("// dead"),
_ => return results,
};
walk_files(root, &mut |file_path, rel| {
if let Ok(content) = std::fs::read_to_string(&file_path) {
for (i, line) in content.lines().enumerate() {
if pattern(line) {
results.push(CleanResult {
file: rel.clone(), kind: kind.to_string(),
line: i + 1, detail: line.trim().chars().take(80).collect(),
});
}
}
}
});
results
}
// ── generate ──────────────────────────────────────────────────────────────────
pub fn generate_todo_list(root: &Path) -> Vec<FindResult> {
find_in_dir("TODO|FIXME|HACK|XXX", root, "")
}
// ── helpers ───────────────────────────────────────────────────────────────────
fn walk_files<F: FnMut(std::path::PathBuf, String)>(root: &Path, cb: &mut F) {
let Ok(entries) = std::fs::read_dir(root) else { return };
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let name = path.file_name().unwrap_or_default().to_string_lossy();
if !matches!(name.as_ref(), ".git" | "target" | "node_modules" | ".next") {
walk_files(&path, cb);
}
} else if let Some(ext) = path.extension() {
if matches!(ext.to_string_lossy().as_ref(), "rs" | "ts" | "js" | "py" | "go" | "md") {
let rel = path.to_string_lossy().to_string();
cb(path, rel);
}
}
}
}
fn regex_simple(pattern: &str) -> Option<Box<dyn Fn(&str) -> bool>> {
// Simple substring/literal match (not full regex to avoid extra dep)
let p = pattern.to_string();
Some(Box::new(move |line: &str| line.contains(p.as_str())))
}
// ── entry point ───────────────────────────────────────────────────────────────
pub async fn run(args: RefactorArgs, cfg: &AppConfig) -> Result<()> {
let root = &cfg.workspace_root;
match args.action {
None => ui::run_refactor_tui(cfg),
Some(RefactorAction::Find { pattern, repos, .. }) => {
let search_root = repos.as_ref()
.map(|r| root.join(r))
.unwrap_or_else(|| root.clone());
let results = find_in_dir(&pattern, &search_root, "");
if results.is_empty() {
println!("No matches found for '{}'", pattern);
} else {
for r in &results {
println!(" {}:{} — {}", r.file, r.line, r.text);
}
println!("\n {} match(es)", results.len());
}
Ok(())
}
Some(RefactorAction::Rename { from, to, repos, dry_run, kind: _ }) => {
let search_root = repos.first()
.map(|r| root.join(r))
.unwrap_or_else(|| root.clone());
let hits = rename_in_dir(&from, &to, &search_root, dry_run);
let label = if dry_run { "Would rename" } else { "Renamed" };
for h in &hits {
println!(" {} {} occurrence(s) in {}", label, h.line, h.file);
}
println!("\n {} file(s) affected", hits.len());
Ok(())
}
Some(RefactorAction::Clean { target }) => {
let results = find_unused(&target, root);
for r in &results {
println!(" {}:{} — {}", r.file, r.line, r.detail);
}
println!("\n {} item(s) flagged", results.len());
Ok(())
}
Some(RefactorAction::Generate { target }) => {
match target.as_str() {
"todo" => {
for r in generate_todo_list(root) {
println!(" {}:{} — {}", r.file, r.line, r.text);
}
}
_ => println!("Unknown generate target: {}", target),
}
Ok(())
}
}
}