Files
Projektarbeit-MYP/backend/import_analyzer.py
Till Tomczak 7a99af7ace 🎉 Feat: Import & Function Analysis Tool Enhancements 🎉
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
2025-06-19 18:13:18 +02:00

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()