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