Skip to main content

tldr_cli/commands/
cognitive.rs

1//! Cognitive complexity command - Calculate SonarQube cognitive complexity
2//!
3//! Analyzes cognitive complexity for functions in a file or directory,
4//! with threshold checking.
5//! Extends the basic complexity command with detailed cognitive metrics.
6
7use std::path::PathBuf;
8
9use anyhow::Result;
10use clap::Args;
11
12use tldr_core::{
13    metrics::{
14        analyze_cognitive, merge_cognitive_reports, walk_source_files, CognitiveOptions,
15        WalkOptions,
16    },
17    validate_file_path, Language,
18};
19
20use crate::output::{format_cognitive_text, OutputFormat, OutputWriter};
21
22/// Calculate cognitive complexity for functions (SonarQube algorithm)
23#[derive(Debug, Args)]
24pub struct CognitiveArgs {
25    /// File or directory to analyze
26    #[arg(default_value = ".")]
27    pub path: PathBuf,
28
29    /// Specific function to analyze (analyzes all if not specified)
30    /// Note: --function is the long form; -f short flag is NOT used to avoid collision with --format
31    #[arg(long)]
32    pub function: Option<String>,
33
34    /// Programming language (auto-detect if not specified)
35    #[arg(long, short = 'l')]
36    pub lang: Option<Language>,
37
38    /// Complexity threshold for violations (default: 15)
39    #[arg(long, default_value = "15")]
40    pub threshold: u32,
41
42    /// High threshold for severe violations (default: 25)
43    #[arg(long, default_value = "25")]
44    pub high_threshold: u32,
45
46    /// Show line-by-line complexity contributors
47    #[arg(long)]
48    pub show_contributors: bool,
49
50    /// Include cyclomatic complexity comparison
51    #[arg(long)]
52    pub include_cyclomatic: bool,
53
54    /// Maximum functions to report (0 = all)
55    #[arg(long, default_value = "50")]
56    pub top: usize,
57
58    /// Exclude patterns (glob syntax), can be specified multiple times
59    #[arg(long, short = 'e')]
60    pub exclude: Vec<String>,
61
62    /// Include hidden files (dotfiles)
63    #[arg(long)]
64    pub include_hidden: bool,
65
66    /// Maximum files to process (0 = unlimited)
67    #[arg(long, default_value = "0")]
68    pub max_files: usize,
69}
70
71impl CognitiveArgs {
72    /// Run the cognitive complexity command
73    pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
74        let writer = OutputWriter::new(format, quiet);
75
76        // Build options
77        let options = CognitiveOptions::new()
78            .with_function(self.function.clone())
79            .with_threshold(self.threshold)
80            .with_high_threshold(self.high_threshold)
81            .with_contributors(self.show_contributors)
82            .with_cyclomatic(self.include_cyclomatic)
83            .with_top(self.top);
84
85        let report = if self.path.is_file() {
86            // Single file: preserve exact current behavior
87            let validated_path = validate_file_path(self.path.to_str().unwrap_or_default(), None)?;
88
89            writer.progress(&format!(
90                "Calculating cognitive complexity for {}...",
91                validated_path.display()
92            ));
93
94            analyze_cognitive(&validated_path, &options)?
95        } else if self.path.is_dir() {
96            // Directory: walk -> analyze each -> merge
97            let walk_options = WalkOptions {
98                lang: self.lang,
99                exclude: self.exclude.clone(),
100                include_hidden: self.include_hidden,
101                gitignore: true,
102                max_files: self.max_files,
103            };
104
105            let (files, walk_warnings) = walk_source_files(&self.path, &walk_options)?;
106
107            writer.progress(&format!(
108                "Analyzing {} files in {}...",
109                files.len(),
110                self.path.display()
111            ));
112
113            let mut reports = Vec::new();
114            let mut extra_warnings = walk_warnings;
115
116            for file in &files {
117                match analyze_cognitive(file, &options) {
118                    Ok(report) => reports.push(report),
119                    Err(e) => {
120                        extra_warnings.push(format!("Failed to analyze {}: {}", file.display(), e));
121                    }
122                }
123            }
124
125            let mut merged = merge_cognitive_reports(reports, &options);
126            // Prepend walk warnings and per-file error warnings
127            let mut all_warnings = extra_warnings;
128            all_warnings.append(&mut merged.warnings);
129            merged.warnings = all_warnings;
130            merged
131        } else {
132            // Path does not exist (or is a special file)
133            return Err(anyhow::anyhow!(
134                "Path does not exist: {}",
135                self.path.display()
136            ));
137        };
138
139        // Output based on format
140        if writer.is_text() {
141            writer.write_text(&format_cognitive_text(&report))?;
142        } else {
143            writer.write(&report)?;
144        }
145
146        Ok(())
147    }
148}