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,488 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mermaid sequence diagram generator.
|
|
3
|
+
|
|
4
|
+
This module generates Mermaid sequence diagram syntax from call trees,
|
|
5
|
+
outputting markdown files with embedded diagrams.
|
|
6
|
+
|
|
7
|
+
Requirements:
|
|
8
|
+
- SWR_MERMAID_00001: Module-Based Participants
|
|
9
|
+
- SWR_MERMAID_00002: Module Column in Function Table
|
|
10
|
+
- SWR_MERMAID_00003: Fallback Behavior
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from ..database.models import AnalysisResult, CallTreeNode, FunctionInfo
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MermaidGenerator:
|
|
21
|
+
"""
|
|
22
|
+
Generates Mermaid sequence diagrams from call trees.
|
|
23
|
+
|
|
24
|
+
This class converts call tree structures into Mermaid diagram syntax,
|
|
25
|
+
creates markdown documents with metadata, and handles formatting options.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
abbreviate_rte: bool = True,
|
|
31
|
+
use_module_names: bool = False,
|
|
32
|
+
include_returns: bool = False,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Initialize the Mermaid generator.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
abbreviate_rte: Whether to abbreviate long RTE function names
|
|
39
|
+
use_module_names: Use SW module names as participants instead of function names
|
|
40
|
+
include_returns: Whether to include return statements in the sequence diagram (default: False)
|
|
41
|
+
"""
|
|
42
|
+
self.abbreviate_rte = abbreviate_rte
|
|
43
|
+
self.use_module_names = use_module_names
|
|
44
|
+
self.include_returns = include_returns
|
|
45
|
+
self.participant_map: Dict[str, str] = {} # Map full names to abbreviated names
|
|
46
|
+
self.next_participant_id = 1
|
|
47
|
+
|
|
48
|
+
def generate(
|
|
49
|
+
self,
|
|
50
|
+
result: AnalysisResult,
|
|
51
|
+
output_path: str,
|
|
52
|
+
include_metadata: bool = True,
|
|
53
|
+
include_function_table: bool = True,
|
|
54
|
+
include_text_tree: bool = True,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Generate Mermaid diagram and save to markdown file.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
result: Analysis result containing call tree
|
|
61
|
+
output_path: Path to output markdown file
|
|
62
|
+
include_metadata: Include metadata section
|
|
63
|
+
include_function_table: Include function details table
|
|
64
|
+
include_text_tree: Include text-based tree representation
|
|
65
|
+
"""
|
|
66
|
+
if not result.call_tree:
|
|
67
|
+
raise ValueError("Cannot generate diagram: call tree is None")
|
|
68
|
+
|
|
69
|
+
# Build content sections
|
|
70
|
+
content = []
|
|
71
|
+
|
|
72
|
+
# Add title
|
|
73
|
+
content.append(f"# Call Tree: {result.root_function}\n")
|
|
74
|
+
|
|
75
|
+
# Add metadata
|
|
76
|
+
if include_metadata:
|
|
77
|
+
content.append(self._generate_metadata(result))
|
|
78
|
+
|
|
79
|
+
# Add sequence diagram
|
|
80
|
+
content.append("## Sequence Diagram\n")
|
|
81
|
+
diagram = self._generate_mermaid_diagram(result.call_tree)
|
|
82
|
+
content.append("```mermaid")
|
|
83
|
+
content.append(diagram)
|
|
84
|
+
content.append("```\n")
|
|
85
|
+
|
|
86
|
+
# Add function details table
|
|
87
|
+
if include_function_table:
|
|
88
|
+
content.append(self._generate_function_table(result.call_tree))
|
|
89
|
+
|
|
90
|
+
# Add text tree
|
|
91
|
+
if include_text_tree:
|
|
92
|
+
content.append(self._generate_text_tree(result.call_tree))
|
|
93
|
+
|
|
94
|
+
# Add circular dependencies if any
|
|
95
|
+
if result.circular_dependencies:
|
|
96
|
+
content.append(self._generate_circular_deps_section(result))
|
|
97
|
+
|
|
98
|
+
# Write to file
|
|
99
|
+
output_file = Path(output_path)
|
|
100
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
output_file.write_text("\n".join(content), encoding="utf-8")
|
|
102
|
+
|
|
103
|
+
def _generate_metadata(self, result: AnalysisResult) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Generate metadata section.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
result: Analysis result
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Markdown formatted metadata
|
|
112
|
+
"""
|
|
113
|
+
lines = [
|
|
114
|
+
"## Metadata\n",
|
|
115
|
+
f"- **Root Function**: `{result.root_function}`",
|
|
116
|
+
f"- **Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
|
117
|
+
f"- **Total Functions**: {result.statistics.total_functions}",
|
|
118
|
+
f"- **Unique Functions**: {result.statistics.unique_functions}",
|
|
119
|
+
f"- **Max Depth**: {result.statistics.max_depth_reached}",
|
|
120
|
+
f"- **Circular Dependencies**: {result.statistics.circular_dependencies_found}",
|
|
121
|
+
"",
|
|
122
|
+
]
|
|
123
|
+
return "\n".join(lines)
|
|
124
|
+
|
|
125
|
+
def _generate_mermaid_diagram(self, root: CallTreeNode) -> str:
|
|
126
|
+
"""
|
|
127
|
+
Generate Mermaid sequence diagram syntax.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
root: Root node of call tree
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Mermaid diagram as string
|
|
134
|
+
"""
|
|
135
|
+
lines = ["sequenceDiagram"]
|
|
136
|
+
|
|
137
|
+
# Collect all participants
|
|
138
|
+
participants = self._collect_participants(root)
|
|
139
|
+
|
|
140
|
+
# Add participant declarations
|
|
141
|
+
for participant in participants:
|
|
142
|
+
if self.abbreviate_rte and participant.startswith("Rte_"):
|
|
143
|
+
abbrev = self._abbreviate_rte_name(participant)
|
|
144
|
+
self.participant_map[participant] = abbrev
|
|
145
|
+
lines.append(f" participant {abbrev} as {participant}")
|
|
146
|
+
else:
|
|
147
|
+
lines.append(f" participant {participant}")
|
|
148
|
+
|
|
149
|
+
lines.append("")
|
|
150
|
+
|
|
151
|
+
# Generate sequence calls
|
|
152
|
+
self._generate_sequence_calls(root, lines)
|
|
153
|
+
|
|
154
|
+
return "\n".join(lines)
|
|
155
|
+
|
|
156
|
+
def _collect_participants(self, root: CallTreeNode) -> List[str]:
|
|
157
|
+
"""
|
|
158
|
+
Collect all unique participants (functions or modules) in tree.
|
|
159
|
+
|
|
160
|
+
Implements: SWR_MERMAID_00001 (Module-Based Participants)
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
root: Root node of call tree
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List of participant names in the order they are first encountered
|
|
167
|
+
"""
|
|
168
|
+
participants = []
|
|
169
|
+
|
|
170
|
+
def traverse(node: CallTreeNode):
|
|
171
|
+
# Use module name if enabled, otherwise use function name
|
|
172
|
+
if self.use_module_names:
|
|
173
|
+
# Use module name if available, otherwise fallback to filename
|
|
174
|
+
participant = (
|
|
175
|
+
node.function_info.sw_module
|
|
176
|
+
or Path(node.function_info.file_path).stem
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
participant = node.function_info.name
|
|
180
|
+
|
|
181
|
+
# Add participant only if not already in the list
|
|
182
|
+
if participant not in participants:
|
|
183
|
+
participants.append(participant)
|
|
184
|
+
|
|
185
|
+
for child in node.children:
|
|
186
|
+
traverse(child)
|
|
187
|
+
|
|
188
|
+
traverse(root)
|
|
189
|
+
return participants
|
|
190
|
+
|
|
191
|
+
def _generate_sequence_calls(
|
|
192
|
+
self, node: CallTreeNode, lines: List[str], caller: Optional[str] = None
|
|
193
|
+
) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Generate sequence call statements recursively.
|
|
196
|
+
|
|
197
|
+
Implements: SWR_MERMAID_00001 (Module-Based Participants with function names on arrows)
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
node: Current node in call tree
|
|
201
|
+
lines: List of lines to append to
|
|
202
|
+
caller: Name of calling function or module (None for root)
|
|
203
|
+
"""
|
|
204
|
+
# Determine current participant (module or function name)
|
|
205
|
+
if self.use_module_names:
|
|
206
|
+
current_participant = (
|
|
207
|
+
node.function_info.sw_module or Path(node.function_info.file_path).stem
|
|
208
|
+
)
|
|
209
|
+
# When using modules, show function name on arrows
|
|
210
|
+
call_label = node.function_info.name
|
|
211
|
+
else:
|
|
212
|
+
current_participant = self._get_participant_name(node.function_info.name)
|
|
213
|
+
# When using function names, show generic "call" label
|
|
214
|
+
call_label = "call"
|
|
215
|
+
|
|
216
|
+
# Add parameters to the call label
|
|
217
|
+
if node.function_info.parameters:
|
|
218
|
+
params_str = self._format_parameters_for_diagram(node.function_info)
|
|
219
|
+
call_label = f"{call_label}({params_str})"
|
|
220
|
+
|
|
221
|
+
# Generate call from caller to current
|
|
222
|
+
if caller:
|
|
223
|
+
if node.is_recursive:
|
|
224
|
+
if self.use_module_names:
|
|
225
|
+
label = f"{call_label} [recursive]"
|
|
226
|
+
else:
|
|
227
|
+
label = "recursive call"
|
|
228
|
+
lines.append(f" {caller}-->>x{current_participant}: {label}")
|
|
229
|
+
else:
|
|
230
|
+
lines.append(f" {caller}->>{current_participant}: {call_label}")
|
|
231
|
+
|
|
232
|
+
# Generate calls to children
|
|
233
|
+
for child in node.children:
|
|
234
|
+
self._generate_sequence_calls(child, lines, current_participant)
|
|
235
|
+
|
|
236
|
+
# Generate return from current to caller (only if include_returns is True)
|
|
237
|
+
if caller and not node.is_recursive and self.include_returns:
|
|
238
|
+
lines.append(f" {current_participant}-->>{caller}: return")
|
|
239
|
+
|
|
240
|
+
def _get_participant_name(self, function_name: str) -> str:
|
|
241
|
+
"""
|
|
242
|
+
Get participant name (possibly abbreviated).
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
function_name: Original function name
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Participant name to use in diagram
|
|
249
|
+
"""
|
|
250
|
+
return str(self.participant_map.get(function_name, function_name))
|
|
251
|
+
|
|
252
|
+
def _abbreviate_rte_name(self, rte_function: str) -> str:
|
|
253
|
+
"""
|
|
254
|
+
Abbreviate RTE function name.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
rte_function: Full RTE function name
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Abbreviated name
|
|
261
|
+
"""
|
|
262
|
+
# Simple abbreviation: Rte_Read_P_Voltage_Value -> Rte_Read_PVV
|
|
263
|
+
parts = rte_function.split("_")
|
|
264
|
+
if len(parts) <= 2:
|
|
265
|
+
return rte_function
|
|
266
|
+
|
|
267
|
+
# Keep Rte_ prefix and first operation, abbreviate rest
|
|
268
|
+
prefix = "_".join(parts[:2]) # e.g., "Rte_Read"
|
|
269
|
+
abbrev_parts = [p[0].upper() for p in parts[2:] if p]
|
|
270
|
+
abbrev = "".join(abbrev_parts)
|
|
271
|
+
|
|
272
|
+
return f"{prefix}_{abbrev}"
|
|
273
|
+
|
|
274
|
+
def _generate_function_table(self, root: CallTreeNode) -> str:
|
|
275
|
+
"""
|
|
276
|
+
Generate markdown table of function details.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
root: Root node of call tree
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Markdown formatted table
|
|
283
|
+
"""
|
|
284
|
+
# Build header based on whether we're showing modules
|
|
285
|
+
if self.use_module_names:
|
|
286
|
+
lines = [
|
|
287
|
+
"## Function Details\n",
|
|
288
|
+
"| Function | Module | File | Line | Return Type | Parameters |",
|
|
289
|
+
"|----------|--------|------|------|-------------|------------|",
|
|
290
|
+
]
|
|
291
|
+
else:
|
|
292
|
+
lines = [
|
|
293
|
+
"## Function Details\n",
|
|
294
|
+
"| Function | File | Line | Return Type | Parameters |",
|
|
295
|
+
"|----------|------|------|-------------|------------|",
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
# Collect all unique functions
|
|
299
|
+
functions = []
|
|
300
|
+
seen = set()
|
|
301
|
+
|
|
302
|
+
def traverse(node: CallTreeNode):
|
|
303
|
+
if node.function_info.name not in seen:
|
|
304
|
+
seen.add(node.function_info.name)
|
|
305
|
+
functions.append(node.function_info)
|
|
306
|
+
for child in node.children:
|
|
307
|
+
traverse(child)
|
|
308
|
+
|
|
309
|
+
traverse(root)
|
|
310
|
+
|
|
311
|
+
# Sort by function name
|
|
312
|
+
functions.sort(key=lambda f: f.name)
|
|
313
|
+
|
|
314
|
+
# Add table rows
|
|
315
|
+
for func in functions:
|
|
316
|
+
file_name = Path(func.file_path).name
|
|
317
|
+
params = self._format_parameters(func)
|
|
318
|
+
|
|
319
|
+
if self.use_module_names:
|
|
320
|
+
module = func.sw_module or "N/A"
|
|
321
|
+
lines.append(
|
|
322
|
+
f"| `{func.name}` | {module} | {file_name} | {func.line_number} | "
|
|
323
|
+
f"`{func.return_type}` | {params} |"
|
|
324
|
+
)
|
|
325
|
+
else:
|
|
326
|
+
lines.append(
|
|
327
|
+
f"| `{func.name}` | {file_name} | {func.line_number} | "
|
|
328
|
+
f"`{func.return_type}` | {params} |"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
lines.append("")
|
|
332
|
+
return "\n".join(lines)
|
|
333
|
+
|
|
334
|
+
def _format_parameters(self, func: FunctionInfo) -> str:
|
|
335
|
+
"""
|
|
336
|
+
Format function parameters for table.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
func: Function information
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Formatted parameter string
|
|
343
|
+
"""
|
|
344
|
+
if not func.parameters:
|
|
345
|
+
return "`void`"
|
|
346
|
+
|
|
347
|
+
param_strs = []
|
|
348
|
+
for param in func.parameters:
|
|
349
|
+
type_str = param.param_type
|
|
350
|
+
if param.is_pointer:
|
|
351
|
+
type_str += "*"
|
|
352
|
+
|
|
353
|
+
if param.name:
|
|
354
|
+
param_strs.append(f"`{type_str} {param.name}`")
|
|
355
|
+
else:
|
|
356
|
+
param_strs.append(f"`{type_str}`")
|
|
357
|
+
|
|
358
|
+
return "<br>".join(param_strs)
|
|
359
|
+
|
|
360
|
+
def _format_parameters_for_diagram(self, func: FunctionInfo) -> str:
|
|
361
|
+
"""
|
|
362
|
+
Format function parameters for sequence diagram display.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
func: Function information
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Formatted parameter string for diagram
|
|
369
|
+
"""
|
|
370
|
+
if not func.parameters:
|
|
371
|
+
return ""
|
|
372
|
+
|
|
373
|
+
param_strs = []
|
|
374
|
+
for param in func.parameters:
|
|
375
|
+
if param.name:
|
|
376
|
+
param_strs.append(param.name)
|
|
377
|
+
else:
|
|
378
|
+
# If no parameter name, use the type
|
|
379
|
+
type_str = param.param_type
|
|
380
|
+
if param.is_pointer:
|
|
381
|
+
type_str += "*"
|
|
382
|
+
param_strs.append(type_str)
|
|
383
|
+
|
|
384
|
+
return ", ".join(param_strs)
|
|
385
|
+
|
|
386
|
+
def _generate_text_tree(self, root: CallTreeNode) -> str:
|
|
387
|
+
"""
|
|
388
|
+
Generate text-based tree representation.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
root: Root node of call tree
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Markdown formatted text tree
|
|
395
|
+
"""
|
|
396
|
+
lines = ["## Call Tree (Text)\n", "```"]
|
|
397
|
+
|
|
398
|
+
def traverse(node: CallTreeNode, prefix: str = "", is_last: bool = True):
|
|
399
|
+
connector = "└── " if is_last else "├── "
|
|
400
|
+
|
|
401
|
+
func_name = node.function_info.name
|
|
402
|
+
file_name = Path(node.function_info.file_path).name
|
|
403
|
+
line = f"{prefix}{connector}{func_name} ({file_name}:{node.function_info.line_number})"
|
|
404
|
+
|
|
405
|
+
if node.is_recursive:
|
|
406
|
+
line += " [RECURSIVE]"
|
|
407
|
+
|
|
408
|
+
lines.append(line)
|
|
409
|
+
|
|
410
|
+
if node.children:
|
|
411
|
+
new_prefix = prefix + (" " if is_last else "│ ")
|
|
412
|
+
for idx, child in enumerate(node.children):
|
|
413
|
+
is_last_child = idx == len(node.children) - 1
|
|
414
|
+
traverse(child, new_prefix, is_last_child)
|
|
415
|
+
|
|
416
|
+
# Start with root
|
|
417
|
+
func_name = root.function_info.name
|
|
418
|
+
file_name = Path(root.function_info.file_path).name
|
|
419
|
+
lines.append(f"{func_name} ({file_name}:{root.function_info.line_number})")
|
|
420
|
+
|
|
421
|
+
for idx, child in enumerate(root.children):
|
|
422
|
+
is_last = idx == len(root.children) - 1
|
|
423
|
+
traverse(child, "", is_last)
|
|
424
|
+
|
|
425
|
+
lines.append("```\n")
|
|
426
|
+
return "\n".join(lines)
|
|
427
|
+
|
|
428
|
+
def _generate_circular_deps_section(self, result: AnalysisResult) -> str:
|
|
429
|
+
"""
|
|
430
|
+
Generate section for circular dependencies.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
result: Analysis result
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Markdown formatted circular dependencies section
|
|
437
|
+
"""
|
|
438
|
+
lines = [
|
|
439
|
+
"## Circular Dependencies\n",
|
|
440
|
+
f"Found {len(result.circular_dependencies)} circular dependencies:\n",
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
for idx, circ_dep in enumerate(result.circular_dependencies, 1):
|
|
444
|
+
cycle_str = " → ".join(circ_dep.cycle)
|
|
445
|
+
lines.append(f"{idx}. **Depth {circ_dep.depth}**: `{cycle_str}`")
|
|
446
|
+
|
|
447
|
+
lines.append("")
|
|
448
|
+
return "\n".join(lines)
|
|
449
|
+
|
|
450
|
+
def generate_to_string(self, result: AnalysisResult) -> str:
|
|
451
|
+
"""
|
|
452
|
+
Generate Mermaid diagram as string without writing to file.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
result: Analysis result
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
Complete markdown document as string
|
|
459
|
+
"""
|
|
460
|
+
if not result.call_tree:
|
|
461
|
+
raise ValueError("Cannot generate diagram: call tree is None")
|
|
462
|
+
|
|
463
|
+
content = []
|
|
464
|
+
|
|
465
|
+
# Add title
|
|
466
|
+
content.append(f"# Call Tree: {result.root_function}\n")
|
|
467
|
+
|
|
468
|
+
# Add metadata
|
|
469
|
+
content.append(self._generate_metadata(result))
|
|
470
|
+
|
|
471
|
+
# Add sequence diagram
|
|
472
|
+
content.append("## Sequence Diagram\n")
|
|
473
|
+
diagram = self._generate_mermaid_diagram(result.call_tree)
|
|
474
|
+
content.append("```mermaid")
|
|
475
|
+
content.append(diagram)
|
|
476
|
+
content.append("```\n")
|
|
477
|
+
|
|
478
|
+
# Add function table
|
|
479
|
+
content.append(self._generate_function_table(result.call_tree))
|
|
480
|
+
|
|
481
|
+
# Add text tree
|
|
482
|
+
content.append(self._generate_text_tree(result.call_tree))
|
|
483
|
+
|
|
484
|
+
# Add circular dependencies
|
|
485
|
+
if result.circular_dependencies:
|
|
486
|
+
content.append(self._generate_circular_deps_section(result))
|
|
487
|
+
|
|
488
|
+
return "\n".join(content)
|