autosar-calltree 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ """
2
+ AUTOSAR Call Tree Analyzer
3
+
4
+ A Python package to analyze C/AUTOSAR codebases and generate function call trees
5
+ with multiple output formats (Mermaid, XMI/UML).
6
+ """
7
+
8
+ from .analyzers.call_tree_builder import CallTreeBuilder
9
+ from .database.function_database import FunctionDatabase
10
+ from .generators.mermaid_generator import MermaidGenerator
11
+ from .parsers.autosar_parser import AutosarParser
12
+ from .parsers.c_parser import CParser
13
+ from .version import __author__, __email__, __version__
14
+
15
+ __all__ = [
16
+ "__version__",
17
+ "__author__",
18
+ "__email__",
19
+ "FunctionDatabase",
20
+ "CallTreeBuilder",
21
+ "MermaidGenerator",
22
+ "AutosarParser",
23
+ "CParser",
24
+ ]
@@ -0,0 +1,5 @@
1
+ """Analyzers package initialization."""
2
+
3
+ from .call_tree_builder import CallTreeBuilder
4
+
5
+ __all__ = ["CallTreeBuilder"]
@@ -0,0 +1,369 @@
1
+ """
2
+ Call tree analyzer module.
3
+
4
+ This module builds call trees by performing depth-first traversal
5
+ of function calls, detecting cycles, and collecting statistics.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import List, Set
10
+
11
+ from ..database.function_database import FunctionDatabase
12
+ from ..database.models import (
13
+ AnalysisResult,
14
+ AnalysisStatistics,
15
+ CallTreeNode,
16
+ CircularDependency,
17
+ FunctionInfo,
18
+ )
19
+
20
+
21
+ class CallTreeBuilder:
22
+ """
23
+ Builds call trees by traversing function calls.
24
+
25
+ This class performs depth-first search through function calls,
26
+ respects maximum depth limits, detects circular dependencies,
27
+ and builds a hierarchical tree structure.
28
+ """
29
+
30
+ def __init__(self, function_db: FunctionDatabase):
31
+ """
32
+ Initialize the call tree builder.
33
+
34
+ Args:
35
+ function_db: Function database to use for lookups
36
+ """
37
+ self.function_db = function_db
38
+ self.visited_functions: Set[str] = set()
39
+ self.call_stack: List[str] = []
40
+ self.circular_dependencies: List[CircularDependency] = []
41
+ self.max_depth_reached = 0
42
+ self.total_nodes = 0
43
+
44
+ def build_tree(
45
+ self, start_function: str, max_depth: int = 3, verbose: bool = False
46
+ ) -> AnalysisResult:
47
+ """
48
+ Build a call tree starting from a function.
49
+
50
+ Args:
51
+ start_function: Name of the function to start from
52
+ max_depth: Maximum depth to traverse
53
+ verbose: Print progress information
54
+
55
+ Returns:
56
+ AnalysisResult containing the call tree and metadata
57
+ """
58
+ # Reset state
59
+ self.visited_functions.clear()
60
+ self.call_stack.clear()
61
+ self.circular_dependencies.clear()
62
+ self.max_depth_reached = 0
63
+ self.total_nodes = 0
64
+
65
+ if verbose:
66
+ print(f"Building call tree for: {start_function}")
67
+ print(f"Max depth: {max_depth}")
68
+
69
+ # Lookup start function
70
+ start_functions = self.function_db.lookup_function(start_function)
71
+
72
+ if not start_functions:
73
+ # Function not found
74
+ if verbose:
75
+ print(f"Error: Function '{start_function}' not found in database")
76
+
77
+ return AnalysisResult(
78
+ root_function=start_function,
79
+ call_tree=None,
80
+ circular_dependencies=[],
81
+ statistics=AnalysisStatistics(
82
+ total_functions=0,
83
+ max_depth_reached=0,
84
+ circular_dependencies_found=0,
85
+ unique_functions=0,
86
+ ),
87
+ errors=[f"Function '{start_function}' not found"],
88
+ )
89
+
90
+ # Use first match (or disambiguate if multiple found)
91
+ start_func_info = start_functions[0]
92
+
93
+ if len(start_functions) > 1 and verbose:
94
+ print(
95
+ f"Warning: Multiple definitions found for '{start_function}', using first match"
96
+ )
97
+ for func in start_functions:
98
+ print(f" - {func.file_path}:{func.line_number}")
99
+
100
+ # Build the tree
101
+ call_tree = self._build_tree_recursive(
102
+ func_info=start_func_info,
103
+ current_depth=0,
104
+ max_depth=max_depth,
105
+ verbose=verbose,
106
+ )
107
+
108
+ # Compute statistics
109
+ unique_functions = len(self.visited_functions)
110
+
111
+ statistics = AnalysisStatistics(
112
+ total_functions=self.total_nodes,
113
+ max_depth_reached=self.max_depth_reached,
114
+ circular_dependencies_found=len(self.circular_dependencies),
115
+ unique_functions=unique_functions,
116
+ )
117
+
118
+ if verbose:
119
+ print("\nAnalysis complete:")
120
+ print(f" - Total nodes: {self.total_nodes}")
121
+ print(f" - Unique functions: {unique_functions}")
122
+ print(f" - Max depth reached: {self.max_depth_reached}")
123
+ print(f" - Circular dependencies: {len(self.circular_dependencies)}")
124
+
125
+ return AnalysisResult(
126
+ root_function=start_function,
127
+ call_tree=call_tree,
128
+ circular_dependencies=self.circular_dependencies,
129
+ statistics=statistics,
130
+ errors=[],
131
+ )
132
+
133
+ def _build_tree_recursive(
134
+ self,
135
+ func_info: FunctionInfo,
136
+ current_depth: int,
137
+ max_depth: int,
138
+ verbose: bool = False,
139
+ ) -> CallTreeNode:
140
+ """
141
+ Recursively build call tree using depth-first search.
142
+
143
+ Args:
144
+ func_info: Current function information
145
+ current_depth: Current depth in the tree
146
+ max_depth: Maximum depth to traverse
147
+ verbose: Print progress information
148
+
149
+ Returns:
150
+ CallTreeNode for current function
151
+ """
152
+ self.total_nodes += 1
153
+
154
+ # Track maximum depth
155
+ if current_depth > self.max_depth_reached:
156
+ self.max_depth_reached = current_depth
157
+
158
+ # Create qualified name for cycle detection
159
+ qualified_name = self._get_qualified_name(func_info)
160
+
161
+ # Check for circular dependency
162
+ if qualified_name in self.call_stack:
163
+ cycle_start_idx = self.call_stack.index(qualified_name)
164
+ cycle = self.call_stack[cycle_start_idx:] + [qualified_name]
165
+
166
+ circular_dep = CircularDependency(cycle=cycle, depth=current_depth)
167
+ self.circular_dependencies.append(circular_dep)
168
+
169
+ if verbose:
170
+ print(f" {' ' * current_depth}Cycle detected: {' -> '.join(cycle)}")
171
+
172
+ # Return node without children
173
+ return CallTreeNode(
174
+ function_info=func_info,
175
+ depth=current_depth,
176
+ children=[],
177
+ is_recursive=True,
178
+ )
179
+
180
+ # Add to visited set and call stack
181
+ self.visited_functions.add(qualified_name)
182
+ self.call_stack.append(qualified_name)
183
+
184
+ # Check depth limit
185
+ if current_depth >= max_depth:
186
+ if verbose:
187
+ print(f" {' ' * current_depth}{func_info.name} (max depth reached)")
188
+
189
+ self.call_stack.pop()
190
+ return CallTreeNode(
191
+ function_info=func_info,
192
+ depth=current_depth,
193
+ children=[],
194
+ is_recursive=False,
195
+ )
196
+
197
+ if verbose:
198
+ indent = " " * current_depth
199
+ print(
200
+ f"{indent}{func_info.name} ({func_info.file_path}:{func_info.line_number})"
201
+ )
202
+
203
+ # Build children nodes
204
+ children = []
205
+
206
+ for called_func_name in func_info.calls:
207
+ # Lookup called function
208
+ called_funcs = self.function_db.lookup_function(
209
+ called_func_name, context_file=str(func_info.file_path)
210
+ )
211
+
212
+ if not called_funcs:
213
+ # Function not found - might be external or library function
214
+ if verbose:
215
+ print(
216
+ f" {' ' * (current_depth + 1)}{called_func_name} (not found)"
217
+ )
218
+ continue
219
+
220
+ # Use first match (prefer function from same file for static functions)
221
+ called_func_info = called_funcs[0]
222
+
223
+ # Recursively build child node
224
+ child_node = self._build_tree_recursive(
225
+ func_info=called_func_info,
226
+ current_depth=current_depth + 1,
227
+ max_depth=max_depth,
228
+ verbose=verbose,
229
+ )
230
+
231
+ children.append(child_node)
232
+
233
+ # Remove from call stack
234
+ self.call_stack.pop()
235
+
236
+ return CallTreeNode(
237
+ function_info=func_info,
238
+ depth=current_depth,
239
+ children=children,
240
+ is_recursive=False,
241
+ )
242
+
243
+ def _get_qualified_name(self, func_info: FunctionInfo) -> str:
244
+ """
245
+ Get qualified name for a function (file::function).
246
+
247
+ Args:
248
+ func_info: Function information
249
+
250
+ Returns:
251
+ Qualified name string
252
+ """
253
+ file_stem = Path(func_info.file_path).stem
254
+ return f"{file_stem}::{func_info.name}"
255
+
256
+ def get_all_functions_in_tree(self, root: CallTreeNode) -> List[FunctionInfo]:
257
+ """
258
+ Get all unique functions in a call tree.
259
+
260
+ Args:
261
+ root: Root node of call tree
262
+
263
+ Returns:
264
+ List of all FunctionInfo objects in tree
265
+ """
266
+ functions = []
267
+ seen = set()
268
+
269
+ def traverse(node: CallTreeNode):
270
+ qualified_name = self._get_qualified_name(node.function_info)
271
+ if qualified_name not in seen:
272
+ seen.add(qualified_name)
273
+ functions.append(node.function_info)
274
+
275
+ for child in node.children:
276
+ traverse(child)
277
+
278
+ traverse(root)
279
+ return functions
280
+
281
+ def get_tree_depth(self, root: CallTreeNode) -> int:
282
+ """
283
+ Get the maximum depth of a call tree.
284
+
285
+ Args:
286
+ root: Root node of call tree
287
+
288
+ Returns:
289
+ Maximum depth
290
+ """
291
+ if not root.children:
292
+ return root.depth
293
+
294
+ max_child_depth = max(self.get_tree_depth(child) for child in root.children)
295
+
296
+ return max_child_depth
297
+
298
+ def get_leaf_nodes(self, root: CallTreeNode) -> List[CallTreeNode]:
299
+ """
300
+ Get all leaf nodes (functions that don't call anything) in tree.
301
+
302
+ Args:
303
+ root: Root node of call tree
304
+
305
+ Returns:
306
+ List of leaf CallTreeNode objects
307
+ """
308
+ leaves = []
309
+
310
+ def traverse(node: CallTreeNode):
311
+ if not node.children:
312
+ leaves.append(node)
313
+ else:
314
+ for child in node.children:
315
+ traverse(child)
316
+
317
+ traverse(root)
318
+ return leaves
319
+
320
+ def print_tree_text(self, root: CallTreeNode, show_file: bool = True) -> str:
321
+ """
322
+ Generate a text representation of the call tree.
323
+
324
+ Args:
325
+ root: Root node of call tree
326
+ show_file: Whether to show file paths
327
+
328
+ Returns:
329
+ Text representation as string
330
+ """
331
+ lines = []
332
+
333
+ def traverse(node: CallTreeNode, prefix: str = "", is_last: bool = True):
334
+ # Build line for current node
335
+ connector = "└── " if is_last else "├── "
336
+
337
+ func_name = node.function_info.name
338
+ if show_file:
339
+ file_name = Path(node.function_info.file_path).name
340
+ line = f"{prefix}{connector}{func_name} ({file_name}:{node.function_info.line_number})"
341
+ else:
342
+ line = f"{prefix}{connector}{func_name}"
343
+
344
+ if node.is_recursive:
345
+ line += " [RECURSIVE]"
346
+
347
+ lines.append(line)
348
+
349
+ # Build lines for children
350
+ if node.children:
351
+ new_prefix = prefix + (" " if is_last else "│ ")
352
+ for idx, child in enumerate(node.children):
353
+ is_last_child = idx == len(node.children) - 1
354
+ traverse(child, new_prefix, is_last_child)
355
+
356
+ # Start with root (no prefix)
357
+ func_name = root.function_info.name
358
+ if show_file:
359
+ file_name = Path(root.function_info.file_path).name
360
+ lines.append(f"{func_name} ({file_name}:{root.function_info.line_number})")
361
+ else:
362
+ lines.append(func_name)
363
+
364
+ # Add children
365
+ for idx, child in enumerate(root.children):
366
+ is_last = idx == len(root.children) - 1
367
+ traverse(child, "", is_last)
368
+
369
+ return "\n".join(lines)
@@ -0,0 +1,5 @@
1
+ """CLI package initialization."""
2
+
3
+ from .main import cli
4
+
5
+ __all__ = ["cli"]