298 lines
13 KiB
Python
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() |