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
120// cha:ignore brain_method
121fn 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
270// cha:ignore long_method
271fn 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}