use crate::config::default_patterns;
use crate::error::Result;
use crate::parse::translation::TranslationEntry;
use crate::search::text_search::TextSearcher;
use regex::Regex;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq)]
pub struct CodeReference {
pub file: PathBuf,
pub line: usize,
pub pattern: String,
pub context: String,
pub key_path: String,
pub context_before: Vec<String>,
pub context_after: Vec<String>,
}
pub struct PatternMatcher {
exclusions: Vec<String>,
searcher: TextSearcher,
patterns: Vec<Regex>,
}
impl PatternMatcher {
pub fn new(base_dir: PathBuf) -> Self {
Self {
exclusions: Vec::new(),
searcher: TextSearcher::new(base_dir),
patterns: default_patterns(),
}
}
pub fn with_patterns(patterns: Vec<Regex>, base_dir: PathBuf) -> Self {
Self {
exclusions: Vec::new(),
searcher: TextSearcher::new(base_dir),
patterns,
}
}
pub fn set_exclusions(&mut self, exclusions: Vec<String>) {
self.exclusions = exclusions;
}
pub fn find_usages(&self, key_path: &str) -> Result<Vec<CodeReference>> {
let matches = self.searcher.search(key_path)?;
let mut code_refs = Vec::new();
for m in matches {
let file_str = m.file.to_string_lossy();
if self.exclusions.iter().any(|ex| file_str.contains(ex)) {
continue;
}
let file_str = m.file.to_string_lossy().to_lowercase();
let path_components: Vec<_> = m
.file
.components()
.map(|c| c.as_os_str().to_string_lossy().to_lowercase())
.collect();
let skip_file = !path_components.is_empty()
&& (path_components[0] == "src"
|| (path_components[0] == "tests"
&& (path_components.len() < 2 || path_components[1] != "fixtures")));
if skip_file
|| file_str.ends_with("readme.md")
|| file_str.ends_with("evaluation.md")
|| file_str.ends_with(".md")
{
continue;
}
for pattern in &self.patterns {
if let Some(captures) = pattern.captures(&m.content) {
if let Some(captured_key) = captures.get(1) {
if captured_key.as_str() == key_path {
code_refs.push(CodeReference {
file: m.file.clone(),
line: m.line,
pattern: pattern.as_str().to_string(),
context: m.content.clone(),
key_path: key_path.to_string(),
context_before: m.context_before.clone(),
context_after: m.context_after.clone(),
});
break; }
}
}
}
}
Ok(code_refs)
}
pub fn find_usages_batch(&self, entries: &[TranslationEntry]) -> Result<Vec<CodeReference>> {
let mut all_refs = Vec::new();
for entry in entries {
let refs = self.find_usages(&entry.key)?;
all_refs.extend(refs);
}
Ok(all_refs)
}
}
impl Default for PatternMatcher {
fn default() -> Self {
Self::new(std::env::current_dir().unwrap())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pattern_matcher_creation() {
let matcher = PatternMatcher::new(std::env::current_dir().unwrap());
assert!(!matcher.patterns.is_empty());
}
#[test]
fn test_code_reference_creation() {
let code_ref = CodeReference {
file: PathBuf::from("test.rb"),
line: 10,
pattern: r#"I18n\.t\(['"]([^'"]+)['"]\)"#.to_string(),
context: r#"I18n.t('invoice.labels.add_new')"#.to_string(),
key_path: "invoice.labels.add_new".to_string(),
context_before: vec![],
context_after: vec![],
};
assert_eq!(code_ref.file, PathBuf::from("test.rb"));
assert_eq!(code_ref.line, 10);
assert_eq!(code_ref.key_path, "invoice.labels.add_new");
}
#[test]
fn test_pattern_matcher_with_custom_patterns() {
let custom_patterns = vec['"]\)"#).unwrap()];
let matcher =
PatternMatcher::with_patterns(custom_patterns, std::env::current_dir().unwrap());
assert_eq!(matcher.patterns.len(), 1);
}
}