Skip to main content

archidoc_rust/
pattern_heuristic.rs

1//! Structural heuristics for GoF pattern detection.
2//!
3//! Each heuristic checks for **structural evidence** of a pattern — not proof.
4//! They are intentionally permissive to avoid false negatives: it's better to
5//! verify a module that loosely matches than to miss one that clearly does.
6//!
7//! A heuristic returning `true` means "there is structural evidence consistent
8//! with this pattern." It does NOT mean "this code correctly implements the
9//! pattern." The promotion from `planned` to `verified` reflects structural
10//! alignment, not behavioral correctness.
11
12use std::path::Path;
13
14use syn::{Item, Visibility};
15
16use crate::walker;
17
18/// Check if Rust source code structurally matches the Observer pattern (H1).
19///
20/// Looks for channel types (mpsc, crossbeam, tokio broadcast/watch),
21/// callback type parameters (Fn/FnMut/FnOnce), or event-related identifiers.
22pub fn check_observer(source: &str) -> bool {
23    // String-based heuristics for channel/callback patterns
24    let indicators = [
25        "mpsc::Sender",
26        "mpsc::Receiver",
27        "mpsc::channel",
28        "crossbeam_channel",
29        "broadcast::Sender",
30        "watch::Sender",
31        "watch::Receiver",
32        "Box<dyn Fn",
33        "Box<dyn FnMut",
34        "Box<dyn FnOnce",
35        "Arc<dyn Fn",
36        "impl Fn(",
37        "impl FnMut(",
38        "impl FnOnce(",
39        "-> Receiver",
40        "-> Sender",
41    ];
42
43    for indicator in &indicators {
44        if source.contains(indicator) {
45            return true;
46        }
47    }
48
49    // Parse with syn to check for method names suggesting observer pattern
50    if let Ok(file) = syn::parse_file(source) {
51        for item in &file.items {
52            if let Item::Trait(trait_item) = item {
53                for method in &trait_item.items {
54                    if let syn::TraitItem::Fn(m) = method {
55                        let name = m.sig.ident.to_string();
56                        if matches!(
57                            name.as_str(),
58                            "subscribe"
59                                | "unsubscribe"
60                                | "notify"
61                                | "on_event"
62                                | "on_update"
63                                | "on_change"
64                                | "emit"
65                                | "publish"
66                                | "add_listener"
67                                | "remove_listener"
68                        ) {
69                            return true;
70                        }
71                    }
72                }
73            }
74        }
75    }
76
77    false
78}
79
80/// Check if Rust source code structurally matches the Strategy pattern (H2).
81///
82/// Looks for trait definitions — a Strategy module defines an interchangeable
83/// behavior contract via a trait.
84pub fn check_strategy(source: &str) -> bool {
85    if let Ok(file) = syn::parse_file(source) {
86        for item in &file.items {
87            if let Item::Trait(_) = item {
88                return true;
89            }
90        }
91    }
92    false
93}
94
95/// Check if Rust source code structurally matches the Facade pattern (H3).
96///
97/// Looks for `pub use` re-exports or `pub mod` declarations — a Facade
98/// provides a simplified entry point by re-exporting from submodules.
99pub fn check_facade(source: &str) -> bool {
100    if let Ok(file) = syn::parse_file(source) {
101        let mut pub_use_count = 0;
102        let mut pub_mod_count = 0;
103
104        for item in &file.items {
105            match item {
106                Item::Use(use_item) => {
107                    if matches!(use_item.vis, Visibility::Public(_)) {
108                        pub_use_count += 1;
109                    }
110                }
111                Item::Mod(mod_item) => {
112                    if matches!(mod_item.vis, Visibility::Public(_)) {
113                        pub_mod_count += 1;
114                    }
115                }
116                _ => {}
117            }
118        }
119
120        // A Facade must have at least one pub use or two pub mod declarations
121        pub_use_count >= 1 || pub_mod_count >= 2
122    } else {
123        false
124    }
125}
126
127/// Check if Rust source code structurally matches the Builder pattern.
128///
129/// Looks for chained setter methods returning Self, or a `build()` method.
130pub fn check_builder(source: &str) -> bool {
131    if let Ok(file) = syn::parse_file(source) {
132        for item in &file.items {
133            if let Item::Impl(impl_item) = item {
134                let mut has_self_return = 0;
135                let mut has_build = false;
136
137                for method in &impl_item.items {
138                    if let syn::ImplItem::Fn(m) = method {
139                        let name = m.sig.ident.to_string();
140                        if name == "build" {
141                            has_build = true;
142                        }
143                        // Check for methods returning Self or &mut Self
144                        if let syn::ReturnType::Type(_, ty) = &m.sig.output {
145                            let ty_str = quote::quote!(#ty).to_string();
146                            if ty_str.contains("Self") {
147                                has_self_return += 1;
148                            }
149                        }
150                    }
151                }
152
153                // Builder pattern: build() method, or 2+ chained setters returning Self
154                if has_build || has_self_return >= 2 {
155                    return true;
156                }
157            }
158        }
159    }
160
161    // String-based fallback
162    let indicators = ["fn build(self)", "fn build(&self)", "fn build(&mut self)"];
163    indicators.iter().any(|i| source.contains(i))
164}
165
166/// Check if Rust source code structurally matches the Factory pattern.
167///
168/// Looks for functions returning trait objects or named create/make methods.
169pub fn check_factory(source: &str) -> bool {
170    let indicators = [
171        "-> Box<dyn",
172        "-> Arc<dyn",
173        "-> Rc<dyn",
174        "fn create(",
175        "fn create_",
176        "fn make(",
177        "fn make_",
178    ];
179
180    for indicator in &indicators {
181        if source.contains(indicator) {
182            return true;
183        }
184    }
185
186    if let Ok(file) = syn::parse_file(source) {
187        for item in &file.items {
188            if let Item::Fn(func) = item {
189                if let syn::ReturnType::Type(_, ty) = &func.sig.output {
190                    let ty_str = quote::quote!(#ty).to_string();
191                    if ty_str.contains("Box < dyn") || ty_str.contains("impl ") {
192                        return true;
193                    }
194                }
195            }
196        }
197    }
198
199    false
200}
201
202/// Check if Rust source code structurally matches the Adapter pattern.
203///
204/// Looks for a struct wrapping another type combined with a trait implementation.
205pub fn check_adapter(source: &str) -> bool {
206    if let Ok(file) = syn::parse_file(source) {
207        let mut has_wrapper_struct = false;
208        let mut has_trait_impl = false;
209
210        for item in &file.items {
211            match item {
212                Item::Struct(s) => {
213                    // A wrapper struct typically has 1-2 fields
214                    if let syn::Fields::Named(fields) = &s.fields {
215                        if (1..=2).contains(&fields.named.len()) {
216                            has_wrapper_struct = true;
217                        }
218                    }
219                }
220                Item::Impl(impl_item) => {
221                    if impl_item.trait_.is_some() {
222                        has_trait_impl = true;
223                    }
224                }
225                _ => {}
226            }
227        }
228
229        return has_wrapper_struct && has_trait_impl;
230    }
231
232    false
233}
234
235/// Check if Rust source code structurally matches the Decorator pattern.
236///
237/// Looks for a struct containing a trait object field that implements the same trait.
238pub fn check_decorator(source: &str) -> bool {
239    let indicators = [
240        "Box<dyn",
241        "Arc<dyn",
242    ];
243
244    let has_trait_object_field = indicators.iter().any(|i| source.contains(i));
245
246    if has_trait_object_field {
247        if let Ok(file) = syn::parse_file(source) {
248            let mut has_struct_with_dyn = false;
249            let mut has_trait_impl = false;
250
251            for item in &file.items {
252                match item {
253                    Item::Struct(s) => {
254                        if let syn::Fields::Named(fields) = &s.fields {
255                            for field in &fields.named {
256                                let ty_str = quote::quote!(#field).to_string();
257                                if ty_str.contains("Box < dyn") || ty_str.contains("Arc < dyn") {
258                                    has_struct_with_dyn = true;
259                                }
260                            }
261                        }
262                    }
263                    Item::Impl(impl_item) => {
264                        if impl_item.trait_.is_some() {
265                            has_trait_impl = true;
266                        }
267                    }
268                    _ => {}
269                }
270            }
271
272            return has_struct_with_dyn && has_trait_impl;
273        }
274    }
275
276    false
277}
278
279/// Check if Rust source code structurally matches the Singleton pattern.
280///
281/// Looks for static/lazy initialization patterns or instance() methods.
282pub fn check_singleton(source: &str) -> bool {
283    let indicators = [
284        "lazy_static!",
285        "once_cell::sync::Lazy",
286        "OnceLock",
287        "OnceCell",
288        "static ref ",
289        "fn instance()",
290        "fn get_instance()",
291    ];
292
293    indicators.iter().any(|i| source.contains(i))
294}
295
296/// Check if Rust source code structurally matches the Command pattern.
297///
298/// Looks for traits with execute/run methods, or enums used for dispatch.
299pub fn check_command(source: &str) -> bool {
300    if let Ok(file) = syn::parse_file(source) {
301        for item in &file.items {
302            if let Item::Trait(trait_item) = item {
303                for method in &trait_item.items {
304                    if let syn::TraitItem::Fn(m) = method {
305                        let name = m.sig.ident.to_string();
306                        if matches!(
307                            name.as_str(),
308                            "execute" | "exec" | "run" | "invoke" | "perform" | "undo" | "redo"
309                        ) {
310                            return true;
311                        }
312                    }
313                }
314            }
315        }
316    }
317
318    false
319}
320
321/// Run the appropriate heuristic for a named GoF pattern.
322pub fn check_pattern(pattern: &str, source: &str) -> bool {
323    match pattern {
324        "Observer" => check_observer(source),
325        "Strategy" => check_strategy(source),
326        "Facade" => check_facade(source),
327        "Builder" => check_builder(source),
328        "Factory" => check_factory(source),
329        "Adapter" => check_adapter(source),
330        "Decorator" => check_decorator(source),
331        "Singleton" => check_singleton(source),
332        "Command" => check_command(source),
333        _ => false,
334    }
335}
336
337/// Scan all `.rs` files in a module's source directory for structural evidence.
338///
339/// Returns true if ANY file in the directory passes the pattern heuristic.
340/// File discovery is delegated to `walker::read_rs_sources` to keep this
341/// module focused on AST analysis.
342pub fn check_module_pattern(pattern: &str, source_dir: &Path) -> bool {
343    walker::read_rs_sources(source_dir)
344        .iter()
345        .any(|(_, source)| check_pattern(pattern, source))
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn strategy_detects_trait() {
354        let source = r#"
355            pub trait Calculator {
356                fn calculate(&self, prices: &[f64]) -> f64;
357            }
358        "#;
359        assert!(check_strategy(source));
360    }
361
362    #[test]
363    fn strategy_rejects_no_trait() {
364        let source = r#"
365            pub struct SimpleCalc;
366            impl SimpleCalc {
367                pub fn calculate(&self, prices: &[f64]) -> f64 {
368                    prices.iter().sum()
369                }
370            }
371        "#;
372        assert!(!check_strategy(source));
373    }
374
375    #[test]
376    fn facade_detects_pub_use() {
377        let source = r#"
378            pub use crate::calc::Calculator;
379            pub use crate::store::DataStore;
380        "#;
381        assert!(check_facade(source));
382    }
383
384    #[test]
385    fn facade_detects_pub_mod() {
386        let source = r#"
387            pub mod calc;
388            pub mod store;
389        "#;
390        assert!(check_facade(source));
391    }
392
393    #[test]
394    fn facade_rejects_private_mods() {
395        let source = r#"
396            mod calc;
397            mod store;
398        "#;
399        assert!(!check_facade(source));
400    }
401
402    #[test]
403    fn observer_detects_channel() {
404        let source = r#"
405            use std::sync::mpsc::Sender;
406            use std::sync::mpsc::Receiver;
407            pub fn create_bus() -> (mpsc::Sender<Event>, mpsc::Receiver<Event>) {
408                std::sync::mpsc::channel()
409            }
410        "#;
411        assert!(check_observer(source));
412    }
413
414    #[test]
415    fn observer_detects_callback_trait() {
416        let source = r#"
417            pub trait EventBus {
418                fn subscribe(&mut self, handler: Box<dyn Fn(Event)>);
419                fn notify(&self, event: Event);
420            }
421        "#;
422        assert!(check_observer(source));
423    }
424
425    #[test]
426    fn observer_rejects_plain_struct() {
427        let source = r#"
428            pub struct Logger {
429                path: String,
430            }
431            impl Logger {
432                pub fn log(&self, msg: &str) {
433                    println!("{}", msg);
434                }
435            }
436        "#;
437        assert!(!check_observer(source));
438    }
439
440    #[test]
441    fn check_pattern_dispatches_correctly() {
442        let strategy_src = "pub trait Algo { fn run(&self); }";
443        assert!(check_pattern("Strategy", strategy_src));
444        assert!(!check_pattern("Observer", strategy_src));
445        assert!(!check_pattern("UnknownPattern", strategy_src));
446    }
447
448}