Compare commits
2 commits
f35326b5e8
...
3311484712
| Author | SHA1 | Date | |
|---|---|---|---|
| 3311484712 | |||
| c4dd8c3b40 |
8 changed files with 739 additions and 434 deletions
|
|
@ -16,7 +16,9 @@ use crate::{circuits, complexity, datapaths, deps, flow, graph, loc, redundancy}
|
|||
pub enum Command {
|
||||
Summary,
|
||||
Loc { top: Option<usize> },
|
||||
Complexity,
|
||||
Branching,
|
||||
Signature,
|
||||
Span,
|
||||
Deps { top: Option<usize> },
|
||||
Map,
|
||||
Flow,
|
||||
|
|
@ -59,7 +61,9 @@ pub fn dispatch(command: Command, rs_files: &[PathBuf], project_path: &Path, jso
|
|||
|| loc::render_loc_json(rs_files, &p),
|
||||
|| loc::render_loc(rs_files, &p, top, verbose),
|
||||
),
|
||||
Command::Complexity => complexity::run(rs_files, &p, json, verbose),
|
||||
Command::Branching => complexity::run_branching(rs_files, &p, json, verbose),
|
||||
Command::Signature => complexity::run_signature(rs_files, &p, json, verbose),
|
||||
Command::Span => complexity::run_span(rs_files, &p, json, verbose),
|
||||
Command::Deps { top } => run_json_or_human(json,
|
||||
|| deps::render_deps_json(rs_files, &p),
|
||||
|| deps::render_deps(rs_files, &p, top, verbose),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use colored::Colorize;
|
||||
use serde::Serialize;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use syn::visit::Visit;
|
||||
use syn::Expr;
|
||||
|
||||
|
|
@ -15,27 +15,11 @@ pub struct FunctionComplexity {
|
|||
pub cyclomatic: usize,
|
||||
pub cognitive: usize,
|
||||
pub nesting_depth: usize,
|
||||
pub generic_complexity: usize,
|
||||
pub param_complexity: usize,
|
||||
pub composite_score: f64,
|
||||
pub line_count: usize,
|
||||
pub param_count: usize,
|
||||
}
|
||||
|
||||
/// Complexity summary for a single file.
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct FileComplexity {
|
||||
pub path: String,
|
||||
pub function_count: usize,
|
||||
pub total_complexity: f64,
|
||||
pub mean_complexity: f64,
|
||||
pub max_complexity: f64,
|
||||
pub total_generics: usize,
|
||||
pub trait_impl_count: usize,
|
||||
pub derive_density: f64,
|
||||
}
|
||||
|
||||
/// Aggregate stats across all functions.
|
||||
pub struct ComplexityStats {
|
||||
pub total_functions: usize,
|
||||
|
|
@ -45,47 +29,60 @@ pub struct ComplexityStats {
|
|||
pub max: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FunctionComplexityJson {
|
||||
name: String,
|
||||
file: String,
|
||||
cyclomatic: usize,
|
||||
cognitive: usize,
|
||||
nesting_depth: usize,
|
||||
generic_complexity: usize,
|
||||
param_complexity: usize,
|
||||
composite_score: f64,
|
||||
line_count: usize,
|
||||
param_count: usize,
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct BranchingRow {
|
||||
pub function: String,
|
||||
pub file: String,
|
||||
pub cyclomatic: usize,
|
||||
pub branch_points: usize,
|
||||
pub nesting_depth: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SignatureRow {
|
||||
pub function: String,
|
||||
pub file: String,
|
||||
pub param_count: usize,
|
||||
pub return_type_complexity: usize,
|
||||
pub generic_param_count: usize,
|
||||
pub trait_bound_count: usize,
|
||||
pub where_predicate_count: usize,
|
||||
pub signature_score: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SignatureFileRow {
|
||||
pub file: String,
|
||||
pub trait_impl_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SpanRow {
|
||||
pub function: String,
|
||||
pub file: String,
|
||||
pub line_count: usize,
|
||||
pub body_stmt_count: usize,
|
||||
pub line_start: usize,
|
||||
pub line_end: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FileComplexityJson {
|
||||
path: String,
|
||||
function_count: usize,
|
||||
total_complexity: f64,
|
||||
mean_complexity: f64,
|
||||
max_complexity: f64,
|
||||
total_generics: usize,
|
||||
trait_impl_count: usize,
|
||||
derive_density: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ComplexityStatsJson {
|
||||
total_functions: usize,
|
||||
mean: f64,
|
||||
std_dev: f64,
|
||||
median: f64,
|
||||
max: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ComplexityJson {
|
||||
struct BranchingJson {
|
||||
cstat_version: String,
|
||||
functions: Vec<FunctionComplexityJson>,
|
||||
files: Vec<FileComplexityJson>,
|
||||
stats: Option<ComplexityStatsJson>,
|
||||
functions: Vec<BranchingRow>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SignatureJson {
|
||||
cstat_version: String,
|
||||
functions: Vec<SignatureRow>,
|
||||
files: Vec<SignatureFileRow>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SpanJson {
|
||||
cstat_version: String,
|
||||
functions: Vec<SpanRow>,
|
||||
}
|
||||
|
||||
/// Compute complexity metrics for a single function.
|
||||
|
|
@ -110,11 +107,7 @@ fn compute_function_complexity(func: &FunctionInfo, source: &str) -> FunctionCom
|
|||
+ generic_complexity as f64 * 0.2
|
||||
+ param_complexity as f64 * 0.1;
|
||||
|
||||
let line_count = if func.line_end >= func.line_start {
|
||||
func.line_end - func.line_start + 1
|
||||
} else {
|
||||
func.body_stmt_count.max(1)
|
||||
};
|
||||
let line_count = function_line_count(func);
|
||||
|
||||
FunctionComplexity {
|
||||
name: func.name.clone(),
|
||||
|
|
@ -122,8 +115,6 @@ fn compute_function_complexity(func: &FunctionInfo, source: &str) -> FunctionCom
|
|||
cyclomatic,
|
||||
cognitive,
|
||||
nesting_depth: func.nesting_depth,
|
||||
generic_complexity,
|
||||
param_complexity,
|
||||
composite_score,
|
||||
line_count,
|
||||
param_count: func.param_count,
|
||||
|
|
@ -376,86 +367,6 @@ pub fn compute_all(symbols: &ProjectSymbols, project_path: &Path) -> Vec<Functio
|
|||
}
|
||||
|
||||
/// Compute per-file complexity aggregation.
|
||||
pub fn compute_file_complexity(
|
||||
symbols: &ProjectSymbols,
|
||||
func_complexities: &[FunctionComplexity],
|
||||
project_path: &Path,
|
||||
) -> Vec<FileComplexity> {
|
||||
let mut file_map: std::collections::HashMap<String, Vec<&FunctionComplexity>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for fc in func_complexities {
|
||||
file_map.entry(fc.file.clone()).or_default().push(fc);
|
||||
}
|
||||
|
||||
let mut file_complexities: Vec<FileComplexity> = symbols
|
||||
.files
|
||||
.iter()
|
||||
.map(|file_syms| {
|
||||
let path = strip_project_path(&file_syms.path.display().to_string(), project_path);
|
||||
let funcs = file_map.get(&path).cloned().unwrap_or_default();
|
||||
let function_count = funcs.len();
|
||||
let scores: Vec<f64> = funcs.iter().map(|f| f.composite_score).collect();
|
||||
let total_complexity: f64 = scores.iter().sum();
|
||||
let mean_complexity = if function_count > 0 {
|
||||
total_complexity / function_count as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let max_complexity = scores.iter().cloned().fold(0.0f64, f64::max);
|
||||
|
||||
let total_generics: usize = file_syms
|
||||
.structs
|
||||
.iter()
|
||||
.map(|s| s.generic_param_count)
|
||||
.sum::<usize>()
|
||||
+ file_syms
|
||||
.enums
|
||||
.iter()
|
||||
.map(|e| e.generic_param_count)
|
||||
.sum::<usize>();
|
||||
let trait_impl_count = file_syms
|
||||
.impls
|
||||
.iter()
|
||||
.filter(|i| i.trait_name.is_some())
|
||||
.count();
|
||||
let total_types = file_syms.structs.len() + file_syms.enums.len();
|
||||
let total_derives: usize = file_syms
|
||||
.structs
|
||||
.iter()
|
||||
.map(|s| s.derive_count)
|
||||
.sum::<usize>()
|
||||
+ file_syms
|
||||
.enums
|
||||
.iter()
|
||||
.map(|e| e.derive_count)
|
||||
.sum::<usize>();
|
||||
let derive_density = if total_types > 0 {
|
||||
total_derives as f64 / total_types as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
FileComplexity {
|
||||
path,
|
||||
function_count,
|
||||
total_complexity,
|
||||
mean_complexity,
|
||||
max_complexity,
|
||||
total_generics,
|
||||
trait_impl_count,
|
||||
derive_density,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
file_complexities.sort_by(|a, b| {
|
||||
b.total_complexity
|
||||
.partial_cmp(&a.total_complexity)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
file_complexities
|
||||
}
|
||||
|
||||
/// Compute aggregate stats across all function complexities.
|
||||
pub fn compute_stats(complexities: &[FunctionComplexity]) -> Option<ComplexityStats> {
|
||||
|
|
@ -495,283 +406,380 @@ fn strip_project_path(path: &str, project_path: &Path) -> String {
|
|||
stripped.to_string()
|
||||
}
|
||||
|
||||
/// Render the function complexity rankings table.
|
||||
fn render_function_rankings(
|
||||
func_complexities: &[FunctionComplexity],
|
||||
_term_w: usize,
|
||||
verbose: bool,
|
||||
) {
|
||||
println!(
|
||||
"\n{}",
|
||||
"\u{2500}\u{2500} Function Complexity Rankings \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}"
|
||||
.bright_cyan()
|
||||
.bold()
|
||||
);
|
||||
fn relative_file(path: &Path, project_path: &Path) -> String {
|
||||
strip_project_path(&path.display().to_string(), project_path)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
render::verbose_block(&[
|
||||
"Functions ranked by composite complexity score (highest = most complex).",
|
||||
"Each column is an individual complexity metric:",
|
||||
" Cycl = cyclomatic complexity = number of linearly independent paths",
|
||||
" through the function (branch points + 1). Higher = more branches.",
|
||||
" Cogn = cognitive complexity (SonarSource model): +1 per control flow break",
|
||||
" (if, for, while, match, &&, ||), plus a nesting penalty per level.",
|
||||
" Nest = deepest nesting depth in the function body.",
|
||||
" Gen = generic complexity = type params + trait bounds + where predicates.",
|
||||
" Param = parameter count + return type complexity.",
|
||||
" Score = composite: 1.0\u{00d7}Cycl + 0.5\u{00d7}Cogn + 0.3\u{00d7}Nest + 0.2\u{00d7}Gen + 0.1\u{00d7}Param.",
|
||||
"Color: red = highest score, green = lowest. Functions sorted descending by Score.",
|
||||
]);
|
||||
render::guide_ref("complexity");
|
||||
}
|
||||
|
||||
let display_count = func_complexities.len().min(20);
|
||||
println!(
|
||||
"{} functions analyzed, showing top {}\n",
|
||||
format!("{}", func_complexities.len()).bold(),
|
||||
format!("{}", display_count).bold(),
|
||||
);
|
||||
|
||||
// Header
|
||||
println!(
|
||||
" {:<40} {:>5} {:>5} {:>5} {:>5} {:>5} {:>7}",
|
||||
"Function".bold().underline(),
|
||||
"Cycl".bold().underline(),
|
||||
"Cogn".bold().underline(),
|
||||
"Nest".bold().underline(),
|
||||
"Gen".bold().underline(),
|
||||
"Param".bold().underline(),
|
||||
"Score".bold().underline(),
|
||||
);
|
||||
|
||||
for (i, fc) in func_complexities.iter().take(display_count).enumerate() {
|
||||
let ratio = i as f64 / display_count.max(1) as f64;
|
||||
let name_display = if fc.name.len() > 38 {
|
||||
format!("{}...", &fc.name[..35])
|
||||
} else {
|
||||
fc.name.clone()
|
||||
};
|
||||
|
||||
let score_str = format!("{:.1}", fc.composite_score);
|
||||
let colored_score = render::bar_color(&score_str, ratio);
|
||||
|
||||
println!(
|
||||
" {:<40} {:>5} {:>5} {:>5} {:>5} {:>5} {}",
|
||||
name_display.dimmed(),
|
||||
fc.cyclomatic,
|
||||
fc.cognitive,
|
||||
fc.nesting_depth,
|
||||
fc.generic_complexity,
|
||||
fc.param_complexity,
|
||||
colored_score,
|
||||
);
|
||||
}
|
||||
|
||||
if func_complexities.len() > display_count {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
" ... and {} more functions",
|
||||
func_complexities.len() - display_count
|
||||
)
|
||||
.dimmed()
|
||||
);
|
||||
fn function_line_count(func: &FunctionInfo) -> usize {
|
||||
if func.line_end >= func.line_start {
|
||||
func.line_end - func.line_start + 1
|
||||
} else {
|
||||
func.body_stmt_count.max(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the per-file complexity bar chart.
|
||||
fn render_file_complexity_chart(
|
||||
file_complexities: &[FileComplexity],
|
||||
term_w: usize,
|
||||
verbose: bool,
|
||||
) {
|
||||
println!(
|
||||
"\n{}",
|
||||
"\u{2500}\u{2500} Per-File Complexity \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}"
|
||||
.bright_cyan()
|
||||
.bold()
|
||||
);
|
||||
fn signature_score(func: &FunctionInfo) -> usize {
|
||||
func.param_count
|
||||
+ func.return_type_complexity
|
||||
+ func.generic_param_count
|
||||
+ func.trait_bound_count
|
||||
+ func.where_predicate_count
|
||||
}
|
||||
|
||||
if verbose {
|
||||
render::verbose_block(&[
|
||||
"Total composite complexity score per file (sum of all function scores in the file).",
|
||||
"Bar length is proportional to the file with the highest total score.",
|
||||
"Color: red = top of ranking, green = bottom. Files sorted descending.",
|
||||
]);
|
||||
fn truncate(value: &str, max: usize) -> String {
|
||||
if value.chars().count() <= max {
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
let file_display_count = file_complexities.len().min(20);
|
||||
let display_files = &file_complexities[..file_display_count];
|
||||
let keep = max.saturating_sub(3);
|
||||
let mut out = value.chars().take(keep).collect::<String>();
|
||||
out.push_str("...");
|
||||
out
|
||||
}
|
||||
|
||||
let max_label_len = display_files
|
||||
pub fn collect_branching(symbols: &ProjectSymbols, project_path: &Path) -> Vec<BranchingRow> {
|
||||
let mut rows: Vec<BranchingRow> = symbols
|
||||
.files
|
||||
.iter()
|
||||
.map(|f| f.path.len())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
.min(40);
|
||||
.flat_map(|file| file.functions.iter())
|
||||
.map(|func| BranchingRow {
|
||||
function: func.name.clone(),
|
||||
file: relative_file(&func.file, project_path),
|
||||
cyclomatic: func.branch_points + 1,
|
||||
branch_points: func.branch_points,
|
||||
nesting_depth: func.nesting_depth,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let max_score = display_files
|
||||
rows.sort_by(|a, b| {
|
||||
b.cyclomatic
|
||||
.cmp(&a.cyclomatic)
|
||||
.then_with(|| b.branch_points.cmp(&a.branch_points))
|
||||
.then_with(|| b.nesting_depth.cmp(&a.nesting_depth))
|
||||
.then_with(|| a.file.cmp(&b.file))
|
||||
.then_with(|| a.function.cmp(&b.function))
|
||||
});
|
||||
rows
|
||||
}
|
||||
|
||||
pub fn collect_signatures(symbols: &ProjectSymbols, project_path: &Path) -> Vec<SignatureRow> {
|
||||
let mut rows: Vec<SignatureRow> = symbols
|
||||
.files
|
||||
.iter()
|
||||
.map(|f| f.total_complexity)
|
||||
.fold(0.0f64, f64::max);
|
||||
.flat_map(|file| file.functions.iter())
|
||||
.map(|func| SignatureRow {
|
||||
function: func.name.clone(),
|
||||
file: relative_file(&func.file, project_path),
|
||||
param_count: func.param_count,
|
||||
return_type_complexity: func.return_type_complexity,
|
||||
generic_param_count: func.generic_param_count,
|
||||
trait_bound_count: func.trait_bound_count,
|
||||
where_predicate_count: func.where_predicate_count,
|
||||
signature_score: signature_score(func),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let score_digits = format!("{:.1}", max_score).len();
|
||||
let bar_budget = term_w
|
||||
.saturating_sub(max_label_len)
|
||||
.saturating_sub(3) // " \u{2502} "
|
||||
.saturating_sub(score_digits)
|
||||
.saturating_sub(2)
|
||||
.max(10);
|
||||
|
||||
println!();
|
||||
for (i, fc) in display_files.iter().enumerate() {
|
||||
let ratio = i as f64 / file_display_count.max(1) as f64;
|
||||
let label = if fc.path.len() > 40 {
|
||||
format!("...{}", &fc.path[fc.path.len() - 37..])
|
||||
} else {
|
||||
fc.path.clone()
|
||||
};
|
||||
|
||||
let bar_len = if max_score > 0.0 {
|
||||
(fc.total_complexity / max_score * bar_budget as f64).ceil() as usize
|
||||
} else {
|
||||
0
|
||||
}
|
||||
.max(if fc.total_complexity > 0.0 { 1 } else { 0 });
|
||||
|
||||
let bar_str = "\u{2588}".repeat(bar_len);
|
||||
let colored_bar = render::bar_color(&bar_str, ratio);
|
||||
|
||||
println!(
|
||||
"{:>width$} \u{2502} {}{} {}",
|
||||
label.dimmed(),
|
||||
colored_bar,
|
||||
" ".repeat(bar_budget.saturating_sub(bar_len)),
|
||||
format!("{:.1}", fc.total_complexity).bold(),
|
||||
width = max_label_len,
|
||||
);
|
||||
}
|
||||
|
||||
if file_complexities.len() > file_display_count {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
" ... and {} more files",
|
||||
file_complexities.len() - file_display_count
|
||||
)
|
||||
.dimmed()
|
||||
);
|
||||
}
|
||||
rows.sort_by(|a, b| {
|
||||
b.signature_score
|
||||
.cmp(&a.signature_score)
|
||||
.then_with(|| b.param_count.cmp(&a.param_count))
|
||||
.then_with(|| b.return_type_complexity.cmp(&a.return_type_complexity))
|
||||
.then_with(|| b.generic_param_count.cmp(&a.generic_param_count))
|
||||
.then_with(|| b.trait_bound_count.cmp(&a.trait_bound_count))
|
||||
.then_with(|| b.where_predicate_count.cmp(&a.where_predicate_count))
|
||||
.then_with(|| a.file.cmp(&b.file))
|
||||
.then_with(|| a.function.cmp(&b.function))
|
||||
});
|
||||
rows
|
||||
}
|
||||
|
||||
/// Render the aggregate complexity statistics box.
|
||||
fn render_complexity_stats(stats: &ComplexityStats, verbose: bool) {
|
||||
println!(
|
||||
"\n{}",
|
||||
"\u{2500}\u{2500} Complexity Statistics \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}"
|
||||
.bright_cyan()
|
||||
.bold()
|
||||
);
|
||||
pub fn collect_signature_files(
|
||||
symbols: &ProjectSymbols,
|
||||
project_path: &Path,
|
||||
) -> Vec<SignatureFileRow> {
|
||||
let mut rows: Vec<SignatureFileRow> = symbols
|
||||
.files
|
||||
.iter()
|
||||
.map(|file_syms| SignatureFileRow {
|
||||
file: relative_file(&file_syms.path, project_path),
|
||||
trait_impl_count: file_syms
|
||||
.impls
|
||||
.iter()
|
||||
.filter(|imp| imp.trait_name.is_some())
|
||||
.count(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if verbose {
|
||||
render::verbose_block(&[
|
||||
"Aggregate statistics for composite complexity across all functions.",
|
||||
"Mean = average score. Std Dev = spread. Median = middle value (robust to outliers).",
|
||||
"Max = highest individual function score in the project.",
|
||||
]);
|
||||
}
|
||||
|
||||
println!("\u{250c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{252c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2510}");
|
||||
render::print_stat_row_wide("Total functions", &format!("{}", stats.total_functions));
|
||||
render::print_stat_row_wide("Mean score", &format!("{:.2}", stats.mean));
|
||||
render::print_stat_row_wide("Std Dev", &format!("{:.2}", stats.std_dev));
|
||||
render::print_stat_row_wide("Median score", &format!("{:.2}", stats.median));
|
||||
render::print_stat_row_wide("Max score", &format!("{:.2}", stats.max));
|
||||
println!("\u{2514}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2534}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2518}");
|
||||
rows.sort_by(|a, b| {
|
||||
b.trait_impl_count
|
||||
.cmp(&a.trait_impl_count)
|
||||
.then_with(|| a.file.cmp(&b.file))
|
||||
});
|
||||
rows
|
||||
}
|
||||
|
||||
/// Render the complexity subcommand output.
|
||||
pub fn render_complexity(symbols: &ProjectSymbols, project_path: &Path, verbose: bool) {
|
||||
let func_complexities = compute_all(symbols, project_path);
|
||||
pub fn collect_spans(symbols: &ProjectSymbols, project_path: &Path) -> Vec<SpanRow> {
|
||||
let mut rows: Vec<SpanRow> = symbols
|
||||
.files
|
||||
.iter()
|
||||
.flat_map(|file| file.functions.iter())
|
||||
.map(|func| SpanRow {
|
||||
function: func.name.clone(),
|
||||
file: relative_file(&func.file, project_path),
|
||||
line_count: function_line_count(func),
|
||||
body_stmt_count: func.body_stmt_count,
|
||||
line_start: func.line_start,
|
||||
line_end: func.line_end,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if func_complexities.is_empty() {
|
||||
rows.sort_by(|a, b| {
|
||||
b.line_count
|
||||
.cmp(&a.line_count)
|
||||
.then_with(|| b.body_stmt_count.cmp(&a.body_stmt_count))
|
||||
.then_with(|| a.file.cmp(&b.file))
|
||||
.then_with(|| a.function.cmp(&b.function))
|
||||
});
|
||||
rows
|
||||
}
|
||||
|
||||
pub fn render_branching(symbols: &ProjectSymbols, project_path: &Path, verbose: bool) {
|
||||
let rows = collect_branching(symbols, project_path);
|
||||
if rows.is_empty() {
|
||||
println!("{}", "No functions found to analyze.".yellow());
|
||||
return;
|
||||
}
|
||||
|
||||
let file_complexities = compute_file_complexity(symbols, &func_complexities, project_path);
|
||||
let stats = compute_stats(&func_complexities);
|
||||
let term_w = render::terminal_width();
|
||||
|
||||
render_function_rankings(&func_complexities, term_w, verbose);
|
||||
render_file_complexity_chart(&file_complexities, term_w, verbose);
|
||||
|
||||
if let Some(ref stats) = stats {
|
||||
render_complexity_stats(stats, verbose);
|
||||
render::section_header("Function Branching Complexity");
|
||||
if verbose {
|
||||
render::verbose_block(&[
|
||||
"Functions ranked by decision/path complexity.",
|
||||
"Cyclomatic complexity is branch_points + 1.",
|
||||
"Branch points and nesting depth come from the parsed control-flow visitor.",
|
||||
"cognitive and composite_score are intentionally not part of this view.",
|
||||
]);
|
||||
}
|
||||
|
||||
let display_count = rows.len().min(20);
|
||||
println!(
|
||||
"{} functions analyzed, showing top {}\n",
|
||||
format!("{}", rows.len()).bold(),
|
||||
format!("{}", display_count).bold(),
|
||||
);
|
||||
println!(
|
||||
" {:<40} {:<30} {:>5} {:>8} {:>5}",
|
||||
"Function".bold().underline(),
|
||||
"File".bold().underline(),
|
||||
"Cycl".bold().underline(),
|
||||
"Branches".bold().underline(),
|
||||
"Nest".bold().underline(),
|
||||
);
|
||||
|
||||
for row in rows.iter().take(display_count) {
|
||||
println!(
|
||||
" {:<40} {:<30} {:>5} {:>8} {:>5}",
|
||||
truncate(&row.function, 40).dimmed(),
|
||||
truncate(&row.file, 30).dimmed(),
|
||||
row.cyclomatic,
|
||||
row.branch_points,
|
||||
row.nesting_depth,
|
||||
);
|
||||
}
|
||||
|
||||
if rows.len() > display_count {
|
||||
println!(
|
||||
"{}",
|
||||
format!(" ... and {} more functions", rows.len() - display_count).dimmed()
|
||||
);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Render complexity analysis as JSON.
|
||||
pub fn render_complexity_json(symbols: &ProjectSymbols, project_path: &Path) {
|
||||
let func_complexities = compute_all(symbols, project_path);
|
||||
let file_complexities = compute_file_complexity(symbols, &func_complexities, project_path);
|
||||
let stats = compute_stats(&func_complexities);
|
||||
|
||||
let functions: Vec<FunctionComplexityJson> = func_complexities
|
||||
.iter()
|
||||
.map(|fc| FunctionComplexityJson {
|
||||
name: fc.name.clone(),
|
||||
file: fc.file.clone(),
|
||||
cyclomatic: fc.cyclomatic,
|
||||
cognitive: fc.cognitive,
|
||||
nesting_depth: fc.nesting_depth,
|
||||
generic_complexity: fc.generic_complexity,
|
||||
param_complexity: fc.param_complexity,
|
||||
composite_score: fc.composite_score,
|
||||
line_count: fc.line_count,
|
||||
param_count: fc.param_count,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let files: Vec<FileComplexityJson> = file_complexities
|
||||
.iter()
|
||||
.map(|fc| FileComplexityJson {
|
||||
path: fc.path.clone(),
|
||||
function_count: fc.function_count,
|
||||
total_complexity: fc.total_complexity,
|
||||
mean_complexity: fc.mean_complexity,
|
||||
max_complexity: fc.max_complexity,
|
||||
total_generics: fc.total_generics,
|
||||
trait_impl_count: fc.trait_impl_count,
|
||||
derive_density: fc.derive_density,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let stats_json = stats.map(|s| ComplexityStatsJson {
|
||||
total_functions: s.total_functions,
|
||||
mean: s.mean,
|
||||
std_dev: s.std_dev,
|
||||
median: s.median,
|
||||
max: s.max,
|
||||
});
|
||||
|
||||
let output = ComplexityJson {
|
||||
pub fn render_branching_json(symbols: &ProjectSymbols, project_path: &Path) {
|
||||
let output = BranchingJson {
|
||||
cstat_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
functions,
|
||||
files,
|
||||
stats: stats_json,
|
||||
functions: collect_branching(symbols, project_path),
|
||||
};
|
||||
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
}
|
||||
|
||||
pub fn run(rs_files: &[std::path::PathBuf], project_path: &Path, json: bool, verbose: bool) {
|
||||
pub fn render_signature(symbols: &ProjectSymbols, project_path: &Path, verbose: bool) {
|
||||
let rows = collect_signatures(symbols, project_path);
|
||||
let file_rows = collect_signature_files(symbols, project_path);
|
||||
|
||||
render::section_header("Function Signature Complexity");
|
||||
if verbose {
|
||||
render::verbose_block(&[
|
||||
"Functions ranked by API-boundary complexity.",
|
||||
"Signature score = params + return type complexity + generic params + trait bounds + where predicates.",
|
||||
"This view answers call/satisfy/reason-about difficulty, not control-flow or implementation span.",
|
||||
]);
|
||||
}
|
||||
|
||||
if rows.is_empty() {
|
||||
println!("{}", "No functions found to analyze.".yellow());
|
||||
} else {
|
||||
let display_count = rows.len().min(20);
|
||||
println!(
|
||||
"{} functions analyzed, showing top {}\n",
|
||||
format!("{}", rows.len()).bold(),
|
||||
format!("{}", display_count).bold(),
|
||||
);
|
||||
println!(
|
||||
" {:<34} {:<24} {:>6} {:>6} {:>8} {:>6} {:>5} {:>5}",
|
||||
"Function".bold().underline(),
|
||||
"File".bold().underline(),
|
||||
"Params".bold().underline(),
|
||||
"Return".bold().underline(),
|
||||
"Generics".bold().underline(),
|
||||
"Bounds".bold().underline(),
|
||||
"Where".bold().underline(),
|
||||
"Score".bold().underline(),
|
||||
);
|
||||
|
||||
for row in rows.iter().take(display_count) {
|
||||
println!(
|
||||
" {:<34} {:<24} {:>6} {:>6} {:>8} {:>6} {:>5} {:>5}",
|
||||
truncate(&row.function, 34).dimmed(),
|
||||
truncate(&row.file, 24).dimmed(),
|
||||
row.param_count,
|
||||
row.return_type_complexity,
|
||||
row.generic_param_count,
|
||||
row.trait_bound_count,
|
||||
row.where_predicate_count,
|
||||
row.signature_score,
|
||||
);
|
||||
}
|
||||
|
||||
if rows.len() > display_count {
|
||||
println!(
|
||||
"{}",
|
||||
format!(" ... and {} more functions", rows.len() - display_count).dimmed()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !file_rows.is_empty() {
|
||||
render::section_header("Trait Implementations by File");
|
||||
let display_count = file_rows.len().min(20);
|
||||
println!(
|
||||
" {:<40} {:>10}",
|
||||
"File".bold().underline(),
|
||||
"TraitImpls".bold().underline(),
|
||||
);
|
||||
for row in file_rows.iter().take(display_count) {
|
||||
println!(
|
||||
" {:<40} {:>10}",
|
||||
truncate(&row.file, 40).dimmed(),
|
||||
row.trait_impl_count,
|
||||
);
|
||||
}
|
||||
if file_rows.len() > display_count {
|
||||
println!(
|
||||
"{}",
|
||||
format!(" ... and {} more files", file_rows.len() - display_count).dimmed()
|
||||
);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn render_signature_json(symbols: &ProjectSymbols, project_path: &Path) {
|
||||
let output = SignatureJson {
|
||||
cstat_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
functions: collect_signatures(symbols, project_path),
|
||||
files: collect_signature_files(symbols, project_path),
|
||||
};
|
||||
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
}
|
||||
|
||||
pub fn render_span(symbols: &ProjectSymbols, project_path: &Path, verbose: bool) {
|
||||
let rows = collect_spans(symbols, project_path);
|
||||
if rows.is_empty() {
|
||||
println!("{}", "No functions found to analyze.".yellow());
|
||||
return;
|
||||
}
|
||||
|
||||
render::section_header("Function Implementation Span");
|
||||
if verbose {
|
||||
render::verbose_block(&[
|
||||
"Functions ranked by implementation-unit size.",
|
||||
"line_count is the inclusive function-local line_start..=line_end span.",
|
||||
"This does not replace cstat loc, which remains file/project LOC.",
|
||||
]);
|
||||
}
|
||||
|
||||
let display_count = rows.len().min(20);
|
||||
println!(
|
||||
"{} functions analyzed, showing top {}\n",
|
||||
format!("{}", rows.len()).bold(),
|
||||
format!("{}", display_count).bold(),
|
||||
);
|
||||
println!(
|
||||
" {:<40} {:<30} {:>5} {:>5} {:>5} {:>5}",
|
||||
"Function".bold().underline(),
|
||||
"File".bold().underline(),
|
||||
"Lines".bold().underline(),
|
||||
"Stmts".bold().underline(),
|
||||
"Start".bold().underline(),
|
||||
"End".bold().underline(),
|
||||
);
|
||||
|
||||
for row in rows.iter().take(display_count) {
|
||||
println!(
|
||||
" {:<40} {:<30} {:>5} {:>5} {:>5} {:>5}",
|
||||
truncate(&row.function, 40).dimmed(),
|
||||
truncate(&row.file, 30).dimmed(),
|
||||
row.line_count,
|
||||
row.body_stmt_count,
|
||||
row.line_start,
|
||||
row.line_end,
|
||||
);
|
||||
}
|
||||
|
||||
if rows.len() > display_count {
|
||||
println!(
|
||||
"{}",
|
||||
format!(" ... and {} more functions", rows.len() - display_count).dimmed()
|
||||
);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn render_span_json(symbols: &ProjectSymbols, project_path: &Path) {
|
||||
let output = SpanJson {
|
||||
cstat_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
functions: collect_spans(symbols, project_path),
|
||||
};
|
||||
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
}
|
||||
|
||||
pub fn run_branching(rs_files: &[PathBuf], project_path: &Path, json: bool, verbose: bool) {
|
||||
let symbols = crate::ast_parser::parse_project(rs_files);
|
||||
if json {
|
||||
render_complexity_json(&symbols, project_path);
|
||||
render_branching_json(&symbols, project_path);
|
||||
} else {
|
||||
render_complexity(&symbols, project_path, verbose);
|
||||
render_branching(&symbols, project_path, verbose);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_signature(rs_files: &[PathBuf], project_path: &Path, json: bool, verbose: bool) {
|
||||
let symbols = crate::ast_parser::parse_project(rs_files);
|
||||
if json {
|
||||
render_signature_json(&symbols, project_path);
|
||||
} else {
|
||||
render_signature(&symbols, project_path, verbose);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_span(rs_files: &[PathBuf], project_path: &Path, json: bool, verbose: bool) {
|
||||
let symbols = crate::ast_parser::parse_project(rs_files);
|
||||
if json {
|
||||
render_span_json(&symbols, project_path);
|
||||
} else {
|
||||
render_span(&symbols, project_path, verbose);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
src/dump.rs
12
src/dump.rs
|
|
@ -180,15 +180,11 @@ fn build_drilldowns(diagnostics: &[diagnostics::Diagnostic]) -> Vec<Drilldown> {
|
|||
// Map categories to subcommands
|
||||
let category_to_command: &[(&[&str], &str)] = &[
|
||||
(
|
||||
&[
|
||||
"bloated_function",
|
||||
"high_complexity",
|
||||
"high_cognitive",
|
||||
"deep_nesting",
|
||||
"too_many_params",
|
||||
],
|
||||
"cstat complexity --json",
|
||||
&["high_complexity", "high_cognitive", "deep_nesting"],
|
||||
"cstat branching --json",
|
||||
),
|
||||
(&["too_many_params"], "cstat signature --json"),
|
||||
(&["bloated_function"], "cstat span --json"),
|
||||
(&["redundant_code"], "cstat redundancy --json"),
|
||||
(&["bloated_file"], "cstat loc --json"),
|
||||
(
|
||||
|
|
|
|||
25
src/guide.rs
25
src/guide.rs
|
|
@ -52,7 +52,7 @@ or `cstat dump` for a machine-readable JSON diagnostic report.",
|
|||
],
|
||||
patterns: &[
|
||||
"Human workflow: run summary first, read the dashboard, drill into specific \
|
||||
dimensions with loc/complexity/deps/flow/graph as needed.",
|
||||
dimensions with loc/branching/signature/span/deps/flow/graph as needed.",
|
||||
"Agent workflow: run dump for the full diagnostic picture, read scores to \
|
||||
identify weak dimensions, use individual commands with --json to explore specifics.",
|
||||
"The -v (verbose) flag adds contextual explanations to any command — useful \
|
||||
|
|
@ -96,22 +96,23 @@ Size metrics measure the volume and distribution of Rust source. `cstat loc` own
|
|||
fn topic_complexity() -> TopicContent {
|
||||
TopicContent {
|
||||
description: "\
|
||||
Complexity metrics quantify different aspects of how difficult code is to \
|
||||
understand, test, and modify. No single metric captures the full picture — \
|
||||
cyclomatic counts paths, cognitive models reading difficulty, nesting measures \
|
||||
Complexity metrics are split by question: branching for decision/path shape, \
|
||||
signature for API-boundary difficulty, and span for function-local implementation \
|
||||
size. No single metric captures the full picture — cyclomatic counts paths, \
|
||||
cognitive models reading difficulty in diagnostics/distributions, nesting measures \
|
||||
structural depth, and parameter count reflects interface width.",
|
||||
metrics: &[
|
||||
("cyclomatic complexity", "Number of linearly independent paths through a function (branch_points + 1). Higher values mean more test cases needed for full coverage."),
|
||||
("cognitive complexity", "SonarSource model: +1 per control flow break, +nesting_level penalty. Models how hard the code is for a human to read."),
|
||||
("cognitive complexity", "SonarSource model: +1 per control flow break, +nesting_level penalty. Used by diagnostics/distributions, not exposed as a top-level probe."),
|
||||
("nesting depth", "Maximum depth of nested control structures. Deep nesting forces readers to hold more context in working memory."),
|
||||
("parameter count", "Number of function parameters. Wide interfaces are harder to call correctly and may indicate a function doing too much."),
|
||||
("composite score", "Weighted combination: 1.0×cyclomatic + 0.5×cognitive + 0.3×nesting + 0.2×generic + 0.1×param. Useful for ranking."),
|
||||
("function span", "Inclusive function-local line range and top-level body statement count. This complements file/project LOC."),
|
||||
],
|
||||
commands: &[
|
||||
"cstat complexity — per-function and per-file complexity rankings",
|
||||
"cstat complexity --json — structured complexity data for all functions",
|
||||
"cstat dist --metric cyclomatic — distribution and outlier analysis",
|
||||
"cstat dist --metric cognitive — cognitive complexity distribution",
|
||||
"cstat branching — decision/path complexity by function",
|
||||
"cstat signature — API-boundary complexity by function and file",
|
||||
"cstat span — function-local implementation size and line ranges",
|
||||
"cstat advanced dist --metric cyclomatic — distribution and outlier analysis",
|
||||
],
|
||||
patterns: &[
|
||||
"High cyclomatic but low cognitive complexity usually means straightforward \
|
||||
|
|
@ -119,8 +120,8 @@ branching (e.g. match statements with simple arms). High cognitive with \
|
|||
moderate cyclomatic often means nested conditionals.",
|
||||
"Functions with nesting depth > 4 are nearly always worth refactoring — \
|
||||
extract inner blocks into helper functions to flatten the structure.",
|
||||
"The composite score is useful for triage: sort by composite to find functions \
|
||||
that are complex along multiple dimensions simultaneously.",
|
||||
"Use branching, signature, and span together: a function can be easy to call \
|
||||
but branch-heavy, short but hard to satisfy generically, or long without many decisions.",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
src/main.rs
28
src/main.rs
|
|
@ -93,8 +93,12 @@ Use --explain to print this usage and JSON field contract without running analys
|
|||
},
|
||||
/// Rust AST symbol counts by kind, including trait impl blocks
|
||||
Symbols,
|
||||
/// Per-function and per-file complexity rankings
|
||||
Complexity,
|
||||
/// Function decision/path complexity rankings
|
||||
Branching,
|
||||
/// Function API boundary complexity rankings
|
||||
Signature,
|
||||
/// Function implementation span rankings
|
||||
Span,
|
||||
/// Module dependency connectome
|
||||
Deps {
|
||||
/// Show only the top N most connected modules
|
||||
|
|
@ -242,8 +246,14 @@ fn main() {
|
|||
symbols::render_symbols_file(project_rs_files, project_path, file, verbose);
|
||||
}
|
||||
}
|
||||
Commands::Complexity => {
|
||||
complexity::run(target_rs_files, project_path, json, verbose);
|
||||
Commands::Branching => {
|
||||
complexity::run_branching(target_rs_files, project_path, json, verbose);
|
||||
}
|
||||
Commands::Signature => {
|
||||
complexity::run_signature(target_rs_files, project_path, json, verbose);
|
||||
}
|
||||
Commands::Span => {
|
||||
complexity::run_span(target_rs_files, project_path, json, verbose);
|
||||
}
|
||||
Commands::Cluster => {
|
||||
if json {
|
||||
|
|
@ -354,8 +364,14 @@ fn main() {
|
|||
symbols::render_symbols(&rs_files, &project_path, verbose);
|
||||
}
|
||||
}
|
||||
Commands::Complexity => {
|
||||
complexity::run(&rs_files, &project_path, json, verbose);
|
||||
Commands::Branching => {
|
||||
complexity::run_branching(&rs_files, &project_path, json, verbose);
|
||||
}
|
||||
Commands::Signature => {
|
||||
complexity::run_signature(&rs_files, &project_path, json, verbose);
|
||||
}
|
||||
Commands::Span => {
|
||||
complexity::run_span(&rs_files, &project_path, json, verbose);
|
||||
}
|
||||
Commands::Cluster => {
|
||||
cluster::render_cluster(&rs_files, &project_path, json, verbose);
|
||||
|
|
|
|||
|
|
@ -133,11 +133,6 @@ pub fn print_stat_row(label: &str, value: &str) {
|
|||
println!("│ {:<11} │ {:>12} │", label.cyan(), value.bold());
|
||||
}
|
||||
|
||||
/// Print a stat row with 16-char label column (used by complexity).
|
||||
pub fn print_stat_row_wide(label: &str, value: &str) {
|
||||
println!("│ {:<16} │ {:>12} │", label.cyan(), value.bold());
|
||||
}
|
||||
|
||||
/// Print a verbose explanation line with dimmed cyan ℹ prefix.
|
||||
pub fn verbose_line(text: &str) {
|
||||
println!(" {}", format!("ℹ {}", text).dimmed());
|
||||
|
|
|
|||
|
|
@ -193,51 +193,53 @@ fn render_loc_bars(file_stats: &[loc::FileLocStats], project_path_canon: &Path,
|
|||
}
|
||||
}
|
||||
|
||||
/// Render the complexity hotspots table.
|
||||
/// Render the branching hotspots table.
|
||||
fn render_complexity_section(func_complexities: &[complexity::FunctionComplexity], verbose: bool) {
|
||||
render::section_header("Complexity Hotspots");
|
||||
render::section_header("Branching Hotspots");
|
||||
|
||||
if verbose {
|
||||
render::verbose_block(&[
|
||||
"Functions ranked by composite complexity score (highest first).",
|
||||
"Composite Score = 1.0×Cyclomatic + 0.5×Cognitive + 0.3×Nesting + 0.2×Generics + 0.1×Params.",
|
||||
"Functions ranked by cyclomatic branching complexity (highest first).",
|
||||
"Cycl = cyclomatic complexity = branch points + 1 (linearly independent paths).",
|
||||
"Cogn = cognitive complexity (SonarSource model): +1 per control flow break, +nesting penalty.",
|
||||
"Nest = deepest nesting depth within the function body.",
|
||||
"Score column: red = highest complexity, green = lowest. See 'cstat complexity' for full detail.",
|
||||
"Branches = cyclomatic - 1. Nest = deepest nesting depth within the function body.",
|
||||
"See 'cstat branching', 'cstat signature', and 'cstat span' for focused detail.",
|
||||
]);
|
||||
}
|
||||
|
||||
if func_complexities.is_empty() {
|
||||
println!(" {}", "No functions found.".dimmed());
|
||||
} else {
|
||||
let display_count = func_complexities.len().min(10);
|
||||
let mut rows: Vec<_> = func_complexities.iter().collect();
|
||||
rows.sort_by(|a, b| {
|
||||
b.cyclomatic
|
||||
.cmp(&a.cyclomatic)
|
||||
.then_with(|| b.nesting_depth.cmp(&a.nesting_depth))
|
||||
.then_with(|| a.file.cmp(&b.file))
|
||||
.then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
let display_count = rows.len().min(10);
|
||||
println!(
|
||||
" {:<36} {:>5} {:>5} {:>5} {:>7}",
|
||||
" {:<36} {:>5} {:>8} {:>5}",
|
||||
"Function".bold().underline(),
|
||||
"Cycl".bold().underline(),
|
||||
"Cogn".bold().underline(),
|
||||
"Branches".bold().underline(),
|
||||
"Nest".bold().underline(),
|
||||
"Score".bold().underline(),
|
||||
);
|
||||
|
||||
for (i, fc) in func_complexities.iter().take(display_count).enumerate() {
|
||||
let ratio = i as f64 / display_count.max(1) as f64;
|
||||
for fc in rows.into_iter().take(display_count) {
|
||||
let name = if fc.name.len() > 34 {
|
||||
format!("{}...", &fc.name[..31])
|
||||
} else {
|
||||
fc.name.clone()
|
||||
};
|
||||
let score_str = format!("{:.1}", fc.composite_score);
|
||||
let colored_score = render::bar_color(&score_str, ratio);
|
||||
|
||||
println!(
|
||||
" {:<36} {:>5} {:>5} {:>5} {}",
|
||||
" {:<36} {:>5} {:>8} {:>5}",
|
||||
name.dimmed(),
|
||||
fc.cyclomatic,
|
||||
fc.cognitive,
|
||||
fc.cyclomatic.saturating_sub(1),
|
||||
fc.nesting_depth,
|
||||
colored_score,
|
||||
);
|
||||
}
|
||||
if func_complexities.len() > display_count {
|
||||
|
|
|
|||
283
tests/function_probes_cli.rs
Normal file
283
tests/function_probes_cli.rs
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const FIXTURE: &str = r#"pub trait Worker {
|
||||
fn work(&self) -> usize;
|
||||
}
|
||||
|
||||
pub struct Engine<T> {
|
||||
value: T,
|
||||
}
|
||||
|
||||
impl<T> Worker for Engine<T>
|
||||
where
|
||||
T: Copy + Into<usize>,
|
||||
{
|
||||
fn work(&self) -> usize {
|
||||
self.value.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simple() -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
pub fn branchy(input: i32) -> i32 {
|
||||
let mut total = 0;
|
||||
if input > 0 {
|
||||
total += 1;
|
||||
}
|
||||
match input {
|
||||
0 => total += 10,
|
||||
1 | 2 => total += 20,
|
||||
_ => total += 30,
|
||||
}
|
||||
for i in 0..input {
|
||||
if i % 2 == 0 {
|
||||
total += i;
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
pub fn hard_signature<T, U>(
|
||||
first: T,
|
||||
second: U,
|
||||
count: usize,
|
||||
) -> Result<Option<(T, U)>, String>
|
||||
where
|
||||
T: Clone + Into<String>,
|
||||
U: Default,
|
||||
{
|
||||
if count == 0 {
|
||||
return Err(String::new());
|
||||
}
|
||||
Ok(Some((first, second)))
|
||||
}
|
||||
"#;
|
||||
|
||||
fn temp_project(name: &str) -> PathBuf {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let root = std::env::temp_dir().join(format!("cstat-function-probes-{name}-{unique}"));
|
||||
fs::create_dir_all(root.join("src")).unwrap();
|
||||
fs::write(
|
||||
root.join("Cargo.toml"),
|
||||
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(root.join("src/lib.rs"), FIXTURE).unwrap();
|
||||
root
|
||||
}
|
||||
|
||||
fn run_cstat(path: &Path, args: &[&str]) -> Output {
|
||||
let bin = env!("CARGO_BIN_EXE_cstat");
|
||||
let mut command = Command::new(bin);
|
||||
command.args(["--no-color", "--path"]);
|
||||
command.arg(path);
|
||||
command.args(args);
|
||||
command.output().expect("invoke cstat binary")
|
||||
}
|
||||
|
||||
fn assert_success(output: &Output) {
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"cstat failed: status={:?} stdout={} stderr={}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_json(output: Output) -> Value {
|
||||
assert_success(&output);
|
||||
serde_json::from_slice(&output.stdout).expect("parse cstat json")
|
||||
}
|
||||
|
||||
fn function_row<'a>(value: &'a Value, function: &str) -> &'a Value {
|
||||
value["functions"]
|
||||
.as_array()
|
||||
.expect("functions array")
|
||||
.iter()
|
||||
.find(|row| row["function"] == function)
|
||||
.unwrap_or_else(|| panic!("missing function row {function}: {value}"))
|
||||
}
|
||||
|
||||
fn assert_absent(row: &Value, keys: &[&str]) {
|
||||
for key in keys {
|
||||
assert!(row.get(*key).is_none(), "unexpected key {key}: {row}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn branching_json_reports_decision_path_fields() {
|
||||
let root = temp_project("branching-json");
|
||||
let value = parse_json(run_cstat(&root, &["--json", "branching"]));
|
||||
let row = function_row(&value, "branchy");
|
||||
|
||||
assert_eq!(row["file"], "src/lib.rs");
|
||||
assert_eq!(row["branch_points"], 5);
|
||||
assert_eq!(row["cyclomatic"], 6);
|
||||
assert_eq!(row["nesting_depth"], 2);
|
||||
assert_absent(
|
||||
row,
|
||||
&[
|
||||
"cognitive",
|
||||
"generic_complexity",
|
||||
"param_complexity",
|
||||
"composite_score",
|
||||
],
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_json_reports_api_boundary_fields() {
|
||||
let root = temp_project("signature-json");
|
||||
let value = parse_json(run_cstat(&root, &["--json", "signature"]));
|
||||
let row = function_row(&value, "hard_signature");
|
||||
|
||||
assert_eq!(row["file"], "src/lib.rs");
|
||||
assert_eq!(row["param_count"], 3);
|
||||
assert_eq!(row["return_type_complexity"], 6);
|
||||
assert_eq!(row["generic_param_count"], 2);
|
||||
assert_eq!(row["trait_bound_count"], 3);
|
||||
assert_eq!(row["where_predicate_count"], 2);
|
||||
assert_eq!(row["signature_score"], 16);
|
||||
assert_absent(
|
||||
row,
|
||||
&["cyclomatic", "cognitive", "line_count", "composite_score"],
|
||||
);
|
||||
|
||||
let file_row = value["files"]
|
||||
.as_array()
|
||||
.expect("files array")
|
||||
.iter()
|
||||
.find(|row| row["file"] == "src/lib.rs")
|
||||
.unwrap_or_else(|| panic!("missing file row: {value}"));
|
||||
assert_eq!(file_row["trait_impl_count"], 1);
|
||||
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn span_json_reports_body_size_and_line_ranges() {
|
||||
let root = temp_project("span-json");
|
||||
let value = parse_json(run_cstat(&root, &["--json", "span"]));
|
||||
let row = function_row(&value, "branchy");
|
||||
|
||||
assert_eq!(row["file"], "src/lib.rs");
|
||||
assert_eq!(row["line_start"], 22);
|
||||
assert_eq!(row["line_end"], 38);
|
||||
assert_eq!(row["line_count"], 17);
|
||||
assert_eq!(row["body_stmt_count"], 5);
|
||||
assert_absent(
|
||||
row,
|
||||
&[
|
||||
"cyclomatic",
|
||||
"cognitive",
|
||||
"signature_score",
|
||||
"composite_score",
|
||||
],
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_human_outputs_use_focused_headings() {
|
||||
let root = temp_project("human");
|
||||
|
||||
let branching = run_cstat(&root, &["branching"]);
|
||||
assert_success(&branching);
|
||||
let branching_stdout = String::from_utf8_lossy(&branching.stdout);
|
||||
assert!(
|
||||
branching_stdout.contains("Function Branching Complexity"),
|
||||
"stdout={branching_stdout}"
|
||||
);
|
||||
assert!(
|
||||
!branching_stdout.contains("Score"),
|
||||
"stdout={branching_stdout}"
|
||||
);
|
||||
|
||||
let signature = run_cstat(&root, &["signature"]);
|
||||
assert_success(&signature);
|
||||
let signature_stdout = String::from_utf8_lossy(&signature.stdout);
|
||||
assert!(
|
||||
signature_stdout.contains("Function Signature Complexity"),
|
||||
"stdout={signature_stdout}"
|
||||
);
|
||||
assert!(
|
||||
!signature_stdout.contains("Cycl"),
|
||||
"stdout={signature_stdout}"
|
||||
);
|
||||
|
||||
let span = run_cstat(&root, &["span"]);
|
||||
assert_success(&span);
|
||||
let span_stdout = String::from_utf8_lossy(&span.stdout);
|
||||
assert!(
|
||||
span_stdout.contains("Function Implementation Span"),
|
||||
"stdout={span_stdout}"
|
||||
);
|
||||
assert!(!span_stdout.contains("Signature"), "stdout={span_stdout}");
|
||||
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_file_paths_scope_probe_json() {
|
||||
let root = temp_project("selected-file");
|
||||
let file = root.join("src/lib.rs");
|
||||
|
||||
for command in ["branching", "signature", "span"] {
|
||||
let value = parse_json(run_cstat(&file, &["--json", command]));
|
||||
let functions = value["functions"].as_array().expect("functions array");
|
||||
assert!(!functions.is_empty(), "no functions for {command}: {value}");
|
||||
for row in functions {
|
||||
assert_eq!(row["file"], "src/lib.rs", "row={row}");
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_complexity_command_is_not_available() {
|
||||
let root = temp_project("old-complexity");
|
||||
let output = run_cstat(&root, &["complexity"]);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"old complexity command unexpectedly succeeded: stdout={} stderr={}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_lists_function_probe_commands() {
|
||||
let root = temp_project("help");
|
||||
let output = run_cstat(&root, &["--help"]);
|
||||
assert_success(&output);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
for command in ["branching", "signature", "span"] {
|
||||
assert!(
|
||||
stdout.contains(command),
|
||||
"root help missing {command}: {stdout}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!stdout.lines().any(|line| line.starts_with(" complexity")),
|
||||
"root help still listed complexity: {stdout}"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
Loading…
Reference in a new issue