Skip to main content

cha_core/
registry.rs

1use std::path::Path;
2
3use crate::{
4    Plugin,
5    config::Config,
6    plugins::{
7        ApiSurfaceAnalyzer, BrainMethodAnalyzer, CognitiveComplexityAnalyzer, CommentsAnalyzer,
8        ComplexityAnalyzer, CouplingAnalyzer, DataClassAnalyzer, DataClumpsAnalyzer,
9        DeadCodeAnalyzer, DesignPatternAdvisor, DivergentChangeAnalyzer, DuplicateCodeAnalyzer,
10        ErrorHandlingAnalyzer, FeatureEnvyAnalyzer, GodClassAnalyzer, HardcodedSecretAnalyzer,
11        HubLikeDependencyAnalyzer, InappropriateIntimacyAnalyzer, LayerViolationAnalyzer,
12        LazyClassAnalyzer, LengthAnalyzer, LongParameterListAnalyzer, MessageChainAnalyzer,
13        MiddleManAnalyzer, NamingAnalyzer, PrimitiveObsessionAnalyzer, RefusedBequestAnalyzer,
14        ShotgunSurgeryAnalyzer, SpeculativeGeneralityAnalyzer, SwitchStatementAnalyzer,
15        TemporaryFieldAnalyzer, TodoTrackerAnalyzer, UnsafeApiAnalyzer,
16    },
17    wasm,
18};
19
20/// Manages plugin registration and lifecycle.
21pub struct PluginRegistry {
22    plugins: Vec<Box<dyn Plugin>>,
23}
24
25impl PluginRegistry {
26    /// Build registry from config, applying thresholds. Language-agnostic —
27    /// disabled_smells filtering happens downstream but WASM plugins don't
28    /// get the list. Prefer `from_config_for_language` inside `analyze`.
29    pub fn from_config(config: &Config, project_dir: &Path) -> Self {
30        Self::from_config_for_language(config, project_dir, "")
31    }
32
33    /// Build registry for a specific file language. WASM plugins receive the
34    /// effective disabled_smells list via the reserved `__disabled_smells__`
35    /// option so they can skip work proactively.
36    pub fn from_config_for_language(config: &Config, project_dir: &Path, language: &str) -> Self {
37        let mut plugins: Vec<Box<dyn Plugin>> = Vec::new();
38
39        register_length(&mut plugins, config);
40        register_complexity(&mut plugins, config);
41        register_simple_plugins(&mut plugins, config);
42        register_layer_violation(&mut plugins, config);
43
44        let disabled_smells = config.disabled_smells_for_language(language);
45
46        for mut wp in wasm::load_wasm_plugins(project_dir) {
47            if config.is_enabled(wp.name()) {
48                let mut opts: Vec<(String, wasm::wit::OptionValue)> = Vec::new();
49                if let Some(pc) = config.plugins.get(wp.name()) {
50                    opts.extend(pc.options.iter().filter_map(|(k, v)| {
51                        wasm::toml_to_option_value(v).map(|ov| (k.clone(), ov))
52                    }));
53                }
54                if !disabled_smells.is_empty() {
55                    opts.push((
56                        "__disabled_smells__".into(),
57                        wasm::wit::OptionValue::ListStr(disabled_smells.clone()),
58                    ));
59                }
60                wp.set_options(opts);
61                plugins.push(Box::new(wp));
62            }
63        }
64
65        Self { plugins }
66    }
67
68    pub fn plugins(&self) -> &[Box<dyn Plugin>] {
69        &self.plugins
70    }
71
72    /// Get all plugin names and descriptions from this registry.
73    pub fn plugin_info(&self) -> Vec<(String, String)> {
74        self.plugins
75            .iter()
76            .map(|p| (p.name().to_string(), p.description().to_string()))
77            .collect()
78    }
79}
80
81/// Apply a usize config option to a field if present.
82fn apply_usize(config: &Config, plugin: &str, key: &str, target: &mut usize) {
83    if let Some(v) = config.get_usize(plugin, key) {
84        *target = v;
85    }
86}
87
88/// Generic helper: register a plugin only if enabled.
89fn register_if_enabled(
90    plugins: &mut Vec<Box<dyn Plugin>>,
91    config: &Config,
92    name: &str,
93    build: impl FnOnce() -> Box<dyn Plugin>,
94) {
95    if config.is_enabled(name) {
96        plugins.push(build());
97    }
98}
99
100fn register_simple_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
101    register_classic_plugins(plugins, config);
102    register_smell_plugins(plugins, config);
103}
104
105fn register_classic_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
106    register_if_enabled(plugins, config, "coupling", || {
107        let mut p = CouplingAnalyzer::default();
108        apply_usize(config, "coupling", "max_imports", &mut p.max_imports);
109        Box::new(p)
110    });
111    register_if_enabled(plugins, config, "naming", || {
112        let mut p = NamingAnalyzer::default();
113        apply_usize(config, "naming", "min_name_length", &mut p.min_name_length);
114        apply_usize(config, "naming", "max_name_length", &mut p.max_name_length);
115        Box::new(p)
116    });
117    register_if_enabled(plugins, config, "duplicate_code", || {
118        Box::new(DuplicateCodeAnalyzer)
119    });
120    register_if_enabled(plugins, config, "dead_code", || Box::new(DeadCodeAnalyzer));
121    register_if_enabled(plugins, config, "api_surface", || {
122        let mut p = ApiSurfaceAnalyzer::default();
123        apply_usize(
124            config,
125            "api_surface",
126            "max_exported_count",
127            &mut p.max_exported_count,
128        );
129        Box::new(p)
130    });
131}
132
133fn register_smell_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
134    register_if_enabled(plugins, config, "long_parameter_list", || {
135        let mut p = LongParameterListAnalyzer::default();
136        apply_usize(
137            config,
138            "long_parameter_list",
139            "max_params",
140            &mut p.max_params,
141        );
142        Box::new(p)
143    });
144    register_if_enabled(plugins, config, "switch_statement", || {
145        let mut p = SwitchStatementAnalyzer::default();
146        apply_usize(config, "switch_statement", "max_arms", &mut p.max_arms);
147        Box::new(p)
148    });
149    register_if_enabled(plugins, config, "message_chain", || {
150        let mut p = MessageChainAnalyzer::default();
151        apply_usize(config, "message_chain", "max_depth", &mut p.max_depth);
152        Box::new(p)
153    });
154    register_if_enabled(plugins, config, "primitive_obsession", || {
155        Box::new(PrimitiveObsessionAnalyzer::default())
156    });
157    register_if_enabled(plugins, config, "data_clumps", || {
158        Box::new(DataClumpsAnalyzer::default())
159    });
160    register_if_enabled(plugins, config, "feature_envy", || {
161        Box::new(FeatureEnvyAnalyzer::default())
162    });
163    register_if_enabled(plugins, config, "middle_man", || {
164        Box::new(MiddleManAnalyzer::default())
165    });
166    register_extended_smell_plugins(plugins, config);
167}
168
169fn register_extended_smell_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
170    register_if_enabled(plugins, config, "comments", || {
171        Box::new(CommentsAnalyzer::default())
172    });
173    register_if_enabled(plugins, config, "lazy_class", || {
174        Box::new(LazyClassAnalyzer::default())
175    });
176    register_if_enabled(plugins, config, "data_class", || {
177        Box::new(DataClassAnalyzer::default())
178    });
179    register_if_enabled(plugins, config, "design_pattern", || {
180        Box::new(DesignPatternAdvisor)
181    });
182    register_if_enabled(plugins, config, "temporary_field", || {
183        Box::new(TemporaryFieldAnalyzer::default())
184    });
185    register_if_enabled(plugins, config, "speculative_generality", || {
186        Box::new(SpeculativeGeneralityAnalyzer)
187    });
188    register_change_preventer_plugins(plugins, config);
189    register_advanced_plugins(plugins, config);
190}
191
192fn register_change_preventer_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
193    register_if_enabled(plugins, config, "refused_bequest", || {
194        Box::new(RefusedBequestAnalyzer::default())
195    });
196    register_if_enabled(plugins, config, "shotgun_surgery", || {
197        Box::new(ShotgunSurgeryAnalyzer::default())
198    });
199    register_if_enabled(plugins, config, "divergent_change", || {
200        Box::new(DivergentChangeAnalyzer::default())
201    });
202    register_if_enabled(plugins, config, "inappropriate_intimacy", || {
203        Box::new(InappropriateIntimacyAnalyzer)
204    });
205    register_if_enabled(plugins, config, "hardcoded_secret", || {
206        Box::new(HardcodedSecretAnalyzer)
207    });
208}
209
210// cha:ignore long_method
211fn register_advanced_plugins(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
212    register_if_enabled(plugins, config, "cognitive_complexity", || {
213        let mut p = CognitiveComplexityAnalyzer::default();
214        apply_usize(
215            config,
216            "cognitive_complexity",
217            "threshold",
218            &mut p.threshold,
219        );
220        Box::new(p)
221    });
222    register_if_enabled(plugins, config, "god_class", || {
223        let mut p = GodClassAnalyzer::default();
224        apply_usize(
225            config,
226            "god_class",
227            "max_external_refs",
228            &mut p.max_external_refs,
229        );
230        apply_usize(config, "god_class", "min_wmc", &mut p.min_wmc);
231        Box::new(p)
232    });
233    register_if_enabled(plugins, config, "brain_method", || {
234        let mut p = BrainMethodAnalyzer::default();
235        apply_usize(config, "brain_method", "min_lines", &mut p.min_lines);
236        apply_usize(
237            config,
238            "brain_method",
239            "min_complexity",
240            &mut p.min_complexity,
241        );
242        Box::new(p)
243    });
244    register_if_enabled(plugins, config, "hub_like_dependency", || {
245        let mut p = HubLikeDependencyAnalyzer::default();
246        apply_usize(
247            config,
248            "hub_like_dependency",
249            "max_imports",
250            &mut p.max_imports,
251        );
252        Box::new(p)
253    });
254    register_if_enabled(plugins, config, "error_handling", || {
255        let mut p = ErrorHandlingAnalyzer::default();
256        apply_usize(
257            config,
258            "error_handling",
259            "max_unwraps_per_function",
260            &mut p.max_unwraps_per_function,
261        );
262        Box::new(p)
263    });
264    register_if_enabled(plugins, config, "todo_tracker", || {
265        Box::new(TodoTrackerAnalyzer)
266    });
267    register_if_enabled(plugins, config, "unsafe_api", || {
268        Box::new(UnsafeApiAnalyzer)
269    });
270}
271
272fn register_length(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
273    if !config.is_enabled("length") {
274        return;
275    }
276    let mut p = LengthAnalyzer::default();
277    if let Some(v) = config.get_usize("length", "max_function_lines") {
278        p.max_function_lines = v;
279    }
280    if let Some(v) = config.get_usize("length", "max_class_methods") {
281        p.max_class_methods = v;
282    }
283    if let Some(v) = config.get_usize("length", "max_class_lines") {
284        p.max_class_lines = v;
285    }
286    if let Some(v) = config.get_usize("length", "max_file_lines") {
287        p.max_file_lines = v;
288    }
289    plugins.push(Box::new(p));
290}
291
292fn register_complexity(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
293    if !config.is_enabled("complexity") {
294        return;
295    }
296    let mut p = ComplexityAnalyzer::default();
297    if let Some(v) = config.get_usize("complexity", "warn_threshold") {
298        p.warn_threshold = v;
299    }
300    if let Some(v) = config.get_usize("complexity", "error_threshold") {
301        p.error_threshold = v;
302    }
303    plugins.push(Box::new(p));
304}
305
306fn register_layer_violation(plugins: &mut Vec<Box<dyn Plugin>>, config: &Config) {
307    if !config.is_enabled("layer_violation") {
308        return;
309    }
310    let p = config
311        .get_str("layer_violation", "layers")
312        .map(|s| LayerViolationAnalyzer::from_config_str(&s))
313        .unwrap_or_default();
314    plugins.push(Box::new(p));
315}