This commit introduces a suite of tools for analyzing and optimizing imports and functions within the backend codebase. The following files have been updated: - backend/FRONTEND_ASSETS_ANALYSE.md - backend/REDUNDANZ_ANALYSE_FINAL.md - backend/SOFORT_L\303\226SCHBARE_FUN
357 lines
14 KiB
Python
357 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Import-Analyzer für das MYP Backend
|
|
|
|
Analysiert alle Python-Dateien auf:
|
|
1. Ungenutzte Import-Statements
|
|
2. Zirkuläre Imports
|
|
3. Redundante Imports
|
|
4. Missing imports
|
|
"""
|
|
|
|
import os
|
|
import ast
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from collections import defaultdict, Counter
|
|
from typing import Dict, List, Set, Tuple, Any
|
|
import json
|
|
|
|
class ImportAnalyzer:
|
|
def __init__(self, backend_path: str):
|
|
self.backend_path = Path(backend_path)
|
|
self.files_data = {}
|
|
self.all_imports = defaultdict(set)
|
|
self.all_usages = defaultdict(set)
|
|
self.module_dependencies = defaultdict(set)
|
|
self.findings = {
|
|
'unused_imports': {},
|
|
'circular_imports': [],
|
|
'redundant_imports': {},
|
|
'missing_imports': {},
|
|
'statistics': {}
|
|
}
|
|
|
|
def analyze_file(self, file_path: Path) -> Dict[str, Any]:
|
|
"""Analysiert eine einzelne Python-Datei"""
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
tree = ast.parse(content)
|
|
|
|
# Sammle Imports
|
|
imports = set()
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
for alias in node.names:
|
|
imports.add(alias.name)
|
|
elif isinstance(node, ast.ImportFrom):
|
|
module = node.module or ''
|
|
for alias in node.names:
|
|
if module:
|
|
imports.add(f"{module}.{alias.name}")
|
|
else:
|
|
imports.add(alias.name)
|
|
|
|
# Sammle verwendete Namen aus dem Code
|
|
used_names = set()
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Name):
|
|
used_names.add(node.id)
|
|
elif isinstance(node, ast.Attribute):
|
|
# Sammle Attribut-Zugriffe
|
|
if isinstance(node.value, ast.Name):
|
|
used_names.add(f"{node.value.id}.{node.attr}")
|
|
elif isinstance(node, ast.Call):
|
|
# Sammle Funktionsaufrufe
|
|
if isinstance(node.func, ast.Name):
|
|
used_names.add(node.func.id)
|
|
elif isinstance(node.func, ast.Attribute):
|
|
if isinstance(node.func.value, ast.Name):
|
|
used_names.add(f"{node.func.value.id}.{node.func.attr}")
|
|
|
|
# Suche auch in String-Literalen nach Verwendungen
|
|
string_references = set()
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Str):
|
|
# Suche nach import-ähnlichen Strings
|
|
for imp in imports:
|
|
if imp in node.s:
|
|
string_references.add(imp)
|
|
|
|
return {
|
|
'imports': imports,
|
|
'used_names': used_names,
|
|
'string_references': string_references,
|
|
'content': content,
|
|
'lines': len(content.splitlines())
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"Fehler beim Analysieren von {file_path}: {e}")
|
|
return {
|
|
'imports': set(),
|
|
'used_names': set(),
|
|
'string_references': set(),
|
|
'content': '',
|
|
'lines': 0,
|
|
'error': str(e)
|
|
}
|
|
|
|
def find_unused_imports(self):
|
|
"""Findet ungenutzte Imports"""
|
|
for file_path, data in self.files_data.items():
|
|
if 'error' in data:
|
|
continue
|
|
|
|
unused = []
|
|
imports = data['imports']
|
|
used_names = data['used_names']
|
|
string_refs = data['string_references']
|
|
|
|
for imp in imports:
|
|
# Verschiedene Formen der Nutzung prüfen
|
|
is_used = False
|
|
|
|
# Direkter Name
|
|
base_name = imp.split('.')[0]
|
|
if base_name in used_names:
|
|
is_used = True
|
|
|
|
# Vollständiger Import-Name
|
|
if imp in used_names:
|
|
is_used = True
|
|
|
|
# In String-Referenzen
|
|
if imp in string_refs:
|
|
is_used = True
|
|
|
|
# Spezielle Fälle für häufige Patterns
|
|
# Flask decorators, etc.
|
|
if not is_used and any(pattern in data['content'] for pattern in [
|
|
f"@{base_name}",
|
|
f"'{imp}'",
|
|
f'"{imp}"',
|
|
f"{base_name}(",
|
|
f".{base_name}",
|
|
]):
|
|
is_used = True
|
|
|
|
if not is_used:
|
|
unused.append(imp)
|
|
|
|
if unused:
|
|
self.findings['unused_imports'][str(file_path)] = unused
|
|
|
|
def find_circular_imports(self):
|
|
"""Findet zirkuläre Imports"""
|
|
# Baue Dependency-Graph
|
|
for file_path, data in self.files_data.items():
|
|
if 'error' in data:
|
|
continue
|
|
|
|
file_module = self.get_module_name(file_path)
|
|
for imp in data['imports']:
|
|
if self.is_local_import(imp):
|
|
self.module_dependencies[file_module].add(imp)
|
|
|
|
# Suche nach Zyklen
|
|
def has_cycle(node, path, visited):
|
|
if node in path:
|
|
cycle_start = path.index(node)
|
|
cycle = path[cycle_start:] + [node]
|
|
return cycle
|
|
|
|
if node in visited:
|
|
return None
|
|
|
|
visited.add(node)
|
|
path.append(node)
|
|
|
|
for neighbor in self.module_dependencies.get(node, []):
|
|
cycle = has_cycle(neighbor, path, visited)
|
|
if cycle:
|
|
return cycle
|
|
|
|
path.pop()
|
|
return None
|
|
|
|
visited = set()
|
|
for module in self.module_dependencies:
|
|
if module not in visited:
|
|
cycle = has_cycle(module, [], set())
|
|
if cycle and cycle not in self.findings['circular_imports']:
|
|
self.findings['circular_imports'].append(cycle)
|
|
|
|
def find_redundant_imports(self):
|
|
"""Findet redundante Imports (mehrfach importiert)"""
|
|
import_count = Counter()
|
|
import_locations = defaultdict(list)
|
|
|
|
for file_path, data in self.files_data.items():
|
|
if 'error' in data:
|
|
continue
|
|
|
|
for imp in data['imports']:
|
|
import_count[imp] += 1
|
|
import_locations[imp].append(str(file_path))
|
|
|
|
# Finde Imports die in mehreren Dateien vorkommen
|
|
for imp, count in import_count.items():
|
|
if count > 1:
|
|
self.findings['redundant_imports'][imp] = {
|
|
'count': count,
|
|
'files': import_locations[imp]
|
|
}
|
|
|
|
def get_module_name(self, file_path: Path) -> str:
|
|
"""Konvertiert Dateipfad zu Modulname"""
|
|
rel_path = file_path.relative_to(self.backend_path)
|
|
if rel_path.name == '__init__.py':
|
|
return str(rel_path.parent).replace('/', '.')
|
|
else:
|
|
return str(rel_path.with_suffix('')).replace('/', '.')
|
|
|
|
def is_local_import(self, imp: str) -> bool:
|
|
"""Prüft ob es ein lokaler Import ist"""
|
|
local_prefixes = ['blueprints', 'utils', 'config', 'models']
|
|
return any(imp.startswith(prefix) for prefix in local_prefixes)
|
|
|
|
def run_analysis(self):
|
|
"""Führt die komplette Analyse durch"""
|
|
print("Sammle Python-Dateien...")
|
|
|
|
# Sammle alle Python-Dateien
|
|
for file_path in self.backend_path.rglob("*.py"):
|
|
# Überspringe bestimmte Verzeichnisse
|
|
if any(part in str(file_path) for part in ['__pycache__', '.git', 'node_modules', 'instance/sessions']):
|
|
continue
|
|
|
|
print(f"Analysiere: {file_path.relative_to(self.backend_path)}")
|
|
self.files_data[file_path] = self.analyze_file(file_path)
|
|
|
|
print(f"\nGefundene Dateien: {len(self.files_data)}")
|
|
|
|
# Führe Analysen durch
|
|
print("Suche nach ungenutzten Imports...")
|
|
self.find_unused_imports()
|
|
|
|
print("Suche nach zirkulären Imports...")
|
|
self.find_circular_imports()
|
|
|
|
print("Suche nach redundanten Imports...")
|
|
self.find_redundant_imports()
|
|
|
|
# Statistiken
|
|
total_imports = sum(len(data.get('imports', [])) for data in self.files_data.values())
|
|
total_lines = sum(data.get('lines', 0) for data in self.files_data.values())
|
|
|
|
self.findings['statistics'] = {
|
|
'total_files': len(self.files_data),
|
|
'total_imports': total_imports,
|
|
'total_lines': total_lines,
|
|
'files_with_unused_imports': len(self.findings['unused_imports']),
|
|
'total_unused_imports': sum(len(imports) for imports in self.findings['unused_imports'].values()),
|
|
'circular_import_chains': len(self.findings['circular_imports']),
|
|
'redundant_import_types': len(self.findings['redundant_imports'])
|
|
}
|
|
|
|
def print_report(self):
|
|
"""Druckt einen detaillierten Bericht"""
|
|
print("\n" + "="*80)
|
|
print("IMPORT-ANALYSE BERICHT")
|
|
print("="*80)
|
|
|
|
stats = self.findings['statistics']
|
|
print(f"\nSTATISTIKEN:")
|
|
print(f" Analysierte Dateien: {stats['total_files']}")
|
|
print(f" Gesamte Imports: {stats['total_imports']}")
|
|
print(f" Gesamte Zeilen: {stats['total_lines']}")
|
|
print(f" Dateien mit ungenutzten Imports: {stats['files_with_unused_imports']}")
|
|
print(f" Ungenutzte Imports gesamt: {stats['total_unused_imports']}")
|
|
print(f" Zirkuläre Import-Ketten: {stats['circular_import_chains']}")
|
|
print(f" Redundante Import-Typen: {stats['redundant_import_types']}")
|
|
|
|
# Ungenutzte Imports
|
|
if self.findings['unused_imports']:
|
|
print(f"\n🚨 UNGENUTZTE IMPORTS ({stats['total_unused_imports']} gefunden):")
|
|
print("-" * 60)
|
|
for file_path, unused in self.findings['unused_imports'].items():
|
|
rel_path = Path(file_path).relative_to(self.backend_path)
|
|
print(f"\n📁 {rel_path}:")
|
|
for i, imp in enumerate(unused, 1):
|
|
print(f" {i:2d}. {imp}")
|
|
|
|
# Zirkuläre Imports
|
|
if self.findings['circular_imports']:
|
|
print(f"\n🔄 ZIRKULÄRE IMPORTS ({len(self.findings['circular_imports'])} Ketten):")
|
|
print("-" * 60)
|
|
for i, cycle in enumerate(self.findings['circular_imports'], 1):
|
|
print(f"\n{i}. Import-Kette:")
|
|
for j, module in enumerate(cycle):
|
|
arrow = " → " if j < len(cycle) - 1 else ""
|
|
print(f" {module}{arrow}")
|
|
|
|
# Redundante Imports
|
|
if self.findings['redundant_imports']:
|
|
print(f"\n📦 REDUNDANTE IMPORTS (Top 20):")
|
|
print("-" * 60)
|
|
sorted_redundant = sorted(
|
|
self.findings['redundant_imports'].items(),
|
|
key=lambda x: x[1]['count'],
|
|
reverse=True
|
|
)[:20]
|
|
|
|
for imp, data in sorted_redundant:
|
|
print(f"\n🔁 {imp} (verwendet in {data['count']} Dateien):")
|
|
for file_path in data['files'][:5]: # Zeige nur erste 5
|
|
rel_path = Path(file_path).relative_to(self.backend_path)
|
|
print(f" - {rel_path}")
|
|
if len(data['files']) > 5:
|
|
print(f" ... und {len(data['files']) - 5} weitere")
|
|
|
|
# Empfehlungen
|
|
print(f"\n💡 EMPFEHLUNGEN:")
|
|
print("-" * 60)
|
|
if stats['total_unused_imports'] > 0:
|
|
print(f"✂️ {stats['total_unused_imports']} ungenutzte Imports entfernen")
|
|
if stats['circular_import_chains'] > 0:
|
|
print(f"🔄 {stats['circular_import_chains']} zirkuläre Import-Ketten auflösen")
|
|
if stats['redundant_import_types'] > 10:
|
|
print(f"📦 Häufig verwendete Imports in gemeinsame Module auslagern")
|
|
|
|
if (stats['total_unused_imports'] == 0 and
|
|
stats['circular_import_chains'] == 0):
|
|
print("✅ Keine kritischen Import-Probleme gefunden!")
|
|
|
|
def save_report(self, output_file: str = "import_analysis_report.json"):
|
|
"""Speichert den Bericht als JSON"""
|
|
# Konvertiere Path-Objekte zu Strings für JSON
|
|
json_findings = {}
|
|
for key, value in self.findings.items():
|
|
if key == 'unused_imports':
|
|
json_findings[key] = {
|
|
str(Path(k).relative_to(self.backend_path)): v
|
|
for k, v in value.items()
|
|
}
|
|
else:
|
|
json_findings[key] = value
|
|
|
|
output_path = self.backend_path / output_file
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
json.dump(json_findings, f, indent=2, ensure_ascii=False)
|
|
|
|
print(f"\n💾 Detaillierter Bericht gespeichert: {output_path}")
|
|
|
|
def main():
|
|
backend_path = Path(__file__).parent
|
|
|
|
analyzer = ImportAnalyzer(str(backend_path))
|
|
analyzer.run_analysis()
|
|
analyzer.print_report()
|
|
analyzer.save_report()
|
|
|
|
if __name__ == "__main__":
|
|
main() |