debtmap/analysis/call_graph/
function_pointer.rs

1//! Function Pointer and Closure Tracking
2//!
3//! This module tracks function pointers, closures, and higher-order functions
4//! to resolve indirect function calls and reduce false positives in dead code detection.
5
6use crate::priority::call_graph::FunctionId;
7use anyhow::Result;
8use im::{HashMap, HashSet, Vector};
9use std::path::Path;
10use syn::visit::Visit;
11use syn::{Expr, ExprCall, ExprClosure, ExprPath, File, Ident, ItemFn, Local, Pat, PatIdent, Type};
12
13/// Information about a closure
14#[derive(Debug, Clone)]
15pub struct ClosureInfo {
16    /// Unique identifier for this closure
17    pub closure_id: String,
18    /// Function that contains this closure
19    pub containing_function: FunctionId,
20    /// Line number where closure is defined
21    pub line: usize,
22    /// Functions that this closure calls
23    pub calls: Vector<FunctionId>,
24    /// Whether this closure captures variables
25    pub captures_variables: bool,
26    /// Parameters captured by reference
27    pub captured_by_ref: HashSet<String>,
28    /// Parameters captured by value
29    pub captured_by_value: HashSet<String>,
30}
31
32/// Information about a function pointer
33#[derive(Debug, Clone)]
34pub struct FunctionPointerInfo {
35    /// Variable name that holds the function pointer
36    pub variable_name: String,
37    /// Function where this pointer is defined
38    pub defining_function: FunctionId,
39    /// Possible target functions
40    pub possible_targets: HashSet<FunctionId>,
41    /// Line number where pointer is defined
42    pub line: usize,
43    /// Whether this is a function parameter
44    pub is_parameter: bool,
45}
46
47/// Information about a function pointer call
48#[derive(Debug, Clone)]
49pub struct FunctionPointerCall {
50    /// Function making the call
51    pub caller: FunctionId,
52    /// Function pointer being called
53    pub pointer_id: String,
54    /// Line number of the call
55    pub line: usize,
56}
57
58/// Information about higher-order function usage
59#[derive(Debug, Clone)]
60pub struct HigherOrderFunctionCall {
61    /// Function making the call
62    pub caller: FunctionId,
63    /// The higher-order function being called (map, filter, etc.)
64    pub hof_function: String,
65    /// Functions passed as arguments
66    pub function_arguments: Vector<FunctionId>,
67    /// Line number of the call
68    pub line: usize,
69}
70
71/// Tracker for function pointers, closures, and higher-order functions
72#[derive(Debug, Clone)]
73pub struct FunctionPointerTracker {
74    /// All closures found
75    closures: HashMap<String, ClosureInfo>,
76    /// All function pointers found
77    function_pointers: HashMap<String, FunctionPointerInfo>,
78    /// Function pointer calls that need resolution
79    pointer_calls: Vector<FunctionPointerCall>,
80    /// Higher-order function calls
81    hof_calls: Vector<HigherOrderFunctionCall>,
82    /// Mapping from variable names to function pointers
83    variable_to_pointer: HashMap<String, String>,
84    /// Functions that might be called through pointers
85    potential_pointer_targets: HashSet<FunctionId>,
86}
87
88impl FunctionPointerTracker {
89    /// Create a new function pointer tracker
90    pub fn new() -> Self {
91        Self {
92            closures: HashMap::new(),
93            function_pointers: HashMap::new(),
94            pointer_calls: Vector::new(),
95            hof_calls: Vector::new(),
96            variable_to_pointer: HashMap::new(),
97            potential_pointer_targets: HashSet::new(),
98        }
99    }
100
101    /// Analyze a file for function pointers and closures
102    pub fn analyze_file(&mut self, file_path: &Path, ast: &File) -> Result<()> {
103        let mut visitor = FunctionPointerVisitor::new(file_path.to_path_buf());
104        visitor.visit_file(ast);
105
106        // Add discovered closures
107        for closure in visitor.closures {
108            let closure_id = closure.closure_id.clone();
109            self.closures.insert(closure_id, closure);
110        }
111
112        // Add discovered function pointers
113        for pointer in visitor.function_pointers {
114            let pointer_id = format!(
115                "{}_{}",
116                pointer.defining_function.name, pointer.variable_name
117            );
118
119            // Update variable mapping
120            self.variable_to_pointer
121                .insert(pointer.variable_name.clone(), pointer_id.clone());
122
123            // Add to potential targets
124            for target in &pointer.possible_targets {
125                self.potential_pointer_targets.insert(target.clone());
126            }
127
128            self.function_pointers.insert(pointer_id, pointer);
129        }
130
131        // Add function pointer calls
132        for call in visitor.pointer_calls {
133            self.pointer_calls.push_back(call);
134        }
135
136        // Add higher-order function calls
137        for hof_call in visitor.hof_calls {
138            // Add targets to potential pointer targets
139            for func_arg in &hof_call.function_arguments {
140                self.potential_pointer_targets.insert(func_arg.clone());
141            }
142            self.hof_calls.push_back(hof_call);
143        }
144
145        Ok(())
146    }
147
148    /// Get all function pointer calls that need resolution
149    pub fn get_function_pointer_calls(&self) -> Vector<FunctionPointerCall> {
150        self.pointer_calls.clone()
151    }
152
153    /// Resolve a function pointer to its possible targets
154    pub fn resolve_pointer_targets(&self, pointer_id: &str) -> Option<Vector<FunctionId>> {
155        self.function_pointers
156            .get(pointer_id)
157            .map(|pointer| pointer.possible_targets.iter().cloned().collect())
158    }
159
160    /// Check if a function might be called through a function pointer
161    pub fn might_be_called_through_pointer(&self, func_id: &FunctionId) -> bool {
162        self.potential_pointer_targets.contains(func_id)
163            || self
164                .closures
165                .values()
166                .any(|closure| closure.calls.contains(func_id))
167    }
168
169    /// Get all higher-order function calls
170    pub fn get_higher_order_calls(&self) -> Vector<HigherOrderFunctionCall> {
171        self.hof_calls.clone()
172    }
173
174    /// Get statistics about function pointer usage
175    pub fn get_statistics(&self) -> FunctionPointerStatistics {
176        let total_closures = self.closures.len();
177        let total_function_pointers = self.function_pointers.len();
178        let total_pointer_calls = self.pointer_calls.len();
179        let total_hof_calls = self.hof_calls.len();
180        let potential_targets = self.potential_pointer_targets.len();
181
182        FunctionPointerStatistics {
183            total_closures,
184            total_function_pointers,
185            total_pointer_calls,
186            total_hof_calls,
187            potential_targets,
188        }
189    }
190
191    /// Get functions that are definitely used through function pointers
192    pub fn get_definitely_used_functions(&self) -> HashSet<FunctionId> {
193        let mut used_functions = HashSet::new();
194
195        // Functions called from closures
196        for closure in self.closures.values() {
197            for called_func in &closure.calls {
198                used_functions.insert(called_func.clone());
199            }
200        }
201
202        // Functions passed to higher-order functions
203        for hof_call in &self.hof_calls {
204            for func_arg in &hof_call.function_arguments {
205                used_functions.insert(func_arg.clone());
206            }
207        }
208
209        used_functions
210    }
211}
212
213/// Statistics about function pointer usage
214#[derive(Debug, Clone)]
215pub struct FunctionPointerStatistics {
216    pub total_closures: usize,
217    pub total_function_pointers: usize,
218    pub total_pointer_calls: usize,
219    pub total_hof_calls: usize,
220    pub potential_targets: usize,
221}
222
223/// Visitor for extracting function pointer and closure information
224struct FunctionPointerVisitor {
225    file_path: std::path::PathBuf,
226    closures: Vec<ClosureInfo>,
227    function_pointers: Vec<FunctionPointerInfo>,
228    pointer_calls: Vec<FunctionPointerCall>,
229    hof_calls: Vec<HigherOrderFunctionCall>,
230    current_function: Option<FunctionId>,
231    closure_counter: usize,
232}
233
234impl FunctionPointerVisitor {
235    fn new(file_path: std::path::PathBuf) -> Self {
236        Self {
237            file_path,
238            closures: Vec::new(),
239            function_pointers: Vec::new(),
240            pointer_calls: Vec::new(),
241            hof_calls: Vec::new(),
242            current_function: None,
243            closure_counter: 0,
244        }
245    }
246
247    fn get_line_number(&self, span: proc_macro2::Span) -> usize {
248        span.start().line
249    }
250
251    fn is_higher_order_function(&self, name: &str) -> bool {
252        matches!(
253            name,
254            "map"
255                | "filter"
256                | "fold"
257                | "reduce"
258                | "for_each"
259                | "find"
260                | "any"
261                | "all"
262                | "collect"
263                | "and_then"
264                | "or_else"
265                | "iter"
266                | "enumerate"
267                | "zip"
268                | "chain"
269                | "take"
270                | "skip"
271        )
272    }
273
274    fn extract_function_name_from_path(&self, path: &ExprPath) -> Option<String> {
275        if path.path.segments.len() == 1 {
276            Some(path.path.segments.first()?.ident.to_string())
277        } else {
278            // For multi-segment paths, join with ::
279            let segments: Vec<String> = path
280                .path
281                .segments
282                .iter()
283                .map(|seg| seg.ident.to_string())
284                .collect();
285            Some(segments.join("::"))
286        }
287    }
288
289    fn analyze_closure(&mut self, closure: &ExprClosure) {
290        if let Some(containing_function) = &self.current_function {
291            self.closure_counter += 1;
292            let closure_id = format!(
293                "{}_closure_{}",
294                containing_function.name, self.closure_counter
295            );
296            let line = self.get_line_number(closure.or1_token.span);
297
298            let mut closure_visitor = ClosureCallVisitor::new();
299            closure_visitor.visit_expr(&closure.body);
300
301            let closure_info = ClosureInfo {
302                closure_id,
303                containing_function: containing_function.clone(),
304                line,
305                calls: closure_visitor.function_calls.into_iter().collect(),
306                captures_variables: closure.capture.is_some(),
307                captured_by_ref: HashSet::new(), // Would need more analysis
308                captured_by_value: HashSet::new(), // Would need more analysis
309            };
310
311            self.closures.push(closure_info);
312        }
313    }
314
315    /// Extract variable name and init expression from a local statement
316    fn extract_pointer_assignment_data(local: &Local) -> Option<(&Ident, &Expr)> {
317        match &local.pat {
318            Pat::Ident(PatIdent { ident, .. }) => {
319                local.init.as_ref().map(|init| (ident, &*init.expr))
320            }
321            _ => None,
322        }
323    }
324
325    fn analyze_function_pointer_assignment(&mut self, local: &Local) {
326        let Some(current_func) = &self.current_function else {
327            return;
328        };
329
330        let Some((ident, init_expr)) = Self::extract_pointer_assignment_data(local) else {
331            return;
332        };
333
334        let var_name = ident.to_string();
335        let line = self.get_line_number(ident.span());
336        let possible_targets = self.extract_possible_targets(init_expr);
337
338        let pointer_info = FunctionPointerInfo {
339            variable_name: var_name,
340            defining_function: current_func.clone(),
341            possible_targets,
342            line,
343            is_parameter: false,
344        };
345
346        self.function_pointers.push(pointer_info);
347    }
348
349    /// Extract possible function targets from an expression
350    fn extract_possible_targets(&self, expr: &Expr) -> HashSet<FunctionId> {
351        let mut possible_targets = HashSet::new();
352
353        if let Expr::Path(path) = expr {
354            if let Some(func_name) = self.extract_function_name_from_path(path) {
355                let target_func = FunctionId {
356                    file: self.file_path.clone(),
357                    name: func_name,
358                    line: 0, // Unknown line for external function
359                };
360                possible_targets.insert(target_func);
361            }
362        }
363
364        possible_targets
365    }
366
367    /// Extract direct function pointer call from expression
368    fn extract_direct_pointer_call(
369        &self,
370        call: &ExprCall,
371        caller: &FunctionId,
372        line: usize,
373    ) -> Option<FunctionPointerCall> {
374        if let Expr::Path(path) = &*call.func {
375            self.extract_function_name_from_path(path)
376                .map(|func_name| FunctionPointerCall {
377                    caller: caller.clone(),
378                    pointer_id: func_name,
379                    line,
380                })
381        } else {
382            None
383        }
384    }
385
386    /// Extract higher-order function call from expression
387    fn extract_hof_call(
388        &self,
389        call: &ExprCall,
390        caller: &FunctionId,
391        line: usize,
392    ) -> Option<HigherOrderFunctionCall> {
393        // Early return if not a path expression
394        let path = match &*call.func {
395            Expr::Path(p) => p,
396            _ => return None,
397        };
398
399        // Extract and validate function name
400        let func_name = self.extract_function_name_from_path(path)?;
401
402        // Check if it's a higher-order function
403        if !self.is_higher_order_function(&func_name) {
404            return None;
405        }
406
407        // Extract function arguments
408        let function_arguments = self.extract_function_arguments(call);
409
410        // Return HOF call if arguments exist
411        (!function_arguments.is_empty()).then(|| HigherOrderFunctionCall {
412            caller: caller.clone(),
413            hof_function: func_name,
414            function_arguments,
415            line,
416        })
417    }
418
419    /// Extract function arguments from call expression
420    fn extract_function_arguments(&self, call: &ExprCall) -> Vector<FunctionId> {
421        let mut function_arguments = Vector::new();
422
423        for arg in &call.args {
424            if let Expr::Path(arg_path) = arg {
425                if let Some(arg_func_name) = self.extract_function_name_from_path(arg_path) {
426                    let func_arg = FunctionId {
427                        file: self.file_path.clone(),
428                        name: arg_func_name,
429                        line: 0,
430                    };
431                    function_arguments.push_back(func_arg);
432                }
433            }
434        }
435
436        function_arguments
437    }
438
439    fn analyze_call_expression(&mut self, call: &ExprCall) {
440        if let Some(caller) = &self.current_function {
441            let line = self.get_line_number(call.paren_token.span.open());
442
443            // Extract and store direct function pointer call
444            if let Some(pointer_call) = self.extract_direct_pointer_call(call, caller, line) {
445                self.pointer_calls.push(pointer_call);
446            }
447
448            // Extract and store higher-order function call
449            if let Some(hof_call) = self.extract_hof_call(call, caller, line) {
450                self.hof_calls.push(hof_call);
451            }
452        }
453    }
454}
455
456impl<'ast> Visit<'ast> for FunctionPointerVisitor {
457    fn visit_item_fn(&mut self, item: &'ast ItemFn) {
458        let func_name = item.sig.ident.to_string();
459        let line = self.get_line_number(item.sig.ident.span());
460
461        self.current_function = Some(FunctionId {
462            file: self.file_path.clone(),
463            name: func_name,
464            line,
465        });
466
467        // Analyze function parameters for function pointers
468        for param in &item.sig.inputs {
469            if let syn::FnArg::Typed(typed_param) = param {
470                if let Type::BareFn(_) = &*typed_param.ty {
471                    // This is a function pointer parameter
472                    if let Pat::Ident(PatIdent { ident, .. }) = &*typed_param.pat {
473                        let param_name = ident.to_string();
474                        let line = self.get_line_number(ident.span());
475
476                        if let Some(current_func) = &self.current_function {
477                            let pointer_info = FunctionPointerInfo {
478                                variable_name: param_name,
479                                defining_function: current_func.clone(),
480                                possible_targets: HashSet::new(), // Unknown targets for parameters
481                                line,
482                                is_parameter: true,
483                            };
484
485                            self.function_pointers.push(pointer_info);
486                        }
487                    }
488                }
489            }
490        }
491
492        // Continue visiting the function body
493        syn::visit::visit_item_fn(self, item);
494
495        self.current_function = None;
496    }
497
498    fn visit_expr_closure(&mut self, expr: &'ast ExprClosure) {
499        self.analyze_closure(expr);
500
501        // Continue visiting
502        syn::visit::visit_expr_closure(self, expr);
503    }
504
505    fn visit_local(&mut self, local: &'ast Local) {
506        self.analyze_function_pointer_assignment(local);
507
508        // Continue visiting
509        syn::visit::visit_local(self, local);
510    }
511
512    fn visit_expr_call(&mut self, call: &'ast ExprCall) {
513        self.analyze_call_expression(call);
514
515        // Continue visiting
516        syn::visit::visit_expr_call(self, call);
517    }
518}
519
520/// Visitor specifically for analyzing function calls within closures
521struct ClosureCallVisitor {
522    function_calls: Vec<FunctionId>,
523}
524
525impl ClosureCallVisitor {
526    fn new() -> Self {
527        Self {
528            function_calls: Vec::new(),
529        }
530    }
531
532    fn extract_function_name_from_path(&self, path: &ExprPath) -> Option<String> {
533        if path.path.segments.len() == 1 {
534            Some(path.path.segments.first()?.ident.to_string())
535        } else {
536            let segments: Vec<String> = path
537                .path
538                .segments
539                .iter()
540                .map(|seg| seg.ident.to_string())
541                .collect();
542            Some(segments.join("::"))
543        }
544    }
545}
546
547impl<'ast> Visit<'ast> for ClosureCallVisitor {
548    fn visit_expr_call(&mut self, call: &'ast ExprCall) {
549        if let Expr::Path(path) = &*call.func {
550            if let Some(func_name) = self.extract_function_name_from_path(path) {
551                let func_id = FunctionId {
552                    file: std::path::PathBuf::new(), // Will be filled in by parent
553                    name: func_name,
554                    line: 0,
555                };
556                self.function_calls.push(func_id);
557            }
558        }
559
560        // Continue visiting
561        syn::visit::visit_expr_call(self, call);
562    }
563
564    fn visit_expr_method_call(&mut self, call: &'ast syn::ExprMethodCall) {
565        let method_name = call.method.to_string();
566        let func_id = FunctionId {
567            file: std::path::PathBuf::new(),
568            name: method_name,
569            line: 0,
570        };
571        self.function_calls.push(func_id);
572
573        // Continue visiting
574        syn::visit::visit_expr_method_call(self, call);
575    }
576}
577
578impl Default for FunctionPointerTracker {
579    fn default() -> Self {
580        Self::new()
581    }
582}
583
584#[cfg(test)]
585mod tests {
586    use super::*;
587    use syn::parse_quote;
588
589    #[test]
590    fn test_extract_direct_pointer_call() {
591        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
592        let caller = FunctionId {
593            file: std::path::PathBuf::from("test.rs"),
594            name: "test_func".to_string(),
595            line: 1,
596        };
597
598        // Test direct function pointer call
599        let call: ExprCall = parse_quote! { func_ptr(42) };
600        let result = visitor.extract_direct_pointer_call(&call, &caller, 10);
601
602        assert!(result.is_some());
603        let pointer_call = result.unwrap();
604        assert_eq!(pointer_call.pointer_id, "func_ptr");
605        assert_eq!(pointer_call.line, 10);
606        assert_eq!(pointer_call.caller.name, "test_func");
607    }
608
609    #[test]
610    fn test_extract_direct_pointer_call_with_method_call() {
611        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
612        let caller = FunctionId {
613            file: std::path::PathBuf::from("test.rs"),
614            name: "test_func".to_string(),
615            line: 1,
616        };
617
618        // Test method call (not a direct pointer call)
619        // Using a different expression type that's not a Path
620        let call: ExprCall = parse_quote! { compute(42) };
621        // Manually change the func to something that's not a Path
622        let mut call = call;
623        call.func = Box::new(parse_quote! { 42 }); // Replace with a literal, not a path
624        let result = visitor.extract_direct_pointer_call(&call, &caller, 10);
625
626        assert!(result.is_none());
627    }
628
629    #[test]
630    fn test_extract_hof_call_map() {
631        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
632        let caller = FunctionId {
633            file: std::path::PathBuf::from("test.rs"),
634            name: "test_func".to_string(),
635            line: 1,
636        };
637
638        // Test higher-order function call with map
639        let call: ExprCall = parse_quote! { map(process_item) };
640        let result = visitor.extract_hof_call(&call, &caller, 15);
641
642        assert!(result.is_some());
643        let hof_call = result.unwrap();
644        assert_eq!(hof_call.hof_function, "map");
645        assert_eq!(hof_call.function_arguments.len(), 1);
646        assert_eq!(hof_call.function_arguments[0].name, "process_item");
647        assert_eq!(hof_call.line, 15);
648    }
649
650    #[test]
651    fn test_extract_hof_call_filter() {
652        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
653        let caller = FunctionId {
654            file: std::path::PathBuf::from("test.rs"),
655            name: "test_func".to_string(),
656            line: 1,
657        };
658
659        // Test higher-order function call with filter
660        let call: ExprCall = parse_quote! { filter(is_valid) };
661        let result = visitor.extract_hof_call(&call, &caller, 20);
662
663        assert!(result.is_some());
664        let hof_call = result.unwrap();
665        assert_eq!(hof_call.hof_function, "filter");
666        assert_eq!(hof_call.function_arguments.len(), 1);
667        assert_eq!(hof_call.function_arguments[0].name, "is_valid");
668    }
669
670    #[test]
671    fn test_extract_hof_call_non_hof() {
672        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
673        let caller = FunctionId {
674            file: std::path::PathBuf::from("test.rs"),
675            name: "test_func".to_string(),
676            line: 1,
677        };
678
679        // Test non-higher-order function call
680        let call: ExprCall = parse_quote! { regular_func(arg) };
681        let result = visitor.extract_hof_call(&call, &caller, 25);
682
683        assert!(result.is_none());
684    }
685
686    #[test]
687    fn test_extract_hof_call_empty_arguments() {
688        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
689        let caller = FunctionId {
690            file: std::path::PathBuf::from("test.rs"),
691            name: "test_func".to_string(),
692            line: 1,
693        };
694
695        // Test higher-order function call with no function arguments
696        let call: ExprCall = parse_quote! { map() };
697        let result = visitor.extract_hof_call(&call, &caller, 30);
698
699        // Should return None when no function arguments
700        assert!(result.is_none());
701    }
702
703    #[test]
704    fn test_extract_hof_call_closure_argument() {
705        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
706        let caller = FunctionId {
707            file: std::path::PathBuf::from("test.rs"),
708            name: "test_func".to_string(),
709            line: 1,
710        };
711
712        // Test call with closure argument (should not extract closure)
713        let call: ExprCall = parse_quote! { map(|x| x + 1) };
714        let result = visitor.extract_hof_call(&call, &caller, 35);
715
716        // Should return None when only closure arguments (not function paths)
717        assert!(result.is_none());
718    }
719
720    #[test]
721    fn test_extract_hof_call_nested_path() {
722        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
723        let caller = FunctionId {
724            file: std::path::PathBuf::from("test.rs"),
725            name: "test_func".to_string(),
726            line: 1,
727        };
728
729        // Test higher-order function with nested path argument
730        let call: ExprCall = parse_quote! { filter(module::is_valid) };
731        let result = visitor.extract_hof_call(&call, &caller, 40);
732
733        assert!(result.is_some());
734        let hof_call = result.unwrap();
735        assert_eq!(hof_call.hof_function, "filter");
736        // Should extract the full path as function name
737        assert_eq!(hof_call.function_arguments.len(), 1);
738        assert_eq!(hof_call.function_arguments[0].name, "module::is_valid");
739    }
740
741    #[test]
742    fn test_extract_function_arguments() {
743        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
744
745        // Test extracting multiple function arguments
746        let call: ExprCall = parse_quote! { fold(initial, combine_func) };
747        let args = visitor.extract_function_arguments(&call);
748
749        assert_eq!(args.len(), 2);
750        assert_eq!(args[0].name, "initial");
751        assert_eq!(args[1].name, "combine_func");
752    }
753
754    #[test]
755    fn test_extract_function_arguments_mixed() {
756        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
757
758        // Test mixed arguments (functions and non-functions)
759        let call: ExprCall = parse_quote! { process(42, handler, "string") };
760        let args = visitor.extract_function_arguments(&call);
761
762        // Should only extract function references
763        assert_eq!(args.len(), 1);
764        assert_eq!(args[0].name, "handler");
765    }
766
767    #[test]
768    fn test_extract_function_arguments_empty() {
769        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
770
771        // Test call with no function arguments
772        let call: ExprCall = parse_quote! { compute(42, "string", true) };
773        let args = visitor.extract_function_arguments(&call);
774
775        assert!(args.is_empty());
776    }
777
778    #[test]
779    fn test_is_higher_order_function() {
780        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
781
782        // Test HOF detection
783        assert!(visitor.is_higher_order_function("map"));
784        assert!(visitor.is_higher_order_function("filter"));
785        assert!(visitor.is_higher_order_function("fold"));
786        assert!(visitor.is_higher_order_function("for_each"));
787        assert!(visitor.is_higher_order_function("find"));
788        assert!(visitor.is_higher_order_function("any"));
789        assert!(visitor.is_higher_order_function("all"));
790
791        // Test non-HOF
792        assert!(!visitor.is_higher_order_function("process"));
793        assert!(!visitor.is_higher_order_function("compute"));
794        assert!(!visitor.is_higher_order_function("regular_func"));
795    }
796
797    #[test]
798    fn test_analyze_call_expression_integration() {
799        let mut visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
800        visitor.current_function = Some(FunctionId {
801            file: std::path::PathBuf::from("test.rs"),
802            name: "test_func".to_string(),
803            line: 1,
804        });
805
806        // Test direct pointer call
807        let call: ExprCall = parse_quote! { callback() };
808        visitor.analyze_call_expression(&call);
809        assert_eq!(visitor.pointer_calls.len(), 1);
810        assert_eq!(visitor.pointer_calls[0].pointer_id, "callback");
811
812        // Test HOF call
813        let hof_call: ExprCall = parse_quote! { map(transform) };
814        visitor.analyze_call_expression(&hof_call);
815        assert_eq!(visitor.hof_calls.len(), 1);
816        assert_eq!(visitor.hof_calls[0].hof_function, "map");
817    }
818
819    #[test]
820    fn test_analyze_call_expression_no_current_function() {
821        let mut visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
822        // No current function set
823
824        let call: ExprCall = parse_quote! { func() };
825        visitor.analyze_call_expression(&call);
826
827        // Should not record any calls without current function context
828        assert!(visitor.pointer_calls.is_empty());
829        assert!(visitor.hof_calls.is_empty());
830    }
831
832    #[test]
833    fn test_extract_possible_targets_with_path() {
834        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
835
836        // Test extracting function target from path expression
837        let expr: Expr = parse_quote! { my_function };
838        let targets = visitor.extract_possible_targets(&expr);
839
840        assert_eq!(targets.len(), 1);
841        let target = targets.iter().next().unwrap();
842        assert_eq!(target.name, "my_function");
843        assert_eq!(target.file, std::path::PathBuf::from("test.rs"));
844    }
845
846    #[test]
847    fn test_extract_possible_targets_with_qualified_path() {
848        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
849
850        // Test extracting function target from qualified path
851        let expr: Expr = parse_quote! { module::submodule::function };
852        let targets = visitor.extract_possible_targets(&expr);
853
854        assert_eq!(targets.len(), 1);
855        let target = targets.iter().next().unwrap();
856        assert_eq!(target.name, "module::submodule::function");
857    }
858
859    #[test]
860    fn test_extract_possible_targets_non_path() {
861        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
862
863        // Test with non-path expression (should return empty set)
864        let expr: Expr = parse_quote! { 42 };
865        let targets = visitor.extract_possible_targets(&expr);
866
867        assert!(targets.is_empty());
868    }
869
870    #[test]
871    fn test_extract_possible_targets_closure() {
872        let visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
873
874        // Test with closure expression (should return empty set)
875        let expr: Expr = parse_quote! { |x| x + 1 };
876        let targets = visitor.extract_possible_targets(&expr);
877
878        assert!(targets.is_empty());
879    }
880
881    #[test]
882    fn test_analyze_function_pointer_assignment_complete() {
883        let mut visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
884        visitor.current_function = Some(FunctionId {
885            file: std::path::PathBuf::from("test.rs"),
886            name: "outer_func".to_string(),
887            line: 1,
888        });
889
890        // Test complete function pointer assignment
891        // Creating a synthetic Local with function assignment
892        let pat = Pat::Ident(PatIdent {
893            attrs: vec![],
894            by_ref: None,
895            mutability: None,
896            ident: parse_quote! { func_ptr },
897            subpat: None,
898        });
899
900        let init_expr: Expr = parse_quote! { my_function };
901        let local = Local {
902            attrs: vec![],
903            let_token: Default::default(),
904            pat,
905            init: Some(syn::LocalInit {
906                eq_token: Default::default(),
907                expr: Box::new(init_expr),
908                diverge: None,
909            }),
910            semi_token: Default::default(),
911        };
912
913        visitor.analyze_function_pointer_assignment(&local);
914
915        assert_eq!(visitor.function_pointers.len(), 1);
916        let pointer = &visitor.function_pointers[0];
917        assert_eq!(pointer.variable_name, "func_ptr");
918        assert!(!pointer.is_parameter);
919        assert_eq!(pointer.possible_targets.len(), 1);
920    }
921
922    #[test]
923    fn test_analyze_function_pointer_assignment_no_init() {
924        let mut visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
925        visitor.current_function = Some(FunctionId {
926            file: std::path::PathBuf::from("test.rs"),
927            name: "outer_func".to_string(),
928            line: 1,
929        });
930
931        // Test local without initialization (should not add pointer)
932        let pat = Pat::Ident(PatIdent {
933            attrs: vec![],
934            by_ref: None,
935            mutability: None,
936            ident: parse_quote! { func_ptr },
937            subpat: None,
938        });
939
940        let local = Local {
941            attrs: vec![],
942            let_token: Default::default(),
943            pat,
944            init: None, // No initialization
945            semi_token: Default::default(),
946        };
947
948        visitor.analyze_function_pointer_assignment(&local);
949
950        assert!(visitor.function_pointers.is_empty());
951    }
952
953    #[test]
954    fn test_analyze_function_pointer_assignment_non_ident_pattern() {
955        let mut visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
956        visitor.current_function = Some(FunctionId {
957            file: std::path::PathBuf::from("test.rs"),
958            name: "outer_func".to_string(),
959            line: 1,
960        });
961
962        // Test with tuple pattern (should not add pointer)
963        let pat: Pat = parse_quote! { (a, b) };
964        let init_expr: Expr = parse_quote! { get_tuple() };
965
966        let local = Local {
967            attrs: vec![],
968            let_token: Default::default(),
969            pat,
970            init: Some(syn::LocalInit {
971                eq_token: Default::default(),
972                expr: Box::new(init_expr),
973                diverge: None,
974            }),
975            semi_token: Default::default(),
976        };
977
978        visitor.analyze_function_pointer_assignment(&local);
979
980        assert!(visitor.function_pointers.is_empty());
981    }
982
983    #[test]
984    fn test_analyze_function_pointer_assignment_no_current_function() {
985        let mut visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
986        // No current function set
987
988        let pat = Pat::Ident(PatIdent {
989            attrs: vec![],
990            by_ref: None,
991            mutability: None,
992            ident: parse_quote! { func_ptr },
993            subpat: None,
994        });
995
996        let init_expr: Expr = parse_quote! { my_function };
997        let local = Local {
998            attrs: vec![],
999            let_token: Default::default(),
1000            pat,
1001            init: Some(syn::LocalInit {
1002                eq_token: Default::default(),
1003                expr: Box::new(init_expr),
1004                diverge: None,
1005            }),
1006            semi_token: Default::default(),
1007        };
1008
1009        visitor.analyze_function_pointer_assignment(&local);
1010
1011        // Should not record without current function context
1012        assert!(visitor.function_pointers.is_empty());
1013    }
1014
1015    #[test]
1016    fn test_extract_pointer_assignment_data_with_ident() {
1017        // Test extracting from a valid identifier pattern with init
1018        let pat: Pat = parse_quote! { func_ptr };
1019        let init_expr: Expr = parse_quote! { my_function };
1020        let local = Local {
1021            attrs: vec![],
1022            let_token: Default::default(),
1023            pat,
1024            init: Some(syn::LocalInit {
1025                eq_token: Default::default(),
1026                expr: Box::new(init_expr),
1027                diverge: None,
1028            }),
1029            semi_token: Default::default(),
1030        };
1031
1032        let result = FunctionPointerVisitor::extract_pointer_assignment_data(&local);
1033        assert!(result.is_some());
1034        let (ident, _expr) = result.unwrap();
1035        assert_eq!(ident.to_string(), "func_ptr");
1036    }
1037
1038    #[test]
1039    fn test_extract_pointer_assignment_data_without_init() {
1040        // Test with identifier pattern but no initialization
1041        let pat: Pat = parse_quote! { func_ptr };
1042        let local = Local {
1043            attrs: vec![],
1044            let_token: Default::default(),
1045            pat,
1046            init: None,
1047            semi_token: Default::default(),
1048        };
1049
1050        let result = FunctionPointerVisitor::extract_pointer_assignment_data(&local);
1051        assert!(result.is_none());
1052    }
1053
1054    #[test]
1055    fn test_extract_pointer_assignment_data_with_non_ident_pattern() {
1056        // Test with a tuple pattern instead of identifier
1057        let pat: Pat = parse_quote! { (a, b) };
1058        let init_expr: Expr = parse_quote! { (func1, func2) };
1059        let local = Local {
1060            attrs: vec![],
1061            let_token: Default::default(),
1062            pat,
1063            init: Some(syn::LocalInit {
1064                eq_token: Default::default(),
1065                expr: Box::new(init_expr),
1066                diverge: None,
1067            }),
1068            semi_token: Default::default(),
1069        };
1070
1071        let result = FunctionPointerVisitor::extract_pointer_assignment_data(&local);
1072        assert!(result.is_none());
1073    }
1074
1075    #[test]
1076    fn test_extract_pointer_assignment_data_with_mut_ident() {
1077        // Test with mutable identifier pattern
1078        let pat: Pat = parse_quote! { mut func_ptr };
1079        let init_expr: Expr = parse_quote! { my_function };
1080        let local = Local {
1081            attrs: vec![],
1082            let_token: Default::default(),
1083            pat,
1084            init: Some(syn::LocalInit {
1085                eq_token: Default::default(),
1086                expr: Box::new(init_expr),
1087                diverge: None,
1088            }),
1089            semi_token: Default::default(),
1090        };
1091
1092        let result = FunctionPointerVisitor::extract_pointer_assignment_data(&local);
1093        assert!(result.is_some());
1094        let (ident, _expr) = result.unwrap();
1095        assert_eq!(ident.to_string(), "func_ptr");
1096    }
1097
1098    #[test]
1099    fn test_analyze_function_pointer_assignment_complete_flow() {
1100        // Test the complete flow with all components working together
1101        let mut visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
1102        visitor.current_function = Some(FunctionId {
1103            file: std::path::PathBuf::from("test.rs"),
1104            name: "parent_func".to_string(),
1105            line: 10,
1106        });
1107
1108        let pat: Pat = parse_quote! { callback };
1109        let init_expr: Expr = parse_quote! { process_item };
1110        let local = Local {
1111            attrs: vec![],
1112            let_token: Default::default(),
1113            pat,
1114            init: Some(syn::LocalInit {
1115                eq_token: Default::default(),
1116                expr: Box::new(init_expr),
1117                diverge: None,
1118            }),
1119            semi_token: Default::default(),
1120        };
1121
1122        visitor.analyze_function_pointer_assignment(&local);
1123
1124        assert_eq!(visitor.function_pointers.len(), 1);
1125        let pointer_info = &visitor.function_pointers[0];
1126        assert_eq!(pointer_info.variable_name, "callback");
1127        assert_eq!(pointer_info.defining_function.name, "parent_func");
1128        assert!(!pointer_info.is_parameter);
1129        assert_eq!(pointer_info.possible_targets.len(), 1);
1130    }
1131
1132    #[test]
1133    fn test_analyze_function_pointer_assignment_with_complex_init() {
1134        // Test with a more complex initialization expression
1135        let mut visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
1136        visitor.current_function = Some(FunctionId {
1137            file: std::path::PathBuf::from("test.rs"),
1138            name: "setup_handlers".to_string(),
1139            line: 5,
1140        });
1141
1142        let pat: Pat = parse_quote! { handler };
1143        // Using a closure expression
1144        let init_expr: Expr = parse_quote! { |x| x + 1 };
1145        let local = Local {
1146            attrs: vec![],
1147            let_token: Default::default(),
1148            pat,
1149            init: Some(syn::LocalInit {
1150                eq_token: Default::default(),
1151                expr: Box::new(init_expr),
1152                diverge: None,
1153            }),
1154            semi_token: Default::default(),
1155        };
1156
1157        visitor.analyze_function_pointer_assignment(&local);
1158
1159        // Should still record the assignment even with non-path expression
1160        assert_eq!(visitor.function_pointers.len(), 1);
1161        let pointer_info = &visitor.function_pointers[0];
1162        assert_eq!(pointer_info.variable_name, "handler");
1163        // Possible targets will be empty for non-path expressions
1164        assert!(pointer_info.possible_targets.is_empty());
1165    }
1166
1167    #[test]
1168    fn test_analyze_function_pointer_assignment_edge_cases() {
1169        // Test various edge cases
1170        let mut visitor = FunctionPointerVisitor::new(std::path::PathBuf::from("test.rs"));
1171        visitor.current_function = Some(FunctionId {
1172            file: std::path::PathBuf::from("test.rs"),
1173            name: "test_func".to_string(),
1174            line: 1,
1175        });
1176
1177        // Edge case 1: Pattern with type annotation
1178        let pat: Pat = parse_quote! { func_ptr };
1179        let init_expr: Expr = parse_quote! { some_func };
1180        let local = Local {
1181            attrs: vec![],
1182            let_token: Default::default(),
1183            pat,
1184            init: Some(syn::LocalInit {
1185                eq_token: Default::default(),
1186                expr: Box::new(init_expr),
1187                diverge: None,
1188            }),
1189            semi_token: Default::default(),
1190        };
1191
1192        visitor.analyze_function_pointer_assignment(&local);
1193        assert_eq!(visitor.function_pointers.len(), 1);
1194
1195        // Edge case 2: Destructuring pattern (should be ignored)
1196        let pat2: Pat = parse_quote! { Point { x, y } };
1197        let init_expr2: Expr = parse_quote! { get_point() };
1198        let local2 = Local {
1199            attrs: vec![],
1200            let_token: Default::default(),
1201            pat: pat2,
1202            init: Some(syn::LocalInit {
1203                eq_token: Default::default(),
1204                expr: Box::new(init_expr2),
1205                diverge: None,
1206            }),
1207            semi_token: Default::default(),
1208        };
1209
1210        visitor.analyze_function_pointer_assignment(&local2);
1211        // Should still be 1, not 2
1212        assert_eq!(visitor.function_pointers.len(), 1);
1213    }
1214}