cs/trace/
function_finder.rs

1use crate::error::{Result, SearchError};
2use crate::search::TextSearcher;
3use regex::Regex;
4use std::collections::HashSet;
5use std::fs;
6use std::path::PathBuf;
7
8/// Represents a function definition found in code
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct FunctionDef {
11    pub name: String,
12    pub file: PathBuf,
13    pub line: usize,
14    pub body: String,
15}
16
17/// Finds function definitions in code using pattern matching
18pub struct FunctionFinder {
19    searcher: TextSearcher,
20    patterns: Vec<Regex>,
21    base_dir: PathBuf,
22}
23
24impl FunctionFinder {
25    /// Create a new FunctionFinder
26    ///
27    /// # Arguments
28    /// * `base_dir` - The base directory of the project to search in
29    pub fn new(base_dir: PathBuf) -> Self {
30        Self {
31            searcher: TextSearcher::new(base_dir.clone()),
32            patterns: Self::default_patterns(),
33            base_dir,
34        }
35    }
36
37    /// Default patterns for finding function definitions across languages
38    fn default_patterns() -> Vec<Regex> {
39        vec![
40            // JavaScript/TypeScript - function declarations
41            Regex::new(r"function\s+(\w+)\s*\(").unwrap(),
42            // JavaScript/TypeScript - arrow functions
43            Regex::new(r"(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>").unwrap(),
44            // JavaScript/TypeScript - method definitions
45            Regex::new(r"^\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{").unwrap(),
46            // JavaScript/TypeScript - export functions
47            Regex::new(r"export\s+function\s+(\w+)").unwrap(),
48            // JavaScript/TypeScript - class methods with modifiers
49            Regex::new(r"^\s*(?:public|private|protected|static|async)\s+(\w+)\s*\(").unwrap(),
50            // Ruby - method definitions
51            Regex::new(r"def\s+(\w+)").unwrap(),
52            // Ruby - class methods
53            Regex::new(r"def\s+self\.(\w+)").unwrap(),
54            // Python - function definitions
55            Regex::new(r"def\s+(\w+)\s*\(").unwrap(),
56            // Rust - function definitions
57            Regex::new(r"fn\s+(\w+)\s*[<(]").unwrap(),
58        ]
59    }
60
61    /// Generate case variants of a function name for cross-case searching
62    ///
63    /// For input "createUser" generates: ["createUser", "create_user", "CreateUser"]
64    /// For input "user_profile" generates: ["user_profile", "userProfile", "UserProfile"]
65    fn generate_case_variants(func_name: &str) -> Vec<String> {
66        let mut variants = HashSet::new();
67
68        // Always include the original
69        variants.insert(func_name.to_string());
70
71        // Generate snake_case variant
72        let snake_case = Self::to_snake_case(func_name);
73        variants.insert(snake_case.clone());
74
75        // Generate camelCase variant
76        let camel_case = Self::to_camel_case(&snake_case);
77        variants.insert(camel_case.clone());
78
79        // Generate PascalCase variant
80        let pascal_case = Self::to_pascal_case(&snake_case);
81        variants.insert(pascal_case);
82
83        variants.into_iter().collect()
84    }
85
86    /// Convert to snake_case
87    fn to_snake_case(input: &str) -> String {
88        let mut result = String::new();
89
90        for (i, ch) in input.chars().enumerate() {
91            if ch.is_uppercase() && i > 0 {
92                result.push('_');
93            }
94            result.push(ch.to_lowercase().next().unwrap());
95        }
96
97        result
98    }
99
100    /// Convert snake_case to camelCase
101    fn to_camel_case(input: &str) -> String {
102        let parts: Vec<&str> = input.split('_').collect();
103        if parts.is_empty() {
104            return String::new();
105        }
106
107        let mut result = parts[0].to_lowercase();
108        for part in parts.iter().skip(1) {
109            if !part.is_empty() {
110                let mut chars = part.chars();
111                if let Some(first) = chars.next() {
112                    result.push(first.to_uppercase().next().unwrap());
113                    result.push_str(&chars.as_str().to_lowercase());
114                }
115            }
116        }
117
118        result
119    }
120
121    /// Convert snake_case to PascalCase
122    fn to_pascal_case(input: &str) -> String {
123        let parts: Vec<&str> = input.split('_').collect();
124        let mut result = String::new();
125
126        for part in parts {
127            if !part.is_empty() {
128                let mut chars = part.chars();
129                if let Some(first) = chars.next() {
130                    result.push(first.to_uppercase().next().unwrap());
131                    result.push_str(&chars.as_str().to_lowercase());
132                }
133            }
134        }
135
136        result
137    }
138
139    /// Find a single function definition, preferring exact matches
140    ///
141    /// This method first tries to find an exact match for the function name.
142    /// If that fails, it tries case-insensitive variants (snake_case, camelCase, PascalCase).
143    ///
144    /// # Arguments
145    /// * `func_name` - The name of the function to find
146    ///
147    /// # Returns
148    /// The best matching `FunctionDef`, or `None` if not found.
149    pub fn find_function(&self, func_name: &str) -> Option<FunctionDef> {
150        // Try exact match first
151        if let Ok(mut defs) = self.find_definition(func_name) {
152            if let Some(def) = defs.pop() {
153                return Some(def);
154            }
155        }
156
157        // Try case variants if exact match fails
158        let variants = Self::generate_case_variants(func_name);
159
160        for variant in variants {
161            if variant != func_name {
162                // Skip the exact match we already tried
163                if let Ok(mut defs) = self.find_definition(&variant) {
164                    if let Some(def) = defs.pop() {
165                        return Some(def);
166                    }
167                }
168            }
169        }
170
171        None
172    }
173
174    /// Find all definitions of a function by name
175    ///
176    /// Searches the codebase for function definitions matching the given name.
177    /// Returns all found definitions with their file locations.
178    pub fn find_definition(&self, func_name: &str) -> Result<Vec<FunctionDef>> {
179        let mut results = Vec::new();
180
181        // Search for the function name in code
182        let matches = self.searcher.search(func_name)?;
183
184        // Filter matches that look like function definitions
185        for m in matches {
186            // Filter out the tool's own source files
187            // Convert absolute path to relative path for filtering
188            let relative_path = match m.file.strip_prefix(&self.base_dir) {
189                Ok(rel_path) => rel_path.to_string_lossy().to_lowercase(),
190                Err(_) => m.file.to_string_lossy().to_lowercase(),
191            };
192
193            if relative_path.starts_with("src/")
194                || (relative_path.starts_with("tests/")
195                    && !relative_path.starts_with("tests/fixtures/"))
196            {
197                continue;
198            }
199
200            let content = &m.content;
201
202            // Check if this line matches any function definition pattern
203            for pattern in &self.patterns {
204                if let Some(captures) = pattern.captures(content) {
205                    if let Some(name_match) = captures.get(1) {
206                        let name = name_match.as_str();
207                        // Ensure exact match (not substring)
208                        if name == func_name {
209                            let file_content = fs::read_to_string(&m.file)?;
210                            let body = file_content
211                                .lines()
212                                .skip(m.line - 1)
213                                .collect::<Vec<_>>()
214                                .join("\n");
215                            results.push(FunctionDef {
216                                name: name.to_string(),
217                                file: m.file.clone(),
218                                line: m.line,
219                                body,
220                            });
221                            break; // Found a match for this line
222                        }
223                    }
224                }
225            }
226        }
227
228        if results.is_empty() {
229            Err(SearchError::Generic(format!(
230                "Function '{}' not found in codebase",
231                func_name
232            )))
233        } else {
234            // Sort results by file path then line number for deterministic ordering
235            results.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
236            Ok(results)
237        }
238    }
239}
240
241impl Default for FunctionFinder {
242    fn default() -> Self {
243        Self::new(std::env::current_dir().unwrap())
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_function_finder_creation() {
253        let finder = FunctionFinder::new(std::env::current_dir().unwrap());
254        assert!(!finder.patterns.is_empty());
255    }
256
257    #[test]
258    fn test_patterns_compile() {
259        let patterns = FunctionFinder::default_patterns();
260        assert_eq!(patterns.len(), 9);
261    }
262
263    #[test]
264    fn test_js_function_pattern() {
265        let patterns = FunctionFinder::default_patterns();
266        let js_pattern = &patterns[0];
267
268        assert!(js_pattern.is_match("function handleClick() {"));
269        assert!(js_pattern.is_match("function processData(x, y) {"));
270        assert!(!js_pattern.is_match("const x = function() {"));
271    }
272
273    #[test]
274    fn test_arrow_function_pattern() {
275        let patterns = FunctionFinder::default_patterns();
276        let arrow_pattern = &patterns[1];
277
278        assert!(arrow_pattern.is_match("const handleClick = () => {"));
279        assert!(arrow_pattern.is_match("let processData = async (x) => {"));
280        assert!(arrow_pattern.is_match("var foo = (a, b) => {"));
281    }
282
283    #[test]
284    fn test_ruby_pattern() {
285        let patterns = FunctionFinder::default_patterns();
286        let ruby_pattern = &patterns[5]; // Updated index for "def \w+" pattern
287
288        assert!(ruby_pattern.is_match("def process_order"));
289        assert!(ruby_pattern.is_match("  def calculate_total"));
290    }
291
292    #[test]
293    fn test_python_pattern() {
294        let patterns = FunctionFinder::default_patterns();
295        let python_pattern = &patterns[7]; // Updated index for Python pattern
296
297        assert!(python_pattern.is_match("def process_data(x):"));
298        assert!(python_pattern.is_match("    def helper():"));
299    }
300
301    #[test]
302    fn test_rust_pattern() {
303        let patterns = FunctionFinder::default_patterns();
304        let rust_pattern = &patterns[8]; // Updated index for Rust pattern
305
306        assert!(rust_pattern.is_match("fn main() {"));
307        assert!(rust_pattern.is_match("fn process<T>(x: T) {"));
308        assert!(rust_pattern.is_match("pub fn calculate("));
309    }
310
311    #[test]
312    fn test_javascript_export_patterns() {
313        let patterns = FunctionFinder::default_patterns();
314        let export_func_pattern = &patterns[3];
315
316        assert!(export_func_pattern.is_match("export function processData"));
317        assert!(export_func_pattern.is_match("export function calculate"));
318    }
319
320    #[test]
321    fn test_javascript_method_patterns() {
322        let patterns = FunctionFinder::default_patterns();
323        let method_pattern = &patterns[2];
324
325        assert!(method_pattern.is_match("  processData() {"));
326        assert!(method_pattern.is_match("    handleClick() {"));
327        assert!(method_pattern.is_match("  async methodName() {"));
328    }
329
330    #[test]
331    fn test_ruby_class_methods() {
332        let patterns = FunctionFinder::default_patterns();
333        let ruby_class_method_pattern = &patterns[6];
334
335        assert!(ruby_class_method_pattern.is_match("def self.create"));
336        assert!(ruby_class_method_pattern.is_match("  def self.find_by_name"));
337    }
338
339    #[test]
340    fn test_case_conversion() {
341        // Test snake_case conversion
342        assert_eq!(FunctionFinder::to_snake_case("createUser"), "create_user");
343        assert_eq!(
344            FunctionFinder::to_snake_case("validateEmailAddress"),
345            "validate_email_address"
346        );
347        assert_eq!(
348            FunctionFinder::to_snake_case("XMLHttpRequest"),
349            "x_m_l_http_request"
350        );
351        assert_eq!(
352            FunctionFinder::to_snake_case("already_snake"),
353            "already_snake"
354        );
355
356        // Test camelCase conversion
357        assert_eq!(FunctionFinder::to_camel_case("create_user"), "createUser");
358        assert_eq!(
359            FunctionFinder::to_camel_case("validate_email_address"),
360            "validateEmailAddress"
361        );
362        assert_eq!(FunctionFinder::to_camel_case("single"), "single");
363
364        // Test PascalCase conversion
365        assert_eq!(FunctionFinder::to_pascal_case("create_user"), "CreateUser");
366        assert_eq!(
367            FunctionFinder::to_pascal_case("validate_email_address"),
368            "ValidateEmailAddress"
369        );
370        assert_eq!(FunctionFinder::to_pascal_case("single"), "Single");
371    }
372
373    #[test]
374    fn test_generate_case_variants() {
375        // Test with camelCase input
376        let variants = FunctionFinder::generate_case_variants("createUser");
377        assert!(variants.contains(&"createUser".to_string()));
378        assert!(variants.contains(&"create_user".to_string()));
379        assert!(variants.contains(&"CreateUser".to_string()));
380
381        // Test with snake_case input
382        let variants = FunctionFinder::generate_case_variants("validate_email");
383        assert!(variants.contains(&"validate_email".to_string()));
384        assert!(variants.contains(&"validateEmail".to_string()));
385        assert!(variants.contains(&"ValidateEmail".to_string()));
386
387        // Test with PascalCase input
388        let variants = FunctionFinder::generate_case_variants("ProcessUserData");
389        assert!(variants.contains(&"ProcessUserData".to_string()));
390        assert!(variants.contains(&"process_user_data".to_string()));
391        assert!(variants.contains(&"processUserData".to_string()));
392    }
393}