Skip to main content

tldr_core/patterns/
detector.rs

1//! Pattern detector - Single-pass AST walker for pattern extraction
2//!
3//! This module provides the PatternDetector that walks an AST once and
4//! collects signals for all pattern categories simultaneously.
5
6use std::path::PathBuf;
7
8use regex::Regex;
9use tree_sitter::{Node, Tree};
10
11use super::signals::*;
12use crate::types::{Evidence, Language};
13
14/// Pattern detector that extracts all pattern signals in a single AST pass
15pub struct PatternDetector {
16    language: Language,
17    file_path: PathBuf,
18}
19
20impl PatternDetector {
21    /// Create a new pattern detector for a specific language and file
22    pub fn new(language: Language, file_path: PathBuf) -> Self {
23        Self {
24            language,
25            file_path,
26        }
27    }
28
29    /// Detect all patterns from a parsed AST tree
30    pub fn detect_all(&self, tree: &Tree, source: &str) -> PatternSignals {
31        let mut signals = PatternSignals::default();
32        self.walk_node(tree.root_node(), source, &mut signals);
33        signals
34    }
35
36    /// Fallback detection using regex for files with parse errors (A23 mitigation)
37    pub fn detect_fallback(&self, source: &str) -> PatternSignals {
38        let mut signals = PatternSignals::default();
39        self.detect_fallback_patterns(source, &mut signals);
40        signals
41    }
42
43    /// Recursively walk AST nodes and collect signals
44    fn walk_node(&self, node: Node, source: &str, signals: &mut PatternSignals) {
45        self.process_node(node, source, signals);
46
47        let mut cursor = node.walk();
48        for child in node.children(&mut cursor) {
49            self.walk_node(child, source, signals);
50        }
51    }
52
53    /// Process a single AST node and extract signals
54    fn process_node(&self, node: Node, source: &str, signals: &mut PatternSignals) {
55        if let Some(profile) = super::language_profile::language_profile(self.language) {
56            profile.process_node(node, source, &self.file_path, signals);
57        }
58    }
59
60    /// Fallback pattern detection using regex (for files with parse errors)
61    fn detect_fallback_patterns(&self, source: &str, signals: &mut PatternSignals) {
62        // Soft delete patterns
63        let is_deleted_re = Regex::new(r"(?i)(is_deleted|isDeleted)\s*[=:]").unwrap();
64        let deleted_at_re = Regex::new(r"(?i)(deleted_at|deletedAt)\s*[=:]").unwrap();
65
66        for (line_num, line) in source.lines().enumerate() {
67            if is_deleted_re.is_match(line) {
68                signals.soft_delete.is_deleted_fields.push(Evidence::new(
69                    self.file_path.display().to_string(),
70                    line_num as u32 + 1,
71                    line.to_string(),
72                ));
73            }
74            if deleted_at_re.is_match(line) {
75                signals.soft_delete.deleted_at_fields.push(Evidence::new(
76                    self.file_path.display().to_string(),
77                    line_num as u32 + 1,
78                    line.to_string(),
79                ));
80            }
81        }
82
83        // Error handling patterns
84        if source.contains("try:") || source.contains("try {") {
85            signals.error_handling.try_except_blocks.push(Evidence::new(
86                self.file_path.display().to_string(),
87                1,
88                "try block detected".to_string(),
89            ));
90        }
91
92        // Async patterns
93        if source.contains("async ") || source.contains("await ") {
94            signals.async_patterns.async_await.push(Evidence::new(
95                self.file_path.display().to_string(),
96                1,
97                "async/await detected".to_string(),
98            ));
99        }
100    }
101}