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