Skip to main content

debtmap/analyzers/call_graph/
graph_builder.rs

1/// Graph building and management functionality for call graph extraction
2use crate::priority::call_graph::{CallGraph, CallType, FunctionCall, FunctionId};
3use std::path::PathBuf;
4use syn::{ImplItemFn, ItemFn};
5
6/// Expression categorization for special handling
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum ExprCategory {
9    Closure,
10    Async,
11    Await,
12    Try,
13    Unsafe,
14    Regular,
15}
16
17/// Builds and manages the call graph
18pub struct GraphBuilder {
19    pub call_graph: CallGraph,
20    current_file: PathBuf,
21    module_path: Vec<String>,
22}
23
24impl GraphBuilder {
25    pub fn new(file: PathBuf) -> Self {
26        Self {
27            call_graph: CallGraph::new(),
28            current_file: file,
29            module_path: Vec::new(),
30        }
31    }
32
33    /// Set the current module path
34    pub fn set_module_path(&mut self, path: Vec<String>) {
35        self.module_path = path;
36    }
37
38    /// Get the current module path
39    pub fn module_path(&self) -> &[String] {
40        &self.module_path
41    }
42
43    /// Push a module to the path
44    pub fn push_module(&mut self, module: String) {
45        self.module_path.push(module);
46    }
47
48    /// Pop a module from the path
49    pub fn pop_module(&mut self) {
50        self.module_path.pop();
51    }
52
53    /// Add a function to the graph
54    pub fn add_function(
55        &mut self,
56        name: String,
57        line: usize,
58        is_test: bool,
59        _is_async: bool,
60        module_path: String,
61    ) -> FunctionId {
62        let function_id = FunctionId::with_module_path(
63            self.current_file.clone(),
64            name.clone(),
65            line,
66            module_path,
67        );
68
69        // Add function with appropriate parameters
70        // Using defaults for entry_point and complexity for now
71        self.call_graph.add_function(
72            function_id.clone(),
73            false, // is_entry_point
74            is_test,
75            0, // complexity (to be calculated)
76            0, // lines (to be calculated)
77        );
78        function_id
79    }
80
81    /// Add a function from an ItemFn
82    pub fn add_function_from_item(
83        &mut self,
84        name: String,
85        line: usize,
86        item_fn: &ItemFn,
87        module_path: String,
88    ) -> FunctionId {
89        let is_test = Self::has_test_attribute(&item_fn.attrs);
90        let is_async = item_fn.sig.asyncness.is_some();
91
92        self.add_function(name, line, is_test, is_async, module_path)
93    }
94
95    /// Add an impl method to the graph
96    pub fn add_impl_method(
97        &mut self,
98        name: String,
99        line: usize,
100        impl_fn: &ImplItemFn,
101        module_path: String,
102    ) -> FunctionId {
103        let is_test = Self::has_test_attribute(&impl_fn.attrs);
104        let is_async = impl_fn.sig.asyncness.is_some();
105
106        self.add_function(name, line, is_test, is_async, module_path)
107    }
108
109    /// Check if a function has a test attribute.
110    ///
111    /// Detects:
112    /// - `#[test]` - standard Rust test
113    /// - `#[tokio::test]` - async tokio test
114    /// - `#[actix_rt::test]` - actix runtime test
115    /// - `#[rstest]` - rstest parameterized test
116    /// - `#[test_case]` - test_case attribute
117    fn has_test_attribute(attrs: &[syn::Attribute]) -> bool {
118        attrs.iter().any(|attr| {
119            let path = attr.path();
120
121            // Check single ident: #[test], #[rstest], #[test_case]
122            if path.is_ident("test") || path.is_ident("rstest") || path.is_ident("test_case") {
123                return true;
124            }
125
126            // Check path-based attributes: #[tokio::test], #[actix_rt::test]
127            let segments: Vec<String> = path.segments.iter().map(|s| s.ident.to_string()).collect();
128
129            if segments.len() == 2 {
130                let first = segments[0].as_str();
131                let second = segments[1].as_str();
132                return (first == "actix_rt" || first == "tokio") && second == "test";
133            }
134
135            false
136        })
137    }
138
139    /// Add a call edge to the graph
140    pub fn add_call(&mut self, caller: FunctionId, callee: FunctionId, call_type: CallType) {
141        self.call_graph.add_call(FunctionCall {
142            caller,
143            callee,
144            call_type,
145        });
146    }
147
148    /// Get all functions in the graph
149    pub fn all_functions(&self) -> impl Iterator<Item = &FunctionId> {
150        self.call_graph.get_all_functions()
151    }
152
153    /// Get the number of functions in the graph
154    pub fn function_count(&self) -> usize {
155        self.call_graph.node_count()
156    }
157
158    /// Merge another call graph into this one
159    pub fn merge(&mut self, other: CallGraph) {
160        self.call_graph.merge(other);
161    }
162
163    /// Extract a function name from a syn path
164    pub fn extract_function_name_from_path(path: &syn::Path) -> Option<String> {
165        // Get the full path as a string
166        let segments: Vec<String> = path
167            .segments
168            .iter()
169            .map(|seg| seg.ident.to_string())
170            .collect();
171
172        if segments.is_empty() {
173            return None;
174        }
175
176        // Join with :: to get the full qualified name
177        Some(segments.join("::"))
178    }
179
180    /// Get line number from a span (placeholder for actual implementation)
181    pub fn get_line_number(&self, span: proc_macro2::Span) -> usize {
182        span.start().line
183    }
184
185    /// Classify an expression for special handling
186    pub fn classify_expr_category(expr: &syn::Expr) -> ExprCategory {
187        match expr {
188            syn::Expr::Closure(_) => ExprCategory::Closure,
189            syn::Expr::Async(_) => ExprCategory::Async,
190            syn::Expr::Await(_) => ExprCategory::Await,
191            syn::Expr::Try(_) => ExprCategory::Try,
192            syn::Expr::Unsafe(_) => ExprCategory::Unsafe,
193            _ => ExprCategory::Regular,
194        }
195    }
196
197    /// Check if an expression category needs special handling
198    pub fn needs_special_handling(category: ExprCategory) -> bool {
199        !matches!(category, ExprCategory::Regular)
200    }
201
202    /// Build a qualified function name with module path
203    pub fn build_qualified_name(&self, base_name: &str) -> String {
204        if self.module_path.is_empty() {
205            base_name.to_string()
206        } else {
207            format!("{}::{}", self.module_path.join("::"), base_name)
208        }
209    }
210
211    /// Build a qualified name for an impl method
212    pub fn build_impl_method_name(&self, impl_type: &str, method_name: &str) -> String {
213        if self.module_path.is_empty() {
214            format!("{}::{}", impl_type, method_name)
215        } else {
216            format!(
217                "{}::{}::{}",
218                self.module_path.join("::"),
219                impl_type,
220                method_name
221            )
222        }
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_module_path_operations() {
232        let mut builder = GraphBuilder::new(PathBuf::from("test.rs"));
233
234        assert!(builder.module_path().is_empty());
235
236        builder.push_module("mod1".to_string());
237        assert_eq!(builder.module_path(), &["mod1"]);
238
239        builder.push_module("mod2".to_string());
240        assert_eq!(builder.module_path(), &["mod1", "mod2"]);
241
242        builder.pop_module();
243        assert_eq!(builder.module_path(), &["mod1"]);
244    }
245
246    #[test]
247    fn test_build_qualified_name() {
248        let mut builder = GraphBuilder::new(PathBuf::from("test.rs"));
249
250        assert_eq!(builder.build_qualified_name("func"), "func");
251
252        builder.push_module("module".to_string());
253        assert_eq!(builder.build_qualified_name("func"), "module::func");
254
255        builder.push_module("submodule".to_string());
256        assert_eq!(
257            builder.build_qualified_name("func"),
258            "module::submodule::func"
259        );
260    }
261
262    #[test]
263    fn test_build_impl_method_name() {
264        let mut builder = GraphBuilder::new(PathBuf::from("test.rs"));
265
266        assert_eq!(
267            builder.build_impl_method_name("MyStruct", "method"),
268            "MyStruct::method"
269        );
270
271        builder.push_module("module".to_string());
272        assert_eq!(
273            builder.build_impl_method_name("MyStruct", "method"),
274            "module::MyStruct::method"
275        );
276    }
277
278    #[test]
279    fn test_classify_expr_category() {
280        use syn::parse_quote;
281
282        let closure: syn::Expr = parse_quote! { |x| x + 1 };
283        assert_eq!(
284            GraphBuilder::classify_expr_category(&closure),
285            ExprCategory::Closure
286        );
287
288        let async_block: syn::Expr = parse_quote! { async { foo().await } };
289        assert_eq!(
290            GraphBuilder::classify_expr_category(&async_block),
291            ExprCategory::Async
292        );
293
294        let regular: syn::Expr = parse_quote! { foo() };
295        assert_eq!(
296            GraphBuilder::classify_expr_category(&regular),
297            ExprCategory::Regular
298        );
299    }
300
301    #[test]
302    fn test_needs_special_handling() {
303        assert!(GraphBuilder::needs_special_handling(ExprCategory::Closure));
304        assert!(GraphBuilder::needs_special_handling(ExprCategory::Async));
305        assert!(GraphBuilder::needs_special_handling(ExprCategory::Await));
306        assert!(!GraphBuilder::needs_special_handling(ExprCategory::Regular));
307    }
308
309    // Unit tests for test function detection (Spec 267)
310
311    #[test]
312    fn test_detect_basic_test_attribute() {
313        use syn::parse_quote;
314
315        let test_fn: ItemFn = parse_quote! {
316            #[test]
317            fn my_test() {
318                assert!(true);
319            }
320        };
321
322        assert!(
323            GraphBuilder::has_test_attribute(&test_fn.attrs),
324            "#[test] attribute should be detected"
325        );
326    }
327
328    #[test]
329    fn test_detect_production_function() {
330        use syn::parse_quote;
331
332        let prod_fn: ItemFn = parse_quote! {
333            fn production_function() {
334                println!("hello");
335            }
336        };
337
338        assert!(
339            !GraphBuilder::has_test_attribute(&prod_fn.attrs),
340            "Function without #[test] should NOT be detected as test"
341        );
342    }
343
344    #[test]
345    fn test_detect_tokio_test_attribute() {
346        use syn::parse_quote;
347
348        let tokio_test_fn: ItemFn = parse_quote! {
349            #[tokio::test]
350            async fn async_test() {
351                assert!(true);
352            }
353        };
354
355        assert!(
356            GraphBuilder::has_test_attribute(&tokio_test_fn.attrs),
357            "#[tokio::test] attribute should be detected"
358        );
359    }
360
361    #[test]
362    fn test_detect_actix_test_attribute() {
363        use syn::parse_quote;
364
365        let actix_test_fn: ItemFn = parse_quote! {
366            #[actix_rt::test]
367            async fn actix_test() {
368                assert!(true);
369            }
370        };
371
372        assert!(
373            GraphBuilder::has_test_attribute(&actix_test_fn.attrs),
374            "#[actix_rt::test] attribute should be detected"
375        );
376    }
377
378    #[test]
379    fn test_detect_rstest_attribute() {
380        use syn::parse_quote;
381
382        let rstest_fn: ItemFn = parse_quote! {
383            #[rstest]
384            fn parameterized_test() {
385                assert!(true);
386            }
387        };
388
389        assert!(
390            GraphBuilder::has_test_attribute(&rstest_fn.attrs),
391            "#[rstest] attribute should be detected"
392        );
393    }
394
395    #[test]
396    fn test_detect_test_case_attribute() {
397        use syn::parse_quote;
398
399        let test_case_fn: ItemFn = parse_quote! {
400            #[test_case]
401            fn test_case_test() {
402                assert!(true);
403            }
404        };
405
406        assert!(
407            GraphBuilder::has_test_attribute(&test_case_fn.attrs),
408            "#[test_case] attribute should be detected"
409        );
410    }
411
412    #[test]
413    fn test_helper_function_without_test_attr_not_detected() {
414        use syn::parse_quote;
415
416        // Helper functions in test modules don't have #[test]
417        let helper_fn: ItemFn = parse_quote! {
418            fn create_test_fixture() -> i32 {
419                42
420            }
421        };
422
423        assert!(
424            !GraphBuilder::has_test_attribute(&helper_fn.attrs),
425            "Helper functions without #[test] should NOT be detected as tests"
426        );
427    }
428
429    #[test]
430    fn test_function_with_other_attributes_not_detected() {
431        use syn::parse_quote;
432
433        let other_fn: ItemFn = parse_quote! {
434            #[inline]
435            #[must_use]
436            fn some_function() -> i32 {
437                42
438            }
439        };
440
441        assert!(
442            !GraphBuilder::has_test_attribute(&other_fn.attrs),
443            "Function with non-test attributes should NOT be detected as test"
444        );
445    }
446}