autosar-calltree 0.3.3__py3-none-any.whl → 0.5.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/analyzers/call_tree_builder.py +9 -1
- autosar_calltree/cli/main.py +42 -3
- autosar_calltree/database/models.py +19 -1
- autosar_calltree/generators/__init__.py +2 -1
- autosar_calltree/generators/mermaid_generator.py +9 -0
- autosar_calltree/generators/xmi_generator.py +416 -0
- autosar_calltree/parsers/c_parser.py +109 -22
- autosar_calltree/version.py +1 -1
- {autosar_calltree-0.3.3.dist-info → autosar_calltree-0.5.0.dist-info}/METADATA +222 -29
- autosar_calltree-0.5.0.dist-info/RECORD +23 -0
- autosar_calltree-0.3.3.dist-info/RECORD +0 -22
- {autosar_calltree-0.3.3.dist-info → autosar_calltree-0.5.0.dist-info}/WHEEL +0 -0
- {autosar_calltree-0.3.3.dist-info → autosar_calltree-0.5.0.dist-info}/entry_points.txt +0 -0
- {autosar_calltree-0.3.3.dist-info → autosar_calltree-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {autosar_calltree-0.3.3.dist-info → autosar_calltree-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -203,7 +203,10 @@ class CallTreeBuilder:
|
|
|
203
203
|
# Build children nodes
|
|
204
204
|
children = []
|
|
205
205
|
|
|
206
|
-
for
|
|
206
|
+
for func_call in func_info.calls:
|
|
207
|
+
called_func_name = func_call.name
|
|
208
|
+
is_conditional = func_call.is_conditional
|
|
209
|
+
|
|
207
210
|
# Lookup called function
|
|
208
211
|
called_funcs = self.function_db.lookup_function(
|
|
209
212
|
called_func_name, context_file=str(func_info.file_path)
|
|
@@ -228,6 +231,11 @@ class CallTreeBuilder:
|
|
|
228
231
|
verbose=verbose,
|
|
229
232
|
)
|
|
230
233
|
|
|
234
|
+
# Mark as optional if it's conditional
|
|
235
|
+
if is_conditional:
|
|
236
|
+
child_node.is_optional = True
|
|
237
|
+
child_node.condition = func_call.condition
|
|
238
|
+
|
|
231
239
|
children.append(child_node)
|
|
232
240
|
|
|
233
241
|
# Remove from call stack
|
autosar_calltree/cli/main.py
CHANGED
|
@@ -17,6 +17,7 @@ from ..analyzers.call_tree_builder import CallTreeBuilder
|
|
|
17
17
|
from ..config.module_config import ModuleConfig
|
|
18
18
|
from ..database.function_database import FunctionDatabase
|
|
19
19
|
from ..generators.mermaid_generator import MermaidGenerator
|
|
20
|
+
from ..generators.xmi_generator import XmiGenerator
|
|
20
21
|
from ..version import __version__
|
|
21
22
|
|
|
22
23
|
console = Console(record=True)
|
|
@@ -233,7 +234,9 @@ def cli(
|
|
|
233
234
|
|
|
234
235
|
builder = CallTreeBuilder(db)
|
|
235
236
|
result = builder.build_tree(
|
|
236
|
-
start_function=start_function,
|
|
237
|
+
start_function=start_function,
|
|
238
|
+
max_depth=max_depth,
|
|
239
|
+
verbose=verbose,
|
|
237
240
|
)
|
|
238
241
|
|
|
239
242
|
progress.update(task, completed=True)
|
|
@@ -295,11 +298,47 @@ def cli(
|
|
|
295
298
|
)
|
|
296
299
|
|
|
297
300
|
if format == "xmi":
|
|
298
|
-
|
|
301
|
+
with Progress(
|
|
302
|
+
SpinnerColumn(),
|
|
303
|
+
TextColumn("[progress.description]{task.description}"),
|
|
304
|
+
console=console,
|
|
305
|
+
transient=True,
|
|
306
|
+
) as progress:
|
|
307
|
+
task = progress.add_task("Generating XMI document...", total=None)
|
|
308
|
+
|
|
309
|
+
xmi_output = output_path.with_suffix(".xmi") if output_path.suffix != ".xmi" else output_path
|
|
310
|
+
|
|
311
|
+
xmi_generator = XmiGenerator(
|
|
312
|
+
use_module_names=use_module_names,
|
|
313
|
+
)
|
|
314
|
+
xmi_generator.generate(result, str(xmi_output))
|
|
315
|
+
|
|
316
|
+
progress.update(task, completed=True)
|
|
317
|
+
|
|
318
|
+
console.print(
|
|
319
|
+
f"[green]Generated[/green] XMI document: [cyan]{xmi_output}[/cyan]"
|
|
320
|
+
)
|
|
299
321
|
|
|
300
322
|
if format == "both":
|
|
323
|
+
with Progress(
|
|
324
|
+
SpinnerColumn(),
|
|
325
|
+
TextColumn("[progress.description]{task.description}"),
|
|
326
|
+
console=console,
|
|
327
|
+
transient=True,
|
|
328
|
+
) as progress:
|
|
329
|
+
task = progress.add_task("Generating XMI document...", total=None)
|
|
330
|
+
|
|
331
|
+
xmi_output = output_path.with_suffix(".xmi")
|
|
332
|
+
|
|
333
|
+
xmi_generator = XmiGenerator(
|
|
334
|
+
use_module_names=use_module_names,
|
|
335
|
+
)
|
|
336
|
+
xmi_generator.generate(result, str(xmi_output))
|
|
337
|
+
|
|
338
|
+
progress.update(task, completed=True)
|
|
339
|
+
|
|
301
340
|
console.print(
|
|
302
|
-
"[
|
|
341
|
+
f"[green]Generated[/green] XMI document: [cyan]{xmi_output}[/cyan]"
|
|
303
342
|
)
|
|
304
343
|
|
|
305
344
|
# Print warnings for circular dependencies
|
|
@@ -22,6 +22,22 @@ class FunctionType(Enum):
|
|
|
22
22
|
UNKNOWN = "unknown"
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
@dataclass
|
|
26
|
+
class FunctionCall:
|
|
27
|
+
"""Represents a function call with its conditional status."""
|
|
28
|
+
|
|
29
|
+
name: str # Name of the called function
|
|
30
|
+
is_conditional: bool = False # True if called inside if/else block
|
|
31
|
+
condition: Optional[str] = None # The if/else condition (e.g., "update_mode == 0x05")
|
|
32
|
+
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
"""String representation."""
|
|
35
|
+
if self.condition:
|
|
36
|
+
return f"{self.name} [{self.condition}]"
|
|
37
|
+
conditional_str = " [conditional]" if self.is_conditional else ""
|
|
38
|
+
return f"{self.name}{conditional_str}"
|
|
39
|
+
|
|
40
|
+
|
|
25
41
|
@dataclass
|
|
26
42
|
class Parameter:
|
|
27
43
|
"""Function parameter information."""
|
|
@@ -53,7 +69,7 @@ class FunctionInfo:
|
|
|
53
69
|
function_type: FunctionType
|
|
54
70
|
memory_class: Optional[str] = None # AUTOSAR memory class (RTE_CODE, etc.)
|
|
55
71
|
parameters: List[Parameter] = field(default_factory=list)
|
|
56
|
-
calls: List[
|
|
72
|
+
calls: List[FunctionCall] = field(default_factory=list) # Functions called within
|
|
57
73
|
called_by: Set[str] = field(default_factory=set) # Functions that call this
|
|
58
74
|
|
|
59
75
|
# AUTOSAR specific
|
|
@@ -102,6 +118,8 @@ class CallTreeNode:
|
|
|
102
118
|
is_recursive: bool = False # True if function already in call stack
|
|
103
119
|
is_truncated: bool = False # True if depth limit reached
|
|
104
120
|
call_count: int = 1 # Number of times this function is called
|
|
121
|
+
is_optional: bool = False # True if this call is conditional (for opt blocks)
|
|
122
|
+
condition: Optional[str] = None # The if/else condition (e.g., "update_mode == 0x05")
|
|
105
123
|
|
|
106
124
|
def add_child(self, child: "CallTreeNode") -> None:
|
|
107
125
|
"""Add a child node."""
|
|
@@ -231,8 +231,17 @@ class MermaidGenerator:
|
|
|
231
231
|
|
|
232
232
|
# Generate calls to children
|
|
233
233
|
for child in node.children:
|
|
234
|
+
# Start opt block for optional calls
|
|
235
|
+
if child.is_optional:
|
|
236
|
+
condition_text = child.condition if child.condition else "Optional call"
|
|
237
|
+
lines.append(f" opt {condition_text}")
|
|
238
|
+
|
|
234
239
|
self._generate_sequence_calls(child, lines, current_participant)
|
|
235
240
|
|
|
241
|
+
# End opt block for optional calls
|
|
242
|
+
if child.is_optional:
|
|
243
|
+
lines.append(" end")
|
|
244
|
+
|
|
236
245
|
# Generate return from current to caller (only if include_returns is True)
|
|
237
246
|
if caller and not node.is_recursive and self.include_returns:
|
|
238
247
|
lines.append(f" {current_participant}-->>{caller}: return")
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""
|
|
2
|
+
XMI (XML Metadata Interchange) generator for UML sequence diagrams.
|
|
3
|
+
|
|
4
|
+
This module generates XMI 2.5 compliant XML documents representing
|
|
5
|
+
UML sequence diagrams from call trees. The XMI format can be imported
|
|
6
|
+
into UML tools like Enterprise Architect, Visual Paradigm, etc.
|
|
7
|
+
|
|
8
|
+
Requirements:
|
|
9
|
+
- SWR_XMI_00001: XMI 2.5 Compliance
|
|
10
|
+
- SWR_XMI_00002: Sequence Diagram Representation
|
|
11
|
+
- SWR_XMI_00003: Opt Block Support (Combined Fragments)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, List, Optional
|
|
16
|
+
from xml.dom import minidom
|
|
17
|
+
from xml.etree.ElementTree import Element, SubElement, tostring
|
|
18
|
+
|
|
19
|
+
from ..database.models import AnalysisResult, CallTreeNode
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class XmiGenerator:
|
|
23
|
+
"""
|
|
24
|
+
Generates XMI 2.5 format UML sequence diagrams from call trees.
|
|
25
|
+
|
|
26
|
+
This class converts call tree structures into XMI XML documents,
|
|
27
|
+
following the UML 2.5 XMI specification for sequence diagrams.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# XMI/UML namespaces
|
|
31
|
+
XMI_NAMESPACE = "http://www.omg.org/spec/XMI/20131001"
|
|
32
|
+
XMI_URI = "http://www.omg.org/spec/XMI/20131001"
|
|
33
|
+
UML_NAMESPACE = "http://www.eclipse.org/uml2/5.0.0/UML"
|
|
34
|
+
UML_URI = "http://www.eclipse.org/uml2/5.0.0/UML"
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
use_module_names: bool = False,
|
|
39
|
+
):
|
|
40
|
+
"""
|
|
41
|
+
Initialize the XMI generator.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
use_module_names: Use SW module names as participants instead of function names
|
|
45
|
+
"""
|
|
46
|
+
self.use_module_names = use_module_names
|
|
47
|
+
self.element_id_counter = 0
|
|
48
|
+
self.participant_map: Dict[str, str] = {} # Map names to XMI IDs
|
|
49
|
+
|
|
50
|
+
def generate(
|
|
51
|
+
self,
|
|
52
|
+
result: AnalysisResult,
|
|
53
|
+
output_path: str,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Generate XMI document and save to file.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
result: Analysis result containing call tree
|
|
60
|
+
output_path: Path to output XMI file
|
|
61
|
+
"""
|
|
62
|
+
if not result.call_tree:
|
|
63
|
+
raise ValueError("Cannot generate XMI: call tree is None")
|
|
64
|
+
|
|
65
|
+
# Build XMI document
|
|
66
|
+
root_element = self._generate_xmi_document(result, result.call_tree)
|
|
67
|
+
|
|
68
|
+
# Pretty print and write to file
|
|
69
|
+
xml_str = self._prettify_xml(root_element)
|
|
70
|
+
output_file = Path(output_path)
|
|
71
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
output_file.write_text(xml_str, encoding="utf-8")
|
|
73
|
+
|
|
74
|
+
def _generate_xmi_document(self, result: AnalysisResult, call_tree: CallTreeNode) -> Element:
|
|
75
|
+
"""
|
|
76
|
+
Generate XMI root element with complete document structure.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
result: Analysis result containing metadata
|
|
80
|
+
call_tree: Root node of the call tree (non-optional)
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Root XML Element for the XMI document
|
|
84
|
+
"""
|
|
85
|
+
# Create root element with XMI namespace
|
|
86
|
+
root = Element(
|
|
87
|
+
f"{{{self.XMI_NAMESPACE}}}XMI",
|
|
88
|
+
attrib={
|
|
89
|
+
f"{{{self.XMI_NAMESPACE}}}version": "4.0",
|
|
90
|
+
"xmlns:xmi": self.XMI_URI,
|
|
91
|
+
"xmlns:uml": self.UML_URI,
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Create UML model
|
|
96
|
+
model = SubElement(root, f"{{{self.UML_NAMESPACE}}}Model", attrib={
|
|
97
|
+
f"{{{self.XMI_NAMESPACE}}}id": self._generate_id(),
|
|
98
|
+
"name": f"CallTree_{result.root_function}",
|
|
99
|
+
"visibility": "public",
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
# Package the model
|
|
103
|
+
packaged_element = SubElement(model, f"{{{self.UML_NAMESPACE}}}packagedElement")
|
|
104
|
+
packaged_element.set(f"{{{self.XMI_NAMESPACE}}}id", self._generate_id())
|
|
105
|
+
packaged_element.set("name", f"Sequence_{result.root_function}")
|
|
106
|
+
packaged_element.set("visibility", "public")
|
|
107
|
+
|
|
108
|
+
# Create interaction (sequence diagram container)
|
|
109
|
+
interaction = self._create_interaction(packaged_element, result)
|
|
110
|
+
|
|
111
|
+
# Collect all participants and create lifelines
|
|
112
|
+
participants = self._collect_participants(call_tree)
|
|
113
|
+
lifeline_elements = self._create_lifelines(interaction, participants)
|
|
114
|
+
|
|
115
|
+
# Create messages (function calls) with opt block support
|
|
116
|
+
self._create_messages(call_tree, lifeline_elements, interaction)
|
|
117
|
+
|
|
118
|
+
return root
|
|
119
|
+
|
|
120
|
+
def _create_interaction(
|
|
121
|
+
self, parent: Element, result: AnalysisResult
|
|
122
|
+
) -> Element:
|
|
123
|
+
"""
|
|
124
|
+
Create UML interaction element (sequence diagram container).
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
parent: Parent element
|
|
128
|
+
result: Analysis result
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Interaction element
|
|
132
|
+
"""
|
|
133
|
+
interaction = SubElement(parent, f"{{{self.UML_NAMESPACE}}}packagedElement")
|
|
134
|
+
interaction.set(f"{{{self.XMI_NAMESPACE}}}id", self._generate_id())
|
|
135
|
+
interaction.set("name", f"CallSequence_{result.root_function}")
|
|
136
|
+
interaction.set("visibility", "public")
|
|
137
|
+
interaction.set("isReentrant", "false")
|
|
138
|
+
|
|
139
|
+
# Set type to Interaction
|
|
140
|
+
interaction_type = SubElement(interaction, f"{{{self.UML_NAMESPACE}}}ownedAttribute")
|
|
141
|
+
interaction_type.set(f"{{{self.XMI_NAMESPACE}}}id", self._generate_id())
|
|
142
|
+
interaction_type.set("name", "interactionType")
|
|
143
|
+
interaction_type.set("visibility", "public")
|
|
144
|
+
interaction_type.set("type", "Interaction")
|
|
145
|
+
|
|
146
|
+
return interaction
|
|
147
|
+
|
|
148
|
+
def _collect_participants(self, root: CallTreeNode) -> List[str]:
|
|
149
|
+
"""
|
|
150
|
+
Collect all unique participants (functions or modules) in tree.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
root: Root node of call tree
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of participant names in the order they are first encountered
|
|
157
|
+
"""
|
|
158
|
+
participants = []
|
|
159
|
+
|
|
160
|
+
def traverse(node: CallTreeNode):
|
|
161
|
+
# Use module name if enabled, otherwise use function name
|
|
162
|
+
if self.use_module_names:
|
|
163
|
+
participant = (
|
|
164
|
+
node.function_info.sw_module
|
|
165
|
+
or Path(node.function_info.file_path).stem
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
participant = node.function_info.name
|
|
169
|
+
|
|
170
|
+
# Add participant only if not already in the list
|
|
171
|
+
if participant not in participants:
|
|
172
|
+
participants.append(participant)
|
|
173
|
+
|
|
174
|
+
for child in node.children:
|
|
175
|
+
traverse(child)
|
|
176
|
+
|
|
177
|
+
traverse(root)
|
|
178
|
+
return participants
|
|
179
|
+
|
|
180
|
+
def _create_lifelines(
|
|
181
|
+
self, interaction: Element, participants: List[str]
|
|
182
|
+
) -> Dict[str, Element]:
|
|
183
|
+
"""
|
|
184
|
+
Create UML lifelines for each participant.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
interaction: Interaction element
|
|
188
|
+
participants: List of participant names
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Dictionary mapping participant names to their lifeline elements
|
|
192
|
+
"""
|
|
193
|
+
lifeline_elements = {}
|
|
194
|
+
|
|
195
|
+
for idx, participant in enumerate(participants):
|
|
196
|
+
# Create lifeline
|
|
197
|
+
lifeline = SubElement(interaction, f"{{{self.UML_NAMESPACE}}}lifeline")
|
|
198
|
+
lifeline_id = self._generate_id()
|
|
199
|
+
lifeline.set(f"{{{self.XMI_NAMESPACE}}}id", lifeline_id)
|
|
200
|
+
lifeline.set("name", participant)
|
|
201
|
+
lifeline.set("visibility", "public")
|
|
202
|
+
|
|
203
|
+
# Create represents property (connects to classifier)
|
|
204
|
+
represents = SubElement(lifeline, f"{{{self.UML_NAMESPACE}}}represents")
|
|
205
|
+
represents_id = self._generate_id()
|
|
206
|
+
represents.set(f"{{{self.XMI_NAMESPACE}}}id", represents_id)
|
|
207
|
+
represents.set("name", participant)
|
|
208
|
+
|
|
209
|
+
# Store mapping
|
|
210
|
+
self.participant_map[participant] = lifeline_id
|
|
211
|
+
lifeline_elements[participant] = lifeline
|
|
212
|
+
|
|
213
|
+
return lifeline_elements
|
|
214
|
+
|
|
215
|
+
def _create_messages(
|
|
216
|
+
self,
|
|
217
|
+
root: CallTreeNode,
|
|
218
|
+
lifeline_elements: Dict[str, Element],
|
|
219
|
+
interaction: Element,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""
|
|
222
|
+
Create UML messages (function calls) between lifelines with opt block support.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
root: Root node of call tree
|
|
226
|
+
lifeline_elements: Dictionary of lifeline elements
|
|
227
|
+
interaction: Interaction element
|
|
228
|
+
"""
|
|
229
|
+
message_counter = 0
|
|
230
|
+
|
|
231
|
+
def traverse(node: CallTreeNode, parent_participant: Optional[str] = None):
|
|
232
|
+
nonlocal message_counter
|
|
233
|
+
|
|
234
|
+
# Determine current participant
|
|
235
|
+
if self.use_module_names:
|
|
236
|
+
current_participant = (
|
|
237
|
+
node.function_info.sw_module
|
|
238
|
+
or Path(node.function_info.file_path).stem
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
current_participant = node.function_info.name
|
|
242
|
+
|
|
243
|
+
# Create message from parent to current
|
|
244
|
+
if parent_participant:
|
|
245
|
+
message = SubElement(interaction, f"{{{self.UML_NAMESPACE}}}message")
|
|
246
|
+
message_id = self._generate_id()
|
|
247
|
+
message.set(f"{{{self.XMI_NAMESPACE}}}id", message_id)
|
|
248
|
+
message.set("name", node.function_info.name)
|
|
249
|
+
message.set("visibility", "public")
|
|
250
|
+
message.set("messageSort", "synchCall")
|
|
251
|
+
|
|
252
|
+
# Set message signature
|
|
253
|
+
signature = self._format_message_signature(node)
|
|
254
|
+
message.set("signature", signature)
|
|
255
|
+
|
|
256
|
+
# Connect to lifelines
|
|
257
|
+
send_event = SubElement(message, f"{{{self.UML_NAMESPACE}}}sendEvent")
|
|
258
|
+
send_event.set(f"{{{self.XMI_NAMESPACE}}}id", self._generate_id())
|
|
259
|
+
|
|
260
|
+
receive_event = SubElement(message, f"{{{self.UML_NAMESPACE}}}receiveEvent")
|
|
261
|
+
receive_event.set(f"{{{self.XMI_NAMESPACE}}}id", self._generate_id())
|
|
262
|
+
|
|
263
|
+
# Set source and target
|
|
264
|
+
message.set(
|
|
265
|
+
"sendEvent",
|
|
266
|
+
f"{{{self.XMI_NAMESPACE}}}{self.participant_map.get(parent_participant, '')}"
|
|
267
|
+
)
|
|
268
|
+
message.set(
|
|
269
|
+
"receiveEvent",
|
|
270
|
+
f"{{{self.XMI_NAMESPACE}}}{self.participant_map.get(current_participant, '')}"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
message_counter += 1
|
|
274
|
+
|
|
275
|
+
# Mark recursive calls
|
|
276
|
+
if node.is_recursive:
|
|
277
|
+
message.set("messageSort", "reply")
|
|
278
|
+
|
|
279
|
+
# Traverse children with opt block support
|
|
280
|
+
for child in node.children:
|
|
281
|
+
if child.is_optional:
|
|
282
|
+
# Create UML combined fragment for opt block
|
|
283
|
+
combined_fragment = SubElement(interaction, f"{{{self.UML_NAMESPACE}}}fragment")
|
|
284
|
+
combined_fragment.set(f"{{{self.XMI_NAMESPACE}}}id", self._generate_id())
|
|
285
|
+
combined_fragment.set("name", "opt")
|
|
286
|
+
combined_fragment.set("visibility", "public")
|
|
287
|
+
combined_fragment.set("interactionOperator", "opt")
|
|
288
|
+
|
|
289
|
+
# Add interaction operand
|
|
290
|
+
operand = SubElement(combined_fragment, f"{{{self.UML_NAMESPACE}}}operand")
|
|
291
|
+
operand.set(f"{{{self.XMI_NAMESPACE}}}id", self._generate_id())
|
|
292
|
+
operand.set("name", child.condition or "condition")
|
|
293
|
+
operand.set("visibility", "public")
|
|
294
|
+
|
|
295
|
+
# Traverse children inside the opt block
|
|
296
|
+
def traverse_opt(opt_node: CallTreeNode, opt_parent: Optional[str] = None):
|
|
297
|
+
nonlocal message_counter
|
|
298
|
+
|
|
299
|
+
if self.use_module_names:
|
|
300
|
+
opt_participant = (
|
|
301
|
+
opt_node.function_info.sw_module
|
|
302
|
+
or Path(opt_node.function_info.file_path).stem
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
opt_participant = opt_node.function_info.name
|
|
306
|
+
|
|
307
|
+
if opt_parent:
|
|
308
|
+
opt_message = SubElement(operand, f"{{{self.UML_NAMESPACE}}}message")
|
|
309
|
+
opt_message.set(f"{{{self.XMI_NAMESPACE}}}id", self._generate_id())
|
|
310
|
+
opt_message.set("name", opt_node.function_info.name)
|
|
311
|
+
opt_message.set("visibility", "public")
|
|
312
|
+
opt_message.set("messageSort", "synchCall")
|
|
313
|
+
|
|
314
|
+
opt_signature = self._format_message_signature(opt_node)
|
|
315
|
+
opt_message.set("signature", opt_signature)
|
|
316
|
+
|
|
317
|
+
opt_send_event = SubElement(opt_message, f"{{{self.UML_NAMESPACE}}}sendEvent")
|
|
318
|
+
opt_send_event.set(f"{{{self.XMI_NAMESPACE}}}id", self._generate_id())
|
|
319
|
+
|
|
320
|
+
opt_receive_event = SubElement(opt_message, f"{{{self.UML_NAMESPACE}}}receiveEvent")
|
|
321
|
+
opt_receive_event.set(f"{{{self.XMI_NAMESPACE}}}id", self._generate_id())
|
|
322
|
+
|
|
323
|
+
opt_message.set(
|
|
324
|
+
"sendEvent",
|
|
325
|
+
f"{{{self.XMI_NAMESPACE}}}{self.participant_map.get(opt_parent, '')}"
|
|
326
|
+
)
|
|
327
|
+
opt_message.set(
|
|
328
|
+
"receiveEvent",
|
|
329
|
+
f"{{{self.XMI_NAMESPACE}}}{self.participant_map.get(opt_participant, '')}"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
message_counter += 1
|
|
333
|
+
|
|
334
|
+
for opt_child in opt_node.children:
|
|
335
|
+
traverse_opt(opt_child, opt_participant)
|
|
336
|
+
|
|
337
|
+
traverse_opt(child, current_participant)
|
|
338
|
+
else:
|
|
339
|
+
traverse(child, current_participant)
|
|
340
|
+
|
|
341
|
+
# Start traversal from root's children
|
|
342
|
+
for child in root.children:
|
|
343
|
+
traverse(child, None)
|
|
344
|
+
|
|
345
|
+
def _format_message_signature(self, node: CallTreeNode) -> str:
|
|
346
|
+
"""
|
|
347
|
+
Format message signature with parameters.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
node: Call tree node
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Formatted signature string
|
|
354
|
+
"""
|
|
355
|
+
func = node.function_info
|
|
356
|
+
|
|
357
|
+
if not func.parameters:
|
|
358
|
+
return f"{func.name}()"
|
|
359
|
+
|
|
360
|
+
params = []
|
|
361
|
+
for param in func.parameters:
|
|
362
|
+
if param.name:
|
|
363
|
+
params.append(param.name)
|
|
364
|
+
else:
|
|
365
|
+
type_str = param.param_type
|
|
366
|
+
if param.is_pointer:
|
|
367
|
+
type_str += "*"
|
|
368
|
+
params.append(type_str)
|
|
369
|
+
|
|
370
|
+
return f"{func.name}({', '.join(params)})"
|
|
371
|
+
|
|
372
|
+
def _generate_id(self) -> str:
|
|
373
|
+
"""
|
|
374
|
+
Generate unique XMI ID.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Unique identifier string
|
|
378
|
+
"""
|
|
379
|
+
self.element_id_counter += 1
|
|
380
|
+
return f"calltree_{self.element_id_counter}"
|
|
381
|
+
|
|
382
|
+
def _prettify_xml(self, element: Element) -> str:
|
|
383
|
+
"""
|
|
384
|
+
Convert XML element to pretty-printed string.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
element: Root XML element
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Pretty-printed XML string
|
|
391
|
+
"""
|
|
392
|
+
rough_string = tostring(element, encoding="unicode")
|
|
393
|
+
reparsed = minidom.parseString(rough_string)
|
|
394
|
+
pretty_xml = reparsed.toprettyxml(indent=" ")
|
|
395
|
+
|
|
396
|
+
# Add XML declaration and comments
|
|
397
|
+
lines = pretty_xml.split('\n')
|
|
398
|
+
filtered_lines = [line for line in lines if line.strip()]
|
|
399
|
+
|
|
400
|
+
return '\n'.join(filtered_lines)
|
|
401
|
+
|
|
402
|
+
def generate_to_string(self, result: AnalysisResult) -> str:
|
|
403
|
+
"""
|
|
404
|
+
Generate XMI document as string without writing to file.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
result: Analysis result
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
XMI document as string
|
|
411
|
+
"""
|
|
412
|
+
if not result.call_tree:
|
|
413
|
+
raise ValueError("Cannot generate XMI: call tree is None")
|
|
414
|
+
|
|
415
|
+
root_element = self._generate_xmi_document(result, result.call_tree)
|
|
416
|
+
return self._prettify_xml(root_element)
|
|
@@ -9,7 +9,7 @@ import re
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import List, Optional
|
|
11
11
|
|
|
12
|
-
from ..database.models import FunctionInfo, FunctionType, Parameter
|
|
12
|
+
from ..database.models import FunctionCall, FunctionInfo, FunctionType, Parameter
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class CParser:
|
|
@@ -429,7 +429,7 @@ class CParser:
|
|
|
429
429
|
|
|
430
430
|
return None
|
|
431
431
|
|
|
432
|
-
def _extract_function_calls(self, function_body: str) -> List[
|
|
432
|
+
def _extract_function_calls(self, function_body: str) -> List[FunctionCall]:
|
|
433
433
|
"""
|
|
434
434
|
Extract function calls from a function body.
|
|
435
435
|
|
|
@@ -437,30 +437,117 @@ class CParser:
|
|
|
437
437
|
function_body: Function body text
|
|
438
438
|
|
|
439
439
|
Returns:
|
|
440
|
-
List of
|
|
440
|
+
List of FunctionCall objects with conditional status and condition text
|
|
441
441
|
"""
|
|
442
|
-
called_functions =
|
|
443
|
-
|
|
444
|
-
#
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
442
|
+
called_functions: List[FunctionCall] = []
|
|
443
|
+
|
|
444
|
+
# Analyze function body to track if/else context
|
|
445
|
+
# We'll parse line by line to detect if/else blocks
|
|
446
|
+
lines = function_body.split("\n")
|
|
447
|
+
in_if_block = False
|
|
448
|
+
in_else_block = False
|
|
449
|
+
current_condition = None
|
|
450
|
+
brace_depth = 0
|
|
451
|
+
|
|
452
|
+
for line in lines:
|
|
453
|
+
stripped = line.strip()
|
|
454
|
+
|
|
455
|
+
# Track if/else block entry and extract condition
|
|
456
|
+
if stripped.startswith("if ") or stripped.startswith("if("):
|
|
457
|
+
in_if_block = True
|
|
458
|
+
# Extract condition from if statement
|
|
459
|
+
# Handle both "if (condition)" and "if(condition)" formats
|
|
460
|
+
if_match = re.match(r'if\s*\(\s*(.+?)\s*\)', stripped)
|
|
461
|
+
if if_match:
|
|
462
|
+
current_condition = if_match.group(1).strip()
|
|
463
|
+
else:
|
|
464
|
+
# Fallback: try to extract everything between "if" and the first '{'
|
|
465
|
+
if_start = stripped.find("if")
|
|
466
|
+
brace_pos = stripped.find("{")
|
|
467
|
+
if brace_pos != -1:
|
|
468
|
+
condition_part = stripped[if_start+2:brace_pos].strip()
|
|
469
|
+
# Remove leading/trailing parentheses
|
|
470
|
+
condition_part = condition_part.lstrip("(").rstrip(")").strip()
|
|
471
|
+
current_condition = condition_part
|
|
472
|
+
else:
|
|
473
|
+
current_condition = "condition"
|
|
474
|
+
elif stripped.startswith("else if") or stripped.startswith("else if("):
|
|
475
|
+
in_if_block = True
|
|
476
|
+
# Extract condition from else if statement
|
|
477
|
+
elif_match = re.match(r'else\s+if\s*\(\s*(.+?)\s*\)', stripped)
|
|
478
|
+
if elif_match:
|
|
479
|
+
current_condition = f"else if {elif_match.group(1).strip()}"
|
|
480
|
+
else:
|
|
481
|
+
current_condition = "else if condition"
|
|
482
|
+
elif stripped.startswith("else") and not stripped.startswith("else if"):
|
|
483
|
+
in_else_block = True
|
|
484
|
+
current_condition = "else"
|
|
485
|
+
|
|
486
|
+
# Track brace depth for nested blocks
|
|
487
|
+
brace_depth += stripped.count("{")
|
|
488
|
+
brace_depth -= stripped.count("}")
|
|
489
|
+
|
|
490
|
+
# Find function calls in this line
|
|
491
|
+
for match in self.function_call_pattern.finditer(line):
|
|
492
|
+
function_name = match.group(1)
|
|
493
|
+
|
|
494
|
+
# Skip C keywords
|
|
495
|
+
if function_name in self.C_KEYWORDS:
|
|
496
|
+
continue
|
|
497
|
+
|
|
498
|
+
# Skip AUTOSAR types (might be casts)
|
|
499
|
+
if function_name in self.AUTOSAR_TYPES:
|
|
500
|
+
continue
|
|
501
|
+
|
|
502
|
+
# Check if this call is inside an if/else block
|
|
503
|
+
is_conditional = (in_if_block or in_else_block) and brace_depth > 0
|
|
504
|
+
|
|
505
|
+
# Add to called functions if not already present
|
|
506
|
+
existing = next((fc for fc in called_functions if fc.name == function_name), None)
|
|
507
|
+
if existing:
|
|
508
|
+
# Update conditional status if this occurrence is conditional
|
|
509
|
+
if is_conditional:
|
|
510
|
+
existing.is_conditional = True
|
|
511
|
+
if current_condition and not existing.condition:
|
|
512
|
+
existing.condition = current_condition
|
|
513
|
+
else:
|
|
514
|
+
called_functions.append(
|
|
515
|
+
FunctionCall(
|
|
516
|
+
name=function_name,
|
|
517
|
+
is_conditional=is_conditional,
|
|
518
|
+
condition=current_condition if is_conditional else None
|
|
519
|
+
)
|
|
520
|
+
)
|
|
455
521
|
|
|
456
|
-
|
|
522
|
+
# Also extract RTE calls explicitly
|
|
523
|
+
for match in self.rte_call_pattern.finditer(line):
|
|
524
|
+
rte_function = match.group(0).rstrip("(").strip()
|
|
525
|
+
|
|
526
|
+
is_conditional = (in_if_block or in_else_block) and brace_depth > 0
|
|
527
|
+
|
|
528
|
+
existing = next((fc for fc in called_functions if fc.name == rte_function), None)
|
|
529
|
+
if existing:
|
|
530
|
+
if is_conditional:
|
|
531
|
+
existing.is_conditional = True
|
|
532
|
+
if current_condition and not existing.condition:
|
|
533
|
+
existing.condition = current_condition
|
|
534
|
+
else:
|
|
535
|
+
called_functions.append(
|
|
536
|
+
FunctionCall(
|
|
537
|
+
name=rte_function,
|
|
538
|
+
is_conditional=is_conditional,
|
|
539
|
+
condition=current_condition if is_conditional else None
|
|
540
|
+
)
|
|
541
|
+
)
|
|
457
542
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
543
|
+
# Exit if/else block when we close the brace
|
|
544
|
+
if brace_depth == 0:
|
|
545
|
+
in_if_block = False
|
|
546
|
+
in_else_block = False
|
|
547
|
+
current_condition = None
|
|
462
548
|
|
|
463
|
-
|
|
549
|
+
# Sort by name for consistent output
|
|
550
|
+
return sorted(called_functions, key=lambda fc: fc.name)
|
|
464
551
|
|
|
465
552
|
def parse_function_declaration(self, declaration: str) -> Optional[FunctionInfo]:
|
|
466
553
|
"""
|
autosar_calltree/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: autosar-calltree
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A Python tool to analyze C/AUTOSAR codebases and generate function call trees with Mermaid and XMI output
|
|
5
5
|
Author-email: melodypapa <melodypapa@outlook.com>
|
|
6
6
|
Maintainer-email: melodypapa <melodypapa@outlook.com>
|
|
@@ -58,17 +58,181 @@ A powerful Python package to analyze C/AUTOSAR codebases and generate function c
|
|
|
58
58
|
- 🔍 **Static Analysis**: Analyzes C source code without compilation
|
|
59
59
|
- 📊 **Multiple Output Formats**:
|
|
60
60
|
- Mermaid sequence diagrams (Markdown)
|
|
61
|
-
- XMI/UML 2.5 (importable to Enterprise Architect,
|
|
61
|
+
- XMI/UML 2.5 (importable to Enterprise Architect, Visual Paradigm, etc.)
|
|
62
62
|
- JSON (for custom processing) - *planned*
|
|
63
63
|
- 🏗️ **SW Module Support**: Map C files to SW modules via YAML configuration for architecture-level diagrams
|
|
64
64
|
- 📈 **Module-Aware Diagrams**: Generate diagrams with SW module names as participants
|
|
65
65
|
- 🎯 **Parameter Display**: Function parameters shown in sequence diagram calls for better visibility
|
|
66
|
+
- 🔄 **Automatic Conditional Detection**: Automatically detects `if`/`else` statements and generates `opt` blocks with actual conditions (Mermaid and XMI)
|
|
66
67
|
- 🚀 **Performance**: Intelligent caching for fast repeated analysis with file-by-file progress reporting
|
|
67
68
|
- 🎯 **Depth Control**: Configurable call tree depth
|
|
68
69
|
- 🔄 **Circular Dependency Detection**: Identifies recursive calls and cycles
|
|
69
70
|
- 📊 **Statistics**: Detailed analysis statistics including module distribution
|
|
70
71
|
- 📝 **Clean Diagrams**: Return statements omitted by default for cleaner sequence diagrams (configurable)
|
|
71
72
|
|
|
73
|
+
## What's New
|
|
74
|
+
|
|
75
|
+
### Version 0.5.0 (2026-02-04)
|
|
76
|
+
|
|
77
|
+
**🎉 Major Feature: Automatic Conditional Call Detection with Opt/Alt/Else Blocks**
|
|
78
|
+
|
|
79
|
+
This release adds intelligent parsing of conditional function calls, automatically detecting `if`/`else` blocks in your C code and representing them as `opt`/`alt`/`else` blocks in both Mermaid and XMI output formats.
|
|
80
|
+
|
|
81
|
+
**Mermaid Example**:
|
|
82
|
+
|
|
83
|
+
**Source Code**:
|
|
84
|
+
```c
|
|
85
|
+
FUNC(void, RTE_CODE) Demo_Update(VAR(uint32, AUTOMATIC) update_mode)
|
|
86
|
+
{
|
|
87
|
+
SW_UpdateState(update_mode);
|
|
88
|
+
|
|
89
|
+
if (update_mode == 0x05) {
|
|
90
|
+
COM_SendLINMessage(0x456, (uint8*)0x20003000);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Generated Mermaid Diagram**:
|
|
96
|
+
```mermaid
|
|
97
|
+
sequenceDiagram
|
|
98
|
+
participant Demo_Update
|
|
99
|
+
participant COM_SendLINMessage
|
|
100
|
+
participant SW_UpdateState
|
|
101
|
+
|
|
102
|
+
Demo_Update->>SW_UpdateState: call(new_state)
|
|
103
|
+
opt update_mode == 0x05
|
|
104
|
+
Demo_Update->>COM_SendLINMessage: call(msg_id, data)
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**XMI Example**:
|
|
109
|
+
```xml
|
|
110
|
+
<uml:fragment name="opt" interactionOperator="opt">
|
|
111
|
+
<uml:operand name="update_mode == 0x05">
|
|
112
|
+
<uml:message name="COM_SendLINMessage" signature="COM_SendLINMessage(msg_id, data)">
|
|
113
|
+
<!-- message events -->
|
|
114
|
+
</uml:message>
|
|
115
|
+
</uml:operand>
|
|
116
|
+
</uml:fragment>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Benefits**:
|
|
120
|
+
- ✅ No manual configuration required - automatic detection
|
|
121
|
+
- ✅ Shows actual condition text for better understanding
|
|
122
|
+
- ✅ Supports nested conditionals
|
|
123
|
+
- ✅ Handles `if`, `else if`, and `else` statements
|
|
124
|
+
- ✅ Works with both Mermaid and XMI output formats
|
|
125
|
+
- ✅ XMI uses UML combined fragments (standard UML 2.5 representation)
|
|
126
|
+
|
|
127
|
+
**Technical Changes**:
|
|
128
|
+
- `FunctionCall` model extended with `is_conditional` and `condition` fields
|
|
129
|
+
- `CallTreeNode` extended with `is_optional` and `condition` fields
|
|
130
|
+
- `CParser` enhanced with line-by-line conditional context tracking
|
|
131
|
+
- `MermaidGenerator` supports `opt`, `alt`, and `else` blocks
|
|
132
|
+
- `XMIGenerator` supports UML combined fragments
|
|
133
|
+
- 298 tests passing with 89% code coverage
|
|
134
|
+
|
|
135
|
+
## Changelog
|
|
136
|
+
|
|
137
|
+
### [0.5.0] - 2026-02-04
|
|
138
|
+
|
|
139
|
+
#### Added
|
|
140
|
+
- **Conditional function call tracking**: Automatic detection of `if`/`else` blocks with condition text extraction
|
|
141
|
+
- **Mermaid opt/alt/else blocks**: Generate `opt`, `alt`, and `else` blocks for conditional calls
|
|
142
|
+
- **XMI combined fragments**: UML 2.5 compliant `opt`/`alt`/`else` fragment generation
|
|
143
|
+
- **FunctionCall model**: Extended with `is_conditional` and `condition` fields
|
|
144
|
+
- **CallTreeNode model**: Extended with `is_optional` and `condition` fields
|
|
145
|
+
- **CParser enhancements**: Line-by-line parsing to track conditional context
|
|
146
|
+
- Requirements documentation for conditional call tracking (SWR_MODEL_00026-00028)
|
|
147
|
+
|
|
148
|
+
#### Fixed
|
|
149
|
+
- Linting errors (flake8 W292, W391) for proper file endings
|
|
150
|
+
- Type annotations in `c_parser.py:442` for mypy compliance
|
|
151
|
+
|
|
152
|
+
#### Technical
|
|
153
|
+
- 298 tests passing, 89% code coverage
|
|
154
|
+
- All CI quality checks passing (ruff, isort, flake8, mypy)
|
|
155
|
+
- Python 3.8-3.12 test matrix
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### [0.4.0] - 2026-02-03
|
|
160
|
+
|
|
161
|
+
#### Added
|
|
162
|
+
- **XMI/UML 2.5 output format**: Complete XMI generation with UML 2.5 compliance
|
|
163
|
+
- **XMIGenerator**: New generator class for XMI document creation
|
|
164
|
+
- **CLI `--format xmi` option**: Support for XMI output format
|
|
165
|
+
- **CLI `--format both` option**: Generate both Mermaid and XMI simultaneously
|
|
166
|
+
- **XMI requirements documentation**: `docs/requirements/requirements_xmi.md`
|
|
167
|
+
- **UML combined fragments**: Support for `opt`, `alt`, `else` interactions
|
|
168
|
+
- **XMI demo output**: `demo/demo_main.xmi` example file
|
|
169
|
+
|
|
170
|
+
#### Technical
|
|
171
|
+
- XMI documents importable into Enterprise Architect, Visual Paradigm, MagicDraw
|
|
172
|
+
- Proper XML namespaces and structure (UML 2.5, XMI 2.5)
|
|
173
|
+
- Message events with sendEvent and receiveEvent elements
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### [0.3.3] - 2026-02-02
|
|
178
|
+
|
|
179
|
+
#### Fixed
|
|
180
|
+
- **AUTOSAR macro false positives**: Performance degradation caused by incorrect macro matching
|
|
181
|
+
- Parser optimization to reduce false positive detections
|
|
182
|
+
- Improved AUTOSAR pattern matching accuracy
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### [0.3.2] - 2026-02-01
|
|
187
|
+
|
|
188
|
+
#### Added
|
|
189
|
+
- **File size display**: Show file sizes during processing in verbose mode
|
|
190
|
+
- Enhanced progress reporting with line counts and file sizes
|
|
191
|
+
- Improved user feedback for large file processing
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### [0.3.1] - 2026-01-31
|
|
196
|
+
|
|
197
|
+
#### Added
|
|
198
|
+
- **Verbose file progress**: File-by-file progress display during database building
|
|
199
|
+
- **Line count reporting**: Show number of lines processed per file
|
|
200
|
+
- **Enhanced cache loading**: File-by-file progress when loading from cache
|
|
201
|
+
- **C parser line-by-line tests**: Comprehensive testing for line-by-line processing
|
|
202
|
+
|
|
203
|
+
#### Fixed
|
|
204
|
+
- Import sorting to pass isort checks
|
|
205
|
+
- Minor documentation updates
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### [0.3.0] - 2026-01-30
|
|
210
|
+
|
|
211
|
+
#### Added
|
|
212
|
+
- **SW Module Configuration System**: YAML-based file-to-module mapping
|
|
213
|
+
- **Module-aware diagrams**: Generate diagrams with SW module names as participants
|
|
214
|
+
- **Comprehensive test suite**: 298 tests across all modules (89% coverage)
|
|
215
|
+
- **Requirements traceability**: Complete traceability matrix between requirements and tests
|
|
216
|
+
- **Integration tests**: End-to-end CLI testing
|
|
217
|
+
- **PyPI publishing workflow**: Automated PyPI releases with OIDC trusted publishing
|
|
218
|
+
- **ModuleConfig class**: Load, validate, and perform module lookups
|
|
219
|
+
- **Module assignment**: Functions tagged with SW module information
|
|
220
|
+
- **FunctionDatabase integration**: Module assignments preserved in cache
|
|
221
|
+
- **CLI `--use-module-names` option**: Enable module-level diagrams
|
|
222
|
+
- **CLI `--module-config` option**: Specify module mapping YAML file
|
|
223
|
+
|
|
224
|
+
#### Technical
|
|
225
|
+
- Glob pattern support for file mappings (e.g., `hw_*.c`)
|
|
226
|
+
- Default module fallback for unmapped files
|
|
227
|
+
- Module lookup caching for performance
|
|
228
|
+
- Cache preserves module assignments across runs
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
### Earlier Versions
|
|
233
|
+
|
|
234
|
+
**0.2.x** - Initial development releases with basic AUTOSAR parsing and Mermaid output
|
|
235
|
+
|
|
72
236
|
## Installation
|
|
73
237
|
|
|
74
238
|
```bash
|
|
@@ -97,6 +261,9 @@ calltree --start-function Demo_Init --source-dir demo --module-config demo/modul
|
|
|
97
261
|
# Specify depth and output
|
|
98
262
|
calltree --start-function Demo_Init --max-depth 2 -o output.md
|
|
99
263
|
|
|
264
|
+
# Generate XMI format (with opt block support)
|
|
265
|
+
calltree --start-function Demo_MainFunction --source-dir demo --format xmi --output demo/demo.xmi
|
|
266
|
+
|
|
100
267
|
# Verbose mode with detailed statistics and cache progress
|
|
101
268
|
calltree --start-function Demo_Init --verbose
|
|
102
269
|
```
|
|
@@ -158,7 +325,7 @@ Options:
|
|
|
158
325
|
|
|
159
326
|
## Output Examples
|
|
160
327
|
|
|
161
|
-
### Mermaid Sequence Diagram
|
|
328
|
+
### Mermaid Sequence Diagram with Opt Blocks
|
|
162
329
|
|
|
163
330
|
```mermaid
|
|
164
331
|
sequenceDiagram
|
|
@@ -168,31 +335,55 @@ sequenceDiagram
|
|
|
168
335
|
participant SoftwareModule
|
|
169
336
|
|
|
170
337
|
DemoModule->>CommunicationModule: COM_InitCommunication(baud_rate, buffer_size)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
DemoModule->>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
DemoModule->>SoftwareModule: SW_InitSoftware(state, config)
|
|
181
|
-
SoftwareModule->>SoftwareModule: SW_InitConfig
|
|
182
|
-
SoftwareModule->>SoftwareModule: SW_InitState
|
|
338
|
+
opt mode > 0x00
|
|
339
|
+
DemoModule->>DemoModule: Demo_Update(mode)
|
|
340
|
+
opt mode == 0x05
|
|
341
|
+
DemoModule->>CommunicationModule: COM_SendLINMessage(msg_id, data)
|
|
342
|
+
end
|
|
343
|
+
DemoModule->>SoftwareModule: SW_UpdateState(new_state)
|
|
344
|
+
end
|
|
345
|
+
DemoModule->>HardwareModule: HW_ReadSensor(sensor_id)
|
|
346
|
+
DemoModule->>SoftwareModule: SW_ProcessData(data, length)
|
|
183
347
|
```
|
|
184
348
|
|
|
185
349
|
**Key Features**:
|
|
186
|
-
-
|
|
187
|
-
-
|
|
350
|
+
- Conditional calls are automatically wrapped in `opt` blocks
|
|
351
|
+
- Shows actual condition text from source code (e.g., `mode > 0x00`, `mode == 0x05`)
|
|
352
|
+
- Supports nested conditionals
|
|
353
|
+
- Participants appear in the order they are first encountered
|
|
354
|
+
- Function parameters are displayed in the call arrows
|
|
188
355
|
- Return statements are omitted by default for cleaner visualization
|
|
189
356
|
- Module names are used as participants when `--use-module-names` is enabled
|
|
190
357
|
|
|
358
|
+
### XMI Output with Opt Blocks
|
|
359
|
+
|
|
360
|
+
The XMI output also supports opt blocks using UML combined fragments:
|
|
361
|
+
|
|
362
|
+
```xml
|
|
363
|
+
<uml:fragment name="opt" interactionOperator="opt">
|
|
364
|
+
<uml:operand name="update_mode == 0x05">
|
|
365
|
+
<uml:message name="COM_SendLINMessage"
|
|
366
|
+
signature="COM_SendLINMessage(msg_id, data)"
|
|
367
|
+
messageSort="synchCall">
|
|
368
|
+
<uml:sendEvent xmi:id="calltree_22"/>
|
|
369
|
+
<uml:receiveEvent xmi:id="calltree_23"/>
|
|
370
|
+
</uml:message>
|
|
371
|
+
</uml:operand>
|
|
372
|
+
</uml:fragment>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**XMI Features**:
|
|
376
|
+
- UML 2.5 compliant XMI documents
|
|
377
|
+
- Combined fragments with `opt` interaction operator
|
|
378
|
+
- Operand elements display the condition text
|
|
379
|
+
- Can be imported into UML tools like Enterprise Architect, Visual Paradigm, MagicDraw
|
|
380
|
+
- Proper XML structure with correct namespaces
|
|
381
|
+
|
|
191
382
|
### Generated Markdown Structure
|
|
192
383
|
|
|
193
384
|
The tool generates comprehensive Markdown files with:
|
|
194
385
|
- Metadata header (timestamp, settings, statistics)
|
|
195
|
-
- Mermaid sequence diagram with function parameters
|
|
386
|
+
- Mermaid sequence diagram with function parameters and opt blocks
|
|
196
387
|
- Function details table with parameter information
|
|
197
388
|
- Text-based call tree
|
|
198
389
|
- Circular dependency warnings
|
|
@@ -261,6 +452,7 @@ calltree --start-function Demo_Init --module-config demo/module_mapping.yaml --u
|
|
|
261
452
|
This generates diagrams with:
|
|
262
453
|
- **Participants**: SW module names (HardwareModule, SoftwareModule, etc.) in the order they are first encountered
|
|
263
454
|
- **Arrows**: Function names with parameters being called between modules
|
|
455
|
+
- **Opt Blocks**: Conditional calls wrapped with actual condition text
|
|
264
456
|
- **Function Table**: Includes module column showing each function's SW module
|
|
265
457
|
- **Clean Visualization**: Return statements omitted by default
|
|
266
458
|
|
|
@@ -284,7 +476,7 @@ autosar-calltree/
|
|
|
284
476
|
│ ├── parsers/ # Code parsers (AUTOSAR, C)
|
|
285
477
|
│ ├── analyzers/ # Analysis logic (call tree, dependencies)
|
|
286
478
|
│ ├── database/ # Data models and caching
|
|
287
|
-
│ ├── generators/ # Output generators (Mermaid)
|
|
479
|
+
│ ├── generators/ # Output generators (Mermaid, XMI)
|
|
288
480
|
│ └── utils/ # Utilities (empty, for future use)
|
|
289
481
|
├── test_demo/ # Demo AUTOSAR C files for testing
|
|
290
482
|
│ ├── demo.c
|
|
@@ -302,7 +494,7 @@ autosar-calltree/
|
|
|
302
494
|
|
|
303
495
|
### Running Tests
|
|
304
496
|
|
|
305
|
-
The project has **comprehensive test coverage** with
|
|
497
|
+
The project has **comprehensive test coverage** with 298 tests across all modules:
|
|
306
498
|
|
|
307
499
|
```bash
|
|
308
500
|
# Run all tests
|
|
@@ -336,20 +528,20 @@ pytest --cov=autosar_calltree --cov-report=html --cov-report=term --omit=tests/
|
|
|
336
528
|
|
|
337
529
|
### Test Coverage
|
|
338
530
|
|
|
339
|
-
The project maintains **
|
|
531
|
+
The project maintains **89% code coverage** across all modules:
|
|
340
532
|
|
|
341
533
|
| Module | Coverage | Tests |
|
|
342
534
|
|--------|----------|-------|
|
|
343
|
-
| Models |
|
|
535
|
+
| Models | 97% | 28 |
|
|
344
536
|
| AUTOSAR Parser | 97% | 15 |
|
|
345
537
|
| C Parser | 86% | 18 |
|
|
346
|
-
| Database |
|
|
347
|
-
| Analyzers |
|
|
538
|
+
| Database | 83% | 20 |
|
|
539
|
+
| Analyzers | 95% | 20 |
|
|
348
540
|
| Config | 97% | 25 |
|
|
349
|
-
| Generators |
|
|
350
|
-
| CLI (Integration) |
|
|
351
|
-
| End-to-End | ~90% |
|
|
352
|
-
| **Total** | **
|
|
541
|
+
| Generators | 89% | 45 |
|
|
542
|
+
| CLI (Integration) | 90% | 14 |
|
|
543
|
+
| End-to-End | ~90% | 120 |
|
|
544
|
+
| **Total** | **89%** | **298** |
|
|
353
545
|
|
|
354
546
|
### Code Quality
|
|
355
547
|
|
|
@@ -469,7 +661,8 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
469
661
|
- [ ] Multi-threading for large codebases
|
|
470
662
|
- [ ] Function complexity metrics
|
|
471
663
|
- [ ] Dead code detection
|
|
472
|
-
- [
|
|
664
|
+
- [x] XMI/UML 2.5 output format with opt block support
|
|
665
|
+
- [x] Automatic conditional call detection with opt blocks
|
|
473
666
|
|
|
474
667
|
## Support
|
|
475
668
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
autosar_calltree/__init__.py,sha256=WiP2aFfs0H3ze4X8tQg6ZiStdzgrp6uC8DPRUyGm_zg,660
|
|
2
|
+
autosar_calltree/version.py,sha256=I6XTh66u90ab-PtD14KrayLlWv4ynW2h4Wmjq52IaW4,142
|
|
3
|
+
autosar_calltree/analyzers/__init__.py,sha256=qUvB7CAa5xNcUYkhD7Rha-9quPG8yQJqg-GikHD41U0,119
|
|
4
|
+
autosar_calltree/analyzers/call_tree_builder.py,sha256=hBiu6zM7agwLrAt7pWljYzW_Aur2zKuvJ_4cZrZlf50,11909
|
|
5
|
+
autosar_calltree/cli/__init__.py,sha256=qwsSUuDyV6i1PYS7Mf6JBkBJKtcldxiIh5S0wOucWVY,76
|
|
6
|
+
autosar_calltree/cli/main.py,sha256=-E0JdU1NrHVrESDXA1L2lb_rNsaE9_WdXXSr8Kf3F04,12709
|
|
7
|
+
autosar_calltree/config/__init__.py,sha256=mSrB2uvrax_MTgGynfPObXYUh6eCe5BZD21_cebfMQM,258
|
|
8
|
+
autosar_calltree/config/module_config.py,sha256=--ptsY6drvVKn_HD5_JDEpCv9D1xI8kiJXi8bwrSFBI,6329
|
|
9
|
+
autosar_calltree/database/__init__.py,sha256=qg0IfUGgkecJQXqe9lPrU0Xbu-hBZa8NOe8E8UZqC_s,405
|
|
10
|
+
autosar_calltree/database/function_database.py,sha256=PJghwv2Duxx7vSIvApyVwg4N_bCTfnk85mtA89BpIuA,17561
|
|
11
|
+
autosar_calltree/database/models.py,sha256=6f__e-oL14rrZyd1aDuPtzFnSZwzvZOxkUVvknJHwHw,7025
|
|
12
|
+
autosar_calltree/generators/__init__.py,sha256=5gl3dF3imJIsvfmdg0bkZdnQ9vFWCTlpMWEhz6I_IZg,178
|
|
13
|
+
autosar_calltree/generators/mermaid_generator.py,sha256=K2Alg4_WvwJB5d8UpxyKt6Bv7JX0h-1oqVRuu7-hSoA,16544
|
|
14
|
+
autosar_calltree/generators/xmi_generator.py,sha256=gxwdErg-ahsWVsTOH1liBmrDqZfn1j_Qq6fnSHCNxJw,15482
|
|
15
|
+
autosar_calltree/parsers/__init__.py,sha256=O0NTHrZqe6GXvU9_bLuAoAV_hxv7gHOgkH8KWSBmX1Y,151
|
|
16
|
+
autosar_calltree/parsers/autosar_parser.py,sha256=iwF1VR4NceEfmc3gPP31CcVNPfRRVTj_aA6N94saXDA,10750
|
|
17
|
+
autosar_calltree/parsers/c_parser.py,sha256=87fn-97XZdlS6lbhzBT9q3Y25DwLWYiFN7tbYQxQaR0,20114
|
|
18
|
+
autosar_calltree-0.5.0.dist-info/licenses/LICENSE,sha256=Xy30Wm38nOLXLZZFgn9oD_3UcayYkm81xtn8IByrBlk,1067
|
|
19
|
+
autosar_calltree-0.5.0.dist-info/METADATA,sha256=8IuIEEw7VNmAb_Fr_lK9DvSvmvBR-6fsZ6dYrtjc9nM,22342
|
|
20
|
+
autosar_calltree-0.5.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
21
|
+
autosar_calltree-0.5.0.dist-info/entry_points.txt,sha256=HfntIC1V_COOhGJ-OhtLKH_2vJ1jQy5Hlz8NmcJg4TQ,54
|
|
22
|
+
autosar_calltree-0.5.0.dist-info/top_level.txt,sha256=eusKGYzQfbhwIFDsUYTby40SdMpe95Y9GtR0l1GVIDQ,17
|
|
23
|
+
autosar_calltree-0.5.0.dist-info/RECORD,,
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
autosar_calltree/__init__.py,sha256=WiP2aFfs0H3ze4X8tQg6ZiStdzgrp6uC8DPRUyGm_zg,660
|
|
2
|
-
autosar_calltree/version.py,sha256=bCTAEX52GYLbYmX6hK94dwlrD8wYqJXSFfSo1Qo_J8A,142
|
|
3
|
-
autosar_calltree/analyzers/__init__.py,sha256=qUvB7CAa5xNcUYkhD7Rha-9quPG8yQJqg-GikHD41U0,119
|
|
4
|
-
autosar_calltree/analyzers/call_tree_builder.py,sha256=tSsIj_BHzT4XQ-XsXXfyU-KyGK61p6O7nLBx1AiwxCA,11627
|
|
5
|
-
autosar_calltree/cli/__init__.py,sha256=qwsSUuDyV6i1PYS7Mf6JBkBJKtcldxiIh5S0wOucWVY,76
|
|
6
|
-
autosar_calltree/cli/main.py,sha256=5svIu95DykrQBtjmhQTOTe-RdM9LkIgnnUnvfvivMM8,11334
|
|
7
|
-
autosar_calltree/config/__init__.py,sha256=mSrB2uvrax_MTgGynfPObXYUh6eCe5BZD21_cebfMQM,258
|
|
8
|
-
autosar_calltree/config/module_config.py,sha256=--ptsY6drvVKn_HD5_JDEpCv9D1xI8kiJXi8bwrSFBI,6329
|
|
9
|
-
autosar_calltree/database/__init__.py,sha256=qg0IfUGgkecJQXqe9lPrU0Xbu-hBZa8NOe8E8UZqC_s,405
|
|
10
|
-
autosar_calltree/database/function_database.py,sha256=PJghwv2Duxx7vSIvApyVwg4N_bCTfnk85mtA89BpIuA,17561
|
|
11
|
-
autosar_calltree/database/models.py,sha256=xo85zao2LDHJMR6FeMVxopPyXxEv7jQUzeh4fzHwNKo,6265
|
|
12
|
-
autosar_calltree/generators/__init__.py,sha256=wlSAuykXkxIjwP9H1C2iY99nbyt14Kp1Y6GxmDEDmDk,122
|
|
13
|
-
autosar_calltree/generators/mermaid_generator.py,sha256=LcmWOACLAw3r7UQlnSlTM-_kpKQ6RGdgc6aNqXeMF7A,16191
|
|
14
|
-
autosar_calltree/parsers/__init__.py,sha256=O0NTHrZqe6GXvU9_bLuAoAV_hxv7gHOgkH8KWSBmX1Y,151
|
|
15
|
-
autosar_calltree/parsers/autosar_parser.py,sha256=iwF1VR4NceEfmc3gPP31CcVNPfRRVTj_aA6N94saXDA,10750
|
|
16
|
-
autosar_calltree/parsers/c_parser.py,sha256=2LCDVZ8bcrBiQe9DK7kNMmvzhGegEDfd57KbI5JB43A,15846
|
|
17
|
-
autosar_calltree-0.3.3.dist-info/licenses/LICENSE,sha256=Xy30Wm38nOLXLZZFgn9oD_3UcayYkm81xtn8IByrBlk,1067
|
|
18
|
-
autosar_calltree-0.3.3.dist-info/METADATA,sha256=U3NTrfrUAqedyo5RzCQS9RLHBL1ToEYgUsMu5VC1tt8,15566
|
|
19
|
-
autosar_calltree-0.3.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
20
|
-
autosar_calltree-0.3.3.dist-info/entry_points.txt,sha256=HfntIC1V_COOhGJ-OhtLKH_2vJ1jQy5Hlz8NmcJg4TQ,54
|
|
21
|
-
autosar_calltree-0.3.3.dist-info/top_level.txt,sha256=eusKGYzQfbhwIFDsUYTby40SdMpe95Y9GtR0l1GVIDQ,17
|
|
22
|
-
autosar_calltree-0.3.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|