Skip to main content

tldr_cli/commands/
smells.rs

1//! Smells command - Detect code smells
2//!
3//! Identifies common code smells like God Class, Long Method, etc.
4//! Auto-routes through daemon when available for ~35x speedup.
5
6use std::path::PathBuf;
7
8use anyhow::Result;
9use clap::Args;
10
11use tldr_core::{
12    analyze_smells_aggregated, detect_smells, SmellType, SmellsReport, ThresholdPreset,
13};
14
15use crate::commands::daemon_router::{params_with_path, try_daemon_route};
16use crate::output::{format_smells_text, OutputFormat, OutputWriter};
17
18/// Detect code smells
19#[derive(Debug, Args)]
20pub struct SmellsArgs {
21    /// Path to analyze (file or directory)
22    #[arg(default_value = ".")]
23    pub path: PathBuf,
24
25    /// Threshold preset
26    #[arg(long, short = 't', default_value = "default")]
27    pub threshold: ThresholdPresetArg,
28
29    /// Filter by smell type
30    #[arg(long, short = 's')]
31    pub smell_type: Option<SmellTypeArg>,
32
33    /// Include suggestions for fixing
34    #[arg(long)]
35    pub suggest: bool,
36
37    /// Deep analysis: aggregate findings from cohesion, coupling, dead code,
38    /// similarity, and cognitive complexity analyzers in addition to the
39    /// standard smell detectors
40    #[arg(long)]
41    pub deep: bool,
42}
43
44/// CLI wrapper for threshold preset
45#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
46pub enum ThresholdPresetArg {
47    /// Strict thresholds for high-quality codebases
48    Strict,
49    /// Default thresholds (recommended)
50    #[default]
51    Default,
52    /// Relaxed thresholds for legacy code
53    Relaxed,
54}
55
56impl From<ThresholdPresetArg> for ThresholdPreset {
57    fn from(arg: ThresholdPresetArg) -> Self {
58        match arg {
59            ThresholdPresetArg::Strict => ThresholdPreset::Strict,
60            ThresholdPresetArg::Default => ThresholdPreset::Default,
61            ThresholdPresetArg::Relaxed => ThresholdPreset::Relaxed,
62        }
63    }
64}
65
66/// CLI wrapper for smell type
67#[derive(Debug, Clone, Copy, clap::ValueEnum)]
68pub enum SmellTypeArg {
69    /// God Class (>20 methods or >500 LOC)
70    GodClass,
71    /// Long Method (>50 LOC or cyclomatic >10)
72    LongMethod,
73    /// Long Parameter List (>5 parameters)
74    LongParameterList,
75    /// Feature Envy
76    FeatureEnvy,
77    /// Data Clumps
78    DataClumps,
79    /// Low Cohesion (LCOM4 >= 2) -- requires --deep
80    LowCohesion,
81    /// Tight Coupling (score >= 0.6) -- requires --deep
82    TightCoupling,
83    /// Dead Code (unreachable functions) -- requires --deep
84    DeadCode,
85    /// Code Clone (similar functions) -- requires --deep
86    CodeClone,
87    /// High Cognitive Complexity (>= 15) -- requires --deep
88    HighCognitiveComplexity,
89    /// Deep Nesting (nesting depth >= 5)
90    DeepNesting,
91    /// Data Class (many fields, few/no methods)
92    DataClass,
93    /// Lazy Element (class with only 1 method and 0-1 fields)
94    LazyElement,
95    /// Message Chain (long method call chains > 3)
96    MessageChain,
97    /// Primitive Obsession (many primitive-typed parameters)
98    PrimitiveObsession,
99    /// Middle Man (>60% delegation) -- requires --deep
100    MiddleMan,
101    /// Refused Bequest (<33% inherited usage) -- requires --deep
102    RefusedBequest,
103    /// Inappropriate Intimacy (bidirectional coupling) -- requires --deep
104    InappropriateIntimacy,
105}
106
107impl From<SmellTypeArg> for SmellType {
108    fn from(arg: SmellTypeArg) -> Self {
109        match arg {
110            SmellTypeArg::GodClass => SmellType::GodClass,
111            SmellTypeArg::LongMethod => SmellType::LongMethod,
112            SmellTypeArg::LongParameterList => SmellType::LongParameterList,
113            SmellTypeArg::FeatureEnvy => SmellType::FeatureEnvy,
114            SmellTypeArg::DataClumps => SmellType::DataClumps,
115            SmellTypeArg::LowCohesion => SmellType::LowCohesion,
116            SmellTypeArg::TightCoupling => SmellType::TightCoupling,
117            SmellTypeArg::DeadCode => SmellType::DeadCode,
118            SmellTypeArg::CodeClone => SmellType::CodeClone,
119            SmellTypeArg::HighCognitiveComplexity => SmellType::HighCognitiveComplexity,
120            SmellTypeArg::DeepNesting => SmellType::DeepNesting,
121            SmellTypeArg::DataClass => SmellType::DataClass,
122            SmellTypeArg::LazyElement => SmellType::LazyElement,
123            SmellTypeArg::MessageChain => SmellType::MessageChain,
124            SmellTypeArg::PrimitiveObsession => SmellType::PrimitiveObsession,
125            SmellTypeArg::MiddleMan => SmellType::MiddleMan,
126            SmellTypeArg::RefusedBequest => SmellType::RefusedBequest,
127            SmellTypeArg::InappropriateIntimacy => SmellType::InappropriateIntimacy,
128        }
129    }
130}
131
132impl SmellsArgs {
133    /// Run the smells command
134    pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
135        let writer = OutputWriter::new(format, quiet);
136
137        // Try daemon first for cached result
138        if let Some(report) = try_daemon_route::<SmellsReport>(
139            &self.path,
140            "smells",
141            params_with_path(Some(&self.path)),
142        ) {
143            // Output based on format
144            if writer.is_text() {
145                let text = format_smells_text(&report);
146                writer.write_text(&text)?;
147                return Ok(());
148            } else {
149                writer.write(&report)?;
150                return Ok(());
151            }
152        }
153
154        // Fallback to direct compute
155        writer.progress(&format!(
156            "Scanning for code smells in {}{}...",
157            self.path.display(),
158            if self.deep { " (deep analysis)" } else { "" }
159        ));
160
161        // Detect smells - use aggregated analysis when --deep is set
162        let report = if self.deep {
163            analyze_smells_aggregated(
164                &self.path,
165                self.threshold.into(),
166                self.smell_type.map(|s| s.into()),
167                self.suggest,
168            )?
169        } else {
170            detect_smells(
171                &self.path,
172                self.threshold.into(),
173                self.smell_type.map(|s| s.into()),
174                self.suggest,
175            )?
176        };
177
178        // Output based on format
179        if writer.is_text() {
180            let text = format_smells_text(&report);
181            writer.write_text(&text)?;
182        } else {
183            writer.write(&report)?;
184        }
185
186        Ok(())
187    }
188}