""" Obsolete-Detector — Erkennung veralteter Projekteinträge nach DB-Import. Reine Analyselogik ohne Qt-Abhängigkeit. Findet XslFile-Einträge im Projekt, die nicht mehr in der Datenbank vorhanden sind. """ from dataclasses import dataclass, field from pathlib import Path from conf import TreeNode, XslFile @dataclass class ObsoleteXslEntry: """Ein XslFile-Eintrag, der nicht mehr in der Datenbank vorhanden ist.""" xsl_file: XslFile parent_node: TreeNode @dataclass class ObsoleteGroup: """Gruppe veralteter XslFile-Einträge unter einem gemeinsamen Eltern-Pfad.""" node_path: list[str] parent_node: TreeNode xsl_entries: list[ObsoleteXslEntry] = field(default_factory=list) def extract_db_xsl_ids(new_nodes: list[TreeNode]) -> set[tuple]: """ Extrahiert alle XslFile-IDs aus den frisch aus der DB geladenen Nodes. Args: new_nodes: Nodes aus _process_sql_data() Returns: Set aller XslFile-IDs (Tupel) """ ids: set[tuple] = set() _collect_ids_recursive(new_nodes, ids) return ids def _collect_ids_recursive(nodes: list, ids: set[tuple]) -> None: for node in nodes: if isinstance(node, XslFile): ids.add(node.id) elif isinstance(node, TreeNode) and node.children: _collect_ids_recursive(node.children, ids) def find_obsolete_xsl_entries( project_nodes: list[TreeNode], db_xsl_ids: set[tuple], ) -> list[ObsoleteGroup]: """ Findet alle XslFile-Einträge im Projekt, die nicht mehr in der DB vorhanden sind. Args: project_nodes: Aktuelle Nodes aus pdf_project.nodes db_xsl_ids: Set aller XslFile-IDs aus der DB (von extract_db_xsl_ids) Returns: Liste von ObsoleteGroup, sortiert nach Hierarchiepfad """ groups: dict[int, ObsoleteGroup] = {} _find_obsolete_recursive(project_nodes, db_xsl_ids, path=[], groups=groups) # Sortieren nach Pfad für stabile Darstellung return sorted(groups.values(), key=lambda g: g.node_path) def _find_obsolete_recursive( nodes: list, db_xsl_ids: set[tuple], path: list[str], groups: dict[int, ObsoleteGroup], ) -> None: for node in nodes: if isinstance(node, TreeNode): _find_obsolete_in_tree_node(node, db_xsl_ids, path, groups) def _find_obsolete_in_tree_node( node: TreeNode, db_xsl_ids: set[tuple], parent_path: list[str], groups: dict[int, ObsoleteGroup], ) -> None: current_path = parent_path + [node.bez] for child in node.children: if isinstance(child, XslFile): if child.id not in db_xsl_ids: node_id = id(node) if node_id not in groups: groups[node_id] = ObsoleteGroup( node_path=current_path, parent_node=node, ) groups[node_id].xsl_entries.append(ObsoleteXslEntry(xsl_file=child, parent_node=node)) elif isinstance(child, TreeNode): _find_obsolete_in_tree_node(child, db_xsl_ids, current_path, groups) def remove_empty_tree_nodes(nodes: list) -> list: """ Entfernt rekursiv alle TreeNodes, deren children-Liste nach der Bereinigung leer ist. XslFile-Einträge werden immer behalten (sie wurden bereits entfernt oder sind noch gültig). Args: nodes: Liste von TreeNode|XslFile Returns: Bereinigte Liste ohne leere TreeNodes """ result = [] for node in nodes: if isinstance(node, TreeNode): node.children = remove_empty_tree_nodes(node.children) if node.children: result.append(node) # leere TreeNodes stillschweigend verwerfen else: result.append(node) return result def collect_unused_xml_files( obsolete_groups: list[ObsoleteGroup], project_dir: Path, is_xml_used_elsewhere_fn, ) -> list[tuple[Path, Path]]: """ Sammelt XML-Dateien der veralteten XslFiles, die nirgends anders mehr verwendet werden. WICHTIG: Muss nach dem Entfernen der XslFiles aus dem Modell aufgerufen werden, damit is_xml_used_elsewhere_fn korrekte Ergebnisse liefert. Args: obsolete_groups: Die veralteten Gruppen project_dir: Absoluter Pfad zum Projektverzeichnis is_xml_used_elsewhere_fn: Callable(xml_path, exclude_xsl_file) -> bool Returns: Liste von (xml_path_relativ, xml_path_absolut) für nicht mehr verwendete XML-Dateien """ unused: list[tuple[Path, Path]] = [] seen: set[str] = set() for group in obsolete_groups: for entry in group.xsl_entries: for xml_file_obj in entry.xsl_file.xmls: xml_path_str = str(xml_file_obj.xml) if xml_path_str in seen: continue seen.add(xml_path_str) xml_abs = project_dir / xml_file_obj.xml if xml_abs.exists(): if not is_xml_used_elsewhere_fn(xml_file_obj.xml, entry.xsl_file): unused.append((xml_file_obj.xml, xml_abs)) return unused