cs/search/
pattern_match.rs

1use crate::config::default_patterns;
2use crate::error::Result;
3use crate::parse::translation::TranslationEntry;
4use crate::search::text_search::TextSearcher;
5use regex::Regex;
6use std::path::PathBuf;
7
8/// Represents a code reference to a translation key
9#[derive(Debug, Clone, PartialEq)]
10pub struct CodeReference {
11    /// Path to the file containing the reference
12    pub file: PathBuf,
13    /// Line number (1-indexed)
14    pub line: usize,
15    /// The regex pattern that matched
16    pub pattern: String,
17    /// The actual line of code containing the match
18    pub context: String,
19    /// The translation key path that was matched
20    pub key_path: String,
21    /// Context lines before the match
22    pub context_before: Vec<String>,
23    /// Context lines after the match
24    pub context_after: Vec<String>,
25}
26
27/// Pattern matcher for finding i18n key usage in code
28pub struct PatternMatcher {
29    exclusions: Vec<String>,
30    searcher: TextSearcher,
31    patterns: Vec<Regex>,
32}
33
34impl PatternMatcher {
35    /// Create a new PatternMatcher with default patterns
36    pub fn new(base_dir: PathBuf) -> Self {
37        Self {
38            exclusions: Vec::new(),
39            searcher: TextSearcher::new(base_dir),
40            patterns: default_patterns(),
41        }
42    }
43
44    /// Create a PatternMatcher with custom patterns
45    pub fn with_patterns(patterns: Vec<Regex>, base_dir: PathBuf) -> Self {
46        Self {
47            exclusions: Vec::new(),
48            searcher: TextSearcher::new(base_dir),
49            patterns,
50        }
51    }
52
53    /// Set exclusion patterns (file or directory names to ignore)
54    pub fn set_exclusions(&mut self, exclusions: Vec<String>) {
55        self.exclusions = exclusions;
56    }
57
58    /// Find all code references for a given translation key
59    pub fn find_usages(&self, key_path: &str) -> Result<Vec<CodeReference>> {
60        // Search for the key path using ripgrep
61        let matches = self.searcher.search(key_path)?;
62
63        let mut code_refs = Vec::new();
64
65        for m in matches {
66            // Apply exclusions: skip if any exclusion matches the file path
67            let file_str = m.file.to_string_lossy();
68            if self.exclusions.iter().any(|ex| file_str.contains(ex)) {
69                continue;
70            }
71
72            // Skip tool's own source files and documentation (cross-platform)
73            let file_str = m.file.to_string_lossy().to_lowercase();
74            let path_components: Vec<_> = m
75                .file
76                .components()
77                .map(|c| c.as_os_str().to_string_lossy().to_lowercase())
78                .collect();
79
80            // Check if path starts with "src" or "tests" (but not "tests/fixtures")
81            let skip_file = !path_components.is_empty()
82                && (path_components[0] == "src"
83                    || (path_components[0] == "tests"
84                        && (path_components.len() < 2 || path_components[1] != "fixtures")));
85
86            // Also skip markdown files
87            if skip_file
88                || file_str.ends_with("readme.md")
89                || file_str.ends_with("evaluation.md")
90                || file_str.ends_with(".md")
91            {
92                continue;
93            }
94
95            // Try to match against each pattern
96            for pattern in &self.patterns {
97                if let Some(captures) = pattern.captures(&m.content) {
98                    // Extract the key from the capture group
99                    if let Some(captured_key) = captures.get(1) {
100                        if captured_key.as_str() == key_path {
101                            code_refs.push(CodeReference {
102                                file: m.file.clone(),
103                                line: m.line,
104                                pattern: pattern.as_str().to_string(),
105                                context: m.content.clone(),
106                                key_path: key_path.to_string(),
107                                context_before: m.context_before.clone(),
108                                context_after: m.context_after.clone(),
109                            });
110                            break; // Found a match, no need to check other patterns
111                        }
112                    }
113                }
114            }
115        }
116
117        Ok(code_refs)
118    }
119
120    /// Find usages for multiple translation entries
121    pub fn find_usages_batch(&self, entries: &[TranslationEntry]) -> Result<Vec<CodeReference>> {
122        let mut all_refs = Vec::new();
123
124        for entry in entries {
125            let refs = self.find_usages(&entry.key)?;
126            all_refs.extend(refs);
127        }
128
129        Ok(all_refs)
130    }
131}
132
133impl Default for PatternMatcher {
134    fn default() -> Self {
135        Self::new(std::env::current_dir().unwrap())
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_pattern_matcher_creation() {
145        let matcher = PatternMatcher::new(std::env::current_dir().unwrap());
146        assert!(!matcher.patterns.is_empty());
147    }
148
149    #[test]
150    fn test_code_reference_creation() {
151        let code_ref = CodeReference {
152            file: PathBuf::from("test.rb"),
153            line: 10,
154            pattern: r#"I18n\.t\(['"]([^'"]+)['"]\)"#.to_string(),
155            context: r#"I18n.t('invoice.labels.add_new')"#.to_string(),
156            key_path: "invoice.labels.add_new".to_string(),
157            context_before: vec![],
158            context_after: vec![],
159        };
160
161        assert_eq!(code_ref.file, PathBuf::from("test.rb"));
162        assert_eq!(code_ref.line, 10);
163        assert_eq!(code_ref.key_path, "invoice.labels.add_new");
164    }
165
166    #[test]
167    fn test_pattern_matcher_with_custom_patterns() {
168        let custom_patterns = vec![Regex::new(r#"custom\.t\(['"]([^'"]+)['"]\)"#).unwrap()];
169        let matcher =
170            PatternMatcher::with_patterns(custom_patterns, std::env::current_dir().unwrap());
171        assert_eq!(matcher.patterns.len(), 1);
172    }
173}