Skip to main content

cha_core/
registry.rs

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
21/// Manages plugin registration and lifecycle.
22pub struct PluginRegistry {
23    plugins: Vec<Box<dyn Plugin>>,
24}
25
26impl PluginRegistry {
27    /// Build registry from config, applying thresholds. Language-agnostic —
28    /// disabled_smells filtering happens downstream but WASM plugins don't
29    /// get the list. Prefer `from_config_for_language` inside `analyze`.
30    pub fn from_config(config: &Config, project_dir: &Path) -> Self {
31        Self::from_config_for_language(config, project_dir, "")
32    }
33
34    /// Build registry for a specific file language. WASM plugins receive the
35    /// effective disabled_smells list via the reserved `__disabled_smells__`
36    /// option so they can skip work proactively.
37    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    /// Get all plugin names and descriptions from this registry.
74    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
82/// Apply a usize config option to a field if present.
83fn 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
89/// Apply a f64 config option to a field if present.
90fn 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
96/// Apply a bool config option to a field if present.
97fn 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
103/// Generic helper: register a plugin only if enabled.
104fn 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) {
121    register_if_enabled(plugins, config, "coupling", || {
122        let mut p = CouplingAnalyzer::default();
123        apply_usize(config, "coupling", "max_imports", &mut p.max_imports);
124        Box::new(p)
125    });
126    register_if_enabled(plugins, config, "naming", || {
127        let mut p = NamingAnalyzer::default();
128        apply_usize(config, "naming", "min_name_length", &mut p.min_name_length);
129        apply_usize(config, "naming", "max_name_length", &mut p.max_name_length);
130        Box::new(p)
131    });
132    register_if_enabled(plugins, config, "duplicate_code", || {
133        Box::new(DuplicateCodeAnalyzer)
134    });
135    register_if_enabled(plugins, config, "dead_code", || Box::new(DeadCodeAnalyzer));
136    register_if_enabled(plugins, config, "api_surface", || {
137        let mut p = ApiSurfaceAnalyzer::default();
138        apply_usize(
139            config,
140            "api_surface",
141            "max_exported_count",
142            &mut p.max_exported_count,
143        );
144        apply_f64(
145            config,
146            "api_surface",
147            "max_exported_ratio",
148            &mut p.max_exported_ratio,
149        );
150        apply_usize(
151            config,
152            "api_surface",
153            "c_max_exported_count",
154            &mut p.c_max_exported_count,
155        );
156        apply_f64(
157            config,
158            "api_surface",
159            "c_max_exported_ratio",
160            &mut p.c_max_exported_ratio,
161        );
162        apply_bool(
163            config,
164            "api_surface",
165            "skip_c_headers",
166            &mut p.skip_c_headers,
167        );
168        Box::new(p)
169    });
170}
171
172fn register_smell_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
173    register_if_enabled(plugins, config, "long_parameter_list", || {
174        let mut p = LongParameterListAnalyzer::default();
175        apply_usize(
176            config,
177            "long_parameter_list",
178            "max_params",
179            &mut p.max_params,
180        );
181        Box::new(p)
182    });
183    register_if_enabled(plugins, config, "switch_statement", || {
184        let mut p = SwitchStatementAnalyzer::default();
185        apply_usize(config, "switch_statement", "max_arms", &mut p.max_arms);
186        Box::new(p)
187    });
188    register_if_enabled(plugins, config, "message_chain", || {
189        let mut p = MessageChainAnalyzer::default();
190        apply_usize(config, "message_chain", "max_depth", &mut p.max_depth);
191        Box::new(p)
192    });
193    register_if_enabled(plugins, config, "primitive_obsession", || {
194        Box::new(PrimitiveObsessionAnalyzer::default())
195    });
196    register_if_enabled(plugins, config, "data_clumps", || {
197        Box::new(DataClumpsAnalyzer::default())
198    });
199    register_if_enabled(plugins, config, "feature_envy", || {
200        Box::new(FeatureEnvyAnalyzer::default())
201    });
202    register_if_enabled(plugins, config, "middle_man", || {
203        Box::new(MiddleManAnalyzer::default())
204    });
205    register_extended_smell_plugins(plugins, config);
206}
207
208fn register_extended_smell_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
209    register_if_enabled(plugins, config, "comments", || {
210        Box::new(CommentsAnalyzer::default())
211    });
212    register_if_enabled(plugins, config, "lazy_class", || {
213        Box::new(LazyClassAnalyzer::default())
214    });
215    register_if_enabled(plugins, config, "data_class", || {
216        Box::new(DataClassAnalyzer::default())
217    });
218    register_if_enabled(plugins, config, "async_callback_leak", || {
219        Box::new(AsyncCallbackLeakAnalyzer)
220    });
221    register_if_enabled(plugins, config, "design_pattern", || {
222        Box::new(DesignPatternAdvisor)
223    });
224    register_if_enabled(plugins, config, "temporary_field", || {
225        Box::new(TemporaryFieldAnalyzer::default())
226    });
227    register_if_enabled(plugins, config, "speculative_generality", || {
228        Box::new(SpeculativeGeneralityAnalyzer)
229    });
230    register_change_preventer_plugins(plugins, config);
231    register_advanced_plugins(plugins, config);
232}
233
234fn register_change_preventer_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
235    register_if_enabled(plugins, config, "refused_bequest", || {
236        Box::new(RefusedBequestAnalyzer::default())
237    });
238    register_if_enabled(plugins, config, "shotgun_surgery", || {
239        Box::new(ShotgunSurgeryAnalyzer::default())
240    });
241    register_if_enabled(plugins, config, "divergent_change", || {
242        Box::new(DivergentChangeAnalyzer::default())
243    });
244    register_if_enabled(plugins, config, "inappropriate_intimacy", || {
245        Box::new(InappropriateIntimacyAnalyzer)
246    });
247    register_if_enabled(plugins, config, "hardcoded_secret", || {
248        Box::new(HardcodedSecretAnalyzer)
249    });
250}
251
252// cha:ignore long_method
253fn register_advanced_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
254    register_if_enabled(plugins, config, "cognitive_complexity", || {
255        let mut p = CognitiveComplexityAnalyzer::default();
256        apply_usize(
257            config,
258            "cognitive_complexity",
259            "threshold",
260            &mut p.threshold,
261        );
262        Box::new(p)
263    });
264    register_if_enabled(plugins, config, "god_class", || {
265        let mut p = GodClassAnalyzer::default();
266        apply_usize(
267            config,
268            "god_class",
269            "max_external_refs",
270            &mut p.max_external_refs,
271        );
272        apply_usize(config, "god_class", "min_wmc", &mut p.min_wmc);
273        Box::new(p)
274    });
275    register_if_enabled(plugins, config, "brain_method", || {
276        let mut p = BrainMethodAnalyzer::default();
277        apply_usize(config, "brain_method", "min_lines", &mut p.min_lines);
278        apply_usize(
279            config,
280            "brain_method",
281            "min_complexity",
282            &mut p.min_complexity,
283        );
284        Box::new(p)
285    });
286    register_if_enabled(plugins, config, "hub_like_dependency", || {
287        let mut p = HubLikeDependencyAnalyzer::default();
288        apply_usize(
289            config,
290            "hub_like_dependency",
291            "max_imports",
292            &mut p.max_imports,
293        );
294        Box::new(p)
295    });
296    register_if_enabled(plugins, config, "error_handling", || {
297        let mut p = ErrorHandlingAnalyzer::default();
298        apply_usize(
299            config,
300            "error_handling",
301            "max_unwraps_per_function",
302            &mut p.max_unwraps_per_function,
303        );
304        Box::new(p)
305    });
306    register_if_enabled(plugins, config, "todo_tracker", || {
307        Box::new(TodoTrackerAnalyzer)
308    });
309    register_if_enabled(plugins, config, "unsafe_api", || {
310        Box::new(UnsafeApiAnalyzer)
311    });
312}
313
314fn register_length(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
315    if !config.is_enabled("length") {
316        return;
317    }
318    let mut p = LengthAnalyzer::default();
319    if let Some(v) = config.get_usize("length", "max_function_lines") {
320        p.max_function_lines = v;
321    }
322    if let Some(v) = config.get_usize("length", "max_class_methods") {
323        p.max_class_methods = v;
324    }
325    if let Some(v) = config.get_usize("length", "max_class_lines") {
326        p.max_class_lines = v;
327    }
328    if let Some(v) = config.get_usize("length", "max_file_lines") {
329        p.max_file_lines = v;
330    }
331    plugins.push(Box::new(p));
332}
333
334fn register_complexity(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
335    if !config.is_enabled("complexity") {
336        return;
337    }
338    let mut p = ComplexityAnalyzer::default();
339    if let Some(v) = config.get_usize("complexity", "warn_threshold") {
340        p.warn_threshold = v;
341    }
342    if let Some(v) = config.get_usize("complexity", "error_threshold") {
343        p.error_threshold = v;
344    }
345    plugins.push(Box::new(p));
346}
347
348fn register_layer_violation(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
349    if !config.is_enabled("layer_violation") {
350        return;
351    }
352    let p = config
353        .get_str("layer_violation", "layers")
354        .map(|s| LayerViolationAnalyzer::from_config_str(&s))
355        .unwrap_or_default();
356    plugins.push(Box::new(p));
357}