1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3pub 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
44fn 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
63fn 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
82fn 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
99fn 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
122fn 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
139fn 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}