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 pub context_before: Vec<String>,
23 pub context_after: Vec<String>,
25}
26
27pub struct PatternMatcher {
29 exclusions: Vec<String>,
30 searcher: TextSearcher,
31 patterns: Vec<Regex>,
32}
33
34impl PatternMatcher {
35 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 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 pub fn set_exclusions(&mut self, exclusions: Vec<String>) {
55 self.exclusions = exclusions;
56 }
57
58 pub fn find_usages(&self, key_path: &str) -> Result<Vec<CodeReference>> {
60 let matches = self.searcher.search(key_path)?;
62
63 let mut code_refs = Vec::new();
64
65 for m in matches {
66 let file_str = m.file.to_string_lossy();
68 if self.exclusions.iter().any(|ex| file_str.contains(ex)) {
69 continue;
70 }
71
72 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 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 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 for pattern in &self.patterns {
97 if let Some(captures) = pattern.captures(&m.content) {
98 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; }
112 }
113 }
114 }
115 }
116
117 Ok(code_refs)
118 }
119
120 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['"]\)"#).unwrap()];
169 let matcher =
170 PatternMatcher::with_patterns(custom_patterns, std::env::current_dir().unwrap());
171 assert_eq!(matcher.patterns.len(), 1);
172 }
173}