Skip to main content

cha_core/plugins/
design_pattern.rs

1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3// cha:ignore large_class
4pub struct DesignPatternAdvisor {
5    pub strategy_min_arms: usize,
6    pub state_min_arms: usize,
7    pub builder_min_params: usize,
8    pub builder_alt_min_params: usize,
9    pub builder_alt_min_optional: usize,
10    pub null_object_min_count: usize,
11    pub template_min_self_calls: usize,
12    pub template_min_methods: usize,
13    pub type_field_keywords: Vec<String>,
14    pub state_field_keywords: Vec<String>,
15}
16
17impl Default for DesignPatternAdvisor {
18    fn default() -> Self {
19        Self {
20            strategy_min_arms: 4,
21            state_min_arms: 3,
22            builder_min_params: 7,
23            builder_alt_min_params: 5,
24            builder_alt_min_optional: 3,
25            null_object_min_count: 3,
26            template_min_self_calls: 3,
27            template_min_methods: 4,
28            type_field_keywords: ["type", "kind", "role", "action", "mode"]
29                .iter()
30                .map(|s| (*s).into())
31                .collect(),
32            state_field_keywords: ["state", "status"].iter().map(|s| (*s).into()).collect(),
33        }
34    }
35}
36
37impl Plugin for DesignPatternAdvisor {
38    fn name(&self) -> &str {
39        "design_pattern"
40    }
41
42    fn smells(&self) -> Vec<String> {
43        vec![
44            "strategy_pattern".into(),
45            "state_pattern".into(),
46            "builder_pattern".into(),
47            "null_object_pattern".into(),
48            "template_method_pattern".into(),
49            "observer_pattern".into(),
50        ]
51    }
52
53    fn description(&self) -> &str {
54        "Suggest design patterns based on code structure"
55    }
56
57    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
58        let mut findings = Vec::new();
59        self.check_strategy(ctx, &mut findings);
60        self.check_state(ctx, &mut findings);
61        self.check_builder(ctx, &mut findings);
62        self.check_null_object(ctx, &mut findings);
63        self.check_template_method(ctx, &mut findings);
64        check_observer(ctx, &mut findings);
65        findings
66    }
67}
68
69impl DesignPatternAdvisor {
70    fn matches_keyword(name: &str, keywords: &[String]) -> bool {
71        let l = name.to_lowercase();
72        keywords.iter().any(|k| l.contains(k.as_str()))
73    }
74
75    fn check_strategy(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
76        for f in &ctx.model.functions {
77            let target = match f.switch_dispatch_target.as_deref() {
78                Some(t)
79                    if f.switch_arms >= self.strategy_min_arms
80                        && Self::matches_keyword(t, &self.type_field_keywords) =>
81                {
82                    t
83                }
84                _ => continue,
85            };
86            findings.push(hint(
87                ctx,
88                (f.start_line, f.name_col, f.name_end_col, Some(&f.name)),
89                "strategy_pattern",
90                format!(
91                    "Function `{}` dispatches on `{}` with {} arms — consider Strategy pattern",
92                    f.name, target, f.switch_arms
93                ),
94            ));
95        }
96    }
97
98    fn check_state(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
99        for f in &ctx.model.functions {
100            let target = match f.switch_dispatch_target.as_deref() {
101                Some(t)
102                    if f.switch_arms >= self.state_min_arms
103                        && Self::matches_keyword(t, &self.state_field_keywords) =>
104                {
105                    t
106                }
107                _ => continue,
108            };
109            findings.push(hint(
110                ctx,
111                (f.start_line, f.name_col, f.name_end_col, Some(&f.name)),
112                "state_pattern",
113                format!(
114                    "Function `{}` dispatches on `{}` — consider State pattern",
115                    f.name, target
116                ),
117            ));
118        }
119    }
120
121    fn check_builder(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
122        for f in &ctx.model.functions {
123            if f.parameter_count >= self.builder_min_params
124                || (f.parameter_count >= self.builder_alt_min_params
125                    && f.optional_param_count >= self.builder_alt_min_optional)
126            {
127                findings.push(hint(
128                    ctx,
129                    (f.start_line, f.name_col, f.name_end_col, Some(&f.name)),
130                    "builder_pattern",
131                    format!(
132                        "Function `{}` has {} params ({} optional) — consider Builder pattern",
133                        f.name, f.parameter_count, f.optional_param_count
134                    ),
135                ));
136            }
137        }
138    }
139
140    fn check_null_object(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
141        let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
142        for f in &ctx.model.functions {
143            for field in &f.null_check_fields {
144                *counts.entry(field).or_default() += 1;
145            }
146        }
147        for (field, count) in &counts {
148            if *count >= self.null_object_min_count {
149                findings.push(hint(
150                    ctx,
151                    (1, 0, 0, None),
152                    "null_object_pattern",
153                    format!(
154                        "Field `{}` is null-checked in {} functions — consider Null Object pattern",
155                        field, count
156                    ),
157                ));
158            }
159        }
160    }
161
162    fn check_template_method(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
163        for c in &ctx.model.classes {
164            if c.self_call_count >= self.template_min_self_calls
165                && c.method_count >= self.template_min_methods
166            {
167                findings.push(hint(
168                    ctx,
169                    (c.start_line, c.name_col, c.name_end_col, Some(&c.name)),
170                    "template_method_pattern",
171                    format!(
172                        "Class `{}` has a method calling {} self-methods — consider Template Method",
173                        c.name, c.self_call_count
174                    ),
175                ));
176            }
177        }
178    }
179}
180
181/// Observer: class has listener fields and/or notify methods.
182fn check_observer(ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
183    for c in &ctx.model.classes {
184        let msg = match (c.has_listener_field, c.has_notify_method) {
185            (true, true) => format!(
186                "Class `{}` uses Observer pattern — ensure proper subscribe/unsubscribe lifecycle",
187                c.name
188            ),
189            (true, false) => format!(
190                "Class `{}` has listener fields but no notify method — consider completing Observer",
191                c.name
192            ),
193            _ => continue,
194        };
195        findings.push(hint(
196            ctx,
197            (c.start_line, c.name_col, c.name_end_col, Some(&c.name)),
198            "observer_pattern",
199            msg,
200        ));
201    }
202}
203
204fn hint(
205    ctx: &AnalysisContext,
206    loc: (usize, usize, usize, Option<&str>),
207    smell: &str,
208    message: String,
209) -> Finding {
210    Finding {
211        smell_name: smell.into(),
212        category: SmellCategory::OoAbusers,
213        severity: Severity::Hint,
214        location: Location {
215            path: ctx.file.path.clone(),
216            start_line: loc.0,
217            start_col: loc.1,
218            end_line: loc.0,
219            end_col: loc.2,
220            name: loc.3.map(String::from),
221        },
222        message,
223        suggested_refactorings: vec![],
224        ..Default::default()
225    }
226}