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}
22
23/// Pattern matcher for finding i18n key usage in code
24pub struct PatternMatcher {
25    searcher: TextSearcher,
26    patterns: Vec<Regex>,
27}
28
29impl PatternMatcher {
30    /// Create a new PatternMatcher with default patterns
31    pub fn new(base_dir: PathBuf) -> Self {
32        Self {
33            searcher: TextSearcher::new(base_dir),
34            patterns: default_patterns(),
35        }
36    }
37
38    /// Create a PatternMatcher with custom patterns
39    pub fn with_patterns(patterns: Vec<Regex>, base_dir: PathBuf) -> Self {
40        Self {
41            searcher: TextSearcher::new(base_dir),
42            patterns,
43        }
44    }
45
46    /// Find all code references for a given translation key
47    pub fn find_usages(&self, key_path: &str) -> Result<Vec<CodeReference>> {
48        // Search for the key path using ripgrep
49        let matches = self.searcher.search(key_path)?;
50
51        let mut code_refs = Vec::new();
52
53        for m in matches {
54            // Skip tool's own source files and documentation
55            let file_str = m.file.to_string_lossy().to_lowercase();
56            if file_str.starts_with("src/")
57                || (file_str.starts_with("tests/") && !file_str.starts_with("tests/fixtures/"))
58                || file_str.ends_with("readme.md")
59                || file_str.ends_with("evaluation.md")
60                || file_str.ends_with(".md")
61            {
62                continue;
63            }
64
65            // Try to match against each pattern
66            for pattern in &self.patterns {
67                if let Some(captures) = pattern.captures(&m.content) {
68                    // Extract the key from the capture group
69                    if let Some(captured_key) = captures.get(1) {
70                        if captured_key.as_str() == key_path {
71                            code_refs.push(CodeReference {
72                                file: m.file.clone(),
73                                line: m.line,
74                                pattern: pattern.as_str().to_string(),
75                                context: m.content.clone(),
76                                key_path: key_path.to_string(),
77                            });
78                            break; // Found a match, no need to check other patterns
79                        }
80                    }
81                }
82            }
83        }
84
85        Ok(code_refs)
86    }
87
88    /// Find usages for multiple translation entries
89    pub fn find_usages_batch(&self, entries: &[TranslationEntry]) -> Result<Vec<CodeReference>> {
90        let mut all_refs = Vec::new();
91
92        for entry in entries {
93            let refs = self.find_usages(&entry.key)?;
94            all_refs.extend(refs);
95        }
96
97        Ok(all_refs)
98    }
99}
100
101impl Default for PatternMatcher {
102    fn default() -> Self {
103        Self::new(std::env::current_dir().unwrap())
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_pattern_matcher_creation() {
113        let matcher = PatternMatcher::new(std::env::current_dir().unwrap());
114        assert!(!matcher.patterns.is_empty());
115    }
116
117    #[test]
118    fn test_code_reference_creation() {
119        let code_ref = CodeReference {
120            file: PathBuf::from("test.rb"),
121            line: 10,
122            pattern: r#"I18n\.t\(['"]([^'"]+)['"]\)"#.to_string(),
123            context: r#"I18n.t('invoice.labels.add_new')"#.to_string(),
124            key_path: "invoice.labels.add_new".to_string(),
125        };
126
127        assert_eq!(code_ref.file, PathBuf::from("test.rb"));
128        assert_eq!(code_ref.line, 10);
129        assert_eq!(code_ref.key_path, "invoice.labels.add_new");
130    }
131
132    #[test]
133    fn test_pattern_matcher_with_custom_patterns() {
134        let custom_patterns = vec![
135            Regex::new(r#"custom\.t\(['"]([^'"]+)['"]\)"#).unwrap(),
136        ];
137        let matcher = PatternMatcher::with_patterns(custom_patterns, std::env::current_dir().unwrap());
138        assert_eq!(matcher.patterns.len(), 1);
139    }
140}