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,330 @@
1
+ """
2
+ Command-line interface for autosar-calltree.
3
+
4
+ This module provides the main CLI entry point using Click.
5
+ """
6
+
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import click
12
+ from rich.console import Console
13
+ from rich.progress import Progress, SpinnerColumn, TextColumn
14
+ from rich.table import Table
15
+
16
+ from ..analyzers.call_tree_builder import CallTreeBuilder
17
+ from ..config.module_config import ModuleConfig
18
+ from ..database.function_database import FunctionDatabase
19
+ from ..generators.mermaid_generator import MermaidGenerator
20
+ from ..version import __version__
21
+
22
+ console = Console(record=True)
23
+
24
+
25
+ @click.command()
26
+ @click.option(
27
+ "--start-function",
28
+ "-s",
29
+ required=False, # Not required if --list-functions or --search is used
30
+ help="Name of the function to start call tree from",
31
+ )
32
+ @click.option(
33
+ "--max-depth",
34
+ "-d",
35
+ default=3,
36
+ type=int,
37
+ help="Maximum depth to traverse (default: 3)",
38
+ )
39
+ @click.option(
40
+ "--source-dir",
41
+ "-i",
42
+ default="./demo",
43
+ type=click.Path(exists=True, file_okay=False, dir_okay=True),
44
+ help="Source directory containing C files (default: ./demo)",
45
+ )
46
+ @click.option(
47
+ "--output",
48
+ "-o",
49
+ default="call_tree.md",
50
+ type=click.Path(),
51
+ help="Output file path (default: call_tree.md)",
52
+ )
53
+ @click.option(
54
+ "--format",
55
+ "-f",
56
+ type=click.Choice(["mermaid", "xmi", "both"], case_sensitive=False),
57
+ default="mermaid",
58
+ help="Output format (default: mermaid)",
59
+ )
60
+ @click.option(
61
+ "--cache-dir",
62
+ type=click.Path(file_okay=False, dir_okay=True),
63
+ help="Cache directory (default: <source-dir>/.cache)",
64
+ )
65
+ @click.option("--no-cache", is_flag=True, help="Disable cache usage")
66
+ @click.option("--rebuild-cache", is_flag=True, help="Force rebuild of cache")
67
+ @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
68
+ @click.option(
69
+ "--list-functions", "-l", is_flag=True, help="List all available functions and exit"
70
+ )
71
+ @click.option(
72
+ "--search", type=str, help="Search for functions matching pattern and exit"
73
+ )
74
+ @click.option(
75
+ "--no-abbreviate-rte",
76
+ is_flag=True,
77
+ help="Do not abbreviate RTE function names in diagrams",
78
+ )
79
+ @click.option(
80
+ "--module-config",
81
+ type=click.Path(exists=True),
82
+ help="Path to YAML file mapping C files to SW modules",
83
+ )
84
+ @click.option(
85
+ "--use-module-names",
86
+ is_flag=True,
87
+ help="Use SW module names as Mermaid participants (requires --module-config)",
88
+ )
89
+ @click.version_option(version=__version__, prog_name="autosar-calltree")
90
+ def cli(
91
+ start_function: str,
92
+ max_depth: int,
93
+ source_dir: str,
94
+ output: str,
95
+ format: str,
96
+ cache_dir: Optional[str],
97
+ no_cache: bool,
98
+ rebuild_cache: bool,
99
+ verbose: bool,
100
+ list_functions: bool,
101
+ search: Optional[str],
102
+ no_abbreviate_rte: bool,
103
+ module_config: Optional[str],
104
+ use_module_names: bool,
105
+ ):
106
+ """
107
+ AUTOSAR Call Tree Analyzer
108
+
109
+ Analyzes C/AUTOSAR codebases and generates function call trees
110
+ with Mermaid sequence diagrams or XMI output.
111
+ """
112
+ try:
113
+ # Print banner
114
+ if not verbose:
115
+ console.print(
116
+ f"[bold cyan]AUTOSAR Call Tree Analyzer v{__version__}[/bold cyan]"
117
+ )
118
+ console.print()
119
+
120
+ # Validate use_module_names requires module_config
121
+ if use_module_names and not module_config:
122
+ console.print(
123
+ "[yellow]Warning:[/yellow] --use-module-names requires --module-config. "
124
+ "Module names will not be used."
125
+ )
126
+ use_module_names = False
127
+
128
+ # Load module configuration if provided
129
+ config = None
130
+ if module_config:
131
+ try:
132
+ config = ModuleConfig(Path(module_config))
133
+ if verbose:
134
+ console.print(
135
+ f"[cyan]Loaded module configuration from {module_config}[/cyan]"
136
+ )
137
+ config_stats = config.get_statistics()
138
+ console.print(
139
+ f" - Specific file mappings: {config_stats['specific_file_mappings']}"
140
+ )
141
+ console.print(
142
+ f" - Pattern mappings: {config_stats['pattern_mappings']}"
143
+ )
144
+ except Exception as e:
145
+ console.print(f"[bold red]Error loading module config:[/bold red] {e}")
146
+ sys.exit(1)
147
+
148
+ # Initialize database
149
+ use_cache = not no_cache
150
+
151
+ with Progress(
152
+ SpinnerColumn(),
153
+ TextColumn("[progress.description]{task.description}"),
154
+ console=console,
155
+ transient=True,
156
+ ) as progress:
157
+ # Build database
158
+ task = progress.add_task(
159
+ f"Building function database from {source_dir}...", total=None
160
+ )
161
+
162
+ db = FunctionDatabase(source_dir, cache_dir=cache_dir, module_config=config)
163
+ db.build_database(
164
+ use_cache=use_cache, rebuild_cache=rebuild_cache, verbose=verbose
165
+ )
166
+
167
+ progress.update(task, completed=True)
168
+
169
+ # Print statistics
170
+ stats = db.get_statistics()
171
+
172
+ if verbose:
173
+ console.print("\n[bold]Database Statistics:[/bold]")
174
+ table = Table(show_header=False)
175
+ table.add_column("Property", style="cyan")
176
+ table.add_column("Value", style="green")
177
+ table.add_row("Files Scanned", str(stats["total_files_scanned"]))
178
+ table.add_row("Functions Found", str(stats["total_functions_found"]))
179
+ table.add_row("Unique Names", str(stats["unique_function_names"]))
180
+ table.add_row("Static Functions", str(stats["static_functions"]))
181
+ table.add_row("Parse Errors", str(stats["parse_errors"]))
182
+ console.print(table)
183
+
184
+ # Print module statistics if available
185
+ if stats.get("module_stats"):
186
+ console.print("\n[bold]Module Distribution:[/bold]")
187
+ for module, count in sorted(stats["module_stats"].items()):
188
+ console.print(f" {module}: {count} functions")
189
+ console.print()
190
+
191
+ # Handle list functions
192
+ if list_functions:
193
+ console.print("[bold]Available Functions:[/bold]\n")
194
+ functions = db.get_all_function_names()
195
+ for idx, func_name in enumerate(functions, 1):
196
+ console.print(f"{idx:4d}. {func_name}")
197
+ console.print(f"\n[cyan]Total: {len(functions)} functions[/cyan]")
198
+ return
199
+
200
+ # Handle search
201
+ if search:
202
+ console.print(f"[bold]Search Results for '{search}':[/bold]\n")
203
+ results = db.search_functions(search)
204
+ if results:
205
+ for func_info in results:
206
+ file_name = Path(func_info.file_path).name
207
+ console.print(
208
+ f" [cyan]{func_info.name}[/cyan] "
209
+ f"({file_name}:{func_info.line_number})"
210
+ )
211
+ console.print(f"\n[cyan]Found {len(results)} matches[/cyan]")
212
+ else:
213
+ console.print(
214
+ f"[yellow]No functions found matching '{search}'[/yellow]"
215
+ )
216
+ return
217
+
218
+ # Validate start_function is provided if not using list/search
219
+ if not start_function:
220
+ console.print("[bold red]Error:[/bold red] --start-function is required")
221
+ console.print("Use --list-functions to see available functions")
222
+ sys.exit(1)
223
+
224
+ # Build call tree
225
+ with Progress(
226
+ SpinnerColumn(),
227
+ TextColumn("[progress.description]{task.description}"),
228
+ console=console,
229
+ transient=True,
230
+ ) as progress:
231
+ task = progress.add_task(
232
+ f"Building call tree for {start_function}...", total=None
233
+ )
234
+
235
+ builder = CallTreeBuilder(db)
236
+ result = builder.build_tree(
237
+ start_function=start_function, max_depth=max_depth, verbose=verbose
238
+ )
239
+
240
+ progress.update(task, completed=True)
241
+
242
+ # Check for errors
243
+ if result.errors:
244
+ console.print("[bold red]Errors:[/bold red]")
245
+ for error in result.errors:
246
+ console.print(f" - {error}")
247
+ sys.exit(1)
248
+
249
+ # Print analysis statistics
250
+ if not verbose:
251
+ console.print("[bold]Analysis Results:[/bold]")
252
+ console.print(
253
+ f" - Total functions: [cyan]{result.statistics.total_functions}[/cyan]"
254
+ )
255
+ console.print(
256
+ f" - Unique functions: [cyan]{result.statistics.unique_functions}[/cyan]"
257
+ )
258
+ console.print(
259
+ f" - Max depth: [cyan]{result.statistics.max_depth_reached}[/cyan]"
260
+ )
261
+ if result.statistics.circular_dependencies_found > 0:
262
+ console.print(
263
+ f" - Circular dependencies: "
264
+ f"[yellow]{result.statistics.circular_dependencies_found}[/yellow]"
265
+ )
266
+ console.print()
267
+
268
+ # Generate output
269
+ output_path = Path(output)
270
+
271
+ if format in ["mermaid", "both"]:
272
+ with Progress(
273
+ SpinnerColumn(),
274
+ TextColumn("[progress.description]{task.description}"),
275
+ console=console,
276
+ transient=True,
277
+ ) as progress:
278
+ task = progress.add_task("Generating Mermaid diagram...", total=None)
279
+
280
+ mermaid_output = (
281
+ output_path
282
+ if format == "mermaid"
283
+ else output_path.with_suffix(".mermaid.md")
284
+ )
285
+
286
+ generator = MermaidGenerator(
287
+ abbreviate_rte=not no_abbreviate_rte,
288
+ use_module_names=use_module_names,
289
+ )
290
+ generator.generate(result, str(mermaid_output))
291
+
292
+ progress.update(task, completed=True)
293
+
294
+ console.print(
295
+ f"[green]Generated[/green] Mermaid diagram: [cyan]{mermaid_output}[/cyan]"
296
+ )
297
+
298
+ if format == "xmi":
299
+ console.print("[yellow]Warning:[/yellow] XMI format not yet implemented")
300
+
301
+ if format == "both":
302
+ console.print(
303
+ "[yellow]Warning:[/yellow] XMI format not yet implemented (only Mermaid generated)"
304
+ )
305
+
306
+ # Print warnings for circular dependencies
307
+ if result.circular_dependencies:
308
+ console.print(
309
+ "\n[bold yellow]Warning:[/bold yellow] Circular dependencies detected!"
310
+ )
311
+ for idx, circ_dep in enumerate(result.circular_dependencies, 1):
312
+ cycle_str = " → ".join(circ_dep.cycle)
313
+ console.print(
314
+ f" {idx}. [yellow]{cycle_str}[/yellow] (depth {circ_dep.depth})"
315
+ )
316
+
317
+ console.print("\n[bold green]Analysis complete![/bold green]")
318
+
319
+ except KeyboardInterrupt:
320
+ console.print("\n[yellow]Interrupted by user[/yellow]")
321
+ sys.exit(130)
322
+ except Exception as e:
323
+ console.print(f"\n[bold red]Error:[/bold red] {e}")
324
+ if verbose:
325
+ console.print_exception()
326
+ sys.exit(1)
327
+
328
+
329
+ if __name__ == "__main__":
330
+ cli()
@@ -0,0 +1,10 @@
1
+ """
2
+ Configuration management for AUTOSAR Call Tree Analyzer.
3
+
4
+ This module provides functionality for managing YAML-based configurations,
5
+ including mapping C source files to SW modules.
6
+ """
7
+
8
+ from .module_config import ModuleConfig
9
+
10
+ __all__ = ["ModuleConfig"]
@@ -0,0 +1,179 @@
1
+ """
2
+ Module configuration management for SW module mappings.
3
+
4
+ This module provides functionality for loading and managing YAML-based
5
+ configurations that map C source files to SW (Software) modules.
6
+
7
+ Requirements:
8
+ - SWR_CONFIG_00001: YAML Configuration File Support
9
+ - SWR_CONFIG_00002: Module Configuration Validation
10
+ """
11
+
12
+ import fnmatch
13
+ import re
14
+ from pathlib import Path
15
+ from typing import Dict, List, Optional, Tuple
16
+
17
+
18
+ class ModuleConfig:
19
+ """
20
+ Manages SW module configuration from YAML file.
21
+
22
+ This class handles loading, validating, and looking up SW module mappings
23
+ for C source files. It supports both specific file mappings and glob pattern
24
+ mappings.
25
+
26
+ Attributes:
27
+ specific_mappings: Dictionary mapping exact filenames to module names
28
+ pattern_mappings: List of compiled regex patterns and module names
29
+ default_module: Default module name for unmapped files (optional)
30
+ _lookup_cache: Cache for filename to module lookups
31
+ """
32
+
33
+ def __init__(self, config_path: Optional[Path] = None) -> None:
34
+ """
35
+ Initialize the module configuration.
36
+
37
+ Args:
38
+ config_path: Path to YAML configuration file (optional)
39
+ """
40
+ self.specific_mappings: Dict[str, str] = {}
41
+ self.pattern_mappings: List[Tuple[re.Pattern, str]] = []
42
+ self.default_module: Optional[str] = None
43
+ self._lookup_cache: Dict[str, Optional[str]] = {}
44
+
45
+ if config_path:
46
+ self.load_config(config_path)
47
+
48
+ def load_config(self, config_path: Path) -> None:
49
+ """
50
+ Load module configuration from YAML file.
51
+
52
+ Implements: SWR_CONFIG_00001 (YAML Configuration File Support)
53
+ Implements: SWR_CONFIG_00002 (Module Configuration Validation)
54
+
55
+ Args:
56
+ config_path: Path to YAML configuration file
57
+
58
+ Raises:
59
+ FileNotFoundError: If config file doesn't exist
60
+ ValueError: If config file format is invalid
61
+ """
62
+ import yaml # type: ignore
63
+
64
+ if not config_path.exists():
65
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
66
+
67
+ with open(config_path, "r", encoding="utf-8") as f:
68
+ data = yaml.safe_load(f)
69
+
70
+ if not isinstance(data, dict):
71
+ raise ValueError(
72
+ "Invalid configuration format: expected dictionary at root level"
73
+ )
74
+
75
+ # Load specific file mappings (exact filename match)
76
+ file_mappings = data.get("file_mappings", {})
77
+ if not isinstance(file_mappings, dict):
78
+ raise ValueError("'file_mappings' must be a dictionary")
79
+
80
+ for filename, module in file_mappings.items():
81
+ if not isinstance(filename, str) or not isinstance(module, str):
82
+ raise ValueError("File mappings must be strings")
83
+ if not module.strip():
84
+ raise ValueError(f"Module name cannot be empty for file: {filename}")
85
+ self.specific_mappings[filename] = module
86
+
87
+ # Load pattern mappings (glob patterns)
88
+ pattern_mappings = data.get("pattern_mappings", {})
89
+ if not isinstance(pattern_mappings, dict):
90
+ raise ValueError("'pattern_mappings' must be a dictionary")
91
+
92
+ for pattern, module in pattern_mappings.items():
93
+ if not isinstance(pattern, str) or not isinstance(module, str):
94
+ raise ValueError("Pattern mappings must be strings")
95
+ if not module.strip():
96
+ raise ValueError(f"Module name cannot be empty for pattern: {pattern}")
97
+
98
+ # Compile glob pattern to regex for faster matching
99
+ regex = fnmatch.translate(pattern)
100
+ compiled = re.compile(regex)
101
+ self.pattern_mappings.append((compiled, module))
102
+
103
+ # Load default module (optional)
104
+ default_module = data.get("default_module")
105
+ if default_module is not None:
106
+ if not isinstance(default_module, str) or not default_module.strip():
107
+ raise ValueError("'default_module' must be a non-empty string")
108
+ self.default_module = default_module
109
+
110
+ def get_module_for_file(self, file_path: Path) -> Optional[str]:
111
+ """
112
+ Get SW module name for a given file.
113
+
114
+ This method first checks specific file mappings, then pattern mappings,
115
+ and finally returns the default module if configured.
116
+
117
+ Lookup results are cached for performance.
118
+
119
+ Args:
120
+ file_path: Path to the source file
121
+
122
+ Returns:
123
+ Module name if found, None otherwise
124
+ """
125
+ filename = file_path.name
126
+
127
+ # Check cache first
128
+ if filename in self._lookup_cache:
129
+ return self._lookup_cache[filename]
130
+
131
+ # Check specific file mappings (exact match)
132
+ if filename in self.specific_mappings:
133
+ module = self.specific_mappings[filename]
134
+ self._lookup_cache[filename] = module
135
+ return module
136
+
137
+ # Check pattern mappings (glob patterns)
138
+ for pattern, module in self.pattern_mappings:
139
+ if pattern.match(filename):
140
+ self._lookup_cache[filename] = module
141
+ return module
142
+
143
+ # Use default module if configured
144
+ if self.default_module is not None:
145
+ self._lookup_cache[filename] = self.default_module
146
+ return self.default_module
147
+
148
+ # No match found
149
+ self._lookup_cache[filename] = None
150
+ return None
151
+
152
+ def validate_config(self) -> List[str]:
153
+ """
154
+ Validate the current configuration.
155
+
156
+ Returns:
157
+ List of validation error messages (empty if valid)
158
+ """
159
+ errors = []
160
+
161
+ if not self.specific_mappings and not self.pattern_mappings:
162
+ errors.append(
163
+ "Configuration must contain either 'file_mappings' or 'pattern_mappings'"
164
+ )
165
+
166
+ return errors
167
+
168
+ def get_statistics(self) -> Dict[str, int]:
169
+ """
170
+ Get statistics about the configuration.
171
+
172
+ Returns:
173
+ Dictionary with configuration statistics
174
+ """
175
+ return {
176
+ "specific_file_mappings": len(self.specific_mappings),
177
+ "pattern_mappings": len(self.pattern_mappings),
178
+ "has_default_module": 1 if self.default_module else 0,
179
+ }
@@ -0,0 +1,23 @@
1
+ """Database package initialization."""
2
+
3
+ from .models import (
4
+ AnalysisResult,
5
+ AnalysisStatistics,
6
+ CallTreeNode,
7
+ CircularDependency,
8
+ FunctionDict,
9
+ FunctionInfo,
10
+ FunctionType,
11
+ Parameter,
12
+ )
13
+
14
+ __all__ = [
15
+ "FunctionType",
16
+ "Parameter",
17
+ "FunctionInfo",
18
+ "CallTreeNode",
19
+ "CircularDependency",
20
+ "AnalysisStatistics",
21
+ "AnalysisResult",
22
+ "FunctionDict",
23
+ ]