1use std::path::Path;
2
3use crate::{
4 Plugin,
5 config::Config,
6 plugins::{
7 ApiSurfaceAnalyzer, AsyncCallbackLeakAnalyzer, BrainMethodAnalyzer,
8 CognitiveComplexityAnalyzer, CommentsAnalyzer, ComplexityAnalyzer, CouplingAnalyzer,
9 DataClassAnalyzer, DataClumpsAnalyzer, DeadCodeAnalyzer, DesignPatternAdvisor,
10 DivergentChangeAnalyzer, DuplicateCodeAnalyzer, ErrorHandlingAnalyzer, FeatureEnvyAnalyzer,
11 GodClassAnalyzer, HardcodedSecretAnalyzer, HubLikeDependencyAnalyzer,
12 InappropriateIntimacyAnalyzer, LayerViolationAnalyzer, LazyClassAnalyzer, LengthAnalyzer,
13 LongParameterListAnalyzer, MessageChainAnalyzer, MiddleManAnalyzer, NamingAnalyzer,
14 PrimitiveObsessionAnalyzer, RefusedBequestAnalyzer, ShotgunSurgeryAnalyzer,
15 SpeculativeGeneralityAnalyzer, SwitchStatementAnalyzer, TemporaryFieldAnalyzer,
16 TodoTrackerAnalyzer, UnsafeApiAnalyzer,
17 },
18 wasm,
19};
20
21pub struct PluginRegistry {
23 plugins: Vec<Box<dyn Plugin>>,
24}
25
26impl PluginRegistry {
27 pub fn from_config(config: &Config, project_dir: &Path) -> Self {
31 Self::from_config_for_language(config, project_dir, "")
32 }
33
34 pub fn from_config_for_language(config: &Config, project_dir: &Path, language: &str) -> Self {
38 let mut plugins: Vec<Box<dyn Plugin>> = Vec::new();
39
40 register_length(&mut plugins, config);
41 register_complexity(&mut plugins, config);
42 register_simple_plugins(&mut plugins, config);
43 register_layer_violation(&mut plugins, config);
44
45 let disabled_smells = config.disabled_smells_for_language(language);
46
47 for mut wp in wasm::load_wasm_plugins(project_dir) {
48 if config.is_enabled(wp.name()) {
49 let mut opts: Vec<(String, wasm::wit::OptionValue)> = Vec::new();
50 if let Some(pc) = config.plugins.get(wp.name()) {
51 opts.extend(pc.options.iter().filter_map(|(k, v)| {
52 wasm::toml_to_option_value(v).map(|ov| (k.clone(), ov))
53 }));
54 }
55 if !disabled_smells.is_empty() {
56 opts.push((
57 "__disabled_smells__".into(),
58 wasm::wit::OptionValue::ListStr(disabled_smells.clone()),
59 ));
60 }
61 wp.set_options(opts);
62 plugins.push(Box::new(wp));
63 }
64 }
65
66 Self { plugins }
67 }
68
69 pub fn plugins(&self) -> &[Box<dyn Plugin>] {
70 &self.plugins
71 }
72
73 pub fn plugin_info(&self) -> Vec<(String, String)> {
75 self.plugins
76 .iter()
77 .map(|p| (p.name().to_string(), p.description().to_string()))
78 .collect()
79 }
80}
81
82fn apply_usize(config: &Config, plugin: &str, key: &str, target: &mut usize) {
84 if let Some(v) = config.get_usize(plugin, key) {
85 *target = v;
86 }
87}
88
89fn apply_f64(config: &Config, plugin: &str, key: &str, target: &mut f64) {
91 if let Some(v) = config.get_f64(plugin, key) {
92 *target = v;
93 }
94}
95
96fn apply_bool(config: &Config, plugin: &str, key: &str, target: &mut bool) {
98 if let Some(v) = config.get_bool(plugin, key) {
99 *target = v;
100 }
101}
102
103fn register_if_enabled(
105 plugins: &mut Vec<Box<dyn Plugin>>,
106 config: &Config,
107 name: &str,
108 build: impl FnOnce() -> Box<dyn Plugin>,
109) {
110 if config.is_enabled(name) {
111 plugins.push(build());
112 }
113}
114
115fn register_simple_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
116 register_classic_plugins(plugins, config);
117 register_smell_plugins(plugins, config);
118}
119
120fn register_classic_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
122 register_if_enabled(plugins, config, "coupling", || {
123 let mut p = CouplingAnalyzer::default();
124 apply_usize(config, "coupling", "max_imports", &mut p.max_imports);
125 Box::new(p)
126 });
127 register_if_enabled(plugins, config, "naming", || {
128 let mut p = NamingAnalyzer::default();
129 apply_usize(config, "naming", "min_name_length", &mut p.min_name_length);
130 apply_usize(config, "naming", "max_name_length", &mut p.max_name_length);
131 Box::new(p)
132 });
133 register_if_enabled(plugins, config, "duplicate_code", || {
134 Box::new(DuplicateCodeAnalyzer)
135 });
136 register_if_enabled(plugins, config, "dead_code", || {
137 let mut p = DeadCodeAnalyzer::default();
138 if let Some(list) = config
139 .plugins
140 .get("dead_code")
141 .and_then(|pc| pc.options.get("entry_points"))
142 .and_then(|v| v.as_array())
143 {
144 let names: Vec<String> = list
145 .iter()
146 .filter_map(|v| v.as_str().map(String::from))
147 .collect();
148 if !names.is_empty() {
149 p.entry_points = names;
150 }
151 }
152 Box::new(p)
153 });
154 register_if_enabled(plugins, config, "api_surface", || {
155 let mut p = ApiSurfaceAnalyzer::default();
156 apply_usize(
157 config,
158 "api_surface",
159 "max_exported_count",
160 &mut p.max_exported_count,
161 );
162 apply_f64(
163 config,
164 "api_surface",
165 "max_exported_ratio",
166 &mut p.max_exported_ratio,
167 );
168 apply_usize(
169 config,
170 "api_surface",
171 "c_max_exported_count",
172 &mut p.c_max_exported_count,
173 );
174 apply_f64(
175 config,
176 "api_surface",
177 "c_max_exported_ratio",
178 &mut p.c_max_exported_ratio,
179 );
180 apply_bool(
181 config,
182 "api_surface",
183 "skip_c_headers",
184 &mut p.skip_c_headers,
185 );
186 Box::new(p)
187 });
188}
189
190fn register_smell_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
191 register_if_enabled(plugins, config, "long_parameter_list", || {
192 let mut p = LongParameterListAnalyzer::default();
193 apply_usize(
194 config,
195 "long_parameter_list",
196 "max_params",
197 &mut p.max_params,
198 );
199 Box::new(p)
200 });
201 register_if_enabled(plugins, config, "switch_statement", || {
202 let mut p = SwitchStatementAnalyzer::default();
203 apply_usize(config, "switch_statement", "max_arms", &mut p.max_arms);
204 Box::new(p)
205 });
206 register_if_enabled(plugins, config, "message_chain", || {
207 let mut p = MessageChainAnalyzer::default();
208 apply_usize(config, "message_chain", "max_depth", &mut p.max_depth);
209 Box::new(p)
210 });
211 register_if_enabled(plugins, config, "primitive_obsession", || {
212 Box::new(PrimitiveObsessionAnalyzer::default())
213 });
214 register_if_enabled(plugins, config, "data_clumps", || {
215 Box::new(DataClumpsAnalyzer::default())
216 });
217 register_if_enabled(plugins, config, "feature_envy", || {
218 Box::new(FeatureEnvyAnalyzer::default())
219 });
220 register_if_enabled(plugins, config, "middle_man", || {
221 Box::new(MiddleManAnalyzer::default())
222 });
223 register_extended_smell_plugins(plugins, config);
224}
225
226fn register_extended_smell_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
227 register_if_enabled(plugins, config, "comments", || {
228 Box::new(CommentsAnalyzer::default())
229 });
230 register_if_enabled(plugins, config, "lazy_class", || {
231 Box::new(LazyClassAnalyzer::default())
232 });
233 register_if_enabled(plugins, config, "data_class", || {
234 Box::new(DataClassAnalyzer::default())
235 });
236 register_if_enabled(plugins, config, "async_callback_leak", || {
237 Box::new(AsyncCallbackLeakAnalyzer)
238 });
239 register_if_enabled(plugins, config, "design_pattern", || {
240 Box::new(DesignPatternAdvisor)
241 });
242 register_if_enabled(plugins, config, "temporary_field", || {
243 Box::new(TemporaryFieldAnalyzer::default())
244 });
245 register_if_enabled(plugins, config, "speculative_generality", || {
246 Box::new(SpeculativeGeneralityAnalyzer)
247 });
248 register_change_preventer_plugins(plugins, config);
249 register_advanced_plugins(plugins, config);
250}
251
252fn register_change_preventer_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
253 register_if_enabled(plugins, config, "refused_bequest", || {
254 Box::new(RefusedBequestAnalyzer::default())
255 });
256 register_if_enabled(plugins, config, "shotgun_surgery", || {
257 Box::new(ShotgunSurgeryAnalyzer::default())
258 });
259 register_if_enabled(plugins, config, "divergent_change", || {
260 Box::new(DivergentChangeAnalyzer::default())
261 });
262 register_if_enabled(plugins, config, "inappropriate_intimacy", || {
263 Box::new(InappropriateIntimacyAnalyzer)
264 });
265 register_if_enabled(plugins, config, "hardcoded_secret", || {
266 Box::new(HardcodedSecretAnalyzer)
267 });
268}
269
270fn register_advanced_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
272 register_if_enabled(plugins, config, "cognitive_complexity", || {
273 let mut p = CognitiveComplexityAnalyzer::default();
274 apply_usize(
275 config,
276 "cognitive_complexity",
277 "threshold",
278 &mut p.threshold,
279 );
280 Box::new(p)
281 });
282 register_if_enabled(plugins, config, "god_class", || {
283 let mut p = GodClassAnalyzer::default();
284 apply_usize(
285 config,
286 "god_class",
287 "max_external_refs",
288 &mut p.max_external_refs,
289 );
290 apply_usize(config, "god_class", "min_wmc", &mut p.min_wmc);
291 Box::new(p)
292 });
293 register_if_enabled(plugins, config, "brain_method", || {
294 let mut p = BrainMethodAnalyzer::default();
295 apply_usize(config, "brain_method", "min_lines", &mut p.min_lines);
296 apply_usize(
297 config,
298 "brain_method",
299 "min_complexity",
300 &mut p.min_complexity,
301 );
302 Box::new(p)
303 });
304 register_if_enabled(plugins, config, "hub_like_dependency", || {
305 let mut p = HubLikeDependencyAnalyzer::default();
306 apply_usize(
307 config,
308 "hub_like_dependency",
309 "max_imports",
310 &mut p.max_imports,
311 );
312 Box::new(p)
313 });
314 register_if_enabled(plugins, config, "error_handling", || {
315 let mut p = ErrorHandlingAnalyzer::default();
316 apply_usize(
317 config,
318 "error_handling",
319 "max_unwraps_per_function",
320 &mut p.max_unwraps_per_function,
321 );
322 Box::new(p)
323 });
324 register_if_enabled(plugins, config, "todo_tracker", || {
325 Box::new(TodoTrackerAnalyzer)
326 });
327 register_if_enabled(plugins, config, "unsafe_api", || {
328 Box::new(UnsafeApiAnalyzer)
329 });
330}
331
332fn register_length(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
333 if !config.is_enabled("length") {
334 return;
335 }
336 let mut p = LengthAnalyzer::default();
337 if let Some(v) = config.get_usize("length", "max_function_lines") {
338 p.max_function_lines = v;
339 }
340 if let Some(v) = config.get_usize("length", "max_class_methods") {
341 p.max_class_methods = v;
342 }
343 if let Some(v) = config.get_usize("length", "max_class_lines") {
344 p.max_class_lines = v;
345 }
346 if let Some(v) = config.get_usize("length", "max_file_lines") {
347 p.max_file_lines = v;
348 }
349 apply_f64(
350 config,
351 "length",
352 "complexity_factor_threshold",
353 &mut p.complexity_factor_threshold,
354 );
355 plugins.push(Box::new(p));
356}
357
358fn register_complexity(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
359 if !config.is_enabled("complexity") {
360 return;
361 }
362 let mut p = ComplexityAnalyzer::default();
363 if let Some(v) = config.get_usize("complexity", "warn_threshold") {
364 p.warn_threshold = v;
365 }
366 if let Some(v) = config.get_usize("complexity", "error_threshold") {
367 p.error_threshold = v;
368 }
369 plugins.push(Box::new(p));
370}
371
372fn register_layer_violation(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
373 if !config.is_enabled("layer_violation") {
374 return;
375 }
376 let p = config
377 .get_str("layer_violation", "layers")
378 .map(|s| LayerViolationAnalyzer::from_config_str(&s))
379 .unwrap_or_default();
380 plugins.push(Box::new(p));
381}