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