Skip to main content

cha_core/plugins/
design_pattern.rs

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