use crate::cli::{ComplexityOutputFormat, DeadCodeOutputFormat, SatdOutputFormat, SatdSeverity, DagType};
use anyhow::{Context, Result};
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::{Path, PathBuf};
use std::sync::mpsc::channel;
use std::time::Duration;
#[cfg(test)]
mod complexity_handlers_tests;
#[derive(Debug, Clone)]
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(|| super::super::analysis_utilities::detect_toolchain(&self.project_path))
}
}
async fn analyze_single_file(
file_path: &Path,
config: &ComplexityConfig,
) -> Result<Vec<crate::services::complexity::FileComplexityMetrics>> {
eprintln!("🔍 Analyzing complexity of file: {}", file_path.display());
let full_path = if file_path.is_absolute() {
file_path.to_path_buf()
} else {
config.project_path.join(file_path)
};
if !full_path.exists() {
anyhow::bail!("File not found: {}", full_path.display());
}
let file_content = std::fs::read_to_string(&full_path)
.context(format!("Failed to read file: {}", full_path.display()))?;
let metrics =
crate::cli::language_analyzer::analyze_file_complexity(&full_path, &file_content).await?;
Ok(vec![metrics])
}
async fn analyze_multiple_files(
files: &[PathBuf],
config: &ComplexityConfig,
) -> Result<Vec<crate::services::complexity::FileComplexityMetrics>> {
eprintln!("🔍 Analyzing complexity of {} files...", files.len());
let mut all_metrics = Vec::new();
for file_path in files {
let full_path = if file_path.is_absolute() {
file_path.clone()
} else {
config.project_path.join(file_path)
};
if !full_path.exists() {
eprintln!("⚠️ Skipping missing file: {}", full_path.display());
continue;
}
let file_content = std::fs::read_to_string(&full_path)
.context(format!("Failed to read file: {}", full_path.display()))?;
let metrics =
crate::cli::language_analyzer::analyze_file_complexity(&full_path, &file_content)
.await?;
all_metrics.push(metrics);
}
Ok(all_metrics)
}
async fn analyze_project(
detected_toolchain: Option<String>,
config: &ComplexityConfig,
) -> Result<Vec<crate::services::complexity::FileComplexityMetrics>> {
if let Some(ref toolchain) = detected_toolchain {
eprintln!("🔍 Analyzing {toolchain} project complexity...");
super::super::analysis_utilities::analyze_project_files(
&config.project_path,
Some(toolchain),
&config.include,
config.max_cyclomatic,
config.max_cognitive,
)
.await
} else {
eprintln!("🔍 Analyzing project complexity (multi-language)...");
super::super::analysis_utilities::analyze_project_files(
&config.project_path,
None, &config.include,
config.max_cyclomatic,
config.max_cognitive,
)
.await
}
}
fn apply_complexity_filters(
file_metrics: &mut Vec<crate::services::complexity::FileComplexityMetrics>,
max_cyclomatic: Option<u16>,
max_cognitive: Option<u16>,
) {
if max_cyclomatic.is_some() || max_cognitive.is_some() {
file_metrics.retain(|file| {
file.functions.iter().any(|func| {
let exceeds_cyclomatic = max_cyclomatic
.is_some_and(|threshold| func.metrics.cyclomatic > threshold);
let exceeds_cognitive = max_cognitive
.is_some_and(|threshold| func.metrics.cognitive > threshold);
exceeds_cyclomatic || exceeds_cognitive
})
});
}
}
fn apply_top_files_limit(
file_metrics: &mut Vec<crate::services::complexity::FileComplexityMetrics>,
top_files: usize,
) {
if top_files > 0 && !file_metrics.is_empty() {
file_metrics.sort_by(|a, b| {
let a_complexity =
f64::from(a.total_complexity.cyclomatic) + f64::from(a.total_complexity.cognitive);
let b_complexity =
f64::from(b.total_complexity.cyclomatic) + f64::from(b.total_complexity.cognitive);
b_complexity
.partial_cmp(&a_complexity)
.unwrap_or(std::cmp::Ordering::Equal)
});
file_metrics.truncate(top_files);
}
}
async fn format_and_write_output(
summary: &crate::services::complexity::ComplexityReport,
file_metrics: &[crate::services::complexity::FileComplexityMetrics],
format: ComplexityOutputFormat,
output: Option<PathBuf>,
top_files: usize,
) -> Result<()> {
use crate::services::complexity::{
format_as_sarif, format_complexity_report, format_complexity_summary,
};
let formatted_output = match format {
ComplexityOutputFormat::Summary => Ok(format_complexity_summary(summary)),
ComplexityOutputFormat::Full => Ok(format_complexity_report(summary)),
ComplexityOutputFormat::Sarif => format_as_sarif(summary)
.map_err(|e| anyhow::anyhow!("SARIF serialization failed: {e}")),
ComplexityOutputFormat::Json => {
let json_output = serde_json::json!({
"summary": summary,
"files": file_metrics,
"top_files_limit": if top_files > 0 { Some(top_files) } else { None },
});
serde_json::to_string_pretty(&json_output)
.map_err(|e| anyhow::anyhow!("JSON serialization failed: {e}"))
}
}?;
if let Some(output_path) = output {
tokio::fs::write(&output_path, &formatted_output).await?;
eprintln!("📝 Results written to: {}", output_path.display());
} else {
println!("{formatted_output}");
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
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<()> {
use crate::services::complexity::aggregate_results_with_thresholds;
if watch {
return handle_watch_mode(
&project_path,
toolchain.as_deref(),
max_cyclomatic,
max_cognitive,
include.clone(),
timeout,
top_files,
format,
output.as_deref(),
);
}
let config = ComplexityConfig::from_args(
project_path,
toolchain,
max_cyclomatic,
max_cognitive,
include,
timeout,
top_files,
);
let mut file_metrics = analyze_files_by_mode(file, files, &config).await?;
apply_complexity_filters(&mut file_metrics, max_cyclomatic, max_cognitive);
apply_top_files_limit(&mut file_metrics, config.top_files);
let summary =
aggregate_results_with_thresholds(file_metrics.clone(), max_cyclomatic, max_cognitive);
format_and_write_output(&summary, &file_metrics, format, output, top_files).await?;
check_complexity_violations(
&file_metrics,
fail_on_violation,
max_cyclomatic,
max_cognitive,
);
Ok(())
}
async fn analyze_files_by_mode(
file: Option<PathBuf>,
files: Vec<PathBuf>,
config: &ComplexityConfig,
) -> Result<Vec<crate::services::complexity::FileComplexityMetrics>> {
eprintln!("⏰ Analysis timeout set to {} seconds", config.timeout);
if let Some(single_file) = file {
analyze_single_file(&single_file, config).await
} else if !files.is_empty() {
analyze_multiple_files(&files, config).await
} else {
let detected_toolchain = config.detect_toolchain();
analyze_project(detected_toolchain, config).await
}
}
fn check_complexity_violations(
file_metrics: &[crate::services::complexity::FileComplexityMetrics],
fail_on_violation: bool,
max_cyclomatic: Option<u16>,
max_cognitive: Option<u16>,
) {
if !fail_on_violation {
return;
}
let has_violations = has_complexity_violations(file_metrics, max_cyclomatic, max_cognitive);
if has_violations {
eprintln!("\n❌ Complexity violations found");
std::process::exit(1);
}
}
fn has_complexity_violations(
file_metrics: &[crate::services::complexity::FileComplexityMetrics],
max_cyclomatic: Option<u16>,
max_cognitive: Option<u16>,
) -> bool {
file_metrics.iter().any(|file| {
file.functions.iter().any(|func| {
let cyclomatic_exceeded = func.metrics.cyclomatic > max_cyclomatic.unwrap_or(20);
let cognitive_exceeded = func.metrics.cognitive > max_cognitive.unwrap_or(15);
cyclomatic_exceeded || cognitive_exceeded
})
})
}
fn create_dead_code_ranking_result(
accurate_report: crate::services::cargo_dead_code_analyzer::AccurateDeadCodeReport,
files_with_dead_code_count: usize,
min_dead_lines: usize,
config: crate::models::dead_code::DeadCodeAnalysisConfig,
) -> crate::models::dead_code::DeadCodeRankingResult {
use crate::models::dead_code::DeadCodeRankingResult;
use chrono::Utc;
DeadCodeRankingResult {
ranked_files: convert_cargo_files_to_metrics(
accurate_report.files_with_dead_code.clone(),
min_dead_lines,
),
summary: create_dead_code_summary(&accurate_report, files_with_dead_code_count),
analysis_timestamp: Utc::now(),
config,
}
}
fn convert_cargo_files_to_metrics(
cargo_files: Vec<crate::services::cargo_dead_code_analyzer::FileDeadCode>,
min_dead_lines: usize,
) -> Vec<crate::models::dead_code::FileDeadCodeMetrics> {
use crate::models::dead_code::{ConfidenceLevel, FileDeadCodeMetrics};
cargo_files
.into_iter()
.map(|file| {
let dead_functions_count = count_dead_items_by_kind(
&file,
&[
crate::services::cargo_dead_code_analyzer::DeadCodeKind::Function,
crate::services::cargo_dead_code_analyzer::DeadCodeKind::Method,
],
);
let dead_classes_count = count_dead_items_by_kind(
&file,
&[
crate::services::cargo_dead_code_analyzer::DeadCodeKind::Struct,
crate::services::cargo_dead_code_analyzer::DeadCodeKind::Enum,
],
);
FileDeadCodeMetrics {
path: file.file_path.display().to_string(),
dead_lines: file.dead_items.len() * 4, total_lines: 100, dead_percentage: file.file_dead_percentage as f32,
dead_functions: dead_functions_count,
dead_classes: dead_classes_count,
dead_modules: 0,
unreachable_blocks: 0,
dead_score: file.file_dead_percentage as f32,
confidence: ConfidenceLevel::High, items: Vec::new(), }
})
.filter(|f| f.dead_lines >= min_dead_lines)
.collect()
}
fn count_dead_items_by_kind(
file: &crate::services::cargo_dead_code_analyzer::FileDeadCode,
kinds: &[crate::services::cargo_dead_code_analyzer::DeadCodeKind],
) -> usize {
file.dead_items
.iter()
.filter(|i| kinds.contains(&i.kind))
.count()
}
fn create_dead_code_summary(
accurate_report: &crate::services::cargo_dead_code_analyzer::AccurateDeadCodeReport,
files_with_dead_code_count: usize,
) -> crate::models::dead_code::DeadCodeSummary {
use crate::models::dead_code::DeadCodeSummary;
DeadCodeSummary {
total_files_analyzed: accurate_report.total_lines / 100, files_with_dead_code: files_with_dead_code_count,
total_dead_lines: accurate_report.dead_lines,
dead_percentage: accurate_report.dead_code_percentage as f32,
dead_functions: get_dead_count_by_types(accurate_report, &["function", "method"]),
dead_classes: get_dead_count_by_types(accurate_report, &["struct", "enum"]),
dead_modules: get_dead_count_by_types(accurate_report, &["module"]),
unreachable_blocks: 0, }
}
fn get_dead_count_by_types(
report: &crate::services::cargo_dead_code_analyzer::AccurateDeadCodeReport,
types: &[&str],
) -> usize {
types
.iter()
.map(|type_name| report.dead_by_type.get(*type_name).copied().unwrap_or(0))
.sum()
}
fn write_top_files_with_satd_section(
output: &mut String,
result: &crate::services::satd_detector::SATDAnalysisResult,
) {
use std::collections::HashMap;
use std::fmt::Write;
writeln!(output, "\n## Top Files with SATD\n").unwrap();
let mut file_counts: HashMap<&std::path::Path, usize> = HashMap::new();
for item in &result.items {
*file_counts.entry(&item.file).or_insert(0) += 1;
}
let mut sorted_files: Vec<_> = file_counts.into_iter().collect();
sorted_files.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
for (i, (file, count)) in sorted_files.iter().take(10).enumerate() {
let filename = file.file_name().unwrap_or_default().to_string_lossy();
writeln!(output, "{}. `{}` - {} SATD items", i + 1, filename, count).unwrap();
}
}
fn write_critical_items_section(
output: &mut String,
result: &crate::services::satd_detector::SATDAnalysisResult,
) {
use std::fmt::Write;
writeln!(output, "\n## Critical Items\n").unwrap();
for item in result
.items
.iter()
.filter(|i| i.severity == crate::services::satd_detector::Severity::Critical)
.take(5)
{
writeln!(
output,
"- `{}:{}` - {}",
item.file.file_name().unwrap_or_default().to_string_lossy(),
item.line,
item.text
)
.unwrap();
}
}
#[allow(clippy::too_many_arguments)]
fn handle_watch_mode(
path: &Path,
toolchain: Option<&str>,
max_cyclomatic: Option<u16>,
max_cognitive: Option<u16>,
include: Vec<String>,
timeout: u64,
top_files: usize,
format: ComplexityOutputFormat,
output: Option<&Path>,
) -> Result<()> {
print_watch_mode_intro(path);
let (mut watcher, rx) = create_file_watcher(path)?;
let config = create_sync_config(
path,
toolchain,
max_cyclomatic,
max_cognitive,
&include,
timeout,
top_files,
format.clone(),
output,
);
run_initial_analysis(&config)?;
watch_for_file_changes(rx, &config, &include, &mut watcher)
}
fn print_watch_mode_intro(path: &Path) {
eprintln!("👁️ Starting watch mode for complexity analysis...");
eprintln!("📁 Watching: {}", path.display());
eprintln!("🔄 Press Ctrl+C to stop watching\n");
}
fn create_file_watcher(
path: &Path,
) -> Result<(RecommendedWatcher, std::sync::mpsc::Receiver<Event>)> {
let (tx, rx) = channel();
let mut watcher = RecommendedWatcher::new(
move |event: Result<Event, notify::Error>| {
if let Ok(event) = event {
let _ = tx.send(event);
}
},
Config::default().with_poll_interval(Duration::from_secs(1)),
)?;
watcher.watch(path, RecursiveMode::Recursive)?;
Ok((watcher, rx))
}
#[allow(clippy::too_many_arguments)]
fn create_sync_config<'a>(
path: &'a Path,
toolchain: Option<&'a str>,
max_cyclomatic: Option<u16>,
max_cognitive: Option<u16>,
include: &'a [String],
timeout: u64,
top_files: usize,
format: ComplexityOutputFormat,
output: Option<&'a Path>,
) -> SyncAnalysisConfig<'a> {
SyncAnalysisConfig {
path,
toolchain,
max_cyclomatic,
max_cognitive,
include,
timeout,
top_files,
format,
output,
}
}
fn run_initial_analysis(config: &SyncAnalysisConfig) -> Result<()> {
eprintln!("📊 Running initial complexity analysis...\n");
run_complexity_analysis_sync(config.clone())
}
fn watch_for_file_changes(
rx: std::sync::mpsc::Receiver<Event>,
config: &SyncAnalysisConfig,
include: &[String],
_watcher: &mut RecommendedWatcher,
) -> Result<()> {
loop {
match rx.recv() {
Ok(event) => {
if should_reanalyze(&event, include) {
handle_file_change_event(&event, config)?;
}
}
Err(e) => {
eprintln!("⚠️ Watch error: {e}");
break;
}
}
}
Ok(())
}
fn handle_file_change_event(event: &Event, config: &SyncAnalysisConfig) -> Result<()> {
eprintln!("\n🔄 File change detected, reanalyzing...");
if let Some(paths) = get_changed_paths(event) {
for changed_path in paths {
eprintln!(" 📝 Changed: {}", changed_path.display());
}
}
eprintln!();
if let Err(e) = run_complexity_analysis_sync(config.clone()) {
eprintln!("⚠️ Analysis error: {e}");
}
Ok(())
}
fn should_reanalyze(event: &Event, include_patterns: &[String]) -> bool {
match event.kind {
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => event
.paths
.iter()
.any(|path| should_analyze_path(path, include_patterns)),
_ => false,
}
}
fn should_analyze_path(path: &std::path::Path, include_patterns: &[String]) -> bool {
let Some(path_str) = path.to_str() else {
return false;
};
if !is_source_code_file(path_str) {
return false;
}
should_include_file(path_str, include_patterns)
}
fn is_source_code_file(path_str: &str) -> bool {
path_str.ends_with(".rs")
|| path_str.ends_with(".ts")
|| path_str.ends_with(".tsx")
|| path_str.ends_with(".js")
|| path_str.ends_with(".jsx")
|| path_str.ends_with(".py")
|| path_str.ends_with(".c")
|| path_str.ends_with(".cpp")
|| path_str.ends_with(".h")
|| path_str.ends_with(".hpp")
}
fn should_include_file(path_str: &str, include_patterns: &[String]) -> bool {
if include_patterns.is_empty() {
return true;
}
include_patterns
.iter()
.any(|pattern| path_str.contains(pattern))
}
fn get_changed_paths(event: &Event) -> Option<&Vec<PathBuf>> {
if event.paths.is_empty() {
None
} else {
Some(&event.paths)
}
}
async fn format_and_output_watch_results(
summary: crate::services::complexity::ComplexityReport,
_file_metrics: Vec<crate::services::complexity::FileComplexityMetrics>,
format: ComplexityOutputFormat,
output: Option<&Path>,
) -> Result<()> {
use crate::services::complexity::format_complexity_summary;
let content = match format {
ComplexityOutputFormat::Json => {
serde_json::to_string_pretty(&summary)?
}
_ => {
format_complexity_summary(&summary)
}
};
print!("\x1B[2J\x1B[1;1H");
if let Some(output_path) = output {
tokio::fs::write(output_path, &content).await?;
eprintln!("✅ Analysis written to: {}", output_path.display());
} else {
println!("{content}");
}
Ok(())
}
#[derive(Debug, Clone)]
struct SyncAnalysisConfig<'a> {
path: &'a Path,
toolchain: Option<&'a str>,
max_cyclomatic: Option<u16>,
max_cognitive: Option<u16>,
include: &'a [String],
timeout: u64,
top_files: usize,
format: ComplexityOutputFormat,
output: Option<&'a Path>,
}
fn run_complexity_analysis_sync(config: SyncAnalysisConfig) -> Result<()> {
let runtime = tokio::runtime::Runtime::new()?;
let complexity_config = ComplexityConfig::from_args(
config.path.to_path_buf(),
config.toolchain.map(String::from),
config.max_cyclomatic,
config.max_cognitive,
config.include.to_vec(),
config.timeout,
config.top_files,
);
runtime.block_on(async {
let mut file_metrics = if config.path.is_file() {
analyze_single_file(config.path, &complexity_config).await?
} else {
let detected_toolchain = complexity_config.detect_toolchain();
analyze_project(detected_toolchain, &complexity_config).await?
};
apply_complexity_filters(
&mut file_metrics,
Some(complexity_config.max_cyclomatic),
Some(complexity_config.max_cognitive),
);
apply_top_files_limit(&mut file_metrics, complexity_config.top_files);
use crate::services::complexity::aggregate_results_with_thresholds;
let summary = aggregate_results_with_thresholds(
file_metrics.clone(),
Some(complexity_config.max_cyclomatic),
Some(complexity_config.max_cognitive),
);
format_and_output_watch_results(summary, file_metrics, config.format, config.output)
.await?;
Ok::<(), anyhow::Error>(())
})?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_analyze_churn(
project_path: PathBuf,
days: u32,
format: crate::models::churn::ChurnOutputFormat,
output: Option<PathBuf>,
top_files: usize,
include: Vec<String>,
exclude: Vec<String>,
) -> Result<()> {
use crate::services::git_analysis::GitAnalysisService;
eprintln!("📊 Analyzing code churn for the last {days} days...");
let filter = create_and_report_file_filter(include, exclude)?;
let mut analysis = GitAnalysisService::analyze_code_churn(&project_path, days)
.map_err(|e| anyhow::anyhow!("Churn analysis failed: {e}"))?;
apply_churn_filters(&mut analysis, &filter, top_files);
eprintln!("✅ Analyzed {} files with changes", analysis.files.len());
format_and_write_churn_output(analysis, format, output).await
}
fn create_and_report_file_filter(
include: Vec<String>,
exclude: Vec<String>,
) -> Result<crate::utils::file_filter::FileFilter> {
if !include.is_empty() || !exclude.is_empty() {
eprintln!("🔍 Applying file filters...");
if !include.is_empty() {
eprintln!(" Include patterns: {include:?}");
}
if !exclude.is_empty() {
eprintln!(" Exclude patterns: {exclude:?}");
}
}
crate::utils::file_filter::FileFilter::new(include, exclude)
}
fn apply_churn_filters(
analysis: &mut crate::models::churn::CodeChurnAnalysis,
filter: &crate::utils::file_filter::FileFilter,
top_files: usize,
) {
if filter.has_filters() {
analysis
.files
.retain(|file| filter.should_include(&file.path));
analysis.summary.total_files_changed = analysis.files.len();
analysis.summary.total_commits = analysis.files.iter().map(|f| f.commit_count).sum();
}
if top_files > 0 && analysis.files.len() > top_files {
analysis
.files
.sort_by(|a, b| b.commit_count.cmp(&a.commit_count));
analysis.files.truncate(top_files);
}
}
async fn format_and_write_churn_output(
analysis: crate::models::churn::CodeChurnAnalysis,
format: crate::models::churn::ChurnOutputFormat,
output: Option<PathBuf>,
) -> Result<()> {
use crate::models::churn::ChurnOutputFormat;
let content = match format {
ChurnOutputFormat::Json => serde_json::to_string_pretty(&analysis)?,
ChurnOutputFormat::Summary => {
super::super::analysis_utilities::format_churn_as_summary(&analysis)?
}
ChurnOutputFormat::Markdown => {
super::super::analysis_utilities::format_churn_as_markdown(&analysis)?
}
ChurnOutputFormat::Csv => super::super::analysis_utilities::format_churn_as_csv(&analysis)?,
};
super::super::analysis_utilities::write_churn_output(content, output).await
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_analyze_dead_code(
path: PathBuf,
format: DeadCodeOutputFormat,
top_files: Option<usize>,
include_unreachable: bool,
min_dead_lines: usize,
include_tests: bool,
output: Option<PathBuf>,
fail_on_violation: bool,
max_percentage: f64,
timeout: u64,
include: Vec<String>,
exclude: Vec<String>,
) -> Result<()> {
eprintln!("☠️ Analyzing dead code in project...");
eprintln!("⏰ Analysis timeout set to {timeout} seconds");
if !include.is_empty() || !exclude.is_empty() {
eprintln!("🔍 Applying file filters...");
if !include.is_empty() {
eprintln!(" Include patterns: {include:?}");
}
if !exclude.is_empty() {
eprintln!(" Exclude patterns: {exclude:?}");
}
}
let timeout_duration = tokio::time::Duration::from_secs(timeout);
let result = tokio::time::timeout(timeout_duration, async {
run_dead_code_analysis_with_filters(
&path,
include_unreachable,
include_tests,
min_dead_lines,
top_files,
include,
exclude,
)
.await
})
.await
.map_err(|_| anyhow::anyhow!("Dead code analysis timed out after {timeout} seconds"))??;
eprintln!(
"📊 Analysis complete: {} files analyzed, {} with dead code",
result.summary.total_files_analyzed, result.summary.files_with_dead_code
);
let formatted_output = format_dead_code_result(&result, &format)?;
write_dead_code_output(formatted_output, output).await?;
if fail_on_violation {
let dead_code_percentage = result.summary.dead_percentage;
if dead_code_percentage > max_percentage as f32 {
eprintln!(
"\n❌ Dead code violations found: {dead_code_percentage:.1}% exceeds threshold of {max_percentage:.1}%"
);
std::process::exit(1);
}
}
Ok(())
}
async fn run_dead_code_analysis_with_filters(
path: &Path,
include_unreachable: bool,
include_tests: bool,
min_dead_lines: usize,
top_files: Option<usize>,
include: Vec<String>,
exclude: Vec<String>,
) -> Result<crate::models::dead_code::DeadCodeResult> {
use crate::models::dead_code::DeadCodeAnalysisConfig;
use crate::services::cargo_dead_code_analyzer::CargoDeadCodeAnalyzer;
use crate::utils::file_filter::FileFilter;
let filter = FileFilter::new(include, exclude)?;
let cargo_analyzer = if include_tests {
CargoDeadCodeAnalyzer::new(path).include_tests()
} else {
CargoDeadCodeAnalyzer::new(path)
};
let accurate_report = cargo_analyzer.analyze().await?;
let config = DeadCodeAnalysisConfig {
include_unreachable,
include_tests,
min_dead_lines,
};
let files_with_dead_code_count = accurate_report.files_with_dead_code.len();
let mut analysis_result = create_dead_code_ranking_result(
accurate_report,
files_with_dead_code_count,
min_dead_lines,
config,
);
if filter.has_filters() {
analysis_result.ranked_files.retain(|file| {
let path = std::path::Path::new(&file.path);
filter.should_include(path)
});
analysis_result.summary.files_with_dead_code = analysis_result.ranked_files.len();
analysis_result.summary.total_dead_lines = analysis_result
.ranked_files
.iter()
.map(|f| f.dead_lines)
.sum();
}
if let Some(limit) = top_files {
if limit > 0 && analysis_result.ranked_files.len() > limit {
analysis_result.ranked_files.truncate(limit);
}
}
Ok(crate::models::dead_code::DeadCodeResult {
summary: analysis_result.summary.clone(),
files: analysis_result.ranked_files,
total_files: analysis_result.summary.total_files_analyzed,
analyzed_files: analysis_result.summary.total_files_analyzed,
})
}
fn format_dead_code_result(
result: &crate::models::dead_code::DeadCodeResult,
format: &DeadCodeOutputFormat,
) -> Result<String> {
match format {
DeadCodeOutputFormat::Json => format_dead_code_as_json(result),
DeadCodeOutputFormat::Sarif => format_dead_code_as_sarif(result),
DeadCodeOutputFormat::Summary => format_dead_code_as_summary(result),
DeadCodeOutputFormat::Markdown => format_dead_code_as_markdown(result),
}
}
fn format_dead_code_as_json(result: &crate::models::dead_code::DeadCodeResult) -> Result<String> {
Ok(serde_json::to_string_pretty(result)?)
}
fn format_dead_code_as_sarif(result: &crate::models::dead_code::DeadCodeResult) -> Result<String> {
use crate::models::dead_code::{ConfidenceLevel, DeadCodeType};
use serde_json::json;
let sarif = json!({
"version": "2.1.0",
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"runs": [{
"tool": {
"driver": {
"name": "pmat",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": "https://github.com/paiml/paiml-mcp-agent-toolkit",
"rules": [{
"id": "dead-code",
"name": "Dead Code Detection",
"shortDescription": {
"text": "Code that is never executed or referenced"
},
"fullDescription": {
"text": "Detects functions, classes, and code blocks that are not reachable from any entry point"
},
"defaultConfiguration": {
"level": "warning"
}
}]
}
},
"results": result.files.iter().flat_map(|file| {
file.items.iter().map(|item| {
let level = match file.confidence {
ConfidenceLevel::High => "error",
ConfidenceLevel::Medium => "warning",
ConfidenceLevel::Low => "note",
};
json!({
"ruleId": "dead-code",
"level": level,
"message": {
"text": format!("{}: {}",
match item.item_type {
DeadCodeType::Function => "Dead function",
DeadCodeType::Class => "Dead class",
DeadCodeType::Variable => "Dead variable",
DeadCodeType::UnreachableCode => "Unreachable code",
},
item.reason
)
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": &file.path
},
"region": {
"startLine": item.line
}
}
}]
})
}).collect::<Vec<_>>()
}).collect::<Vec<_>>()
}]
});
Ok(serde_json::to_string_pretty(&sarif)?)
}
fn format_dead_code_as_summary(
result: &crate::models::dead_code::DeadCodeResult,
) -> Result<String> {
let mut output = String::new();
write_dead_code_header(&mut output, result)?;
if result.summary.dead_functions > 0 {
write_dead_code_by_type_section(&mut output, &result.summary)?;
}
if !result.files.is_empty() {
write_top_files_section(&mut output, &result.files)?;
}
Ok(output)
}
fn write_dead_code_header(
output: &mut String,
result: &crate::models::dead_code::DeadCodeResult,
) -> Result<()> {
use std::fmt::Write;
writeln!(output, "# Dead Code Analysis Summary\n")?;
writeln!(output, "📊 **Files analyzed**: {}", result.total_files)?;
writeln!(
output,
"☠️ **Files with dead code**: {}",
result.summary.files_with_dead_code
)?;
writeln!(
output,
"📏 **Total dead lines**: {}",
result.summary.total_dead_lines
)?;
writeln!(
output,
"📈 **Dead code percentage**: {:.2}%\n",
result.summary.dead_percentage
)?;
Ok(())
}
fn write_dead_code_by_type_section(
output: &mut String,
summary: &crate::models::dead_code::DeadCodeSummary,
) -> Result<()> {
use std::fmt::Write;
writeln!(output, "## Dead Code by Type\n")?;
writeln!(output, "- **Dead functions**: {}", summary.dead_functions)?;
writeln!(output, "- **Dead classes**: {}", summary.dead_classes)?;
writeln!(output, "- **Dead variables**: {}", summary.dead_modules)?;
writeln!(
output,
"- **Unreachable blocks**: {}",
summary.unreachable_blocks
)?;
Ok(())
}
fn write_top_files_section(
output: &mut String,
files: &[crate::models::dead_code::FileDeadCodeMetrics],
) -> Result<()> {
use std::fmt::Write;
writeln!(output, "\n## Top Files with Dead Code\n")?;
for (i, file) in files.iter().take(10).enumerate() {
writeln!(
output,
"{}. `{}` - {:.1}% dead ({} lines)",
i + 1,
file.path,
file.dead_percentage,
file.dead_lines
)?;
}
Ok(())
}
fn format_dead_code_as_markdown(
result: &crate::models::dead_code::DeadCodeResult,
) -> Result<String> {
let mut sections = Vec::new();
sections.push(format_dead_code_summary_section(result));
if result.summary.dead_functions > 0 {
sections.push(format_dead_code_breakdown_section(&result.summary));
}
if !result.files.is_empty() {
sections.push(format_dead_code_file_details_section(&result.files));
}
sections.push(format_dead_code_recommendations_section());
Ok(sections.join("\n"))
}
fn format_dead_code_summary_section(result: &crate::models::dead_code::DeadCodeResult) -> String {
format!(
"# Dead Code Analysis Report\n\n\
## Summary\n\n\
| Metric | Value |\n\
|--------|-------|\n\
| Files Analyzed | {} |\n\
| Files with Dead Code | {} |\n\
| Total Dead Lines | {} |\n\
| Dead Code Percentage | {:.2}% |\n",
result.total_files,
result.summary.files_with_dead_code,
result.summary.total_dead_lines,
result.summary.dead_percentage
)
}
fn format_dead_code_breakdown_section(
summary: &crate::models::dead_code::DeadCodeSummary,
) -> String {
format!(
"## Dead Code Breakdown\n\n\
| Type | Count |\n\
|------|-------|\n\
| Functions | {} |\n\
| Classes | {} |\n\
| Variables | {} |\n\
| Unreachable Blocks | {} |\n",
summary.dead_functions,
summary.dead_classes,
summary.dead_modules,
summary.unreachable_blocks
)
}
fn format_dead_code_file_details_section(
files: &[crate::models::dead_code::FileDeadCodeMetrics],
) -> String {
let mut output = String::from(
"## File Details\n\n\
| File | Dead % | Dead Lines | Confidence | Items |\n\
|------|--------|------------|------------|-------|\n",
);
for file in files.iter().take(20) {
output.push_str(&format!(
"| {} | {:.1}% | {} | {:?} | {} |\n",
file.path,
file.dead_percentage,
file.dead_lines,
file.confidence,
file.items.len()
));
}
output
}
fn format_dead_code_recommendations_section() -> String {
"## Recommendations\n\n\
1. **Review High Confidence Dead Code**: Start with files marked as high confidence.\n\
2. **Check Test Coverage**: Dead code often indicates missing tests.\n\
3. **Consider Refactoring**: Large amounts of dead code may indicate design issues.\n\
4. **Remove Carefully**: Ensure code is truly dead before removal.\n"
.to_string()
}
async fn write_dead_code_output(content: String, output: Option<PathBuf>) -> Result<()> {
match output {
Some(path) => {
tokio::fs::write(&path, content).await?;
eprintln!("📝 Results written to: {}", path.display());
}
None => {
println!("{content}");
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_analyze_satd(
path: PathBuf,
format: SatdOutputFormat,
severity: Option<SatdSeverity>,
critical_only: bool,
include_tests: bool,
strict: bool,
evolution: bool,
days: u32,
metrics: bool,
output: Option<PathBuf>,
top_files: usize,
fail_on_violation: bool,
timeout: u64,
) -> Result<()> {
print_satd_analysis_info(strict, timeout);
let mut result = run_satd_analysis(&path, include_tests, strict, timeout).await?;
apply_satd_filters(&mut result, severity, critical_only, top_files);
eprintln!(
"📊 Found {} SATD items in {} files",
result.items.len(),
result.files_with_debt
);
let content = format_satd_output(&result, format, metrics, evolution, days)?;
write_satd_output(content, output).await?;
check_satd_violations(&result, fail_on_violation)?;
Ok(())
}
fn print_satd_analysis_info(strict: bool, timeout: u64) {
eprintln!("🔍 Analyzing self-admitted technical debt...");
eprintln!("⏰ Analysis timeout set to {timeout} seconds");
if strict {
eprintln!("📝 Using strict mode (only explicit SATD markers)");
}
}
async fn run_satd_analysis(
path: &Path,
include_tests: bool,
strict: bool,
timeout: u64,
) -> Result<crate::services::satd_detector::SATDAnalysisResult> {
use crate::services::satd_detector::SATDDetector;
let detector = if strict {
SATDDetector::new_strict()
} else {
SATDDetector::new()
};
let timeout_duration = tokio::time::Duration::from_secs(timeout);
let result = tokio::time::timeout(timeout_duration, async {
detector.analyze_project(path, include_tests).await
})
.await
.map_err(|_| anyhow::anyhow!("SATD analysis timed out after {timeout} seconds"))??;
Ok(result)
}
fn apply_satd_filters(
result: &mut crate::services::satd_detector::SATDAnalysisResult,
severity: Option<SatdSeverity>,
critical_only: bool,
top_files: usize,
) {
use crate::services::satd_detector::Severity as DetectorSeverity;
if let Some(min_severity) = severity {
let min_detector_severity = match min_severity {
SatdSeverity::Critical => DetectorSeverity::Critical,
SatdSeverity::High => DetectorSeverity::High,
SatdSeverity::Medium => DetectorSeverity::Medium,
SatdSeverity::Low => DetectorSeverity::Low,
};
result
.items
.retain(|item| item.severity >= min_detector_severity);
}
if critical_only {
result
.items
.retain(|item| item.severity == DetectorSeverity::Critical);
}
if top_files > 0 {
filter_top_files(result, top_files);
}
}
fn filter_top_files(
result: &mut crate::services::satd_detector::SATDAnalysisResult,
top_files: usize,
) {
use std::collections::HashMap;
let mut file_counts: HashMap<std::path::PathBuf, usize> = HashMap::new();
for item in &result.items {
*file_counts.entry(item.file.clone()).or_insert(0) += 1;
}
let mut sorted_files: Vec<_> = file_counts.into_iter().collect();
sorted_files.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
let top_file_paths: std::collections::HashSet<_> = sorted_files
.into_iter()
.take(top_files)
.map(|(path, _)| path)
.collect();
result
.items
.retain(|item| top_file_paths.contains(&item.file));
}
fn format_satd_output(
result: &crate::services::satd_detector::SATDAnalysisResult,
format: SatdOutputFormat,
metrics: bool,
evolution: bool,
days: u32,
) -> Result<String> {
match format {
SatdOutputFormat::Json => Ok(serde_json::to_string_pretty(&result)?),
SatdOutputFormat::Sarif => {
let sarif = generate_satd_sarif(result);
Ok(serde_json::to_string_pretty(&sarif)?)
}
SatdOutputFormat::Summary => Ok(format_satd_summary(result, metrics)),
SatdOutputFormat::Markdown => Ok(format_satd_markdown(result, metrics, evolution, days)),
}
}
async fn write_satd_output(content: String, output: Option<PathBuf>) -> Result<()> {
if let Some(output_path) = output {
tokio::fs::write(&output_path, &content).await?;
eprintln!("✅ SATD analysis written to: {}", output_path.display());
} else {
println!("{content}");
}
Ok(())
}
fn check_satd_violations(
result: &crate::services::satd_detector::SATDAnalysisResult,
fail_on_violation: bool,
) -> Result<()> {
if fail_on_violation && !result.items.is_empty() {
eprintln!(
"\n❌ SATD violations found: {} technical debt items",
result.items.len()
);
std::process::exit(1);
}
Ok(())
}
fn generate_satd_sarif(
result: &crate::services::satd_detector::SATDAnalysisResult,
) -> serde_json::Value {
serde_json::json!({
"version": "2.1.0",
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"runs": [{
"tool": {
"driver": {
"name": "pmat",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": "https://github.com/paiml/paiml-mcp-agent-toolkit",
"rules": [{
"id": "satd",
"name": "Self-Admitted Technical Debt",
"shortDescription": {
"text": "Technical debt explicitly documented in code comments"
},
"fullDescription": {
"text": "Detects TODO, FIXME, HACK, and other technical debt markers in comments"
},
"defaultConfiguration": {
"level": "warning"
}
}]
}
},
"results": result.items.iter().map(|item| {
let level = match item.severity {
crate::services::satd_detector::Severity::Critical => "error",
crate::services::satd_detector::Severity::High => "error",
crate::services::satd_detector::Severity::Medium => "warning",
crate::services::satd_detector::Severity::Low => "note",
};
serde_json::json!({
"ruleId": "satd",
"level": level,
"message": {
"text": format!("{} debt: {}", item.category, item.text)
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": item.file.to_string_lossy()
},
"region": {
"startLine": item.line,
"startColumn": item.column
}
}
}]
})
}).collect::<Vec<_>>()
}]
})
}
#[must_use]
pub fn format_satd_summary(
result: &crate::services::satd_detector::SATDAnalysisResult,
metrics: bool,
) -> String {
use std::fmt::Write;
let mut output = String::new();
writeln!(&mut output, "# SATD Analysis Summary\n").unwrap();
writeln!(
&mut output,
"📊 **Files analyzed**: {}",
result.total_files_analyzed
)
.unwrap();
writeln!(
&mut output,
"📁 **Files with SATD**: {}",
result.files_with_debt
)
.unwrap();
writeln!(
&mut output,
"🔍 **Total SATD items**: {}",
result.items.len()
)
.unwrap();
if metrics && !result.summary.by_severity.is_empty() {
writeln!(&mut output, "\n## By Severity\n").unwrap();
for (severity, count) in &result.summary.by_severity {
writeln!(&mut output, "- **{severity}**: {count}").unwrap();
}
}
if metrics && !result.summary.by_category.is_empty() {
writeln!(&mut output, "\n## By Category\n").unwrap();
for (category, count) in &result.summary.by_category {
writeln!(&mut output, "- **{category}**: {count}").unwrap();
}
}
if !result.items.is_empty() {
write_top_files_with_satd_section(&mut output, result);
write_critical_items_section(&mut output, result);
}
output
}
fn format_satd_markdown(
result: &crate::services::satd_detector::SATDAnalysisResult,
metrics: bool,
_evolution: bool,
_days: u32,
) -> String {
use std::fmt::Write;
let mut output = String::new();
writeln!(&mut output, "# Self-Admitted Technical Debt Report\n").unwrap();
writeln!(
&mut output,
"Generated: {}",
result.analysis_timestamp.format("%Y-%m-%d %H:%M:%S UTC")
)
.unwrap();
writeln!(&mut output, "\n## Summary\n").unwrap();
writeln!(&mut output, "| Metric | Value |").unwrap();
writeln!(&mut output, "|--------|-------|").unwrap();
writeln!(
&mut output,
"| Files Analyzed | {} |",
result.total_files_analyzed
)
.unwrap();
writeln!(
&mut output,
"| Files with SATD | {} |",
result.files_with_debt
)
.unwrap();
writeln!(&mut output, "| Total SATD Items | {} |", result.items.len()).unwrap();
if metrics {
writeln!(&mut output, "\n## Distribution\n").unwrap();
writeln!(&mut output, "### By Severity\n").unwrap();
writeln!(&mut output, "| Severity | Count |").unwrap();
writeln!(&mut output, "|----------|-------|").unwrap();
for (severity, count) in &result.summary.by_severity {
writeln!(&mut output, "| {severity} | {count} |").unwrap();
}
writeln!(&mut output, "\n### By Category\n").unwrap();
writeln!(&mut output, "| Category | Count |").unwrap();
writeln!(&mut output, "|----------|-------|").unwrap();
for (category, count) in &result.summary.by_category {
writeln!(&mut output, "| {category} | {count} |").unwrap();
}
}
use std::collections::BTreeMap;
let mut by_file: BTreeMap<
&std::path::Path,
Vec<&crate::services::satd_detector::TechnicalDebt>,
> = BTreeMap::new();
for item in &result.items {
by_file.entry(&item.file).or_default().push(item);
}
writeln!(&mut output, "\n## SATD Items by File\n").unwrap();
for (file, items) in by_file.iter().take(20) {
writeln!(&mut output, "### {}\n", file.display()).unwrap();
writeln!(&mut output, "| Line | Severity | Category | Text |").unwrap();
writeln!(&mut output, "|------|----------|----------|------|").unwrap();
for item in items {
writeln!(
&mut output,
"| {} | {:?} | {} | {} |",
item.line,
item.severity,
item.category,
item.text.replace('|', "\\|")
)
.unwrap();
}
writeln!(&mut output).unwrap();
}
output
}
#[allow(clippy::too_many_arguments)]
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},
};
eprintln!("🔄 Generating dependency analysis graph...");
let toolchain =
super::super::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 graph = DagBuilder::build_from_project(&project_context);
eprintln!(
"📊 Generated graph with {} nodes and {} edges",
graph.nodes.len(),
graph.edges.len()
);
let enriched_graph = graph;
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)
};
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(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_complexity_handlers_basic() {
assert_eq!(1 + 1, 2);
}
#[test]
fn test_dead_code_summary_shows_top_files() {
let result = crate::models::dead_code::DeadCodeResult {
summary: crate::models::dead_code::DeadCodeSummary {
total_files_analyzed: 5,
files_with_dead_code: 2,
total_dead_lines: 45,
dead_percentage: 15.5,
dead_functions: 3,
dead_classes: 1,
dead_modules: 2,
unreachable_blocks: 1,
},
files: vec![
crate::models::dead_code::FileDeadCodeMetrics {
path: "src/main.rs".to_string(),
dead_lines: 25,
total_lines: 100,
dead_percentage: 25.0,
dead_functions: 1,
dead_classes: 0,
dead_modules: 0,
unreachable_blocks: 0,
dead_score: 0.0,
confidence: crate::models::dead_code::ConfidenceLevel::High,
items: vec![crate::models::dead_code::DeadCodeItem {
name: "dead_function".to_string(),
item_type: crate::models::dead_code::DeadCodeType::Function,
line: 10,
reason: "Never called".to_string(),
}],
},
crate::models::dead_code::FileDeadCodeMetrics {
path: "src/lib.rs".to_string(),
dead_lines: 20,
total_lines: 150,
dead_percentage: 13.3,
dead_functions: 0,
dead_classes: 1,
dead_modules: 0,
unreachable_blocks: 0,
dead_score: 0.0,
confidence: crate::models::dead_code::ConfidenceLevel::Medium,
items: vec![crate::models::dead_code::DeadCodeItem {
name: "unused_struct".to_string(),
item_type: crate::models::dead_code::DeadCodeType::Class,
line: 5,
reason: "Never instantiated".to_string(),
}],
},
],
total_files: 5,
analyzed_files: 5,
};
let summary = format_dead_code_as_summary(&result).unwrap();
assert!(summary.contains("# Dead Code Analysis Summary"));
assert!(summary.contains("**Files analyzed**: 5"));
assert!(summary.contains("**Files with dead code**: 2"));
assert!(summary.contains("## Top Files with Dead Code"));
assert!(summary.contains("1. `src/main.rs` - 25.0% dead (25 lines)"));
assert!(summary.contains("2. `src/lib.rs` - 13.3% dead (20 lines)"));
assert!(summary.contains("## Dead Code by Type"));
assert!(summary.contains("**Dead functions**: 3"));
}
#[test]
fn test_dead_code_summary_empty_files() {
let result = crate::models::dead_code::DeadCodeResult {
summary: crate::models::dead_code::DeadCodeSummary {
total_files_analyzed: 10,
files_with_dead_code: 0,
total_dead_lines: 0,
dead_percentage: 0.0,
dead_functions: 0,
dead_classes: 0,
dead_modules: 0,
unreachable_blocks: 0,
},
files: vec![],
total_files: 10,
analyzed_files: 10,
};
let summary = format_dead_code_as_summary(&result).unwrap();
assert!(summary.contains("# Dead Code Analysis Summary"));
assert!(summary.contains("**Files analyzed**: 10"));
assert!(summary.contains("**Files with dead code**: 0"));
assert!(!summary.contains("## Top Files with Dead Code"));
assert!(!summary.contains("## Dead Code by Type"));
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}