Files
superport/analyze_unused_files.py

298 lines
13 KiB
Python

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