cs/trace/
function_finder.rs1use crate::error::{Result, SearchError};
2use crate::search::TextSearcher;
3use regex::Regex;
4use std::collections::HashSet;
5use std::fs;
6use std::path::PathBuf;
7
8#[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
17pub struct FunctionFinder {
19 searcher: TextSearcher,
20 patterns: Vec<Regex>,
21 base_dir: PathBuf,
22}
23
24impl FunctionFinder {
25 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 fn default_patterns() -> Vec<Regex> {
39 vec![
40 Regex::new(r"function\s+(\w+)\s*\(").unwrap(),
42 Regex::new(r"(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>").unwrap(),
44 Regex::new(r"^\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{").unwrap(),
46 Regex::new(r"export\s+function\s+(\w+)").unwrap(),
48 Regex::new(r"^\s*(?:public|private|protected|static|async)\s+(\w+)\s*\(").unwrap(),
50 Regex::new(r"def\s+(\w+)").unwrap(),
52 Regex::new(r"def\s+self\.(\w+)").unwrap(),
54 Regex::new(r"def\s+(\w+)\s*\(").unwrap(),
56 Regex::new(r"fn\s+(\w+)\s*[<(]").unwrap(),
58 ]
59 }
60
61 fn generate_case_variants(func_name: &str) -> Vec<String> {
66 let mut variants = HashSet::new();
67
68 variants.insert(func_name.to_string());
70
71 let snake_case = Self::to_snake_case(func_name);
73 variants.insert(snake_case.clone());
74
75 let camel_case = Self::to_camel_case(&snake_case);
77 variants.insert(camel_case.clone());
78
79 let pascal_case = Self::to_pascal_case(&snake_case);
81 variants.insert(pascal_case);
82
83 variants.into_iter().collect()
84 }
85
86 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 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 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 pub fn find_function(&self, func_name: &str) -> Option<FunctionDef> {
150 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 let variants = Self::generate_case_variants(func_name);
159
160 for variant in variants {
161 if variant != func_name {
162 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 pub fn find_definition(&self, func_name: &str) -> Result<Vec<FunctionDef>> {
179 let mut results = Vec::new();
180
181 let matches = self.searcher.search(func_name)?;
183
184 for m in matches {
186 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 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 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; }
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 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]; 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]; 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]; 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 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 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 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 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 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 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}