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