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.
- autosar_calltree/__init__.py +24 -0
- autosar_calltree/analyzers/__init__.py +5 -0
- autosar_calltree/analyzers/call_tree_builder.py +369 -0
- autosar_calltree/cli/__init__.py +5 -0
- autosar_calltree/cli/main.py +330 -0
- autosar_calltree/config/__init__.py +10 -0
- autosar_calltree/config/module_config.py +179 -0
- autosar_calltree/database/__init__.py +23 -0
- autosar_calltree/database/function_database.py +505 -0
- autosar_calltree/database/models.py +189 -0
- autosar_calltree/generators/__init__.py +5 -0
- autosar_calltree/generators/mermaid_generator.py +488 -0
- autosar_calltree/parsers/__init__.py +6 -0
- autosar_calltree/parsers/autosar_parser.py +314 -0
- autosar_calltree/parsers/c_parser.py +415 -0
- autosar_calltree/version.py +5 -0
- autosar_calltree-0.3.0.dist-info/METADATA +482 -0
- autosar_calltree-0.3.0.dist-info/RECORD +22 -0
- autosar_calltree-0.3.0.dist-info/WHEEL +5 -0
- autosar_calltree-0.3.0.dist-info/entry_points.txt +2 -0
- autosar_calltree-0.3.0.dist-info/licenses/LICENSE +21 -0
- autosar_calltree-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -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,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)
|