#[cfg(feature = "watch")]
use crate::cli::ComplexityOutputFormat;
#[cfg(feature = "watch")]
use crate::services::complexity::FileComplexityMetrics;
#[cfg(feature = "watch")]
use anyhow::Result;
#[cfg(feature = "watch")]
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
#[cfg(feature = "watch")]
use std::path::{Path, PathBuf};
#[cfg(feature = "watch")]
use std::sync::mpsc::channel;
#[cfg(feature = "watch")]
use std::time::Duration;
#[cfg(feature = "watch")]
use super::analysis::{
analyze_project, analyze_single_file, apply_complexity_filters, apply_top_files_limit,
};
#[cfg(feature = "watch")]
use super::ComplexityConfig;
#[cfg(feature = "watch")]
#[derive(Debug, Clone)]
pub(super) struct SyncAnalysisConfig<'a> {
pub(super) path: &'a Path,
pub(super) toolchain: Option<&'a str>,
pub(super) max_cyclomatic: Option<u16>,
pub(super) max_cognitive: Option<u16>,
pub(super) include: &'a [String],
pub(super) timeout: u64,
pub(super) top_files: usize,
pub(super) format: ComplexityOutputFormat,
pub(super) output: Option<&'a Path>,
}
#[cfg(feature = "watch")]
#[allow(clippy::too_many_arguments)]
pub(super) 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,
output,
);
run_initial_analysis(&config)?;
watch_for_file_changes(rx, &config, &include, &mut watcher)
}
#[cfg(feature = "watch")]
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");
}
#[cfg(feature = "watch")]
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))
}
#[cfg(feature = "watch")]
#[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,
}
}
#[cfg(feature = "watch")]
fn run_initial_analysis(config: &SyncAnalysisConfig) -> Result<()> {
eprintln!("📊 Running initial complexity analysis...\n");
run_complexity_analysis_sync(config.clone())
}
#[cfg(feature = "watch")]
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(())
}
#[cfg(feature = "watch")]
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(())
}
#[cfg(feature = "watch")]
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,
}
}
#[cfg(feature = "watch")]
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)
}
#[cfg(feature = "watch")]
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(".cu")
|| path_str.ends_with(".cuh")
|| path_str.ends_with(".h")
|| path_str.ends_with(".hpp")
}
#[cfg(feature = "watch")]
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))
}
#[cfg(feature = "watch")]
fn get_changed_paths(event: &Event) -> Option<&Vec<PathBuf>> {
if event.paths.is_empty() {
None
} else {
Some(&event.paths)
}
}
#[cfg(feature = "watch")]
async fn format_and_output_watch_results(
summary: crate::services::complexity::ComplexityReport,
_file_metrics: Vec<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(())
}
#[cfg(feature = "watch")]
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(())
}