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