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 (cross-platform)
187            // Convert absolute path to relative path for filtering
188            let relative_path_buf = match m.file.strip_prefix(&self.base_dir) {
189                Ok(rel_path) => rel_path.to_path_buf(),
190                Err(_) => m.file.clone(),
191            };
192
193            // Check path components (works on both Unix and Windows)
194            let path_components: Vec<_> = relative_path_buf
195                .components()
196                .map(|c| c.as_os_str().to_string_lossy().to_lowercase())
197                .collect();
198
199            if !path_components.is_empty() {
200                if path_components[0] == "src" {
201                    continue;
202                }
203                if path_components[0] == "tests"
204                    && (path_components.len() < 2 || path_components[1] != "fixtures")
205                {
206                    continue;
207                }
208            }
209
210            let content = &m.content;
211
212            // Check if this line matches any function definition pattern
213            for pattern in &self.patterns {
214                if let Some(captures) = pattern.captures(content) {
215                    if let Some(name_match) = captures.get(1) {
216                        let name = name_match.as_str();
217                        // Ensure exact match (not substring)
218                        if name == func_name {
219                            let file_content = fs::read_to_string(&m.file)?;
220                            let body = file_content
221                                .lines()
222                                .skip(m.line - 1)
223                                .collect::<Vec<_>>()
224                                .join("\n");
225                            results.push(FunctionDef {
226                                name: name.to_string(),
227                                file: m.file.clone(),
228                                line: m.line,
229                                body,
230                            });
231                            break; // Found a match for this line
232                        }
233                    }
234                }
235            }
236        }
237
238        if results.is_empty() {
239            Err(SearchError::Generic(format!(
240                "Function '{}' not found in codebase",
241                func_name
242            )))
243        } else {
244            // Sort results by file path then line number for deterministic ordering
245            results.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
246            Ok(results)
247        }
248    }
249}
250
251impl Default for FunctionFinder {
252    fn default() -> Self {
253        Self::new(std::env::current_dir().unwrap())
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_function_finder_creation() {
263        let finder = FunctionFinder::new(std::env::current_dir().unwrap());
264        assert!(!finder.patterns.is_empty());
265    }
266
267    #[test]
268    fn test_patterns_compile() {
269        let patterns = FunctionFinder::default_patterns();
270        assert_eq!(patterns.len(), 9);
271    }
272
273    #[test]
274    fn test_js_function_pattern() {
275        let patterns = FunctionFinder::default_patterns();
276        let js_pattern = &patterns[0];
277
278        assert!(js_pattern.is_match("function handleClick() {"));
279        assert!(js_pattern.is_match("function processData(x, y) {"));
280        assert!(!js_pattern.is_match("const x = function() {"));
281    }
282
283    #[test]
284    fn test_arrow_function_pattern() {
285        let patterns = FunctionFinder::default_patterns();
286        let arrow_pattern = &patterns[1];
287
288        assert!(arrow_pattern.is_match("const handleClick = () => {"));
289        assert!(arrow_pattern.is_match("let processData = async (x) => {"));
290        assert!(arrow_pattern.is_match("var foo = (a, b) => {"));
291    }
292
293    #[test]
294    fn test_ruby_pattern() {
295        let patterns = FunctionFinder::default_patterns();
296        let ruby_pattern = &patterns[5]; // Updated index for "def \w+" pattern
297
298        assert!(ruby_pattern.is_match("def process_order"));
299        assert!(ruby_pattern.is_match("  def calculate_total"));
300    }
301
302    #[test]
303    fn test_python_pattern() {
304        let patterns = FunctionFinder::default_patterns();
305        let python_pattern = &patterns[7]; // Updated index for Python pattern
306
307        assert!(python_pattern.is_match("def process_data(x):"));
308        assert!(python_pattern.is_match("    def helper():"));
309    }
310
311    #[test]
312    fn test_rust_pattern() {
313        let patterns = FunctionFinder::default_patterns();
314        let rust_pattern = &patterns[8]; // Updated index for Rust pattern
315
316        assert!(rust_pattern.is_match("fn main() {"));
317        assert!(rust_pattern.is_match("fn process<T>(x: T) {"));
318        assert!(rust_pattern.is_match("pub fn calculate("));
319    }
320
321    #[test]
322    fn test_javascript_export_patterns() {
323        let patterns = FunctionFinder::default_patterns();
324        let export_func_pattern = &patterns[3];
325
326        assert!(export_func_pattern.is_match("export function processData"));
327        assert!(export_func_pattern.is_match("export function calculate"));
328    }
329
330    #[test]
331    fn test_javascript_method_patterns() {
332        let patterns = FunctionFinder::default_patterns();
333        let method_pattern = &patterns[2];
334
335        assert!(method_pattern.is_match("  processData() {"));
336        assert!(method_pattern.is_match("    handleClick() {"));
337        assert!(method_pattern.is_match("  async methodName() {"));
338    }
339
340    #[test]
341    fn test_ruby_class_methods() {
342        let patterns = FunctionFinder::default_patterns();
343        let ruby_class_method_pattern = &patterns[6];
344
345        assert!(ruby_class_method_pattern.is_match("def self.create"));
346        assert!(ruby_class_method_pattern.is_match("  def self.find_by_name"));
347    }
348
349    #[test]
350    fn test_case_conversion() {
351        // Test snake_case conversion
352        assert_eq!(FunctionFinder::to_snake_case("createUser"), "create_user");
353        assert_eq!(
354            FunctionFinder::to_snake_case("validateEmailAddress"),
355            "validate_email_address"
356        );
357        assert_eq!(
358            FunctionFinder::to_snake_case("XMLHttpRequest"),
359            "x_m_l_http_request"
360        );
361        assert_eq!(
362            FunctionFinder::to_snake_case("already_snake"),
363            "already_snake"
364        );
365
366        // Test camelCase conversion
367        assert_eq!(FunctionFinder::to_camel_case("create_user"), "createUser");
368        assert_eq!(
369            FunctionFinder::to_camel_case("validate_email_address"),
370            "validateEmailAddress"
371        );
372        assert_eq!(FunctionFinder::to_camel_case("single"), "single");
373
374        // Test PascalCase conversion
375        assert_eq!(FunctionFinder::to_pascal_case("create_user"), "CreateUser");
376        assert_eq!(
377            FunctionFinder::to_pascal_case("validate_email_address"),
378            "ValidateEmailAddress"
379        );
380        assert_eq!(FunctionFinder::to_pascal_case("single"), "Single");
381    }
382
383    #[test]
384    fn test_generate_case_variants() {
385        // Test with camelCase input
386        let variants = FunctionFinder::generate_case_variants("createUser");
387        assert!(variants.contains(&"createUser".to_string()));
388        assert!(variants.contains(&"create_user".to_string()));
389        assert!(variants.contains(&"CreateUser".to_string()));
390
391        // Test with snake_case input
392        let variants = FunctionFinder::generate_case_variants("validate_email");
393        assert!(variants.contains(&"validate_email".to_string()));
394        assert!(variants.contains(&"validateEmail".to_string()));
395        assert!(variants.contains(&"ValidateEmail".to_string()));
396
397        // Test with PascalCase input
398        let variants = FunctionFinder::generate_case_variants("ProcessUserData");
399        assert!(variants.contains(&"ProcessUserData".to_string()));
400        assert!(variants.contains(&"process_user_data".to_string()));
401        assert!(variants.contains(&"processUserData".to_string()));
402    }
403}