223 lines
7.7 KiB
Python
223 lines
7.7 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""
|
|
Analyze function-level imports to determine which could be moved to header.
|
|
|
|
Categories:
|
|
1. CIRCULAR - Import would cause circular dependency (must stay in function)
|
|
2. REDUNDANT - Same import already exists in header (can be removed)
|
|
3. MOVABLE - Could potentially be moved to header
|
|
"""
|
|
|
|
import csv
|
|
from pathlib import Path
|
|
from typing import Dict, List, Set, Tuple
|
|
from collections import defaultdict
|
|
|
|
# Paths
|
|
SCRIPT_DIR = Path(__file__).parent
|
|
INPUT_FILE = SCRIPT_DIR / "import_analysis.csv"
|
|
OUTPUT_FILE = SCRIPT_DIR / "function_imports_analysis.txt"
|
|
|
|
|
|
def _getContainer(moduleName: str) -> str:
|
|
"""Extract container name from module path."""
|
|
if moduleName == "gateway.app":
|
|
return "app"
|
|
|
|
parts = moduleName.replace("gateway.", "").split(".")
|
|
if len(parts) < 2:
|
|
return "app"
|
|
|
|
container = parts[1]
|
|
|
|
# Skip tests and scripts
|
|
if container in ("tests", "scripts") or container.startswith("script_"):
|
|
return None
|
|
if parts[0] in ("tests", "scripts"):
|
|
return None
|
|
|
|
# Handle features sub-containers
|
|
if container == "features" and len(parts) > 2:
|
|
return f"features.{parts[2]}"
|
|
|
|
return container
|
|
|
|
|
|
def _analyzeImports() -> Tuple[Dict, Dict, Dict, Set]:
|
|
"""
|
|
Analyze imports and return:
|
|
- headerImports: Dict[module] -> Set[imported_modules]
|
|
- functionImports: Dict[module] -> List[(imported_module, function_name)]
|
|
- allModuleImports: Dict[module] -> Set[all_imports] (for circular detection)
|
|
- allModules: Set of all modules
|
|
"""
|
|
headerImports = defaultdict(set)
|
|
functionImports = defaultdict(list)
|
|
allModuleImports = defaultdict(set)
|
|
allModules = set()
|
|
|
|
with open(INPUT_FILE, "r", encoding="utf-8") as f:
|
|
reader = csv.DictReader(f)
|
|
for row in reader:
|
|
sourceFull = row["module_name"]
|
|
targetFull = row["imported_module_name"]
|
|
position = row["position"]
|
|
|
|
# Skip external imports and relative imports
|
|
if not targetFull.startswith("modules."):
|
|
continue
|
|
if targetFull.startswith("(relative)"):
|
|
continue
|
|
|
|
# Skip tests/scripts
|
|
sourceContainer = _getContainer(sourceFull)
|
|
if sourceContainer is None:
|
|
continue
|
|
|
|
# Add gateway prefix for consistency
|
|
targetFull = f"gateway.{targetFull}"
|
|
|
|
allModules.add(sourceFull)
|
|
allModules.add(targetFull)
|
|
allModuleImports[sourceFull].add(targetFull)
|
|
|
|
if position == "header":
|
|
headerImports[sourceFull].add(targetFull)
|
|
else:
|
|
# Extract function name
|
|
funcName = position.replace("function ", "")
|
|
functionImports[sourceFull].append((targetFull, funcName))
|
|
|
|
return dict(headerImports), dict(functionImports), dict(allModuleImports), allModules
|
|
|
|
|
|
def _detectCircularDependency(source: str, target: str, allModuleImports: Dict) -> bool:
|
|
"""
|
|
Check if moving target import to header would create circular dependency.
|
|
Returns True if target already imports source (directly or indirectly).
|
|
"""
|
|
visited = set()
|
|
|
|
def _canReach(current: str, goal: str) -> bool:
|
|
if current == goal:
|
|
return True
|
|
if current in visited:
|
|
return False
|
|
visited.add(current)
|
|
|
|
for imported in allModuleImports.get(current, []):
|
|
if _canReach(imported, goal):
|
|
return True
|
|
return False
|
|
|
|
# Check if target can reach source through its imports
|
|
return _canReach(target, source)
|
|
|
|
|
|
def main():
|
|
"""Main analysis function."""
|
|
print("Analyzing function imports...")
|
|
headerImports, functionImports, allModuleImports, allModules = _analyzeImports()
|
|
|
|
# Categorize function imports
|
|
circular = [] # Must stay in function (would cause circular import)
|
|
redundant = [] # Already imported in header (can be removed)
|
|
movable = [] # Could be moved to header
|
|
|
|
totalFunctionImports = 0
|
|
|
|
for source, imports in sorted(functionImports.items()):
|
|
headerSet = headerImports.get(source, set())
|
|
|
|
for target, funcName in imports:
|
|
totalFunctionImports += 1
|
|
|
|
# Check if already in header
|
|
if target in headerSet:
|
|
redundant.append((source, target, funcName))
|
|
continue
|
|
|
|
# Check for circular dependency
|
|
if _detectCircularDependency(source, target, allModuleImports):
|
|
circular.append((source, target, funcName))
|
|
continue
|
|
|
|
# Otherwise, could be moved to header
|
|
movable.append((source, target, funcName))
|
|
|
|
# Generate report
|
|
lines = []
|
|
lines.append("=" * 80)
|
|
lines.append("FUNCTION IMPORTS ANALYSIS")
|
|
lines.append("=" * 80)
|
|
lines.append(f"\nTotal function imports (internal modules): {totalFunctionImports}")
|
|
lines.append(f" - CIRCULAR (must stay): {len(circular):4}")
|
|
lines.append(f" - REDUNDANT (can remove): {len(redundant):4}")
|
|
lines.append(f" - MOVABLE (can move): {len(movable):4}")
|
|
|
|
# Group movable by source module
|
|
movableBySource = defaultdict(list)
|
|
for source, target, funcName in movable:
|
|
movableBySource[source].append((target, funcName))
|
|
|
|
lines.append(f"\n\n{'=' * 80}")
|
|
lines.append("MOVABLE TO HEADER (grouped by source module)")
|
|
lines.append("These imports could potentially be moved to the module header.")
|
|
lines.append("=" * 80)
|
|
|
|
for source in sorted(movableBySource.keys()):
|
|
imports = movableBySource[source]
|
|
lines.append(f"\n{source}")
|
|
lines.append("-" * len(source))
|
|
for target, funcName in sorted(set(imports)):
|
|
shortTarget = target.replace("gateway.", "")
|
|
lines.append(f" [{funcName}] {shortTarget}")
|
|
|
|
# Redundant imports
|
|
if redundant:
|
|
lines.append(f"\n\n{'=' * 80}")
|
|
lines.append("REDUNDANT IMPORTS (already in header - can be removed)")
|
|
lines.append("=" * 80)
|
|
|
|
redundantBySource = defaultdict(list)
|
|
for source, target, funcName in redundant:
|
|
redundantBySource[source].append((target, funcName))
|
|
|
|
for source in sorted(redundantBySource.keys()):
|
|
imports = redundantBySource[source]
|
|
lines.append(f"\n{source}")
|
|
lines.append("-" * len(source))
|
|
for target, funcName in sorted(set(imports)):
|
|
shortTarget = target.replace("gateway.", "")
|
|
lines.append(f" [{funcName}] {shortTarget}")
|
|
|
|
# Circular imports (for reference)
|
|
if circular:
|
|
lines.append(f"\n\n{'=' * 80}")
|
|
lines.append("CIRCULAR DEPENDENCY (must stay in function)")
|
|
lines.append("=" * 80)
|
|
|
|
circularBySource = defaultdict(list)
|
|
for source, target, funcName in circular:
|
|
circularBySource[source].append((target, funcName))
|
|
|
|
for source in sorted(circularBySource.keys()):
|
|
imports = circularBySource[source]
|
|
lines.append(f"\n{source}")
|
|
lines.append("-" * len(source))
|
|
for target, funcName in sorted(set(imports)):
|
|
shortTarget = target.replace("gateway.", "")
|
|
lines.append(f" [{funcName}] {shortTarget}")
|
|
|
|
# Write report
|
|
report = "\n".join(lines)
|
|
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
|
|
f.write(report)
|
|
|
|
print(report)
|
|
print(f"\n\nReport saved to: {OUTPUT_FILE}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|