Analyzes cross-module name dependencies with scope awareness.
| 822 | # ─── Dependency Analyzer ────────────────────────────────────────────────────── |
| 823 | |
| 824 | class DependencyAnalyzer: |
| 825 | """Analyzes cross-module name dependencies with scope awareness.""" |
| 826 | |
| 827 | def __init__(self, analyzer: ArchitectureAnalyzer, nodes: List[NodeInfo]) -> None: |
| 828 | self.analyzer = analyzer |
| 829 | self.nodes = nodes |
| 830 | # Build symbol → module map (only non-import nodes) |
| 831 | self.symbol_to_module: Dict[str, str] = {} |
| 832 | for n in nodes: |
| 833 | if n.kind not in ("import",) and not n.name.startswith("_import_") and not n.name.startswith("_try_"): |
| 834 | self.symbol_to_module[n.name] = n.target_module |
| 835 | |
| 836 | def compute_dependency_map(self, module_path: str, module_nodes: List[NodeInfo]) -> Dict[str, Set[str]]: |
| 837 | """Return dependency module -> symbols for one generated module.""" |
| 838 | deps: Dict[str, Set[str]] = defaultdict(set) |
| 839 | for node in module_nodes: |
| 840 | if node.kind == "import": |
| 841 | continue |
| 842 | for name in self._node_referenced_symbols(node): |
| 843 | source_mod = self.symbol_to_module.get(name) |
| 844 | if source_mod and source_mod != module_path and source_mod != "_imports": |
| 845 | deps[source_mod].add(name) |
| 846 | return deps |
| 847 | |
| 848 | def compute_imports(self, module_path: str, module_nodes: List[NodeInfo]) -> List[str]: |
| 849 | """ |
| 850 | For a given output module, compute the correct relative `from .X import Y` |
| 851 | statements needed to satisfy cross-module references. |
| 852 | Returns sorted list of import lines. |
| 853 | """ |
| 854 | deps = self.compute_dependency_map(module_path, module_nodes) |
| 855 | |
| 856 | import_lines = [] |
| 857 | for dep_mod, names in sorted(deps.items()): |
| 858 | if not names: |
| 859 | continue |
| 860 | rel = self._relative_import(module_path, dep_mod) |
| 861 | names_str = ", ".join(sorted(names)) |
| 862 | import_lines.append(f"from {rel} import {names_str}") |
| 863 | |
| 864 | return import_lines |
| 865 | |
| 866 | def compute_referenced_names(self, module_nodes: List[NodeInfo]) -> Set[str]: |
| 867 | """Return all referenced names across nodes in one generated module.""" |
| 868 | referenced: Set[str] = set() |
| 869 | for node in module_nodes: |
| 870 | if node.kind == "import": |
| 871 | continue |
| 872 | referenced.update(self._node_referenced_symbols(node)) |
| 873 | return referenced |
| 874 | |
| 875 | def _node_referenced_symbols(self, node: NodeInfo) -> Set[str]: |
| 876 | source = self.analyzer.get_source_lines(node.lineno, node.end_lineno) |
| 877 | if not source.strip(): |
| 878 | return set() |
| 879 | try: |
| 880 | table = symtable.symtable(source, str(self.analyzer.source_path), "exec") |
| 881 | except SyntaxError: |