use anyhow::Result;
use colored::Colorize;
use crate::exit_codes::*;
#[derive(Debug)]
pub struct ImpactArgs {
pub session_id: Option<String>,
pub workspace_path: Option<String>,
pub file_path: String,
pub max_depth: i32,
pub json: bool,
pub verbose: bool,
}
#[derive(Debug)]
pub struct LibraryArgs {
pub session_id: Option<String>,
pub workspace_path: Option<String>,
pub library_name: String,
pub json: bool,
pub verbose: bool,
}
#[derive(Debug)]
pub struct DepsArgs {
pub session_id: Option<String>,
pub workspace_path: Option<String>,
pub file_path: String,
pub json: bool,
pub verbose: bool,
}
#[derive(Debug)]
pub struct CriticalArgs {
pub session_id: Option<String>,
pub workspace_path: Option<String>,
pub limit: i32,
pub sort_by: String,
pub json: bool,
pub verbose: bool,
}
#[derive(Debug)]
pub struct FunctionImpactArgs {
pub session_id: Option<String>,
pub workspace_path: Option<String>,
pub function: String,
pub max_depth: i32,
pub json: bool,
pub verbose: bool,
}
#[derive(Debug)]
pub struct StatsArgs {
pub session_id: Option<String>,
pub workspace_path: Option<String>,
pub json: bool,
pub verbose: bool,
}
pub async fn execute_impact(args: ImpactArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!("{} Analyzing impact of: {}", "→".cyan(), args.file_path);
}
let graph = match crate::local_graph::build_analysis_graph(&workspace_path, args.verbose) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
let impact = unfault_analysis::graph::traversal::get_impact(
&graph,
&args.file_path,
args.max_depth as usize,
);
if impact.affected_files.is_empty() {
eprintln!(
"{} No downstream dependencies found for '{}'",
"ℹ".cyan(),
args.file_path
);
return Ok(EXIT_SUCCESS);
}
if args.json {
println!("{}", serde_json::to_string_pretty(&impact)?);
} else {
println!(
"\n{} Impact analysis for {}",
"📊".bright_blue(),
args.file_path.bright_blue()
);
println!(
" {} {} file(s) affected:\n",
"→".cyan(),
impact.affected_files.len()
);
for file in &impact.affected_files {
println!(" {}", file);
}
println!();
}
Ok(EXIT_SUCCESS)
}
pub async fn execute_library(args: LibraryArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!("{} Finding files using: {}", "→".cyan(), args.library_name);
}
let graph = match crate::local_graph::build_analysis_graph(&workspace_path, args.verbose) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
let deps = unfault_analysis::graph::traversal::get_dependencies(&graph, &args.library_name);
if args.json {
println!("{}", serde_json::to_string_pretty(&deps)?);
} else {
println!(
"\n{} Files using '{}':",
"📦".bright_blue(),
args.library_name.bright_blue()
);
if deps.library_users.is_empty() && deps.dependencies.is_empty() {
println!(" No files found using '{}'", args.library_name);
} else {
for file in &deps.library_users {
println!(" {}", file);
}
for file in &deps.dependencies {
println!(" {}", file);
}
}
println!();
}
Ok(EXIT_SUCCESS)
}
pub async fn execute_deps(args: DepsArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!("{} Finding dependencies of: {}", "→".cyan(), args.file_path);
}
let graph = match crate::local_graph::build_analysis_graph(&workspace_path, args.verbose) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
let deps = unfault_analysis::graph::traversal::get_dependencies(&graph, &args.file_path);
if args.json {
println!("{}", serde_json::to_string_pretty(&deps)?);
} else {
println!(
"\n{} Dependencies of {}",
"📦".bright_blue(),
args.file_path.bright_blue()
);
if !deps.dependencies.is_empty() {
println!(" Internal modules:");
for dep in &deps.dependencies {
println!(" {}", dep);
}
}
if !deps.library_users.is_empty() {
println!(" External libraries:");
for lib in &deps.library_users {
println!(" {}", lib);
}
}
if deps.dependencies.is_empty() && deps.library_users.is_empty() {
println!(" No dependencies found for '{}'", args.file_path);
}
println!();
}
Ok(EXIT_SUCCESS)
}
pub async fn execute_critical(args: CriticalArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!(
"{} Finding top {} critical files (sorted by {})",
"→".cyan(),
args.limit,
args.sort_by
);
}
let graph = match crate::local_graph::build_analysis_graph(&workspace_path, args.verbose) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
if args.sort_by == "importance_score" {
let ranked = unfault_analysis::sre::ranker::top_n(&graph, &[], args.limit as usize);
if args.json {
println!("{}", serde_json::to_string_pretty(&ranked)?);
} else {
println!(
"\n{} Most critical files (composite importance score):\n",
"📊".bright_blue()
);
if ranked.is_empty() {
println!(" No files found in graph.");
} else {
for (i, rf) in ranked.iter().enumerate() {
println!(
" {}. {} (score: {:.2} centrality: {:.2} lib-risk: {:.2} debt: {:.2})",
i + 1,
rf.file_path.bright_blue(),
rf.importance_score,
rf.centrality_score,
rf.library_risk_score,
rf.finding_density_score,
);
}
}
println!();
}
} else {
let centrality =
unfault_analysis::graph::traversal::get_centrality(&graph, args.limit as usize);
if args.json {
println!("{}", serde_json::to_string_pretty(¢rality)?);
} else {
println!(
"\n{} Most critical files (by import count):\n",
"📊".bright_blue()
);
if centrality.central_files.is_empty() {
println!(" No import relationships found.");
} else {
for (i, (path, score)) in centrality.central_files.iter().enumerate() {
println!(
" {}. {} (imported {} times)",
i + 1,
path.bright_blue(),
(*score as i32).to_string().yellow()
);
}
}
println!();
}
}
Ok(EXIT_SUCCESS)
}
pub async fn execute_stats(args: StatsArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!("{} Building graph statistics...", "→".cyan());
}
let graph = match crate::local_graph::build_analysis_graph(&workspace_path, args.verbose) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
let overview = unfault_analysis::graph::traversal::workspace_overview(&graph);
if args.json {
println!("{}", serde_json::to_string_pretty(&overview)?);
} else {
println!("\n{} Graph Statistics\n", "📊".bright_blue());
println!(" Files: {}", overview.file_count.to_string().yellow());
println!(
" Functions: {}",
overview.function_count.to_string().yellow()
);
println!(" Languages: {}", overview.languages.join(", ").cyan());
if !overview.frameworks.is_empty() {
println!(" Frameworks: {}", overview.frameworks.join(", ").cyan());
}
println!(
" Nodes: {}",
graph.graph.node_count().to_string().yellow()
);
println!(
" Edges: {}",
graph.graph.edge_count().to_string().yellow()
);
if !overview.entrypoints.is_empty() {
println!("\n Entrypoints:");
for ep in &overview.entrypoints {
println!(" {}", ep);
}
}
println!();
}
Ok(EXIT_SUCCESS)
}
pub async fn execute_function_impact(args: FunctionImpactArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
let (_file_path, function_name) = match args.function.split_once(':') {
Some((file, func)) => (file.to_string(), func.to_string()),
None => {
eprintln!(
"{} Function must be in format file:function (e.g., main.py:process_user)",
"Error:".red().bold()
);
return Ok(EXIT_ERROR);
}
};
if args.verbose {
eprintln!("{} Analyzing call flow of: {}", "→".cyan(), args.function);
}
let graph = match crate::local_graph::build_analysis_graph(&workspace_path, args.verbose) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
let flow = unfault_analysis::graph::traversal::extract_flow(
&graph,
&function_name,
args.max_depth as usize,
);
if args.json {
println!("{}", serde_json::to_string_pretty(&flow)?);
} else {
println!();
println!(
"{} {} {}",
"🔗".cyan(),
"Function Call Graph:".bold(),
function_name.bright_white()
);
println!();
if flow.paths.is_empty() {
println!(" {} No call paths found from this function.", "ℹ".blue());
println!();
return Ok(EXIT_SUCCESS);
}
println!(
" {} Found {} call path(s)",
"→".cyan(),
flow.paths.len().to_string().bold()
);
println!();
for (i, path) in flow.paths.iter().enumerate() {
println!(" Path {}:", i + 1);
for node in path {
let indent = " ".repeat(node.depth + 2);
let file_info = node
.file_path
.as_deref()
.map(|p| format!(" ({})", p))
.unwrap_or_default();
println!(
"{}{} {}{}",
indent,
"→".cyan(),
node.name.bright_white(),
file_info.dimmed()
);
}
println!();
}
}
Ok(EXIT_SUCCESS)
}
#[derive(Debug)]
pub struct DumpArgs {
pub workspace_path: Option<String>,
pub calls_only: bool,
pub file: Option<String>,
pub verbose: bool,
}
pub fn execute_dump(args: DumpArgs) -> Result<i32> {
use crate::session::graph_builder::build_local_graph;
let workspace_path = match &args.workspace_path {
Some(path) => std::path::PathBuf::from(path),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!(
"{} Building local code graph for: {}",
"→".cyan(),
workspace_path.display()
);
}
let graph = build_local_graph(&workspace_path, None, args.verbose)?;
if args.verbose {
eprintln!(
"{} Graph built: {} files, {} functions, {} call edges",
"✓".green(),
graph.files.len(),
graph.functions.len(),
graph.calls.len()
);
eprintln!();
}
if args.calls_only {
let calls: Vec<_> = if let Some(ref file_filter) = args.file {
graph
.calls
.iter()
.filter(|c| c.caller_file.contains(file_filter))
.collect()
} else {
graph.calls.iter().collect()
};
println!("{}", serde_json::to_string_pretty(&calls)?);
} else if let Some(ref file_filter) = args.file {
#[derive(serde::Serialize)]
struct FileGraph {
file: Option<crate::session::graph_builder::FileNode>,
functions: Vec<crate::session::graph_builder::FunctionNode>,
outgoing_calls: Vec<crate::session::graph_builder::CallEdge>,
incoming_calls: Vec<crate::session::graph_builder::CallEdge>,
imports: Vec<crate::session::graph_builder::ImportEdge>,
}
let file_graph = FileGraph {
file: graph
.files
.iter()
.find(|f| f.path.contains(file_filter))
.cloned(),
functions: graph
.functions
.iter()
.filter(|f| f.file_path.contains(file_filter))
.cloned()
.collect(),
outgoing_calls: graph
.calls
.iter()
.filter(|c| c.caller_file.contains(file_filter))
.cloned()
.collect(),
incoming_calls: graph
.calls
.iter()
.filter(|c| {
graph
.functions
.iter()
.any(|f| f.qualified_name == c.callee && f.file_path.contains(file_filter))
})
.cloned()
.collect(),
imports: graph
.imports
.iter()
.filter(|i| i.from_file.contains(file_filter) || i.to_file.contains(file_filter))
.cloned()
.collect(),
};
println!("{}", serde_json::to_string_pretty(&file_graph)?);
} else {
println!("{}", serde_json::to_string_pretty(&graph)?);
}
Ok(EXIT_SUCCESS)
}