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/// Generic helper: register a plugin only if enabled.
90fn register_if_enabled(
91    plugins: &mut Vec<Box<dyn Plugin>>,
92    config: &Config,
93    name: &str,
94    build: impl FnOnce() -> Box<dyn Plugin>,
95) {
96    if config.is_enabled(name) {
97        plugins.push(build());
98    }
99}
100
101fn register_simple_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
102    register_classic_plugins(plugins, config);
103    register_smell_plugins(plugins, config);
104}
105
106fn register_classic_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
107    register_if_enabled(plugins, config, "coupling", || {
108        let mut p = CouplingAnalyzer::default();
109        apply_usize(config, "coupling", "max_imports", &mut p.max_imports);
110        Box::new(p)
111    });
112    register_if_enabled(plugins, config, "naming", || {
113        let mut p = NamingAnalyzer::default();
114        apply_usize(config, "naming", "min_name_length", &mut p.min_name_length);
115        apply_usize(config, "naming", "max_name_length", &mut p.max_name_length);
116        Box::new(p)
117    });
118    register_if_enabled(plugins, config, "duplicate_code", || {
119        Box::new(DuplicateCodeAnalyzer)
120    });
121    register_if_enabled(plugins, config, "dead_code", || Box::new(DeadCodeAnalyzer));
122    register_if_enabled(plugins, config, "api_surface", || {
123        let mut p = ApiSurfaceAnalyzer::default();
124        apply_usize(
125            config,
126            "api_surface",
127            "max_exported_count",
128            &mut p.max_exported_count,
129        );
130        Box::new(p)
131    });
132}
133
134fn register_smell_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
135    register_if_enabled(plugins, config, "long_parameter_list", || {
136        let mut p = LongParameterListAnalyzer::default();
137        apply_usize(
138            config,
139            "long_parameter_list",
140            "max_params",
141            &mut p.max_params,
142        );
143        Box::new(p)
144    });
145    register_if_enabled(plugins, config, "switch_statement", || {
146        let mut p = SwitchStatementAnalyzer::default();
147        apply_usize(config, "switch_statement", "max_arms", &mut p.max_arms);
148        Box::new(p)
149    });
150    register_if_enabled(plugins, config, "message_chain", || {
151        let mut p = MessageChainAnalyzer::default();
152        apply_usize(config, "message_chain", "max_depth", &mut p.max_depth);
153        Box::new(p)
154    });
155    register_if_enabled(plugins, config, "primitive_obsession", || {
156        Box::new(PrimitiveObsessionAnalyzer::default())
157    });
158    register_if_enabled(plugins, config, "data_clumps", || {
159        Box::new(DataClumpsAnalyzer::default())
160    });
161    register_if_enabled(plugins, config, "feature_envy", || {
162        Box::new(FeatureEnvyAnalyzer::default())
163    });
164    register_if_enabled(plugins, config, "middle_man", || {
165        Box::new(MiddleManAnalyzer::default())
166    });
167    register_extended_smell_plugins(plugins, config);
168}
169
170fn register_extended_smell_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
171    register_if_enabled(plugins, config, "comments", || {
172        Box::new(CommentsAnalyzer::default())
173    });
174    register_if_enabled(plugins, config, "lazy_class", || {
175        Box::new(LazyClassAnalyzer::default())
176    });
177    register_if_enabled(plugins, config, "data_class", || {
178        Box::new(DataClassAnalyzer::default())
179    });
180    register_if_enabled(plugins, config, "async_callback_leak", || {
181        Box::new(AsyncCallbackLeakAnalyzer)
182    });
183    register_if_enabled(plugins, config, "design_pattern", || {
184        Box::new(DesignPatternAdvisor)
185    });
186    register_if_enabled(plugins, config, "temporary_field", || {
187        Box::new(TemporaryFieldAnalyzer::default())
188    });
189    register_if_enabled(plugins, config, "speculative_generality", || {
190        Box::new(SpeculativeGeneralityAnalyzer)
191    });
192    register_change_preventer_plugins(plugins, config);
193    register_advanced_plugins(plugins, config);
194}
195
196fn register_change_preventer_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
197    register_if_enabled(plugins, config, "refused_bequest", || {
198        Box::new(RefusedBequestAnalyzer::default())
199    });
200    register_if_enabled(plugins, config, "shotgun_surgery", || {
201        Box::new(ShotgunSurgeryAnalyzer::default())
202    });
203    register_if_enabled(plugins, config, "divergent_change", || {
204        Box::new(DivergentChangeAnalyzer::default())
205    });
206    register_if_enabled(plugins, config, "inappropriate_intimacy", || {
207        Box::new(InappropriateIntimacyAnalyzer)
208    });
209    register_if_enabled(plugins, config, "hardcoded_secret", || {
210        Box::new(HardcodedSecretAnalyzer)
211    });
212}
213
214// cha:ignore long_method
215fn register_advanced_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
216    register_if_enabled(plugins, config, "cognitive_complexity", || {
217        let mut p = CognitiveComplexityAnalyzer::default();
218        apply_usize(
219            config,
220            "cognitive_complexity",
221            "threshold",
222            &mut p.threshold,
223        );
224        Box::new(p)
225    });
226    register_if_enabled(plugins, config, "god_class", || {
227        let mut p = GodClassAnalyzer::default();
228        apply_usize(
229            config,
230            "god_class",
231            "max_external_refs",
232            &mut p.max_external_refs,
233        );
234        apply_usize(config, "god_class", "min_wmc", &mut p.min_wmc);
235        Box::new(p)
236    });
237    register_if_enabled(plugins, config, "brain_method", || {
238        let mut p = BrainMethodAnalyzer::default();
239        apply_usize(config, "brain_method", "min_lines", &mut p.min_lines);
240        apply_usize(
241            config,
242            "brain_method",
243            "min_complexity",
244            &mut p.min_complexity,
245        );
246        Box::new(p)
247    });
248    register_if_enabled(plugins, config, "hub_like_dependency", || {
249        let mut p = HubLikeDependencyAnalyzer::default();
250        apply_usize(
251            config,
252            "hub_like_dependency",
253            "max_imports",
254            &mut p.max_imports,
255        );
256        Box::new(p)
257    });
258    register_if_enabled(plugins, config, "error_handling", || {
259        let mut p = ErrorHandlingAnalyzer::default();
260        apply_usize(
261            config,
262            "error_handling",
263            "max_unwraps_per_function",
264            &mut p.max_unwraps_per_function,
265        );
266        Box::new(p)
267    });
268    register_if_enabled(plugins, config, "todo_tracker", || {
269        Box::new(TodoTrackerAnalyzer)
270    });
271    register_if_enabled(plugins, config, "unsafe_api", || {
272        Box::new(UnsafeApiAnalyzer)
273    });
274}
275
276fn register_length(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
277    if !config.is_enabled("length") {
278        return;
279    }
280    let mut p = LengthAnalyzer::default();
281    if let Some(v) = config.get_usize("length", "max_function_lines") {
282        p.max_function_lines = v;
283    }
284    if let Some(v) = config.get_usize("length", "max_class_methods") {
285        p.max_class_methods = v;
286    }
287    if let Some(v) = config.get_usize("length", "max_class_lines") {
288        p.max_class_lines = v;
289    }
290    if let Some(v) = config.get_usize("length", "max_file_lines") {
291        p.max_file_lines = v;
292    }
293    plugins.push(Box::new(p));
294}
295
296fn register_complexity(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
297    if !config.is_enabled("complexity") {
298        return;
299    }
300    let mut p = ComplexityAnalyzer::default();
301    if let Some(v) = config.get_usize("complexity", "warn_threshold") {
302        p.warn_threshold = v;
303    }
304    if let Some(v) = config.get_usize("complexity", "error_threshold") {
305        p.error_threshold = v;
306    }
307    plugins.push(Box::new(p));
308}
309
310fn register_layer_violation(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
311    if !config.is_enabled("layer_violation") {
312        return;
313    }
314    let p = config
315        .get_str("layer_violation", "layers")
316        .map(|s| LayerViolationAnalyzer::from_config_str(&s))
317        .unwrap_or_default();
318    plugins.push(Box::new(p));
319}