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 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
33fn 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
52fn 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
71fn 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
88fn 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
111fn 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
128fn 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}