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.
Files changed (53) hide show
  1. dash_devtools/__init__.py +8 -0
  2. dash_devtools/__main__.py +11 -0
  3. dash_devtools/ai_engine.py +441 -0
  4. dash_devtools/browser.py +541 -0
  5. dash_devtools/cli.py +1452 -0
  6. dash_devtools/database.py +338 -0
  7. dash_devtools/dbdiagram.py +183 -0
  8. dash_devtools/e2e.py +329 -0
  9. dash_devtools/fixers/__init__.py +57 -0
  10. dash_devtools/fixers/migration_fixer.py +115 -0
  11. dash_devtools/fixers/ux_fixer.py +106 -0
  12. dash_devtools/fixers/version_bumper.py +115 -0
  13. dash_devtools/gas_mes_test.py +1241 -0
  14. dash_devtools/generators/__init__.py +84 -0
  15. dash_devtools/health.py +476 -0
  16. dash_devtools/hooks/__init__.py +250 -0
  17. dash_devtools/hooks/pre_commit.py +161 -0
  18. dash_devtools/hooks/pre_push.py +275 -0
  19. dash_devtools/init_test.py +352 -0
  20. dash_devtools/markdown_report.py +309 -0
  21. dash_devtools/migrators/__init__.py +21 -0
  22. dash_devtools/perf.py +321 -0
  23. dash_devtools/report.py +667 -0
  24. dash_devtools/reporters/__init__.py +11 -0
  25. dash_devtools/spec.py +230 -0
  26. dash_devtools/stats.py +355 -0
  27. dash_devtools/test_suite.py +690 -0
  28. dash_devtools/testing.py +416 -0
  29. dash_devtools/validators/__init__.py +157 -0
  30. dash_devtools/validators/backend/__init__.py +12 -0
  31. dash_devtools/validators/backend/nodejs.py +245 -0
  32. dash_devtools/validators/backend/python.py +439 -0
  33. dash_devtools/validators/code_quality.py +243 -0
  34. dash_devtools/validators/common/__init__.py +11 -0
  35. dash_devtools/validators/common/quality.py +319 -0
  36. dash_devtools/validators/common/security.py +270 -0
  37. dash_devtools/validators/common/spec.py +273 -0
  38. dash_devtools/validators/detector.py +394 -0
  39. dash_devtools/validators/frontend/__init__.py +14 -0
  40. dash_devtools/validators/frontend/angular.py +245 -0
  41. dash_devtools/validators/frontend/gas.py +310 -0
  42. dash_devtools/validators/frontend/vite.py +539 -0
  43. dash_devtools/validators/migration.py +292 -0
  44. dash_devtools/validators/performance.py +167 -0
  45. dash_devtools/validators/security.py +205 -0
  46. dash_devtools/vision/__init__.py +368 -0
  47. dash_devtools/watch.py +266 -0
  48. dash_devtools/word_report.py +690 -0
  49. dash_devtools-1.0.0.dist-info/METADATA +834 -0
  50. dash_devtools-1.0.0.dist-info/RECORD +53 -0
  51. dash_devtools-1.0.0.dist-info/WHEEL +5 -0
  52. dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
  53. dash_devtools-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,292 @@
1
+ """
2
+ UI 框架驗證器
3
+
4
+ 檢查項目:
5
+ 1. Shoelace 元件正確使用
6
+ 2. 禁止使用 Emoji 作為圖示(應用 sl-icon)
7
+ 3. 重複 class 屬性
8
+ 4. Shoelace CSS 變數正確使用
9
+ 5. 不完整 HTML 標籤
10
+ """
11
+
12
+ import re
13
+ from pathlib import Path
14
+
15
+
16
+ # 常見 Emoji 圖示(這些應該用 sl-icon 取代)
17
+ ICON_EMOJI_PATTERNS = [
18
+ # 工具與操作
19
+ r'[\U0001F527\U0001F528\U0001F529]', # 🔧🔨🔩 wrench/hammer
20
+ r'[\U0001F504\U0001F503]', # 🔄🔃 refresh
21
+ r'[\U0001F50D\U0001F50E]', # 🔍🔎 search
22
+ r'[\u2699\uFE0F]?', # ⚙️ gear
23
+ r'[\U0001F5D1\uFE0F]?', # 🗑️ trash
24
+ r'[\u270F\uFE0F]?', # ✏️ pencil
25
+ r'[\u2795]', # ➕ plus
26
+ r'[\u2796]', # ➖ minus
27
+ # 狀態指示
28
+ r'[\u2705]', # ✅ check mark
29
+ r'[\u274C]', # ❌ cross mark
30
+ r'[\u26A0\uFE0F]?', # ⚠️ warning
31
+ r'[\U0001F6A8]', # 🚨 alert
32
+ r'[\u2139\uFE0F]?', # ℹ️ info
33
+ # 物件與符號
34
+ r'[\U0001F3E2]', # 🏢 building
35
+ r'[\U0001F4CB]', # 📋 clipboard
36
+ r'[\U0001F4C5\U0001F4C6]', # 📅📆 calendar
37
+ r'[\U0001F464\U0001F465]', # 👤👥 person/people
38
+ r'[\U0001F512\U0001F513]', # 🔒🔓 lock
39
+ r'[\U0001F510]', # 🔐 key lock
40
+ r'[\u231B]', # ⏳ hourglass
41
+ r'[\u23F3]', # ⏳ hourglass flowing
42
+ # 數字圓圈(應用 sl-icon 的 1-circle 等)
43
+ r'[1-9]\uFE0F?\u20E3', # 1️⃣ 2️⃣ etc
44
+ ]
45
+
46
+
47
+ class MigrationValidator:
48
+ """UI 框架驗證器"""
49
+
50
+ name = 'migration'
51
+
52
+ def __init__(self, project_path):
53
+ self.project_path = Path(project_path)
54
+ self.project_name = self.project_path.name
55
+ self.src_path = self.project_path / 'src'
56
+ self.result = {
57
+ 'name': self.name,
58
+ 'passed': True,
59
+ 'errors': [],
60
+ 'warnings': [],
61
+ 'checks': {}
62
+ }
63
+
64
+ def run(self):
65
+ """執行所有驗證"""
66
+ if not self.project_path.exists():
67
+ self.result['passed'] = False
68
+ self.result['errors'].append(f'專案路徑不存在: {self.project_path}')
69
+ return self.result
70
+
71
+ # 判斷專案類型
72
+ is_angular = (self.project_path / 'angular.json').exists()
73
+
74
+ if is_angular:
75
+ # Angular 專案使用 PrimeNG,跳過 Shoelace 檢查
76
+ self.result['checks']['framework'] = 'Angular + PrimeNG'
77
+ else:
78
+ # 非 Angular 專案應使用 Shoelace
79
+ self.check_shoelace_usage()
80
+ self.check_emoji_icons()
81
+
82
+ # 通用檢查
83
+ self.check_duplicate_classes()
84
+ self.check_incomplete_html_tags()
85
+ self.check_empty_buttons()
86
+ self.check_empty_event_handlers()
87
+
88
+ return self.result
89
+
90
+ def check_shoelace_usage(self):
91
+ """檢查 Shoelace 是否正確使用"""
92
+ # 檢查 index.html 是否有 Shoelace CDN
93
+ index_html = self.project_path / 'index.html'
94
+ has_shoelace_css = False
95
+ has_shoelace_js = False
96
+
97
+ if index_html.exists():
98
+ content = index_html.read_text(encoding='utf-8')
99
+ has_shoelace_css = 'shoelace' in content and '.css' in content
100
+ has_shoelace_js = 'shoelace' in content and '.js' in content
101
+
102
+ # 檢查 package.json
103
+ pkg_path = self.project_path / 'package.json'
104
+ has_shoelace_dep = False
105
+
106
+ if pkg_path.exists():
107
+ content = pkg_path.read_text(encoding='utf-8')
108
+ has_shoelace_dep = '@shoelace-style/shoelace' in content
109
+
110
+ self.result['checks']['shoelace_usage'] = {
111
+ 'has_css': has_shoelace_css,
112
+ 'has_js': has_shoelace_js,
113
+ 'has_dependency': has_shoelace_dep
114
+ }
115
+
116
+ # Shoelace 是預期的框架,缺少才是問題
117
+ if not has_shoelace_css and not has_shoelace_dep:
118
+ self.result['warnings'].append('未偵測到 Shoelace(非 Angular 專案建議使用)')
119
+
120
+ def check_emoji_icons(self):
121
+ """檢查 Emoji 圖示(應用 sl-icon 取代)"""
122
+ if not self.src_path.exists():
123
+ return
124
+
125
+ total_count = 0
126
+ file_issues = {}
127
+
128
+ # 合併所有 emoji 模式
129
+ combined_pattern = '|'.join(ICON_EMOJI_PATTERNS)
130
+
131
+ for ext in ['*.js', '*.html']:
132
+ for file_path in self.src_path.rglob(ext):
133
+ try:
134
+ content = file_path.read_text(encoding='utf-8')
135
+ # 排除 console.log 中的 emoji(允許 log 用 emoji)
136
+ # 只檢查 HTML 樣板字串中的 emoji
137
+ template_content = self._extract_template_strings(content)
138
+
139
+ matches = re.findall(combined_pattern, template_content)
140
+ if matches:
141
+ rel_path = str(file_path.relative_to(self.project_path))
142
+ file_issues[rel_path] = len(matches)
143
+ total_count += len(matches)
144
+ except Exception:
145
+ pass
146
+
147
+ self.result['checks']['emoji_icons'] = {
148
+ 'count': total_count,
149
+ 'files': file_issues
150
+ }
151
+
152
+ if total_count > 0:
153
+ self.result['warnings'].append(
154
+ f'Emoji 圖示: {total_count} 個(建議改用 sl-icon)'
155
+ )
156
+
157
+ def _extract_template_strings(self, content):
158
+ """提取 HTML 樣板字串內容"""
159
+ # 匹配 `...` 樣板字串
160
+ template_matches = re.findall(r'`[^`]*`', content, re.DOTALL)
161
+ return '\n'.join(template_matches)
162
+
163
+ def check_duplicate_classes(self):
164
+ """檢查重複 class 屬性"""
165
+ if not self.src_path.exists():
166
+ return
167
+
168
+ pattern = r'class="[^"]*"\s+class="'
169
+ total_count = 0
170
+ file_issues = {}
171
+
172
+ for file_path in self.src_path.rglob('*.js'):
173
+ try:
174
+ content = file_path.read_text(encoding='utf-8')
175
+ matches = re.findall(pattern, content)
176
+ if matches:
177
+ rel_path = str(file_path.relative_to(self.project_path))
178
+ file_issues[rel_path] = len(matches)
179
+ total_count += len(matches)
180
+ except Exception:
181
+ pass
182
+
183
+ self.result['checks']['duplicate_classes'] = {
184
+ 'count': total_count,
185
+ 'files': file_issues
186
+ }
187
+
188
+ if total_count > 0:
189
+ self.result['passed'] = False
190
+ self.result['errors'].append(f'重複 class: {total_count} 個')
191
+
192
+ def check_incomplete_html_tags(self):
193
+ """檢查不完整的 HTML 標籤"""
194
+ if not self.src_path.exists():
195
+ return
196
+
197
+ tags_to_check = ['select', 'textarea', 'table', 'ul', 'ol']
198
+ issues = []
199
+
200
+ for file_path in self.src_path.rglob('*.js'):
201
+ try:
202
+ content = file_path.read_text(encoding='utf-8')
203
+ rel_path = str(file_path.relative_to(self.project_path))
204
+
205
+ for tag in tags_to_check:
206
+ open_count = len(re.findall(rf'<{tag}[^>]*>', content))
207
+ close_count = len(re.findall(rf'</{tag}>', content))
208
+
209
+ if open_count > close_count:
210
+ diff = open_count - close_count
211
+ issues.append({
212
+ 'file': rel_path,
213
+ 'tag': tag,
214
+ 'missing': diff
215
+ })
216
+ except Exception:
217
+ pass
218
+
219
+ self.result['checks']['incomplete_html'] = {
220
+ 'count': len(issues),
221
+ 'issues': issues
222
+ }
223
+
224
+ if issues:
225
+ self.result['passed'] = False
226
+ for issue in issues[:5]:
227
+ self.result['errors'].append(
228
+ f"HTML 標籤不完整: {issue['file']} 缺少 {issue['missing']} 個 </{issue['tag']}>"
229
+ )
230
+
231
+ def check_empty_buttons(self):
232
+ """檢查空白按鈕內容"""
233
+ if not self.src_path.exists():
234
+ return
235
+
236
+ pattern = r'<button[^>]*>\s*\n?\s*</button>'
237
+ issues = []
238
+
239
+ for file_path in self.src_path.rglob('*.js'):
240
+ try:
241
+ content = file_path.read_text(encoding='utf-8')
242
+ matches = re.findall(pattern, content)
243
+ if matches:
244
+ rel_path = str(file_path.relative_to(self.project_path))
245
+ issues.append({
246
+ 'file': rel_path,
247
+ 'count': len(matches)
248
+ })
249
+ except Exception:
250
+ pass
251
+
252
+ self.result['checks']['empty_buttons'] = {
253
+ 'count': sum(i['count'] for i in issues),
254
+ 'files': issues
255
+ }
256
+
257
+ if issues:
258
+ total = sum(i['count'] for i in issues)
259
+ self.result['warnings'].append(f'空白按鈕: {total} 個')
260
+
261
+ def check_empty_event_handlers(self):
262
+ """檢查空白事件處理器"""
263
+ if not self.src_path.exists():
264
+ return
265
+
266
+ pattern = r"addEventListener\s*\(\s*['\"]['\"]"
267
+ issues = []
268
+
269
+ for file_path in self.src_path.rglob('*.js'):
270
+ try:
271
+ content = file_path.read_text(encoding='utf-8')
272
+ matches = re.findall(pattern, content)
273
+ if matches:
274
+ rel_path = str(file_path.relative_to(self.project_path))
275
+ issues.append({
276
+ 'file': rel_path,
277
+ 'count': len(matches)
278
+ })
279
+ except Exception:
280
+ pass
281
+
282
+ self.result['checks']['empty_event_handlers'] = {
283
+ 'count': sum(i['count'] for i in issues) if issues else 0,
284
+ 'files': issues
285
+ }
286
+
287
+ if issues:
288
+ self.result['passed'] = False
289
+ for issue in issues:
290
+ self.result['errors'].append(
291
+ f"空白事件處理器: {issue['file']} 有 {issue['count']} 個"
292
+ )
@@ -0,0 +1,167 @@
1
+ """
2
+ 效能驗證器
3
+
4
+ 檢查項目:
5
+ 1. Bundle 大小限制
6
+ 2. 圖片優化
7
+ 3. 未使用的依賴
8
+ 4. CSS 編譯狀態
9
+ """
10
+
11
+ import json
12
+ import subprocess
13
+ from pathlib import Path
14
+
15
+
16
+ class PerformanceValidator:
17
+ """效能驗證器"""
18
+
19
+ name = 'performance'
20
+
21
+ # 限制設定
22
+ LIMITS = {
23
+ 'max_css_kb': 200, # CSS 最大 200KB
24
+ 'min_css_kb': 30, # CSS 最小 30KB (確保 Tailwind 編譯)
25
+ 'max_js_kb': 500, # JS 最大 500KB
26
+ 'max_image_kb': 500, # 圖片最大 500KB
27
+ }
28
+
29
+ def __init__(self, project_path):
30
+ self.project_path = Path(project_path)
31
+ self.project_name = self.project_path.name
32
+ self.dist_path = self.project_path / 'dist'
33
+ self.result = {
34
+ 'name': self.name,
35
+ 'passed': True,
36
+ 'errors': [],
37
+ 'warnings': [],
38
+ 'checks': {}
39
+ }
40
+
41
+ def run(self):
42
+ """執行所有驗證"""
43
+ if not self.project_path.exists():
44
+ self.result['passed'] = False
45
+ self.result['errors'].append(f'專案路徑不存在: {self.project_path}')
46
+ return self.result
47
+
48
+ self.check_bundle_size()
49
+ self.check_image_sizes()
50
+ self.check_unused_dependencies()
51
+
52
+ return self.result
53
+
54
+ def check_bundle_size(self):
55
+ """檢查 Bundle 大小"""
56
+ if not self.dist_path.exists():
57
+ self.result['warnings'].append('dist 目錄不存在,跳過 bundle 檢查')
58
+ return
59
+
60
+ css_files = list(self.dist_path.rglob('*.css'))
61
+ js_files = list(self.dist_path.rglob('*.js'))
62
+
63
+ # CSS 檢查
64
+ total_css_size = sum(f.stat().st_size for f in css_files)
65
+ css_kb = total_css_size / 1024
66
+
67
+ self.result['checks']['css_bundle'] = {
68
+ 'size_kb': round(css_kb, 1),
69
+ 'files': len(css_files)
70
+ }
71
+
72
+ if css_kb < self.LIMITS['min_css_kb']:
73
+ self.result['warnings'].append(
74
+ f'CSS 過小 ({css_kb:.1f} KB),可能 Tailwind 未正確編譯'
75
+ )
76
+ elif css_kb > self.LIMITS['max_css_kb']:
77
+ self.result['warnings'].append(
78
+ f'CSS 過大 ({css_kb:.1f} KB),考慮優化'
79
+ )
80
+
81
+ # JS 檢查
82
+ total_js_size = sum(f.stat().st_size for f in js_files)
83
+ js_kb = total_js_size / 1024
84
+
85
+ self.result['checks']['js_bundle'] = {
86
+ 'size_kb': round(js_kb, 1),
87
+ 'files': len(js_files)
88
+ }
89
+
90
+ if js_kb > self.LIMITS['max_js_kb']:
91
+ self.result['warnings'].append(
92
+ f'JS 過大 ({js_kb:.1f} KB),考慮 code splitting'
93
+ )
94
+
95
+ def check_image_sizes(self):
96
+ """檢查圖片大小"""
97
+ public_path = self.project_path / 'public'
98
+ src_path = self.project_path / 'src'
99
+
100
+ large_images = []
101
+ extensions = ['*.png', '*.jpg', '*.jpeg', '*.gif', '*.webp', '*.svg']
102
+
103
+ for path in [public_path, src_path]:
104
+ if not path.exists():
105
+ continue
106
+ for ext in extensions:
107
+ for img in path.rglob(ext):
108
+ size_kb = img.stat().st_size / 1024
109
+ if size_kb > self.LIMITS['max_image_kb']:
110
+ large_images.append({
111
+ 'file': str(img.relative_to(self.project_path)),
112
+ 'size_kb': round(size_kb, 1)
113
+ })
114
+
115
+ self.result['checks']['images'] = {
116
+ 'large_count': len(large_images),
117
+ 'files': large_images
118
+ }
119
+
120
+ if large_images:
121
+ for img in large_images[:3]:
122
+ self.result['warnings'].append(
123
+ f"圖片過大: {img['file']} ({img['size_kb']} KB)"
124
+ )
125
+
126
+ def check_unused_dependencies(self):
127
+ """檢查未使用的依賴"""
128
+ pkg_path = self.project_path / 'package.json'
129
+ if not pkg_path.exists():
130
+ return
131
+
132
+ try:
133
+ pkg = json.loads(pkg_path.read_text(encoding='utf-8'))
134
+ deps = list(pkg.get('dependencies', {}).keys())
135
+
136
+ # 簡單檢查:搜尋 src 中是否有 import
137
+ src_path = self.project_path / 'src'
138
+ if not src_path.exists():
139
+ return
140
+
141
+ all_content = ''
142
+ for f in src_path.rglob('*.js'):
143
+ try:
144
+ all_content += f.read_text(encoding='utf-8')
145
+ except Exception:
146
+ pass
147
+
148
+ unused = []
149
+ for dep in deps:
150
+ # 跳過一些特殊依賴
151
+ if dep.startswith('@types/') or dep in ['vite', 'tailwindcss', 'daisyui']:
152
+ continue
153
+ if dep not in all_content and f"'{dep}'" not in all_content:
154
+ unused.append(dep)
155
+
156
+ self.result['checks']['unused_deps'] = {
157
+ 'count': len(unused),
158
+ 'packages': unused[:10] # 只顯示前 10 個
159
+ }
160
+
161
+ if unused:
162
+ self.result['warnings'].append(
163
+ f'可能未使用的依賴: {", ".join(unused[:5])}'
164
+ )
165
+
166
+ except Exception:
167
+ pass
@@ -0,0 +1,205 @@
1
+ """
2
+ 安全性驗證器
3
+
4
+ 檢查項目:
5
+ 1. API Key / Token 外洩
6
+ 2. 密碼硬編碼
7
+ 3. .env 檔案提交
8
+ 4. 敏感資料暴露
9
+ """
10
+
11
+ import re
12
+ from pathlib import Path
13
+
14
+
15
+ class SecurityValidator:
16
+ """安全性驗證器"""
17
+
18
+ name = 'security'
19
+
20
+ # 敏感資料正則表達式
21
+ SENSITIVE_PATTERNS = [
22
+ (r'(?i)(api[_-]?key|apikey)\s*[=:]\s*["\']?[a-zA-Z0-9_-]{20,}', 'API Key'),
23
+ (r'(?i)(secret|token)\s*[=:]\s*["\']?[a-zA-Z0-9_-]{20,}', 'Secret/Token'),
24
+ (r'(?i)password\s*[=:]\s*["\'][^"\']+["\']', '密碼'),
25
+ (r'sk-[a-zA-Z0-9]{48}', 'OpenAI API Key'),
26
+ (r'sk_live_[a-zA-Z0-9]{24,}', 'Stripe Live Key'),
27
+ (r'ghp_[a-zA-Z0-9]{36}', 'GitHub Token'),
28
+ # Clerk 只檢查 secret key (sk_),publishable key (pk_) 是公開的
29
+ (r'CLERK_SECRET_KEY\s*=\s*["\']?sk_[a-zA-Z0-9_-]{20,}', 'Clerk Secret Key'),
30
+ ]
31
+
32
+ # 敏感檔案
33
+ SENSITIVE_FILES = [
34
+ '.env',
35
+ '.env.local',
36
+ '.env.production',
37
+ 'credentials.json',
38
+ 'service-account.json',
39
+ 'private.key',
40
+ '*.pem',
41
+ ]
42
+
43
+ # 忽略目錄
44
+ IGNORE_DIRS = [
45
+ 'node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
46
+ '.angular', 'venv', '.venv', '.cache', 'coverage'
47
+ ]
48
+
49
+ def __init__(self, project_path):
50
+ self.project_path = Path(project_path)
51
+ self.project_name = self.project_path.name
52
+ self.result = {
53
+ 'name': self.name,
54
+ 'passed': True,
55
+ 'errors': [],
56
+ 'warnings': [],
57
+ 'checks': {}
58
+ }
59
+
60
+ def run(self):
61
+ """執行所有驗證"""
62
+ if not self.project_path.exists():
63
+ self.result['passed'] = False
64
+ self.result['errors'].append(f'專案路徑不存在: {self.project_path}')
65
+ return self.result
66
+
67
+ self.check_sensitive_files()
68
+ self.check_hardcoded_secrets()
69
+ self.check_gitignore()
70
+
71
+ return self.result
72
+
73
+ def check_sensitive_files(self):
74
+ """檢查敏感檔案是否被追蹤"""
75
+ issues = []
76
+
77
+ # 找出所有巢狀的 git repo (要跳過)
78
+ nested_repos = []
79
+ for git_dir in self.project_path.rglob('.git'):
80
+ if git_dir.parent != self.project_path:
81
+ nested_repos.append(str(git_dir.parent))
82
+
83
+ for pattern in self.SENSITIVE_FILES:
84
+ if '*' in pattern:
85
+ files = list(self.project_path.rglob(pattern))
86
+ else:
87
+ files = [self.project_path / pattern]
88
+
89
+ for f in files:
90
+ if f.exists() and f.is_file():
91
+ # 跳過巢狀 git repo
92
+ if any(str(f).startswith(repo) for repo in nested_repos):
93
+ continue
94
+ # 跳過忽略目錄
95
+ if any(ignore in str(f) for ignore in self.IGNORE_DIRS):
96
+ continue
97
+ # 檢查是否在 .gitignore 中
98
+ if not self._is_gitignored(f):
99
+ issues.append(str(f.relative_to(self.project_path)))
100
+
101
+ self.result['checks']['sensitive_files'] = {
102
+ 'count': len(issues),
103
+ 'files': issues
104
+ }
105
+
106
+ if issues:
107
+ self.result['passed'] = False
108
+ for f in issues:
109
+ self.result['errors'].append(f'敏感檔案未忽略: {f}')
110
+
111
+ def check_hardcoded_secrets(self):
112
+ """檢查硬編碼的敏感資料"""
113
+ issues = []
114
+
115
+ for file_path in self._get_source_files():
116
+ try:
117
+ content = file_path.read_text(encoding='utf-8')
118
+ rel_path = str(file_path.relative_to(self.project_path))
119
+
120
+ for pattern, desc in self.SENSITIVE_PATTERNS:
121
+ matches = re.findall(pattern, content)
122
+ if matches:
123
+ issues.append({
124
+ 'file': rel_path,
125
+ 'type': desc,
126
+ 'count': len(matches)
127
+ })
128
+ except Exception:
129
+ pass
130
+
131
+ self.result['checks']['hardcoded_secrets'] = {
132
+ 'count': len(issues),
133
+ 'issues': issues
134
+ }
135
+
136
+ if issues:
137
+ self.result['passed'] = False
138
+ for issue in issues:
139
+ self.result['errors'].append(
140
+ f"發現 {issue['type']} 在 {issue['file']}"
141
+ )
142
+
143
+ def check_gitignore(self):
144
+ """檢查 .gitignore 設定"""
145
+ gitignore = self.project_path / '.gitignore'
146
+ required_patterns = ['.env', 'node_modules', '*.log']
147
+ missing = []
148
+
149
+ if gitignore.exists():
150
+ content = gitignore.read_text(encoding='utf-8')
151
+ for pattern in required_patterns:
152
+ if pattern not in content:
153
+ missing.append(pattern)
154
+ else:
155
+ missing = required_patterns
156
+
157
+ self.result['checks']['gitignore'] = {
158
+ 'exists': gitignore.exists(),
159
+ 'missing_patterns': missing
160
+ }
161
+
162
+ if missing:
163
+ for pattern in missing:
164
+ self.result['warnings'].append(f'.gitignore 缺少: {pattern}')
165
+
166
+ def _get_source_files(self):
167
+ """取得所有原始碼檔案"""
168
+ extensions = ['*.js', '*.ts', '*.jsx', '*.tsx', '*.py', '*.json', '*.yaml', '*.yml']
169
+ files = []
170
+
171
+ # 找出所有巢狀的 git repo (要跳過)
172
+ nested_repos = []
173
+ for git_dir in self.project_path.rglob('.git'):
174
+ if git_dir.parent != self.project_path:
175
+ nested_repos.append(str(git_dir.parent))
176
+
177
+ for ext in extensions:
178
+ for f in self.project_path.rglob(ext):
179
+ # 跳過忽略目錄
180
+ if any(ignore in str(f) for ignore in self.IGNORE_DIRS):
181
+ continue
182
+ # 跳過巢狀 git repo
183
+ if any(str(f).startswith(repo) for repo in nested_repos):
184
+ continue
185
+ files.append(f)
186
+
187
+ return files
188
+
189
+ def _is_gitignored(self, file_path):
190
+ """檢查檔案是否在 .gitignore 中"""
191
+ gitignore = self.project_path / '.gitignore'
192
+ if not gitignore.exists():
193
+ return False
194
+
195
+ content = gitignore.read_text(encoding='utf-8')
196
+ rel_path = str(file_path.relative_to(self.project_path))
197
+
198
+ for line in content.splitlines():
199
+ line = line.strip()
200
+ if not line or line.startswith('#'):
201
+ continue
202
+ if line in rel_path or rel_path.startswith(line):
203
+ return True
204
+
205
+ return False