debtmap/analyzers/call_graph/
graph_builder.rs1use crate::priority::call_graph::{CallGraph, CallType, FunctionCall, FunctionId};
3use std::path::PathBuf;
4use syn::{ImplItemFn, ItemFn};
5
6#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum ExprCategory {
9 Closure,
10 Async,
11 Await,
12 Try,
13 Unsafe,
14 Regular,
15}
16
17pub 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 pub fn set_module_path(&mut self, path: Vec<String>) {
35 self.module_path = path;
36 }
37
38 pub fn module_path(&self) -> &[String] {
40 &self.module_path
41 }
42
43 pub fn push_module(&mut self, module: String) {
45 self.module_path.push(module);
46 }
47
48 pub fn pop_module(&mut self) {
50 self.module_path.pop();
51 }
52
53 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 self.call_graph.add_function(
72 function_id.clone(),
73 false, is_test,
75 0, 0, );
78 function_id
79 }
80
81 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 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 fn has_test_attribute(attrs: &[syn::Attribute]) -> bool {
118 attrs.iter().any(|attr| {
119 let path = attr.path();
120
121 if path.is_ident("test") || path.is_ident("rstest") || path.is_ident("test_case") {
123 return true;
124 }
125
126 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 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 pub fn all_functions(&self) -> impl Iterator<Item = &FunctionId> {
150 self.call_graph.get_all_functions()
151 }
152
153 pub fn function_count(&self) -> usize {
155 self.call_graph.node_count()
156 }
157
158 pub fn merge(&mut self, other: CallGraph) {
160 self.call_graph.merge(other);
161 }
162
163 pub fn extract_function_name_from_path(path: &syn::Path) -> Option<String> {
165 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 Some(segments.join("::"))
178 }
179
180 pub fn get_line_number(&self, span: proc_macro2::Span) -> usize {
182 span.start().line
183 }
184
185 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 pub fn needs_special_handling(category: ExprCategory) -> bool {
199 !matches!(category, ExprCategory::Regular)
200 }
201
202 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 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(®ular),
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 #[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 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}