dash-devtools 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dash_devtools/__init__.py +8 -0
- dash_devtools/__main__.py +11 -0
- dash_devtools/ai_engine.py +441 -0
- dash_devtools/browser.py +541 -0
- dash_devtools/cli.py +1452 -0
- dash_devtools/database.py +338 -0
- dash_devtools/dbdiagram.py +183 -0
- dash_devtools/e2e.py +329 -0
- dash_devtools/fixers/__init__.py +57 -0
- dash_devtools/fixers/migration_fixer.py +115 -0
- dash_devtools/fixers/ux_fixer.py +106 -0
- dash_devtools/fixers/version_bumper.py +115 -0
- dash_devtools/gas_mes_test.py +1241 -0
- dash_devtools/generators/__init__.py +84 -0
- dash_devtools/health.py +476 -0
- dash_devtools/hooks/__init__.py +250 -0
- dash_devtools/hooks/pre_commit.py +161 -0
- dash_devtools/hooks/pre_push.py +275 -0
- dash_devtools/init_test.py +352 -0
- dash_devtools/markdown_report.py +309 -0
- dash_devtools/migrators/__init__.py +21 -0
- dash_devtools/perf.py +321 -0
- dash_devtools/report.py +667 -0
- dash_devtools/reporters/__init__.py +11 -0
- dash_devtools/spec.py +230 -0
- dash_devtools/stats.py +355 -0
- dash_devtools/test_suite.py +690 -0
- dash_devtools/testing.py +416 -0
- dash_devtools/validators/__init__.py +157 -0
- dash_devtools/validators/backend/__init__.py +12 -0
- dash_devtools/validators/backend/nodejs.py +245 -0
- dash_devtools/validators/backend/python.py +439 -0
- dash_devtools/validators/code_quality.py +243 -0
- dash_devtools/validators/common/__init__.py +11 -0
- dash_devtools/validators/common/quality.py +319 -0
- dash_devtools/validators/common/security.py +270 -0
- dash_devtools/validators/common/spec.py +273 -0
- dash_devtools/validators/detector.py +394 -0
- dash_devtools/validators/frontend/__init__.py +14 -0
- dash_devtools/validators/frontend/angular.py +245 -0
- dash_devtools/validators/frontend/gas.py +310 -0
- dash_devtools/validators/frontend/vite.py +539 -0
- dash_devtools/validators/migration.py +292 -0
- dash_devtools/validators/performance.py +167 -0
- dash_devtools/validators/security.py +205 -0
- dash_devtools/vision/__init__.py +368 -0
- dash_devtools/watch.py +266 -0
- dash_devtools/word_report.py +690 -0
- dash_devtools-1.0.0.dist-info/METADATA +834 -0
- dash_devtools-1.0.0.dist-info/RECORD +53 -0
- dash_devtools-1.0.0.dist-info/WHEEL +5 -0
- dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
- dash_devtools-1.0.0.dist-info/top_level.txt +1 -0
dash_devtools/spec.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenSpec CLI 包裝模組
|
|
3
|
+
|
|
4
|
+
Spec-Driven Development (SDD) 工作流程整合。
|
|
5
|
+
使用 @fission-ai/openspec npm 套件。
|
|
6
|
+
|
|
7
|
+
安裝:npm install -g @fission-ai/openspec@latest
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import subprocess
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class SpecResult:
|
|
19
|
+
"""OpenSpec 操作結果"""
|
|
20
|
+
success: bool
|
|
21
|
+
message: str = ""
|
|
22
|
+
data: dict = field(default_factory=dict)
|
|
23
|
+
error: str = ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OpenSpecWrapper:
|
|
27
|
+
"""OpenSpec CLI 包裝器"""
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def _check_installed() -> bool:
|
|
31
|
+
"""檢查 openspec 是否安裝"""
|
|
32
|
+
try:
|
|
33
|
+
result = subprocess.run(
|
|
34
|
+
['which', 'openspec'],
|
|
35
|
+
capture_output=True,
|
|
36
|
+
text=True
|
|
37
|
+
)
|
|
38
|
+
return result.returncode == 0
|
|
39
|
+
except Exception:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _run_command(args: list, cwd: Optional[str] = None) -> SpecResult:
|
|
44
|
+
"""執行 openspec 指令"""
|
|
45
|
+
if not OpenSpecWrapper._check_installed():
|
|
46
|
+
return SpecResult(
|
|
47
|
+
success=False,
|
|
48
|
+
error="openspec 未安裝。請執行: npm install -g @fission-ai/openspec@latest"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
cmd = ['openspec'] + args
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
cmd,
|
|
55
|
+
cwd=cwd,
|
|
56
|
+
capture_output=True,
|
|
57
|
+
text=True
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if result.returncode == 0:
|
|
61
|
+
return SpecResult(
|
|
62
|
+
success=True,
|
|
63
|
+
message=result.stdout.strip(),
|
|
64
|
+
data={}
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
return SpecResult(
|
|
68
|
+
success=False,
|
|
69
|
+
error=result.stderr.strip() or result.stdout.strip()
|
|
70
|
+
)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
return SpecResult(
|
|
73
|
+
success=False,
|
|
74
|
+
error=str(e)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def init(project_path: str) -> SpecResult:
|
|
79
|
+
"""初始化 OpenSpec
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
project_path: 專案路徑
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
SpecResult: 操作結果
|
|
86
|
+
"""
|
|
87
|
+
result = OpenSpecWrapper._run_command(['init'], cwd=project_path)
|
|
88
|
+
if result.success:
|
|
89
|
+
result.message = "OpenSpec 初始化完成"
|
|
90
|
+
result.data = {
|
|
91
|
+
'openspec_dir': str(Path(project_path) / 'openspec'),
|
|
92
|
+
'specs_dir': str(Path(project_path) / 'openspec' / 'specs'),
|
|
93
|
+
'changes_dir': str(Path(project_path) / 'openspec' / 'changes')
|
|
94
|
+
}
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def list_changes(project_path: str) -> SpecResult:
|
|
99
|
+
"""列出活動變更
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
project_path: 專案路徑
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
SpecResult: 包含變更清單
|
|
106
|
+
"""
|
|
107
|
+
result = OpenSpecWrapper._run_command(['list'], cwd=project_path)
|
|
108
|
+
if result.success:
|
|
109
|
+
# 解析輸出為結構化資料
|
|
110
|
+
changes = []
|
|
111
|
+
for line in result.message.split('\n'):
|
|
112
|
+
line = line.strip()
|
|
113
|
+
if line and not line.startswith('─') and not line.startswith('Active'):
|
|
114
|
+
changes.append(line)
|
|
115
|
+
result.data = {'changes': changes, 'count': len(changes)}
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def view(project_path: str) -> None:
|
|
120
|
+
"""開啟互動式儀表板 (需要 TTY)
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
project_path: 專案路徑
|
|
124
|
+
"""
|
|
125
|
+
subprocess.run(['openspec', 'view'], cwd=project_path)
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def show(project_path: str, change_name: str) -> SpecResult:
|
|
129
|
+
"""顯示變更詳情
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
project_path: 專案路徑
|
|
133
|
+
change_name: 變更名稱
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
SpecResult: 包含變更詳情
|
|
137
|
+
"""
|
|
138
|
+
result = OpenSpecWrapper._run_command(['show', change_name], cwd=project_path)
|
|
139
|
+
if result.success:
|
|
140
|
+
result.data = {'change_name': change_name, 'content': result.message}
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def validate(project_path: str, change_name: str) -> SpecResult:
|
|
145
|
+
"""驗證規格格式
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
project_path: 專案路徑
|
|
149
|
+
change_name: 變更名稱
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
SpecResult: 驗證結果
|
|
153
|
+
"""
|
|
154
|
+
result = OpenSpecWrapper._run_command(['validate', change_name], cwd=project_path)
|
|
155
|
+
if result.success:
|
|
156
|
+
result.data = {'change_name': change_name, 'valid': True}
|
|
157
|
+
else:
|
|
158
|
+
result.data = {'change_name': change_name, 'valid': False}
|
|
159
|
+
return result
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def archive(project_path: str, change_name: str, yes: bool = False) -> SpecResult:
|
|
163
|
+
"""歸檔完成的變更
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
project_path: 專案路徑
|
|
167
|
+
change_name: 變更名稱
|
|
168
|
+
yes: 自動確認
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
SpecResult: 歸檔結果
|
|
172
|
+
"""
|
|
173
|
+
args = ['archive', change_name]
|
|
174
|
+
if yes:
|
|
175
|
+
args.append('-y')
|
|
176
|
+
result = OpenSpecWrapper._run_command(args, cwd=project_path)
|
|
177
|
+
if result.success:
|
|
178
|
+
result.message = f"變更 '{change_name}' 已歸檔"
|
|
179
|
+
result.data = {'change_name': change_name, 'archived': True}
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get_spec_status(project_path: str) -> dict:
|
|
184
|
+
"""取得專案的 OpenSpec 狀態摘要
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
project_path: 專案路徑
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
dict: 狀態摘要
|
|
191
|
+
"""
|
|
192
|
+
path = Path(project_path)
|
|
193
|
+
openspec_dir = path / 'openspec'
|
|
194
|
+
|
|
195
|
+
status = {
|
|
196
|
+
'initialized': openspec_dir.exists(),
|
|
197
|
+
'specs_count': 0,
|
|
198
|
+
'changes_count': 0,
|
|
199
|
+
'archive_count': 0,
|
|
200
|
+
'stale_changes': [], # 超過 7 天未處理的提案
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if not openspec_dir.exists():
|
|
204
|
+
return status
|
|
205
|
+
|
|
206
|
+
# 統計 specs
|
|
207
|
+
specs_dir = openspec_dir / 'specs'
|
|
208
|
+
if specs_dir.exists():
|
|
209
|
+
status['specs_count'] = len(list(specs_dir.glob('*.md')))
|
|
210
|
+
|
|
211
|
+
# 統計 changes
|
|
212
|
+
changes_dir = openspec_dir / 'changes'
|
|
213
|
+
if changes_dir.exists():
|
|
214
|
+
import time
|
|
215
|
+
now = time.time()
|
|
216
|
+
seven_days = 7 * 24 * 60 * 60
|
|
217
|
+
|
|
218
|
+
for change_file in changes_dir.glob('*.md'):
|
|
219
|
+
status['changes_count'] += 1
|
|
220
|
+
# 檢查是否過期
|
|
221
|
+
mtime = change_file.stat().st_mtime
|
|
222
|
+
if now - mtime > seven_days:
|
|
223
|
+
status['stale_changes'].append(change_file.stem)
|
|
224
|
+
|
|
225
|
+
# 統計 archive
|
|
226
|
+
archive_dir = openspec_dir / 'archive'
|
|
227
|
+
if archive_dir.exists():
|
|
228
|
+
status['archive_count'] = len(list(archive_dir.glob('*.md')))
|
|
229
|
+
|
|
230
|
+
return status
|
dash_devtools/stats.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""
|
|
2
|
+
程式碼統計儀表板
|
|
3
|
+
|
|
4
|
+
視覺化專案統計資訊:
|
|
5
|
+
- 語言分佈
|
|
6
|
+
- 檔案數量與行數
|
|
7
|
+
- 複雜度指標
|
|
8
|
+
- 技術債務追蹤
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Dict, List, Tuple
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# 語言設定
|
|
25
|
+
LANGUAGE_CONFIG = {
|
|
26
|
+
'.py': {'name': 'Python', 'color': 'yellow', 'icon': ''},
|
|
27
|
+
'.js': {'name': 'JavaScript', 'color': 'yellow', 'icon': ''},
|
|
28
|
+
'.ts': {'name': 'TypeScript', 'color': 'blue', 'icon': ''},
|
|
29
|
+
'.tsx': {'name': 'TSX', 'color': 'cyan', 'icon': ''},
|
|
30
|
+
'.jsx': {'name': 'JSX', 'color': 'cyan', 'icon': ''},
|
|
31
|
+
'.html': {'name': 'HTML', 'color': 'orange1', 'icon': ''},
|
|
32
|
+
'.css': {'name': 'CSS', 'color': 'magenta', 'icon': ''},
|
|
33
|
+
'.scss': {'name': 'SCSS', 'color': 'magenta', 'icon': ''},
|
|
34
|
+
'.json': {'name': 'JSON', 'color': 'green', 'icon': ''},
|
|
35
|
+
'.md': {'name': 'Markdown', 'color': 'white', 'icon': ''},
|
|
36
|
+
'.vue': {'name': 'Vue', 'color': 'green', 'icon': ''},
|
|
37
|
+
'.svelte': {'name': 'Svelte', 'color': 'orange1', 'icon': ''},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# 忽略的目錄
|
|
41
|
+
IGNORE_DIRS = {
|
|
42
|
+
'node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
|
|
43
|
+
'venv', '.venv', '.angular', '.cache', 'coverage', '.nuxt',
|
|
44
|
+
'.output', '.turbo', 'vendor'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class FileStats:
|
|
50
|
+
"""單一檔案統計"""
|
|
51
|
+
path: str
|
|
52
|
+
extension: str
|
|
53
|
+
lines: int
|
|
54
|
+
code_lines: int
|
|
55
|
+
comment_lines: int
|
|
56
|
+
blank_lines: int
|
|
57
|
+
size_bytes: int
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class LanguageStats:
|
|
62
|
+
"""語言統計"""
|
|
63
|
+
name: str
|
|
64
|
+
files: int = 0
|
|
65
|
+
lines: int = 0
|
|
66
|
+
code_lines: int = 0
|
|
67
|
+
size_bytes: int = 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class ProjectStats:
|
|
72
|
+
"""專案統計總覽"""
|
|
73
|
+
name: str
|
|
74
|
+
total_files: int = 0
|
|
75
|
+
total_lines: int = 0
|
|
76
|
+
total_code_lines: int = 0
|
|
77
|
+
total_size_bytes: int = 0
|
|
78
|
+
languages: Dict[str, LanguageStats] = field(default_factory=dict)
|
|
79
|
+
largest_files: List[Tuple[str, int]] = field(default_factory=list)
|
|
80
|
+
complexity_issues: List[str] = field(default_factory=list)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class StatsCollector:
|
|
84
|
+
"""程式碼統計收集器"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, project_path: str):
|
|
87
|
+
self.project_path = Path(project_path)
|
|
88
|
+
self.project_name = self.project_path.name
|
|
89
|
+
|
|
90
|
+
def collect(self) -> ProjectStats:
|
|
91
|
+
"""收集專案統計"""
|
|
92
|
+
stats = ProjectStats(name=self.project_name)
|
|
93
|
+
file_stats_list = []
|
|
94
|
+
|
|
95
|
+
for file_path in self.project_path.rglob('*'):
|
|
96
|
+
# 跳過目錄
|
|
97
|
+
if file_path.is_dir():
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
# 跳過忽略的目錄
|
|
101
|
+
if any(ignore in file_path.parts for ignore in IGNORE_DIRS):
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# 只處理已知的語言
|
|
105
|
+
ext = file_path.suffix.lower()
|
|
106
|
+
if ext not in LANGUAGE_CONFIG:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
file_stat = self._analyze_file(file_path)
|
|
111
|
+
file_stats_list.append(file_stat)
|
|
112
|
+
|
|
113
|
+
# 更新語言統計
|
|
114
|
+
lang_name = LANGUAGE_CONFIG[ext]['name']
|
|
115
|
+
if lang_name not in stats.languages:
|
|
116
|
+
stats.languages[lang_name] = LanguageStats(name=lang_name)
|
|
117
|
+
|
|
118
|
+
lang_stats = stats.languages[lang_name]
|
|
119
|
+
lang_stats.files += 1
|
|
120
|
+
lang_stats.lines += file_stat.lines
|
|
121
|
+
lang_stats.code_lines += file_stat.code_lines
|
|
122
|
+
lang_stats.size_bytes += file_stat.size_bytes
|
|
123
|
+
|
|
124
|
+
# 更新總計
|
|
125
|
+
stats.total_files += 1
|
|
126
|
+
stats.total_lines += file_stat.lines
|
|
127
|
+
stats.total_code_lines += file_stat.code_lines
|
|
128
|
+
stats.total_size_bytes += file_stat.size_bytes
|
|
129
|
+
|
|
130
|
+
# 檢查複雜度問題
|
|
131
|
+
if file_stat.lines > 500:
|
|
132
|
+
stats.complexity_issues.append(
|
|
133
|
+
f'{file_path.name}: {file_stat.lines} 行 (建議拆分)'
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
# 找出最大的檔案
|
|
140
|
+
file_stats_list.sort(key=lambda x: x.lines, reverse=True)
|
|
141
|
+
stats.largest_files = [
|
|
142
|
+
(fs.path, fs.lines) for fs in file_stats_list[:10]
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
return stats
|
|
146
|
+
|
|
147
|
+
def _analyze_file(self, file_path: Path) -> FileStats:
|
|
148
|
+
"""分析單一檔案"""
|
|
149
|
+
content = file_path.read_text(encoding='utf-8', errors='ignore')
|
|
150
|
+
lines = content.splitlines()
|
|
151
|
+
|
|
152
|
+
code_lines = 0
|
|
153
|
+
comment_lines = 0
|
|
154
|
+
blank_lines = 0
|
|
155
|
+
|
|
156
|
+
ext = file_path.suffix.lower()
|
|
157
|
+
in_block_comment = False
|
|
158
|
+
|
|
159
|
+
for line in lines:
|
|
160
|
+
stripped = line.strip()
|
|
161
|
+
|
|
162
|
+
if not stripped:
|
|
163
|
+
blank_lines += 1
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# Python 風格註解
|
|
167
|
+
if ext == '.py':
|
|
168
|
+
if stripped.startswith('#'):
|
|
169
|
+
comment_lines += 1
|
|
170
|
+
elif stripped.startswith('"""') or stripped.startswith("'''"):
|
|
171
|
+
in_block_comment = not in_block_comment
|
|
172
|
+
comment_lines += 1
|
|
173
|
+
elif in_block_comment:
|
|
174
|
+
comment_lines += 1
|
|
175
|
+
else:
|
|
176
|
+
code_lines += 1
|
|
177
|
+
|
|
178
|
+
# JS/TS 風格註解
|
|
179
|
+
elif ext in ['.js', '.ts', '.jsx', '.tsx', '.css', '.scss']:
|
|
180
|
+
if stripped.startswith('//'):
|
|
181
|
+
comment_lines += 1
|
|
182
|
+
elif stripped.startswith('/*'):
|
|
183
|
+
in_block_comment = True
|
|
184
|
+
comment_lines += 1
|
|
185
|
+
elif stripped.endswith('*/'):
|
|
186
|
+
in_block_comment = False
|
|
187
|
+
comment_lines += 1
|
|
188
|
+
elif in_block_comment:
|
|
189
|
+
comment_lines += 1
|
|
190
|
+
else:
|
|
191
|
+
code_lines += 1
|
|
192
|
+
|
|
193
|
+
# HTML 風格
|
|
194
|
+
elif ext == '.html':
|
|
195
|
+
if '<!--' in stripped:
|
|
196
|
+
comment_lines += 1
|
|
197
|
+
else:
|
|
198
|
+
code_lines += 1
|
|
199
|
+
|
|
200
|
+
else:
|
|
201
|
+
code_lines += 1
|
|
202
|
+
|
|
203
|
+
return FileStats(
|
|
204
|
+
path=str(file_path.relative_to(self.project_path)),
|
|
205
|
+
extension=ext,
|
|
206
|
+
lines=len(lines),
|
|
207
|
+
code_lines=code_lines,
|
|
208
|
+
comment_lines=comment_lines,
|
|
209
|
+
blank_lines=blank_lines,
|
|
210
|
+
size_bytes=file_path.stat().st_size
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def render_stats_report(stats: ProjectStats):
|
|
215
|
+
"""渲染統計報告"""
|
|
216
|
+
|
|
217
|
+
# 標題
|
|
218
|
+
title = Text()
|
|
219
|
+
title.append(f"\n {stats.name} ", style="bold white")
|
|
220
|
+
title.append("程式碼統計\n", style="dim")
|
|
221
|
+
console.print(Panel(title, border_style="cyan"))
|
|
222
|
+
|
|
223
|
+
# 總覽
|
|
224
|
+
size_kb = stats.total_size_bytes / 1024
|
|
225
|
+
size_display = f"{size_kb:.1f} KB" if size_kb < 1024 else f"{size_kb/1024:.1f} MB"
|
|
226
|
+
|
|
227
|
+
console.print(f" [dim]檔案數:[/dim] [cyan]{stats.total_files:,}[/cyan]")
|
|
228
|
+
console.print(f" [dim]總行數:[/dim] [cyan]{stats.total_lines:,}[/cyan]")
|
|
229
|
+
console.print(f" [dim]程式碼行數:[/dim] [cyan]{stats.total_code_lines:,}[/cyan]")
|
|
230
|
+
console.print(f" [dim]大小:[/dim] [cyan]{size_display}[/cyan]")
|
|
231
|
+
console.print()
|
|
232
|
+
|
|
233
|
+
# 語言分佈 - 水平條狀圖
|
|
234
|
+
console.print(" [bold]語言分佈[/bold]")
|
|
235
|
+
console.print()
|
|
236
|
+
|
|
237
|
+
if stats.languages:
|
|
238
|
+
max_lines = max(lang.lines for lang in stats.languages.values())
|
|
239
|
+
bar_width = 35
|
|
240
|
+
|
|
241
|
+
# 按行數排序
|
|
242
|
+
sorted_langs = sorted(
|
|
243
|
+
stats.languages.values(),
|
|
244
|
+
key=lambda x: x.lines,
|
|
245
|
+
reverse=True
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
for lang in sorted_langs[:8]: # 最多顯示 8 種語言
|
|
249
|
+
# 找出對應的顏色
|
|
250
|
+
color = 'white'
|
|
251
|
+
for ext, config in LANGUAGE_CONFIG.items():
|
|
252
|
+
if config['name'] == lang.name:
|
|
253
|
+
color = config['color']
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
filled = int((lang.lines / max_lines) * bar_width) if max_lines > 0 else 0
|
|
257
|
+
percentage = (lang.lines / stats.total_lines * 100) if stats.total_lines > 0 else 0
|
|
258
|
+
|
|
259
|
+
bar = Text()
|
|
260
|
+
bar.append(f" {lang.name:12} ", style="white")
|
|
261
|
+
bar.append("█" * filled, style=color)
|
|
262
|
+
bar.append("░" * (bar_width - filled), style="dim")
|
|
263
|
+
bar.append(f" {percentage:5.1f}%", style=color)
|
|
264
|
+
bar.append(f" ({lang.lines:,} 行)", style="dim")
|
|
265
|
+
|
|
266
|
+
console.print(bar)
|
|
267
|
+
|
|
268
|
+
console.print()
|
|
269
|
+
|
|
270
|
+
# 最大的檔案
|
|
271
|
+
if stats.largest_files:
|
|
272
|
+
console.print(" [bold]最大檔案 Top 5[/bold]")
|
|
273
|
+
console.print()
|
|
274
|
+
|
|
275
|
+
for i, (path, lines) in enumerate(stats.largest_files[:5], 1):
|
|
276
|
+
color = 'red' if lines > 500 else 'yellow' if lines > 300 else 'green'
|
|
277
|
+
console.print(f" {i}. [{color}]{lines:4} 行[/{color}] {path}")
|
|
278
|
+
|
|
279
|
+
console.print()
|
|
280
|
+
|
|
281
|
+
# 複雜度警告
|
|
282
|
+
if stats.complexity_issues:
|
|
283
|
+
console.print(" [bold yellow]複雜度警告[/bold yellow]")
|
|
284
|
+
console.print()
|
|
285
|
+
for issue in stats.complexity_issues[:5]:
|
|
286
|
+
console.print(f" [yellow]![/yellow] {issue}")
|
|
287
|
+
console.print()
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
'project': stats.name,
|
|
291
|
+
'total_files': stats.total_files,
|
|
292
|
+
'total_lines': stats.total_lines,
|
|
293
|
+
'languages': {k: {'files': v.files, 'lines': v.lines} for k, v in stats.languages.items()},
|
|
294
|
+
'complexity_issues': len(stats.complexity_issues)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def run_stats(project_path: str) -> dict:
|
|
299
|
+
"""執行程式碼統計"""
|
|
300
|
+
collector = StatsCollector(project_path)
|
|
301
|
+
stats = collector.collect()
|
|
302
|
+
return render_stats_report(stats)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def run_stats_all(projects: List[str]) -> dict:
|
|
306
|
+
"""多專案統計比較"""
|
|
307
|
+
results = []
|
|
308
|
+
|
|
309
|
+
# 標題
|
|
310
|
+
console.print(Panel(
|
|
311
|
+
Text("\n 專案統計比較\n", style="bold white"),
|
|
312
|
+
border_style="cyan"
|
|
313
|
+
))
|
|
314
|
+
|
|
315
|
+
# 建立比較表格
|
|
316
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
317
|
+
table.add_column("專案", style="white")
|
|
318
|
+
table.add_column("檔案", justify="right")
|
|
319
|
+
table.add_column("行數", justify="right")
|
|
320
|
+
table.add_column("主要語言", justify="center")
|
|
321
|
+
table.add_column("大小", justify="right")
|
|
322
|
+
|
|
323
|
+
for project in projects:
|
|
324
|
+
try:
|
|
325
|
+
collector = StatsCollector(project)
|
|
326
|
+
stats = collector.collect()
|
|
327
|
+
|
|
328
|
+
# 找出主要語言
|
|
329
|
+
main_lang = '-'
|
|
330
|
+
if stats.languages:
|
|
331
|
+
main_lang = max(stats.languages.values(), key=lambda x: x.lines).name
|
|
332
|
+
|
|
333
|
+
size_kb = stats.total_size_bytes / 1024
|
|
334
|
+
size_display = f"{size_kb:.0f} KB" if size_kb < 1024 else f"{size_kb/1024:.1f} MB"
|
|
335
|
+
|
|
336
|
+
table.add_row(
|
|
337
|
+
stats.name,
|
|
338
|
+
f"{stats.total_files:,}",
|
|
339
|
+
f"{stats.total_lines:,}",
|
|
340
|
+
main_lang,
|
|
341
|
+
size_display
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
results.append({
|
|
345
|
+
'project': stats.name,
|
|
346
|
+
'files': stats.total_files,
|
|
347
|
+
'lines': stats.total_lines
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
except Exception as e:
|
|
351
|
+
table.add_row(project.split('/')[-1], "-", "-", "-", f"[red]錯誤[/red]")
|
|
352
|
+
|
|
353
|
+
console.print(table)
|
|
354
|
+
|
|
355
|
+
return {'projects': results}
|