cs/search/
pattern_match.rs1use 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#[derive(Debug, Clone, PartialEq)]
10pub struct CodeReference {
11 pub file: PathBuf,
13 pub line: usize,
15 pub pattern: String,
17 pub context: String,
19 pub key_path: String,
21}
22
23pub struct PatternMatcher {
25 exclusions: Vec<String>,
26 searcher: TextSearcher,
27 patterns: Vec<Regex>,
28}
29
30impl PatternMatcher {
31 pub fn new(base_dir: PathBuf) -> Self {
33 Self {
34 exclusions: Vec::new(),
35 searcher: TextSearcher::new(base_dir),
36 patterns: default_patterns(),
37 }
38 }
39
40 pub fn with_patterns(patterns: Vec<Regex>, base_dir: PathBuf) -> Self {
42 Self {
43 exclusions: Vec::new(),
44 searcher: TextSearcher::new(base_dir),
45 patterns,
46 }
47 }
48
49 pub fn set_exclusions(&mut self, exclusions: Vec<String>) {
51 self.exclusions = exclusions;
52 }
53
54 pub fn find_usages(&self, key_path: &str) -> Result<Vec<CodeReference>> {
56 let matches = self.searcher.search(key_path)?;
58
59 let mut code_refs = Vec::new();
60
61 for m in matches {
62 let file_str = m.file.to_string_lossy();
64 if self.exclusions.iter().any(|ex| file_str.contains(ex)) {
65 continue;
66 }
67
68 let file_str = m.file.to_string_lossy().to_lowercase();
70 let path_components: Vec<_> = m
71 .file
72 .components()
73 .map(|c| c.as_os_str().to_string_lossy().to_lowercase())
74 .collect();
75
76 let skip_file = !path_components.is_empty()
78 && (path_components[0] == "src"
79 || (path_components[0] == "tests"
80 && (path_components.len() < 2 || path_components[1] != "fixtures")));
81
82 if skip_file
84 || file_str.ends_with("readme.md")
85 || file_str.ends_with("evaluation.md")
86 || file_str.ends_with(".md")
87 {
88 continue;
89 }
90
91 for pattern in &self.patterns {
93 if let Some(captures) = pattern.captures(&m.content) {
94 if let Some(captured_key) = captures.get(1) {
96 if captured_key.as_str() == key_path {
97 code_refs.push(CodeReference {
98 file: m.file.clone(),
99 line: m.line,
100 pattern: pattern.as_str().to_string(),
101 context: m.content.clone(),
102 key_path: key_path.to_string(),
103 });
104 break; }
106 }
107 }
108 }
109 }
110
111 Ok(code_refs)
112 }
113
114 pub fn find_usages_batch(&self, entries: &[TranslationEntry]) -> Result<Vec<CodeReference>> {
116 let mut all_refs = Vec::new();
117
118 for entry in entries {
119 let refs = self.find_usages(&entry.key)?;
120 all_refs.extend(refs);
121 }
122
123 Ok(all_refs)
124 }
125}
126
127impl Default for PatternMatcher {
128 fn default() -> Self {
129 Self::new(std::env::current_dir().unwrap())
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn test_pattern_matcher_creation() {
139 let matcher = PatternMatcher::new(std::env::current_dir().unwrap());
140 assert!(!matcher.patterns.is_empty());
141 }
142
143 #[test]
144 fn test_code_reference_creation() {
145 let code_ref = CodeReference {
146 file: PathBuf::from("test.rb"),
147 line: 10,
148 pattern: r#"I18n\.t\(['"]([^'"]+)['"]\)"#.to_string(),
149 context: r#"I18n.t('invoice.labels.add_new')"#.to_string(),
150 key_path: "invoice.labels.add_new".to_string(),
151 };
152
153 assert_eq!(code_ref.file, PathBuf::from("test.rb"));
154 assert_eq!(code_ref.line, 10);
155 assert_eq!(code_ref.key_path, "invoice.labels.add_new");
156 }
157
158 #[test]
159 fn test_pattern_matcher_with_custom_patterns() {
160 let custom_patterns = vec['"]\)"#).unwrap()];
161 let matcher =
162 PatternMatcher::with_patterns(custom_patterns, std::env::current_dir().unwrap());
163 assert_eq!(matcher.patterns.len(), 1);
164 }
165}