mod analysis;
mod churn;
mod output;
mod satd;
mod watch;
pub use churn::handle_analyze_churn;
pub use output::format_satd_summary;
pub use satd::handle_analyze_satd;
use crate::cli::{ComplexityOutputFormat, DagType};
use anyhow::Result;
use std::path::PathBuf;
#[cfg(test)]
pub(crate) use analysis::{analyze_multiple_files, analyze_single_file, has_complexity_violations};
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod complexity_handlers_tests;
#[derive(Debug, Clone)]
pub(crate) struct ComplexityConfig {
project_path: PathBuf,
toolchain: Option<String>,
max_cyclomatic: u16,
max_cognitive: u16,
include: Vec<String>,
timeout: u64,
top_files: usize,
}
impl ComplexityConfig {
fn from_args(
project_path: PathBuf,
toolchain: Option<String>,
max_cyclomatic: Option<u16>,
max_cognitive: Option<u16>,
include: Vec<String>,
timeout: u64,
top_files: usize,
) -> Self {
Self {
project_path,
toolchain,
max_cyclomatic: max_cyclomatic.unwrap_or(10),
max_cognitive: max_cognitive.unwrap_or(15),
include,
timeout,
top_files,
}
}
fn detect_toolchain(&self) -> Option<String> {
self.toolchain
.clone()
.or_else(|| crate::cli::analysis_utilities::detect_toolchain(&self.project_path))
}
}
#[allow(clippy::too_many_arguments)]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn handle_analyze_complexity(
project_path: PathBuf,
file: Option<PathBuf>,
files: Vec<PathBuf>,
toolchain: Option<String>,
format: ComplexityOutputFormat,
output: Option<PathBuf>,
max_cyclomatic: Option<u16>,
max_cognitive: Option<u16>,
include: Vec<String>,
watch: bool,
top_files: usize,
fail_on_violation: bool,
timeout: u64,
) -> Result<()> {
if watch {
#[cfg(feature = "watch")]
{
return watch::handle_watch_mode(
&project_path,
toolchain.as_deref(),
max_cyclomatic,
max_cognitive,
include,
timeout,
top_files,
format,
output.as_deref(),
);
}
#[cfg(not(feature = "watch"))]
{
anyhow::bail!("Watch mode requires the 'watch' feature. Rebuild with: cargo build --features watch");
}
}
crate::cli::ensure_analysis_path_exists(&project_path)?;
let config = ComplexityConfig::from_args(
project_path,
toolchain,
max_cyclomatic,
max_cognitive,
include,
timeout,
top_files,
);
let mut file_metrics = analysis::analyze_files_by_mode(file, files, &config).await?;
let original_file_count = file_metrics.len();
let _filtered_count =
analysis::apply_complexity_filters(&mut file_metrics, max_cyclomatic, max_cognitive);
let analyzed_file_count = file_metrics.len();
let aggregated_metrics = file_metrics.clone();
analysis::apply_top_files_limit(&mut file_metrics, config.top_files);
let files_truncated = file_metrics.len() < analyzed_file_count;
if original_file_count > 0 && file_metrics.is_empty() {
eprintln!(
"\n⚠️ Warning: All {} file(s) were filtered out",
original_file_count
);
eprintln!(" No functions found exceeding the complexity thresholds:");
if let Some(cyc) = max_cyclomatic {
eprintln!(" - Cyclomatic complexity > {}", cyc);
}
if let Some(cog) = max_cognitive {
eprintln!(" - Cognitive complexity > {}", cog);
}
eprintln!("\n💡 Suggestions:");
eprintln!(" 1. Lower the thresholds using --max-cyclomatic or --max-cognitive");
eprintln!(" 2. Remove thresholds to see all files");
eprintln!(" 3. Use --verbose to see detailed analysis of all files\n");
}
let summary = analysis::build_report_over_analyzed_files(
aggregated_metrics,
file_metrics.clone(),
max_cyclomatic,
max_cognitive,
);
let listing = output::ListingDisclosure {
top_files,
files_listed: file_metrics.len(),
files_analyzed: analyzed_file_count,
files_discovered: original_file_count,
truncated: files_truncated,
};
output::format_and_write_output(&summary, &file_metrics, format, output, listing).await?;
analysis::check_complexity_violations(
&file_metrics,
fail_on_violation,
max_cyclomatic,
max_cognitive,
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn handle_analyze_dag(
dag_type: DagType,
project_path: PathBuf,
output: Option<PathBuf>,
max_depth: Option<usize>,
target_nodes: Option<usize>,
filter_external: bool,
show_complexity: bool,
_include_duplicates: bool,
_include_dead_code: bool,
enhanced: bool,
) -> Result<()> {
use crate::services::{
context::analyze_project,
mermaid_generator::{MermaidGenerator, MermaidOptions},
};
crate::cli::ensure_analysis_path_exists(&project_path)?;
eprintln!("🔄 Generating dependency analysis graph...");
let toolchain =
crate::cli::detect_primary_language(&project_path).unwrap_or_else(|| "rust".to_string());
let project_context = analyze_project(&project_path, &toolchain).await?;
eprintln!("📁 Analyzed {} files", project_context.files.len());
use crate::services::dag_builder::DagBuilder;
let mut graph = DagBuilder::build_from_project(&project_context);
crate::services::dag_call_edges::add_call_edges(&mut graph, &project_path);
let enriched_graph = filter_graph_by_dag_type(graph, &dag_type);
let options = MermaidOptions {
max_depth,
filter_external,
group_by_module: enhanced,
show_complexity,
};
let generator = MermaidGenerator::new(options);
let mermaid_content = if enhanced || target_nodes.is_some() {
use crate::services::fixed_graph_builder::{GraphConfig, GroupingStrategy};
let config = GraphConfig {
max_nodes: target_nodes.unwrap_or(100),
max_edges: target_nodes.map_or(400, |n| n * 4),
grouping: GroupingStrategy::Module,
};
generator.generate_with_config(&enriched_graph, &config)
} else {
generator.generate(&enriched_graph)
};
report_graph_size(&dag_type, &enriched_graph, &mermaid_content);
if let Some(output_path) = output {
tokio::fs::write(&output_path, &mermaid_content).await?;
eprintln!("✅ DAG written to: {}", output_path.display());
if output_path.extension().is_some_and(|ext| ext == "mmd") {
eprintln!("\n💡 To view the graph:");
eprintln!(" - Copy content to https://mermaid.live");
eprintln!(" - Or use VS Code with Mermaid extension");
}
} else {
println!("{mermaid_content}");
}
Ok(())
}
fn filter_graph_by_dag_type(
graph: crate::models::dag::DependencyGraph,
dag_type: &DagType,
) -> crate::models::dag::DependencyGraph {
use crate::models::dag::EdgeType;
match dag_type {
DagType::CallGraph => graph.filter_by_edge_types(&[EdgeType::Calls]),
DagType::ImportGraph => graph.filter_by_edge_types(&[EdgeType::Imports]),
DagType::Inheritance => {
graph.filter_by_edge_types(&[EdgeType::Inherits, EdgeType::Implements])
}
DagType::FullDependency => graph,
}
}
fn report_graph_size(
dag_type: &DagType,
graph: &crate::models::dag::DependencyGraph,
mermaid_content: &str,
) {
let (rendered_nodes, rendered_edges) = count_rendered_elements(mermaid_content);
eprintln!("📊 {dag_type}: rendered {rendered_nodes} nodes and {rendered_edges} edges");
if rendered_nodes < graph.nodes.len() || rendered_edges < graph.edges.len() {
eprintln!(
" (analyzed {} nodes and {} edges; the diagram is capped for readability)",
graph.nodes.len(),
graph.edges.len()
);
}
}
fn count_rendered_elements(mermaid_content: &str) -> (usize, usize) {
let mut nodes = 0;
let mut edges = 0;
for line in mermaid_content.lines() {
let trimmed = line.trim();
if trimmed.is_empty()
|| trimmed.starts_with("graph ")
|| trimmed.starts_with("style ")
|| trimmed.starts_with("classDef ")
|| trimmed.starts_with("subgraph")
|| trimmed == "end"
{
continue;
}
if is_mermaid_edge_line(trimmed) {
edges += 1;
} else {
nodes += 1;
}
}
(nodes, edges)
}
fn is_mermaid_edge_line(line: &str) -> bool {
line.contains("-->") || line.contains("-.->") || line.contains(" --- ")
}