Parses the source file with AST and extracts top-level node metadata.
| 322 | # ─── Architecture Analyzer ─────────────────────────────────────────────────── |
| 323 | |
| 324 | class ArchitectureAnalyzer: |
| 325 | """Parses the source file with AST and extracts top-level node metadata.""" |
| 326 | |
| 327 | def __init__(self, source_path: Path) -> None: |
| 328 | self.source_path = source_path |
| 329 | self.source_lines: List[str] = [] |
| 330 | self.tree: ast.Module | None = None |
| 331 | self.nodes: List[NodeInfo] = [] |
| 332 | self.import_statements: List[ImportStatement] = [] |
| 333 | |
| 334 | def analyze(self) -> List[NodeInfo]: |
| 335 | """Parse and classify all top-level nodes.""" |
| 336 | text = self.source_path.read_text(encoding="utf-8") |
| 337 | self.source_lines = text.splitlines(keepends=True) |
| 338 | print(f" Parsing {len(self.source_lines):,} lines with AST...") |
| 339 | self.tree = ast.parse(text, filename=str(self.source_path)) |
| 340 | self.nodes = [] |
| 341 | self.import_statements = [] |
| 342 | |
| 343 | for node in self.tree.body: |
| 344 | self._register_top_level_import(node) |
| 345 | infos = self._extract_node_info(node) |
| 346 | self.nodes.extend(infos) |
| 347 | |
| 348 | return self.nodes |
| 349 | |
| 350 | def _extract_node_info(self, node: ast.AST) -> List[NodeInfo]: |
| 351 | """Convert one AST body node into NodeInfo records.""" |
| 352 | results: List[NodeInfo] = [] |
| 353 | |
| 354 | if isinstance(node, (ast.ClassDef,)): |
| 355 | bases = [self._name_of(b) for b in node.bases] |
| 356 | info = NodeInfo( |
| 357 | name=node.name, |
| 358 | kind="class", |
| 359 | lineno=node.lineno, |
| 360 | end_lineno=node.end_lineno or node.lineno, |
| 361 | bases=bases, |
| 362 | ) |
| 363 | info.source_hash = self._hash_lines(info.lineno, info.end_lineno) |
| 364 | results.append(info) |
| 365 | |
| 366 | elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): |
| 367 | info = NodeInfo( |
| 368 | name=node.name, |
| 369 | kind="function", |
| 370 | lineno=node.lineno, |
| 371 | end_lineno=node.end_lineno or node.lineno, |
| 372 | ) |
| 373 | info.source_hash = self._hash_lines(info.lineno, info.end_lineno) |
| 374 | results.append(info) |
| 375 | |
| 376 | elif isinstance(node, ast.Assign): |
| 377 | # Capture top-level assignments (constants, config dicts, etc.) |
| 378 | for target in node.targets: |
| 379 | name = self._name_of(target) |
| 380 | if name: |
| 381 | end = node.end_lineno or node.lineno |