#!/usr/bin/env python3 import os import re from pathlib import Path from typing import Set, Dict, List, Tuple import json class DartFileAnalyzer: def __init__(self, project_root: str): self.project_root = Path(project_root) self.lib_path = self.project_root / "lib" self.all_files: Set[Path] = set() self.used_files: Set[Path] = set() self.import_graph: Dict[Path, Set[Path]] = {} # 진입점들 self.entry_points = [ self.lib_path / "main.dart", self.lib_path / "injection_container.dart", self.lib_path / "screens/common/app_layout.dart" ] def find_all_dart_files(self) -> None: """모든 .dart 파일을 찾기 (생성 파일 제외)""" for file_path in self.lib_path.rglob("*.dart"): if not (file_path.name.endswith(".g.dart") or file_path.name.endswith(".freezed.dart")): self.all_files.add(file_path) self.import_graph[file_path] = set() def extract_imports(self, file_path: Path) -> Set[Path]: """파일에서 import 문을 추출하고 실제 파일 경로로 변환""" imports = set() try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() # import 패턴 매칭 import_patterns = [ r"import\s+['\"]package:superport/(.+?)['\"]", r"import\s+['\"](.+?\.dart)['\"]" ] for pattern in import_patterns: matches = re.findall(pattern, content) for match in matches: if match.startswith('package:superport/'): # package:superport/ import relative_path = match.replace('package:superport/', '') imported_file = self.lib_path / relative_path else: # 상대 경로 import if match.startswith('../') or match.startswith('./'): imported_file = file_path.parent / match imported_file = imported_file.resolve() else: imported_file = file_path.parent / match if imported_file.exists() and imported_file.suffix == '.dart': imports.add(imported_file) except Exception as e: print(f"Warning: Could not read {file_path}: {e}") return imports def build_import_graph(self) -> None: """모든 파일의 import 관계를 분석""" print("Building import graph...") for file_path in self.all_files: self.import_graph[file_path] = self.extract_imports(file_path) def trace_used_files(self, start_file: Path, visited: Set[Path] = None) -> None: """진입점에서 시작해서 사용되는 모든 파일을 추적""" if visited is None: visited = set() if start_file in visited or start_file not in self.all_files: return visited.add(start_file) self.used_files.add(start_file) # 이 파일에서 import하는 모든 파일들을 재귀적으로 추적 for imported_file in self.import_graph.get(start_file, set()): self.trace_used_files(imported_file, visited) def find_unused_files(self) -> Set[Path]: """사용되지 않는 파일들 찾기""" print("Tracing used files from entry points...") # 각 진입점에서 시작해서 사용되는 파일들을 추적 for entry_point in self.entry_points: if entry_point.exists(): print(f"Tracing from {entry_point.relative_to(self.project_root)}") self.trace_used_files(entry_point) else: print(f"Warning: Entry point {entry_point} does not exist") # test 파일들도 별도 진입점으로 처리 (필요시) test_files = list(self.project_root.glob("test/**/*.dart")) for test_file in test_files: if test_file.suffix == '.dart' and not (test_file.name.endswith('.g.dart') or test_file.name.endswith('.freezed.dart')): print(f"Tracing from test file: {test_file.relative_to(self.project_root)}") self.trace_used_files(test_file) unused = self.all_files - self.used_files return unused def categorize_unused_files(self, unused_files: Set[Path]) -> Dict[str, List[Path]]: """사용되지 않는 파일들을 카테고리별로 분류""" categories = { 'generated_files': [], 'models': [], 'screens': [], 'services': [], 'repositories': [], 'controllers': [], 'widgets': [], 'utils': [], 'others': [] } for file_path in unused_files: relative_path = file_path.relative_to(self.lib_path) path_str = str(relative_path) if file_path.name.endswith('.g.dart') or file_path.name.endswith('.freezed.dart'): categories['generated_files'].append(file_path) elif 'models/' in path_str: categories['models'].append(file_path) elif 'screens/' in path_str: categories['screens'].append(file_path) elif 'services/' in path_str: categories['services'].append(file_path) elif 'repositories/' in path_str: categories['repositories'].append(file_path) elif 'controller' in path_str: categories['controllers'].append(file_path) elif 'widgets/' in path_str: categories['widgets'].append(file_path) elif 'utils/' in path_str: categories['utils'].append(file_path) else: categories['others'].append(file_path) return categories def check_safety_for_deletion(self, file_path: Path) -> Tuple[bool, str]: """파일 삭제의 안전성 검사""" relative_path = file_path.relative_to(self.lib_path) path_str = str(relative_path) # 안전하게 삭제 가능한 조건들 if file_path.name.endswith('.g.dart') or file_path.name.endswith('.freezed.dart'): return True, "자동 생성 파일 - 소스 파일 삭제 후 재생성 가능" # 위험할 수 있는 파일들 critical_keywords = ['main.dart', 'injection', 'app_layout', 'core/', 'constants/'] if any(keyword in path_str for keyword in critical_keywords): return False, "핵심 시스템 파일 - 삭제 전 추가 검토 필요" # pubspec.yaml에서 참조되는지 확인 필요 if 'assets/' in path_str or 'fonts/' in path_str: return False, "에셋 파일 - pubspec.yaml 참조 확인 필요" return True, "안전하게 삭제 가능" def generate_report(self) -> None: """분석 결과 보고서 생성""" print("\n" + "="*80) print("FLUTTER 프로젝트 사용되지 않는 파일 분석 보고서") print("="*80) unused_files = self.find_unused_files() categories = self.categorize_unused_files(unused_files) print(f"\n📊 총 분석 파일: {len(self.all_files)}개") print(f"✅ 사용 중인 파일: {len(self.used_files)}개") print(f"❌ 사용되지 않는 파일: {len(unused_files)}개") print(f"📈 사용률: {len(self.used_files)/len(self.all_files)*100:.1f}%") # 진입점 정보 print(f"\n🚀 분석 진입점:") for entry_point in self.entry_points: status = "✅" if entry_point.exists() else "❌" print(f" {status} {entry_point.relative_to(self.project_root)}") # 카테고리별 미사용 파일 print(f"\n📂 카테고리별 사용되지 않는 파일:") total_safe_to_delete = 0 total_need_review = 0 for category, files in categories.items(): if not files: continue print(f"\n📁 {category.upper()} ({len(files)}개):") safe_count = 0 review_count = 0 for file_path in sorted(files): relative_path = file_path.relative_to(self.lib_path) is_safe, reason = self.check_safety_for_deletion(file_path) if is_safe: safe_count += 1 print(f" ✅ lib/{relative_path}") else: review_count += 1 print(f" ⚠️ lib/{relative_path} - {reason}") print(f" 안전 삭제: {safe_count}개, 검토 필요: {review_count}개") total_safe_to_delete += safe_count total_need_review += review_count # 삭제 권장사항 print(f"\n🎯 삭제 권장사항:") print(f" ✅ 즉시 안전 삭제 가능: {total_safe_to_delete}개") print(f" ⚠️ 추가 검토 후 삭제: {total_need_review}개") # Git status와 비교 print(f"\n📋 Git Status와 비교 분석:") try: import subprocess result = subprocess.run(['git', 'status', '--porcelain'], cwd=self.project_root, capture_output=True, text=True) git_status = result.stdout deleted_files = [] for line in git_status.split('\n'): if line.startswith('D ') and line.endswith('.dart'): deleted_files.append(line[3:]) # Remove 'D ' prefix if deleted_files: print(f" 🗑️ Git에서 이미 삭제된 파일: {len(deleted_files)}개") for deleted_file in deleted_files[:10]: # Show first 10 print(f" - {deleted_file}") if len(deleted_files) > 10: print(f" ... 및 {len(deleted_files) - 10}개 더") else: print(f" ✅ Git에서 삭제된 dart 파일 없음") except Exception as e: print(f" ❌ Git status 확인 실패: {e}") # 중요한 패턴 분석 print(f"\n🔍 중요 패턴 분석:") # .g.dart / .freezed.dart 파일들의 소스 파일 존재 여부 generated_files = categories.get('generated_files', []) if generated_files: orphaned_generated = [] for gen_file in generated_files: if gen_file.name.endswith('.g.dart'): source_file = gen_file.with_suffix('').with_suffix('.dart') elif gen_file.name.endswith('.freezed.dart'): source_file = gen_file.parent / gen_file.name.replace('.freezed.dart', '.dart') else: continue if source_file not in self.all_files and not source_file.exists(): orphaned_generated.append(gen_file) if orphaned_generated: print(f" ⚠️ 소스 파일이 없는 생성 파일: {len(orphaned_generated)}개") for orphaned in orphaned_generated[:5]: print(f" - lib/{orphaned.relative_to(self.lib_path)}") else: print(f" ✅ 모든 생성 파일의 소스 파일 존재함") print(f"\n💡 권장사항:") print(f" 1. 자동 생성 파일들을 먼저 정리하세요") print(f" 2. 테스트 파일에서만 사용되는 파일들을 확인하세요") print(f" 3. 삭제 전에 git grep으로 문자열 참조 확인을 권장합니다") print(f" 4. 백업을 만든 후 단계적으로 삭제하세요") print("\n" + "="*80) def main(): project_root = "/Users/maximilian.j.sul/Documents/flutter/superport" analyzer = DartFileAnalyzer(project_root) print("🔍 Flutter 프로젝트 사용되지 않는 파일 분석 시작...") print(f"📂 프로젝트 경로: {project_root}") analyzer.find_all_dart_files() print(f"📄 발견된 총 Dart 파일: {len(analyzer.all_files)}개") analyzer.build_import_graph() analyzer.generate_report() if __name__ == "__main__": main()