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,394 @@
1
+ """
2
+ 專案類型偵測器 v2.1
3
+
4
+ 自動偵測專案的技術堆疊:
5
+ - 前端:Angular / Vue+Vite / React / Vanilla JS / GAS (Google Apps Script)
6
+ - 後端:Node.js / Python (FastAPI/Flask/Django)
7
+ - 部署:Vercel Serverless / Vercel Proxy / Render / GAS Web App
8
+
9
+ 新增功能:
10
+ - 區分「Serverless API」與「純 Proxy 閘道」
11
+ - 偵測 Vue 3 + DaisyUI 組合
12
+ - 偵測 GAS 專案(appsscript.json)
13
+ """
14
+
15
+ import json
16
+ from pathlib import Path
17
+ from typing import Set
18
+
19
+
20
+ class ProjectDetector:
21
+ """專案類型偵測器"""
22
+
23
+ def __init__(self, project_path):
24
+ self.project_path = Path(project_path)
25
+ self.project_name = self.project_path.name
26
+
27
+ def detect(self) -> dict:
28
+ """偵測專案類型,回傳技術堆疊資訊"""
29
+ result = {
30
+ 'name': self.project_name,
31
+ 'types': set(),
32
+ 'frontend': None,
33
+ 'backend': None,
34
+ 'ui_framework': None,
35
+ 'deployment': None,
36
+ 'details': {}
37
+ }
38
+
39
+ # 優先偵測 GAS 專案(有 appsscript.json)
40
+ gas = self._detect_gas()
41
+ if gas:
42
+ result['types'].add('gas')
43
+ result['frontend'] = gas['type']
44
+ result['ui_framework'] = gas.get('ui_framework')
45
+ result['deployment'] = {'type': 'gas-webapp', 'description': 'Google Apps Script Web App'}
46
+ result['details']['gas'] = gas
47
+ result['types'] = list(result['types'])
48
+ return result
49
+
50
+ # 偵測前端
51
+ frontend = self._detect_frontend()
52
+ if frontend:
53
+ result['types'].add('frontend')
54
+ result['frontend'] = frontend['type']
55
+ result['ui_framework'] = frontend.get('ui_framework')
56
+ result['details']['frontend'] = frontend
57
+
58
+ # 偵測後端
59
+ backend = self._detect_backend()
60
+ if backend:
61
+ result['types'].add('backend')
62
+ result['backend'] = backend['type']
63
+ result['details']['backend'] = backend
64
+
65
+ # 偵測部署模式
66
+ deployment = self._detect_deployment()
67
+ if deployment:
68
+ result['deployment'] = deployment
69
+ result['details']['deployment'] = deployment
70
+
71
+ # 轉換 set 為 list(JSON 序列化用)
72
+ result['types'] = list(result['types'])
73
+
74
+ return result
75
+
76
+ def _detect_frontend(self) -> dict | None:
77
+ """偵測前端技術"""
78
+ package_json = self.project_path / 'package.json'
79
+
80
+ if not package_json.exists():
81
+ return None
82
+
83
+ try:
84
+ pkg = json.loads(package_json.read_text(encoding='utf-8'))
85
+ deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
86
+
87
+ # Angular 偵測
88
+ if '@angular/core' in deps:
89
+ ui_framework = None
90
+ if 'primeng' in deps:
91
+ ui_framework = 'primeng'
92
+
93
+ return {
94
+ 'type': 'angular',
95
+ 'version': deps.get('@angular/core', 'unknown'),
96
+ 'ui_framework': ui_framework,
97
+ 'has_tailwind': 'tailwindcss' in deps
98
+ }
99
+
100
+ # Vue + Vite 偵測
101
+ if 'vue' in deps:
102
+ ui_framework = None
103
+ if 'daisyui' in deps:
104
+ ui_framework = 'daisyui'
105
+ elif '@shoelace-style/shoelace' in deps:
106
+ ui_framework = 'shoelace'
107
+
108
+ return {
109
+ 'type': 'vue-vite' if 'vite' in deps else 'vue',
110
+ 'version': deps.get('vue', 'unknown'),
111
+ 'ui_framework': ui_framework,
112
+ 'has_tailwind': 'tailwindcss' in deps or '@tailwindcss/vite' in deps,
113
+ 'has_typescript': 'typescript' in deps or 'vue-tsc' in deps
114
+ }
115
+
116
+ # Vite (非 Vue)
117
+ if 'vite' in deps:
118
+ ui_framework = None
119
+ if 'daisyui' in deps:
120
+ ui_framework = 'daisyui'
121
+ elif '@shoelace-style/shoelace' in deps:
122
+ ui_framework = 'shoelace'
123
+
124
+ return {
125
+ 'type': 'vite',
126
+ 'version': deps.get('vite', 'unknown'),
127
+ 'ui_framework': ui_framework,
128
+ 'has_tailwind': 'tailwindcss' in deps
129
+ }
130
+
131
+ # React 偵測
132
+ if 'react' in deps:
133
+ return {
134
+ 'type': 'react',
135
+ 'version': deps.get('react', 'unknown'),
136
+ 'ui_framework': self._detect_react_ui(deps)
137
+ }
138
+
139
+ # 原生 JS(有 package.json 但無框架)
140
+ return {
141
+ 'type': 'vanilla',
142
+ 'ui_framework': None
143
+ }
144
+
145
+ except Exception:
146
+ return None
147
+
148
+ def _detect_react_ui(self, deps: dict) -> str | None:
149
+ """偵測 React UI 框架"""
150
+ if '@mui/material' in deps:
151
+ return 'mui'
152
+ if 'antd' in deps:
153
+ return 'antd'
154
+ if '@chakra-ui/react' in deps:
155
+ return 'chakra'
156
+ return None
157
+
158
+ def _detect_gas(self) -> dict | None:
159
+ """偵測 Google Apps Script 專案"""
160
+ appsscript_json = self.project_path / 'appsscript.json'
161
+
162
+ if not appsscript_json.exists():
163
+ return None
164
+
165
+ try:
166
+ config = json.loads(appsscript_json.read_text(encoding='utf-8'))
167
+
168
+ # 偵測 UI 框架(從 HTML 檔案內容判斷)
169
+ ui_framework = None
170
+ html_files = list(self.project_path.glob('*.html'))
171
+
172
+ for html_file in html_files:
173
+ content = html_file.read_text(encoding='utf-8')
174
+ if 'shoelace' in content.lower() or 'sl-' in content:
175
+ ui_framework = 'shoelace'
176
+ break
177
+ elif 'daisyui' in content.lower():
178
+ ui_framework = 'daisyui'
179
+ break
180
+
181
+ # 檢查是否有 Vue
182
+ has_vue = any('vue' in f.read_text(encoding='utf-8').lower()
183
+ for f in html_files if f.exists())
184
+
185
+ return {
186
+ 'type': 'gas',
187
+ 'runtime': config.get('runtimeVersion', 'V8'),
188
+ 'webapp': config.get('webapp', {}),
189
+ 'ui_framework': ui_framework,
190
+ 'has_vue': has_vue,
191
+ 'html_files': [f.name for f in html_files],
192
+ 'js_files': [f.name for f in self.project_path.glob('*.js')]
193
+ }
194
+
195
+ except Exception:
196
+ return None
197
+
198
+ def _detect_backend(self) -> dict | None:
199
+ """偵測後端技術"""
200
+ # Python 後端偵測
201
+ python_result = self._detect_python_backend()
202
+ if python_result:
203
+ return python_result
204
+
205
+ # Node.js 後端偵測
206
+ nodejs_result = self._detect_nodejs_backend()
207
+ if nodejs_result:
208
+ return nodejs_result
209
+
210
+ return None
211
+
212
+ def _detect_python_backend(self) -> dict | None:
213
+ """偵測 Python 後端"""
214
+ requirements = self.project_path / 'requirements.txt'
215
+ pyproject = self.project_path / 'pyproject.toml'
216
+ setup_py = self.project_path / 'setup.py'
217
+
218
+ if not any(f.exists() for f in [requirements, pyproject, setup_py]):
219
+ return None
220
+
221
+ framework = None
222
+ deps_content = ''
223
+
224
+ if requirements.exists():
225
+ deps_content = requirements.read_text(encoding='utf-8').lower()
226
+ elif pyproject.exists():
227
+ deps_content = pyproject.read_text(encoding='utf-8').lower()
228
+
229
+ # 偵測框架
230
+ if 'fastapi' in deps_content:
231
+ framework = 'fastapi'
232
+ elif 'flask' in deps_content:
233
+ framework = 'flask'
234
+ elif 'django' in deps_content:
235
+ framework = 'django'
236
+ elif 'streamlit' in deps_content:
237
+ framework = 'streamlit'
238
+
239
+ return {
240
+ 'type': 'python',
241
+ 'framework': framework
242
+ }
243
+
244
+ def _detect_nodejs_backend(self) -> dict | None:
245
+ """偵測 Node.js 後端"""
246
+ package_json = self.project_path / 'package.json'
247
+
248
+ if not package_json.exists():
249
+ return None
250
+
251
+ try:
252
+ pkg = json.loads(package_json.read_text(encoding='utf-8'))
253
+ deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
254
+
255
+ # Vercel Serverless 偵測(有 api/ 目錄且有函數檔案)
256
+ api_dir = self.project_path / 'api'
257
+ if api_dir.exists():
258
+ api_files = list(api_dir.rglob('*.js')) + list(api_dir.rglob('*.ts'))
259
+ if api_files:
260
+ return {
261
+ 'type': 'nodejs',
262
+ 'framework': 'vercel-serverless'
263
+ }
264
+
265
+ # Express 偵測
266
+ if 'express' in deps:
267
+ return {
268
+ 'type': 'nodejs',
269
+ 'framework': 'express'
270
+ }
271
+
272
+ # Fastify 偵測
273
+ if 'fastify' in deps:
274
+ return {
275
+ 'type': 'nodejs',
276
+ 'framework': 'fastify'
277
+ }
278
+
279
+ # NestJS 偵測
280
+ if '@nestjs/core' in deps:
281
+ return {
282
+ 'type': 'nodejs',
283
+ 'framework': 'nestjs'
284
+ }
285
+
286
+ except Exception:
287
+ pass
288
+
289
+ return None
290
+
291
+ def _detect_deployment(self) -> dict | None:
292
+ """偵測部署模式"""
293
+ vercel_json = self.project_path / 'vercel.json'
294
+
295
+ if not vercel_json.exists():
296
+ return None
297
+
298
+ try:
299
+ config = json.loads(vercel_json.read_text(encoding='utf-8'))
300
+ rewrites = config.get('rewrites', [])
301
+
302
+ # 檢查是否為純 Proxy 模式
303
+ is_proxy_only = False
304
+ proxy_target = None
305
+
306
+ for rewrite in rewrites:
307
+ dest = rewrite.get('destination', '')
308
+ source = rewrite.get('source', '')
309
+
310
+ # 如果 destination 是外部 URL,則是 Proxy
311
+ if dest.startswith('http://') or dest.startswith('https://'):
312
+ is_proxy_only = True
313
+ proxy_target = dest.split('/')[2] # 取得域名
314
+
315
+ # 檢查是否有 api/ 目錄(Serverless)
316
+ api_dir = self.project_path / 'api'
317
+ has_api_functions = api_dir.exists() and any(api_dir.rglob('*.ts')) or any(api_dir.rglob('*.js'))
318
+
319
+ if is_proxy_only and not has_api_functions:
320
+ return {
321
+ 'type': 'vercel-proxy',
322
+ 'proxy_target': proxy_target,
323
+ 'description': '純前端 + API 代理'
324
+ }
325
+ elif has_api_functions:
326
+ return {
327
+ 'type': 'vercel-serverless',
328
+ 'has_proxy': is_proxy_only,
329
+ 'description': 'Serverless Functions' + (' + Proxy' if is_proxy_only else '')
330
+ }
331
+ else:
332
+ return {
333
+ 'type': 'vercel-static',
334
+ 'description': '純靜態網站'
335
+ }
336
+
337
+ except Exception:
338
+ pass
339
+
340
+ return None
341
+
342
+ def get_applicable_validators(self) -> Set[str]:
343
+ """取得適用的驗證器類型"""
344
+ info = self.detect()
345
+ validators = {'common'} # 通用驗證器永遠適用
346
+
347
+ frontend_type = info['frontend']
348
+ if frontend_type == 'gas':
349
+ validators.add('frontend.gas')
350
+ elif frontend_type == 'angular':
351
+ validators.add('frontend.angular')
352
+ elif frontend_type in ['vite', 'vanilla', 'vue-vite', 'vue']:
353
+ validators.add('frontend.vite')
354
+ elif frontend_type == 'react':
355
+ validators.add('frontend.react')
356
+
357
+ backend_type = info['backend']
358
+ if backend_type == 'python':
359
+ validators.add('backend.python')
360
+ elif backend_type == 'nodejs':
361
+ validators.add('backend.nodejs')
362
+
363
+ return validators
364
+
365
+ def get_project_summary(self) -> str:
366
+ """取得專案摘要(供 CLI 顯示)"""
367
+ info = self.detect()
368
+
369
+ parts = []
370
+
371
+ # 前端
372
+ if info['frontend']:
373
+ frontend_str = info['frontend']
374
+ if info['ui_framework']:
375
+ frontend_str += f" + {info['ui_framework']}"
376
+ parts.append(f"Frontend: {frontend_str}")
377
+
378
+ # 後端
379
+ if info['backend']:
380
+ backend = info['details'].get('backend', {})
381
+ framework = backend.get('framework', '')
382
+ backend_str = f"{info['backend']}"
383
+ if framework:
384
+ backend_str += f" ({framework})"
385
+ parts.append(f"Backend: {backend_str}")
386
+
387
+ # 部署
388
+ if info['deployment']:
389
+ deploy = info['deployment']
390
+ parts.append(f"Deploy: {deploy.get('type', 'unknown')}")
391
+ if deploy.get('proxy_target'):
392
+ parts.append(f"Proxy: {deploy['proxy_target']}")
393
+
394
+ return ' | '.join(parts) if parts else 'Unknown project type'
@@ -0,0 +1,14 @@
1
+ """
2
+ 前端驗證器
3
+
4
+ 支援框架:
5
+ - Vite + Tailwind + DaisyUI
6
+ - Angular + PrimeNG
7
+ - GAS (Google Apps Script) + Vue 3 + Shoelace/DaisyUI
8
+ """
9
+
10
+ from .vite import ViteValidator
11
+ from .angular import AngularValidator
12
+ from .gas import GasValidator
13
+
14
+ __all__ = ['ViteValidator', 'AngularValidator', 'GasValidator']
@@ -0,0 +1,245 @@
1
+ """
2
+ Angular 專案驗證器
3
+
4
+ 檢查項目:
5
+ 1. PrimeNG 設定
6
+ 2. 組件結構
7
+ 3. Service 注入
8
+ 4. 模組匯入
9
+ 5. 表單雙向綁定
10
+ """
11
+
12
+ import re
13
+ import json
14
+ from pathlib import Path
15
+
16
+
17
+ class AngularValidator:
18
+ """Angular 專案驗證器"""
19
+
20
+ name = 'angular'
21
+
22
+ # 忽略目錄
23
+ IGNORE_DIRS = [
24
+ 'node_modules', '.git', 'dist', '.angular', '__pycache__'
25
+ ]
26
+
27
+ def __init__(self, project_path):
28
+ self.project_path = Path(project_path)
29
+ self.project_name = self.project_path.name
30
+ self.src_path = self.project_path / 'src'
31
+ self.result = {
32
+ 'name': self.name,
33
+ 'passed': True,
34
+ 'errors': [],
35
+ 'warnings': [],
36
+ 'checks': {}
37
+ }
38
+
39
+ def run(self):
40
+ """執行所有驗證"""
41
+ if not self.project_path.exists():
42
+ self.result['passed'] = False
43
+ self.result['errors'].append(f'專案路徑不存在: {self.project_path}')
44
+ return self.result
45
+
46
+ self.check_primeng_config()
47
+ self.check_component_structure()
48
+ self.check_imports()
49
+ self.check_form_bindings()
50
+ self.check_service_injection()
51
+
52
+ return self.result
53
+
54
+ def check_primeng_config(self):
55
+ """檢查 PrimeNG 設定"""
56
+ app_config = self.src_path / 'app' / 'app.config.ts'
57
+
58
+ if not app_config.exists():
59
+ self.result['checks']['primeng_config'] = {'skipped': '無 app.config.ts'}
60
+ return
61
+
62
+ try:
63
+ content = app_config.read_text(encoding='utf-8')
64
+ has_provider = 'providePrimeNG' in content
65
+ has_theme = '@primeng/themes' in content
66
+
67
+ self.result['checks']['primeng_config'] = {
68
+ 'has_provider': has_provider,
69
+ 'has_theme': has_theme
70
+ }
71
+
72
+ if not has_provider:
73
+ self.result['warnings'].append('缺少 providePrimeNG 設定')
74
+ if not has_theme:
75
+ self.result['warnings'].append('缺少 PrimeNG 主題設定')
76
+ except Exception:
77
+ pass
78
+
79
+ def check_component_structure(self):
80
+ """檢查組件結構"""
81
+ issues = []
82
+
83
+ for file_path in self.src_path.rglob('*.component.ts'):
84
+ if self._should_skip(file_path):
85
+ continue
86
+ try:
87
+ content = file_path.read_text(encoding='utf-8')
88
+ rel_path = str(file_path.relative_to(self.project_path))
89
+ line_count = len(content.splitlines())
90
+
91
+ # 檢查組件大小
92
+ if line_count > 500:
93
+ issues.append({
94
+ 'file': rel_path,
95
+ 'issue': f'組件過大 ({line_count} 行)',
96
+ 'severity': 'warning'
97
+ })
98
+
99
+ # 檢查是否有 standalone
100
+ if 'standalone: true' not in content and '@Component' in content:
101
+ issues.append({
102
+ 'file': rel_path,
103
+ 'issue': '建議使用 standalone component',
104
+ 'severity': 'info'
105
+ })
106
+
107
+ except Exception:
108
+ pass
109
+
110
+ self.result['checks']['component_structure'] = {
111
+ 'count': len(issues),
112
+ 'issues': issues
113
+ }
114
+
115
+ for issue in issues:
116
+ if issue['severity'] == 'warning':
117
+ self.result['warnings'].append(f"{issue['file']}: {issue['issue']}")
118
+
119
+ def check_imports(self):
120
+ """檢查模組匯入"""
121
+ issues = []
122
+
123
+ # 檢查是否有遺漏的 PrimeNG 模組匯入
124
+ primeng_components = {
125
+ 'p-table': 'TableModule',
126
+ 'p-button': 'ButtonModule',
127
+ 'p-dialog': 'DialogModule',
128
+ 'p-drawer': 'DrawerModule',
129
+ 'p-inputtext': 'InputTextModule',
130
+ 'p-select': 'SelectModule',
131
+ 'p-datepicker': 'DatePickerModule',
132
+ 'p-inputnumber': 'InputNumberModule',
133
+ 'p-tag': 'TagModule',
134
+ 'p-tabs': 'TabsModule',
135
+ 'pInputText': 'InputTextModule',
136
+ }
137
+
138
+ for file_path in self.src_path.rglob('*.component.ts'):
139
+ if self._should_skip(file_path):
140
+ continue
141
+ try:
142
+ content = file_path.read_text(encoding='utf-8')
143
+ rel_path = str(file_path.relative_to(self.project_path))
144
+
145
+ for component, module in primeng_components.items():
146
+ if component in content and module not in content:
147
+ issues.append({
148
+ 'file': rel_path,
149
+ 'component': component,
150
+ 'missing_module': module
151
+ })
152
+ except Exception:
153
+ pass
154
+
155
+ self.result['checks']['imports'] = {
156
+ 'count': len(issues),
157
+ 'issues': issues
158
+ }
159
+
160
+ if issues:
161
+ for issue in issues[:5]:
162
+ self.result['errors'].append(
163
+ f"{issue['file']}: 使用 {issue['component']} 但未匯入 {issue['missing_module']}"
164
+ )
165
+ self.result['passed'] = False
166
+
167
+ def check_form_bindings(self):
168
+ """檢查表單綁定"""
169
+ issues = []
170
+
171
+ for file_path in self.src_path.rglob('*.html'):
172
+ if self._should_skip(file_path):
173
+ continue
174
+ try:
175
+ content = file_path.read_text(encoding='utf-8')
176
+ rel_path = str(file_path.relative_to(self.project_path))
177
+
178
+ # 檢查是否有未綁定的 input
179
+ # 找出 <input> 但沒有 [(ngModel)] 或 [formControl]
180
+ inputs = re.findall(r'<input[^>]*>', content)
181
+ for inp in inputs:
182
+ if 'ngModel' not in inp and 'formControl' not in inp and 'type="submit"' not in inp:
183
+ # 可能是有意為之,只記錄警告
184
+ pass
185
+
186
+ # 檢查 PrimeNG 組件綁定
187
+ primeng_inputs = re.findall(r'<p-(?:inputtext|inputnumber|select|datepicker)[^>]*>', content)
188
+ for pinput in primeng_inputs:
189
+ if 'ngModel' not in pinput and 'formControl' not in pinput:
190
+ issues.append({
191
+ 'file': rel_path,
192
+ 'issue': 'PrimeNG 輸入元件缺少資料綁定'
193
+ })
194
+
195
+ except Exception:
196
+ pass
197
+
198
+ self.result['checks']['form_bindings'] = {
199
+ 'count': len(issues),
200
+ 'issues': issues
201
+ }
202
+
203
+ if issues:
204
+ for issue in issues[:3]:
205
+ self.result['warnings'].append(f"{issue['file']}: {issue['issue']}")
206
+
207
+ def check_service_injection(self):
208
+ """檢查 Service 注入"""
209
+ issues = []
210
+
211
+ for file_path in self.src_path.rglob('*.service.ts'):
212
+ if self._should_skip(file_path):
213
+ continue
214
+ try:
215
+ content = file_path.read_text(encoding='utf-8')
216
+ rel_path = str(file_path.relative_to(self.project_path))
217
+
218
+ # 檢查是否有 @Injectable
219
+ if 'class ' in content and '@Injectable' not in content:
220
+ issues.append({
221
+ 'file': rel_path,
222
+ 'issue': '缺少 @Injectable 裝飾器'
223
+ })
224
+
225
+ # 檢查是否使用 providedIn: 'root'
226
+ if '@Injectable' in content and "providedIn: 'root'" not in content:
227
+ issues.append({
228
+ 'file': rel_path,
229
+ 'issue': "建議使用 providedIn: 'root'"
230
+ })
231
+
232
+ except Exception:
233
+ pass
234
+
235
+ self.result['checks']['service_injection'] = {
236
+ 'count': len(issues),
237
+ 'issues': issues
238
+ }
239
+
240
+ for issue in issues:
241
+ self.result['warnings'].append(f"{issue['file']}: {issue['issue']}")
242
+
243
+ def _should_skip(self, file_path):
244
+ """檢查是否應該跳過該檔案"""
245
+ return any(ignore in str(file_path) for ignore in self.IGNORE_DIRS)