1use 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_for_smells, try_daemon_route};
17use crate::output::{format_smells_text, OutputFormat, OutputWriter};
18
19#[derive(Debug, Args)]
21pub struct SmellsArgs {
22 #[arg(default_value = ".")]
24 pub path: PathBuf,
25
26 #[arg(long, short = 'l')]
28 pub lang: Option<Language>,
29
30 #[arg(long, short = 't', default_value = "default")]
32 pub threshold: ThresholdPresetArg,
33
34 #[arg(long, short = 's')]
36 pub smell_type: Option<SmellTypeArg>,
37
38 #[arg(long)]
40 pub suggest: bool,
41
42 #[arg(long)]
46 pub deep: bool,
47
48 #[arg(long)]
50 pub no_default_ignore: bool,
51
52 #[arg(long)]
58 pub files: Vec<PathBuf>,
59
60 #[arg(long)]
63 pub include_tests: bool,
64}
65
66#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
68pub enum ThresholdPresetArg {
69 Strict,
71 #[default]
73 Default,
74 Relaxed,
76}
77
78impl From<ThresholdPresetArg> for ThresholdPreset {
79 fn from(arg: ThresholdPresetArg) -> Self {
80 match arg {
81 ThresholdPresetArg::Strict => ThresholdPreset::Strict,
82 ThresholdPresetArg::Default => ThresholdPreset::Default,
83 ThresholdPresetArg::Relaxed => ThresholdPreset::Relaxed,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Copy, clap::ValueEnum)]
90pub enum SmellTypeArg {
91 GodClass,
93 LongMethod,
95 LongParameterList,
97 FeatureEnvy,
99 DataClumps,
101 LowCohesion,
103 TightCoupling,
105 DeadCode,
107 CodeClone,
109 HighCognitiveComplexity,
111 DeepNesting,
113 DataClass,
115 LazyElement,
117 MessageChain,
119 PrimitiveObsession,
121 MiddleMan,
123 RefusedBequest,
125 InappropriateIntimacy,
127}
128
129impl From<SmellTypeArg> for SmellType {
130 fn from(arg: SmellTypeArg) -> Self {
131 match arg {
132 SmellTypeArg::GodClass => SmellType::GodClass,
133 SmellTypeArg::LongMethod => SmellType::LongMethod,
134 SmellTypeArg::LongParameterList => SmellType::LongParameterList,
135 SmellTypeArg::FeatureEnvy => SmellType::FeatureEnvy,
136 SmellTypeArg::DataClumps => SmellType::DataClumps,
137 SmellTypeArg::LowCohesion => SmellType::LowCohesion,
138 SmellTypeArg::TightCoupling => SmellType::TightCoupling,
139 SmellTypeArg::DeadCode => SmellType::DeadCode,
140 SmellTypeArg::CodeClone => SmellType::CodeClone,
141 SmellTypeArg::HighCognitiveComplexity => SmellType::HighCognitiveComplexity,
142 SmellTypeArg::DeepNesting => SmellType::DeepNesting,
143 SmellTypeArg::DataClass => SmellType::DataClass,
144 SmellTypeArg::LazyElement => SmellType::LazyElement,
145 SmellTypeArg::MessageChain => SmellType::MessageChain,
146 SmellTypeArg::PrimitiveObsession => SmellType::PrimitiveObsession,
147 SmellTypeArg::MiddleMan => SmellType::MiddleMan,
148 SmellTypeArg::RefusedBequest => SmellType::RefusedBequest,
149 SmellTypeArg::InappropriateIntimacy => SmellType::InappropriateIntimacy,
150 }
151 }
152}
153
154impl SmellsArgs {
155 pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
157 let writer = OutputWriter::new(format, quiet);
158
159 let include_tests = self.include_tests || !self.files.is_empty();
163
164 let project_root = if self.path.is_dir() {
171 dunce::canonicalize(&self.path).unwrap_or_else(|_| self.path.clone())
175 } else {
176 self.path
178 .parent()
179 .map(|p| dunce::canonicalize(p).unwrap_or_else(|_| p.to_path_buf()))
180 .unwrap_or_else(|| PathBuf::from("."))
181 };
182 let mut validated_files: Vec<PathBuf> = Vec::with_capacity(self.files.len());
183 for f in &self.files {
184 let f_str = f.to_str().ok_or_else(|| {
185 anyhow::anyhow!("--files entry contains non-UTF8 bytes: {:?}", f)
186 })?;
187 let canonical =
188 tldr_core::validation::validate_file_path(f_str, Some(&project_root))
189 .map_err(|e| anyhow::anyhow!("--files {}: {}", f.display(), e))?;
190 validated_files.push(canonical);
191 }
192
193 if let Some(report) = try_daemon_route::<SmellsReport>(
195 &self.path,
196 "smells",
197 params_for_smells(Some(&self.path), &validated_files, include_tests),
198 ) {
199 if writer.is_text() {
201 let text = format_smells_text(&report);
202 writer.write_text(&text)?;
203 return Ok(());
204 } else {
205 writer.write(&report)?;
206 return Ok(());
207 }
208 }
209
210 writer.progress(&format!(
212 "Scanning for code smells in {}{}...",
213 self.path.display(),
214 if self.deep { " (deep analysis)" } else { "" }
215 ));
216
217 let walker_opts = SmellsWalkerOpts {
219 no_default_ignore: self.no_default_ignore,
220 lang: self.lang,
221 files: validated_files,
222 include_tests,
223 };
224 let report = if self.deep {
225 analyze_smells_aggregated_with_walker_opts(
226 &self.path,
227 self.threshold.into(),
228 self.smell_type.map(|s| s.into()),
229 self.suggest,
230 walker_opts,
231 )?
232 } else {
233 detect_smells_with_walker_opts(
234 &self.path,
235 self.threshold.into(),
236 self.smell_type.map(|s| s.into()),
237 self.suggest,
238 walker_opts,
239 )?
240 };
241
242 if writer.is_text() {
244 let text = format_smells_text(&report);
245 writer.write_text(&text)?;
246 } else {
247 writer.write(&report)?;
248 }
249
250 Ok(())
251 }
252}