Routes each NodeInfo to an output module path using layout rules.
| 720 | # ─── Module Router ──────────────────────────────────────────────────────────── |
| 721 | |
| 722 | class ModuleRouter: |
| 723 | """Routes each NodeInfo to an output module path using layout rules.""" |
| 724 | |
| 725 | def __init__(self, layout: Dict[str, List[str]]) -> None: |
| 726 | self.layout = layout |
| 727 | self._fallback_classifier = AutoLayoutGenerator() |
| 728 | # Pre-compile: list of (module_path, list_of_matchers) |
| 729 | self._rules: List[Tuple[str, List[Tuple[str, object]]]] = [] |
| 730 | for mod_path, patterns in layout.items(): |
| 731 | matchers: List[Tuple[str, object]] = [] |
| 732 | for pat in patterns: |
| 733 | if pat.startswith("~"): |
| 734 | matchers.append(("regex", re.compile(pat[1:]))) |
| 735 | else: |
| 736 | matchers.append(("exact", pat)) |
| 737 | self._rules.append((mod_path, matchers)) |
| 738 | |
| 739 | def route(self, node: NodeInfo) -> str: |
| 740 | """Return the target module path for a node.""" |
| 741 | # Import nodes always go to a special _imports collector |
| 742 | if node.kind == "import": |
| 743 | return "_imports" |
| 744 | |
| 745 | name = node.name |
| 746 | for mod_path, matchers in self._rules: |
| 747 | for kind, matcher in matchers: |
| 748 | if kind == "exact" and name == matcher: |
| 749 | return mod_path |
| 750 | |
| 751 | # Broad regex buckets only apply after exact-name routing. |
| 752 | for mod_path, matchers in self._rules: |
| 753 | if not matchers: |
| 754 | continue |
| 755 | for kind, matcher in matchers: |
| 756 | if kind == "regex" and matcher.search(name): |
| 757 | return mod_path |
| 758 | |
| 759 | fallback_module = self._fallback_classifier._classify(node) |
| 760 | if fallback_module != "_unclassified.py": |
| 761 | return fallback_module |
| 762 | |
| 763 | return "_unclassified.py" |
| 764 | |
| 765 | def assign_all(self, nodes: List[NodeInfo]) -> None: |
| 766 | """Assign target_module to all nodes in-place.""" |
| 767 | for node in nodes: |
| 768 | node.target_module = self.route(node) |
| 769 | self._adopt_contextual_expression_nodes(nodes) |
| 770 | |
| 771 | def _adopt_contextual_expression_nodes(self, nodes: List[NodeInfo]) -> None: |
| 772 | """Attach anonymous top-level expressions to the nearest classified neighbor.""" |
| 773 | for index, node in enumerate(nodes): |
| 774 | if node.target_module != "_unclassified.py": |
| 775 | continue |
| 776 | if node.kind != "expression": |
| 777 | continue |
| 778 | contextual_module = self._nearest_contextual_module(nodes, index) |
| 779 | if contextual_module: |