cs/
lib.rs

1pub mod config;
2pub mod error;
3pub mod output;
4pub mod parse;
5pub mod search;
6pub mod trace;
7pub mod tree;
8
9use std::path::PathBuf;
10
11// Re-export commonly used types
12pub use config::default_patterns;
13pub use error::{Result, SearchError};
14pub use output::TreeFormatter;
15pub use parse::{KeyExtractor, TranslationEntry, YamlParser};
16pub use search::{CodeReference, Match, PatternMatcher, TextSearcher};
17pub use trace::{
18    CallExtractor, CallGraphBuilder, CallNode, CallTree, FunctionDef, FunctionFinder,
19    TraceDirection,
20};
21pub use tree::{Location, NodeType, ReferenceTree, ReferenceTreeBuilder, TreeNode};
22
23/// Query parameters for tracing
24#[derive(Debug, Clone)]
25pub struct TraceQuery {
26    pub function_name: String,
27    pub direction: TraceDirection,
28    pub max_depth: usize,
29    pub base_dir: Option<PathBuf>,
30    pub exclude_patterns: Vec<String>,
31}
32
33impl TraceQuery {
34    pub fn new(function_name: String, direction: TraceDirection, max_depth: usize) -> Self {
35        Self {
36            function_name,
37            direction,
38            max_depth,
39            base_dir: None,
40            exclude_patterns: Vec::new(),
41        }
42    }
43
44    pub fn with_base_dir(mut self, base_dir: PathBuf) -> Self {
45        self.base_dir = Some(base_dir);
46        self
47    }
48
49    pub fn with_exclusions(mut self, exclusions: Vec<String>) -> Self {
50        self.exclude_patterns = exclusions;
51        self
52    }
53}
54
55/// Query parameters for searching
56#[derive(Debug, Clone)]
57pub struct SearchQuery {
58    pub text: String,
59    pub case_sensitive: bool,
60    pub base_dir: Option<PathBuf>,
61    pub exclude_patterns: Vec<String>,
62}
63
64impl SearchQuery {
65    pub fn new(text: String) -> Self {
66        Self {
67            text,
68            case_sensitive: false,
69            base_dir: None,
70            exclude_patterns: Vec::new(),
71        }
72    }
73
74    pub fn with_case_sensitive(mut self, case_sensitive: bool) -> Self {
75        self.case_sensitive = case_sensitive;
76        self
77    }
78
79    pub fn with_base_dir(mut self, base_dir: PathBuf) -> Self {
80        self.base_dir = Some(base_dir);
81        self
82    }
83
84    pub fn with_exclusions(mut self, exclusions: Vec<String>) -> Self {
85        self.exclude_patterns = exclusions;
86        self
87    }
88}
89
90/// Result of a search operation
91#[derive(Debug)]
92pub struct SearchResult {
93    pub query: String,
94    pub translation_entries: Vec<TranslationEntry>,
95    pub code_references: Vec<CodeReference>,
96}
97
98/// Main orchestrator function that coordinates the entire search workflow
99///
100/// This function:
101/// 1. Searches for translation entries matching the query text
102/// 2. Extracts translation keys from YAML files
103/// 3. Finds code references for each translation key
104/// 4. Returns a SearchResult with all findings
105pub fn run_search(query: SearchQuery) -> Result<SearchResult> {
106    // Determine the base directory to search
107    let base_dir = query
108        .base_dir
109        .clone()
110        .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
111
112    // Compute exclusions: Default (based on project type) + Manual (from query)
113    let project_type = config::detect_project_type(&base_dir);
114    let mut exclusions: Vec<String> = config::get_default_exclusions(project_type)
115        .iter()
116        .map(|&s| s.to_string())
117        .collect();
118    exclusions.extend(query.exclude_patterns.clone());
119
120    // Step 1: Extract translation entries matching the search text
121    let mut extractor = KeyExtractor::new();
122    extractor.set_exclusions(exclusions.clone());
123    let translation_entries = extractor.extract(&base_dir, &query.text)?;
124
125    if translation_entries.is_empty() {
126        return Ok(SearchResult {
127            query: query.text,
128            translation_entries: vec![],
129            code_references: vec![],
130        });
131    }
132
133    // Step 2: Find code references for each translation entry
134    // Search for full key AND partial keys (for namespace caching patterns)
135    let mut matcher = PatternMatcher::new(base_dir);
136    matcher.set_exclusions(exclusions);
137    let mut all_code_refs = Vec::new();
138
139    for entry in &translation_entries {
140        // Generate all key variations (full key + partial keys)
141        let key_variations = generate_partial_keys(&entry.key);
142
143        // Search for each key variation
144        for key in &key_variations {
145            let code_refs = matcher.find_usages(key)?;
146            all_code_refs.extend(code_refs);
147        }
148    }
149
150    // Deduplicate code references (in case same reference matches multiple key variations)
151    all_code_refs.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
152    all_code_refs.dedup_by(|a, b| a.file == b.file && a.line == b.line);
153
154    Ok(SearchResult {
155        query: query.text,
156        translation_entries,
157        code_references: all_code_refs,
158    })
159}
160
161/// Orchestrates the call graph tracing process
162///
163/// This function:
164/// 1. Finds the starting function definition
165/// 2. Extracts function calls or callers based on the direction
166/// 3. Builds a call graph tree up to the specified depth
167///
168/// # Arguments
169/// * `query` - Configuration for the trace operation
170///
171/// # Returns
172/// A `CallTree` representing the call graph, or `None` if the start function is not found.
173pub fn run_trace(query: TraceQuery) -> Result<Option<CallTree>> {
174    let base_dir = query
175        .base_dir
176        .clone()
177        .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
178
179    let finder = FunctionFinder::new(base_dir.clone());
180    if let Some(start_fn) = finder.find_function(&query.function_name) {
181        let extractor = CallExtractor::new(base_dir);
182        let builder = CallGraphBuilder::new(query.direction, query.max_depth, &finder, &extractor);
183        builder.build_trace(&start_fn)
184    } else {
185        Ok(None)
186    }
187}
188
189/// Helper function to filter translation files from search results
190pub fn filter_translation_files(matches: &[Match]) -> Vec<PathBuf> {
191    matches
192        .iter()
193        .filter(|m| {
194            let path = m.file.to_string_lossy();
195            path.ends_with(".yml") || path.ends_with(".yaml")
196        })
197        .map(|m| m.file.clone())
198        .collect()
199}
200
201/// Generate partial keys from a full translation key for common i18n patterns
202///
203/// For a key like "invoice.labels.add_new", this generates:
204/// - "invoice.labels.add_new" (full key)
205/// - "labels.add_new" (without first segment - namespace pattern)
206/// - "invoice.labels" (without last segment - parent namespace pattern)
207pub fn generate_partial_keys(full_key: &str) -> Vec<String> {
208    let mut keys = Vec::new();
209
210    // Always include the full key
211    keys.push(full_key.to_string());
212
213    let segments: Vec<&str> = full_key.split('.').collect();
214
215    // Only generate partial keys if we have at least 2 segments
216    if segments.len() >= 2 {
217        // Generate key without first segment (e.g., "labels.add_new" from "invoice.labels.add_new")
218        // This matches patterns like: ns = I18n.t('invoice.labels'); ns.t('add_new')
219        if segments.len() > 1 {
220            let without_first = segments[1..].join(".");
221            keys.push(without_first);
222        }
223
224        // Generate key without last segment (e.g., "invoice.labels" from "invoice.labels.add_new")
225        // This matches patterns like: labels = I18n.t('invoice.labels'); labels.t('add_new')
226        if segments.len() > 1 {
227            let without_last = segments[..segments.len() - 1].join(".");
228            keys.push(without_last);
229        }
230    }
231
232    keys
233}