1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3pub 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
181fn 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}