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
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
程式碼品質驗證器
|
|
3
|
+
|
|
4
|
+
檢查項目:
|
|
5
|
+
1. 檔案行數限制 (500 行)
|
|
6
|
+
2. 命名規範
|
|
7
|
+
3. 中文註解
|
|
8
|
+
4. 禁止簡體字
|
|
9
|
+
5. 禁止 Emoji(應使用 icon font)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CodeQualityValidator:
|
|
17
|
+
"""程式碼品質驗證器"""
|
|
18
|
+
|
|
19
|
+
name = 'code_quality'
|
|
20
|
+
|
|
21
|
+
# 設定
|
|
22
|
+
MAX_FILE_LINES = 500
|
|
23
|
+
|
|
24
|
+
# 常見簡體字
|
|
25
|
+
SIMPLIFIED_CHINESE = [
|
|
26
|
+
'这', '个', '们', '为', '与', '来', '对', '时', '后', '进',
|
|
27
|
+
'发', '会', '过', '着', '动', '机', '关', '开', '门', '问',
|
|
28
|
+
'间', '还', '应', '该', '当', '电', '并', '长', '设', '现',
|
|
29
|
+
'实', '点', '将', '从', '头', '见', '两', '无', '产', '业',
|
|
30
|
+
'经', '变', '虽', '统', '义', '语', '说', '话', '认', '让',
|
|
31
|
+
'请', '马', '车', '书', '学', '习', '写', '医', '药', '师'
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# 忽略目錄
|
|
35
|
+
IGNORE_DIRS = [
|
|
36
|
+
'node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
|
|
37
|
+
'.angular', 'venv', '.venv', '.cache', 'coverage'
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# 日文專案(允許使用日文漢字)
|
|
41
|
+
JAPANESE_PROJECTS = ['jinkochino']
|
|
42
|
+
|
|
43
|
+
def __init__(self, project_path):
|
|
44
|
+
self.project_path = Path(project_path)
|
|
45
|
+
self.project_name = self.project_path.name
|
|
46
|
+
self.result = {
|
|
47
|
+
'name': self.name,
|
|
48
|
+
'passed': True,
|
|
49
|
+
'errors': [],
|
|
50
|
+
'warnings': [],
|
|
51
|
+
'checks': {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def run(self):
|
|
55
|
+
"""執行所有驗證"""
|
|
56
|
+
if not self.project_path.exists():
|
|
57
|
+
self.result['passed'] = False
|
|
58
|
+
self.result['errors'].append(f'專案路徑不存在: {self.project_path}')
|
|
59
|
+
return self.result
|
|
60
|
+
|
|
61
|
+
self.check_file_length()
|
|
62
|
+
self.check_simplified_chinese()
|
|
63
|
+
self.check_naming_conventions()
|
|
64
|
+
self.check_emoji_usage()
|
|
65
|
+
|
|
66
|
+
return self.result
|
|
67
|
+
|
|
68
|
+
def check_file_length(self):
|
|
69
|
+
"""檢查檔案行數"""
|
|
70
|
+
large_files = []
|
|
71
|
+
|
|
72
|
+
for file_path in self._get_source_files():
|
|
73
|
+
try:
|
|
74
|
+
content = file_path.read_text(encoding='utf-8')
|
|
75
|
+
line_count = len(content.splitlines())
|
|
76
|
+
|
|
77
|
+
if line_count > self.MAX_FILE_LINES:
|
|
78
|
+
large_files.append({
|
|
79
|
+
'file': str(file_path.relative_to(self.project_path)),
|
|
80
|
+
'lines': line_count
|
|
81
|
+
})
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
self.result['checks']['file_length'] = {
|
|
86
|
+
'count': len(large_files),
|
|
87
|
+
'files': large_files
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if large_files:
|
|
91
|
+
for f in large_files[:5]:
|
|
92
|
+
self.result['warnings'].append(
|
|
93
|
+
f"檔案過長: {f['file']} ({f['lines']} 行)"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def check_simplified_chinese(self):
|
|
97
|
+
"""檢查簡體字"""
|
|
98
|
+
# 跳過日文專案
|
|
99
|
+
if self.project_name in self.JAPANESE_PROJECTS:
|
|
100
|
+
self.result['checks']['simplified_chinese'] = {
|
|
101
|
+
'count': 0,
|
|
102
|
+
'files': [],
|
|
103
|
+
'skipped': '日文專案'
|
|
104
|
+
}
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
issues = []
|
|
108
|
+
|
|
109
|
+
for file_path in self._get_source_files():
|
|
110
|
+
try:
|
|
111
|
+
content = file_path.read_text(encoding='utf-8')
|
|
112
|
+
found_chars = []
|
|
113
|
+
|
|
114
|
+
for char in self.SIMPLIFIED_CHINESE:
|
|
115
|
+
if char in content:
|
|
116
|
+
found_chars.append(char)
|
|
117
|
+
|
|
118
|
+
if found_chars:
|
|
119
|
+
issues.append({
|
|
120
|
+
'file': str(file_path.relative_to(self.project_path)),
|
|
121
|
+
'chars': found_chars[:5]
|
|
122
|
+
})
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
self.result['checks']['simplified_chinese'] = {
|
|
127
|
+
'count': len(issues),
|
|
128
|
+
'files': issues
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if issues:
|
|
132
|
+
for issue in issues[:3]:
|
|
133
|
+
self.result['errors'].append(
|
|
134
|
+
f"發現簡體字在 {issue['file']}: {', '.join(issue['chars'])}"
|
|
135
|
+
)
|
|
136
|
+
self.result['passed'] = False
|
|
137
|
+
|
|
138
|
+
def check_naming_conventions(self):
|
|
139
|
+
"""檢查命名規範"""
|
|
140
|
+
issues = []
|
|
141
|
+
|
|
142
|
+
for file_path in self._get_source_files():
|
|
143
|
+
# 檢查檔案名稱
|
|
144
|
+
name = file_path.stem
|
|
145
|
+
|
|
146
|
+
# JS/TS 檔案應該是 kebab-case 或 PascalCase
|
|
147
|
+
if file_path.suffix in ['.js', '.ts', '.jsx', '.tsx']:
|
|
148
|
+
if not self._is_valid_js_filename(name):
|
|
149
|
+
issues.append({
|
|
150
|
+
'file': str(file_path.relative_to(self.project_path)),
|
|
151
|
+
'issue': '檔名應為 kebab-case 或 PascalCase'
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
self.result['checks']['naming'] = {
|
|
155
|
+
'count': len(issues),
|
|
156
|
+
'issues': issues
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if issues:
|
|
160
|
+
for issue in issues[:3]:
|
|
161
|
+
self.result['warnings'].append(
|
|
162
|
+
f"命名問題: {issue['file']} - {issue['issue']}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _get_source_files(self):
|
|
166
|
+
"""取得所有原始碼檔案"""
|
|
167
|
+
extensions = ['*.js', '*.ts', '*.jsx', '*.tsx', '*.py', '*.css', '*.scss']
|
|
168
|
+
files = []
|
|
169
|
+
|
|
170
|
+
for ext in extensions:
|
|
171
|
+
for f in self.project_path.rglob(ext):
|
|
172
|
+
if not any(ignore in str(f) for ignore in self.IGNORE_DIRS):
|
|
173
|
+
files.append(f)
|
|
174
|
+
|
|
175
|
+
return files
|
|
176
|
+
|
|
177
|
+
def _is_valid_js_filename(self, name):
|
|
178
|
+
"""檢查 JS 檔名是否有效"""
|
|
179
|
+
# kebab-case
|
|
180
|
+
if re.match(r'^[a-z][a-z0-9-]*$', name):
|
|
181
|
+
return True
|
|
182
|
+
# PascalCase
|
|
183
|
+
if re.match(r'^[A-Z][a-zA-Z0-9]*$', name):
|
|
184
|
+
return True
|
|
185
|
+
# camelCase
|
|
186
|
+
if re.match(r'^[a-z][a-zA-Z0-9]*$', name):
|
|
187
|
+
return True
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
def check_emoji_usage(self):
|
|
191
|
+
"""檢查程式碼中的 Emoji 使用(應改用 icon font)"""
|
|
192
|
+
# 精確的 emoji Unicode 範圍(避免誤判中文字)
|
|
193
|
+
emoji_pattern = re.compile(
|
|
194
|
+
"["
|
|
195
|
+
"\U0001F300-\U0001F5FF" # 雜項符號和象形文字
|
|
196
|
+
"\U0001F600-\U0001F64F" # 表情符號
|
|
197
|
+
"\U0001F680-\U0001F6FF" # 交通和地圖符號
|
|
198
|
+
"\U0001F900-\U0001F9FF" # 補充符號和象形文字
|
|
199
|
+
"\U0001FA00-\U0001FA6F" # 棋類符號
|
|
200
|
+
"\U0001FA70-\U0001FAFF" # 符號和象形文字擴展-A
|
|
201
|
+
"\U00002600-\U000026FF" # 雜項符號
|
|
202
|
+
"\U00002700-\U000027BF" # 裝飾符號
|
|
203
|
+
"]+",
|
|
204
|
+
flags=re.UNICODE
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
issues = []
|
|
208
|
+
|
|
209
|
+
for file_path in self._get_source_files():
|
|
210
|
+
# 只檢查 JS/TS 檔案(不檢查 CSS)
|
|
211
|
+
if file_path.suffix not in ['.js', '.ts', '.jsx', '.tsx']:
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
content = file_path.read_text(encoding='utf-8')
|
|
216
|
+
matches = emoji_pattern.findall(content)
|
|
217
|
+
|
|
218
|
+
if matches:
|
|
219
|
+
# 過濾掉註解中的 emoji
|
|
220
|
+
unique_emojis = list(set(matches))[:5]
|
|
221
|
+
issues.append({
|
|
222
|
+
'file': str(file_path.relative_to(self.project_path)),
|
|
223
|
+
'emojis': unique_emojis,
|
|
224
|
+
'count': len(matches)
|
|
225
|
+
})
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
self.result['checks']['emoji_usage'] = {
|
|
230
|
+
'count': sum(i['count'] for i in issues) if issues else 0,
|
|
231
|
+
'files': issues
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if issues:
|
|
235
|
+
total = sum(i['count'] for i in issues)
|
|
236
|
+
self.result['errors'].append(
|
|
237
|
+
f"[EMOJI] 程式碼中禁止使用 Emoji: {total} 個(請改用 icon font)"
|
|
238
|
+
)
|
|
239
|
+
for issue in issues[:5]:
|
|
240
|
+
self.result['errors'].append(
|
|
241
|
+
f" {issue['file']}: {''.join(issue['emojis'])}"
|
|
242
|
+
)
|
|
243
|
+
self.result['passed'] = False
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""
|
|
2
|
+
程式碼品質驗證器(通用)
|
|
3
|
+
|
|
4
|
+
檢查項目:
|
|
5
|
+
1. 檔案行數限制 (500 行)
|
|
6
|
+
2. 命名規範
|
|
7
|
+
3. 中文註解
|
|
8
|
+
4. 禁止簡體字
|
|
9
|
+
5. 禁止 Emoji(應使用 icon font)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from fnmatch import fnmatch
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class QualityValidator:
|
|
18
|
+
"""程式碼品質驗證器"""
|
|
19
|
+
|
|
20
|
+
name = 'quality'
|
|
21
|
+
|
|
22
|
+
# 設定
|
|
23
|
+
MAX_FILE_LINES = 500
|
|
24
|
+
|
|
25
|
+
# 常見簡體字
|
|
26
|
+
SIMPLIFIED_CHINESE = [
|
|
27
|
+
'这', '个', '们', '为', '与', '来', '对', '时', '后', '进',
|
|
28
|
+
'发', '会', '过', '着', '动', '机', '关', '开', '门', '问',
|
|
29
|
+
'间', '还', '应', '该', '当', '电', '并', '长', '设', '现',
|
|
30
|
+
'实', '点', '将', '从', '头', '见', '两', '无', '产', '业',
|
|
31
|
+
'经', '变', '虽', '统', '义', '语', '说', '话', '认', '让',
|
|
32
|
+
'请', '马', '车', '书', '学', '习', '写', '医', '药', '师'
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# 忽略目錄
|
|
36
|
+
IGNORE_DIRS = [
|
|
37
|
+
'node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
|
|
38
|
+
'.angular', 'venv', '.venv', '.cache', 'coverage',
|
|
39
|
+
'playwright-report'
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# 日文專案(允許使用日文漢字)
|
|
43
|
+
JAPANESE_PROJECTS = ['jinkochino']
|
|
44
|
+
|
|
45
|
+
def __init__(self, project_path):
|
|
46
|
+
self.project_path = Path(project_path)
|
|
47
|
+
self.project_name = self.project_path.name
|
|
48
|
+
self.scanignore = self._parse_scanignore()
|
|
49
|
+
self.result = {
|
|
50
|
+
'name': self.name,
|
|
51
|
+
'passed': True,
|
|
52
|
+
'errors': [],
|
|
53
|
+
'warnings': [],
|
|
54
|
+
'checks': {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def run(self):
|
|
58
|
+
"""執行所有驗證"""
|
|
59
|
+
if not self.project_path.exists():
|
|
60
|
+
self.result['passed'] = False
|
|
61
|
+
self.result['errors'].append(f'專案路徑不存在: {self.project_path}')
|
|
62
|
+
return self.result
|
|
63
|
+
|
|
64
|
+
self.check_file_length()
|
|
65
|
+
self.check_simplified_chinese()
|
|
66
|
+
self.check_naming_conventions()
|
|
67
|
+
self.check_emoji_usage()
|
|
68
|
+
|
|
69
|
+
return self.result
|
|
70
|
+
|
|
71
|
+
def check_file_length(self):
|
|
72
|
+
"""檢查檔案行數"""
|
|
73
|
+
large_files = []
|
|
74
|
+
|
|
75
|
+
for file_path in self._get_source_files():
|
|
76
|
+
try:
|
|
77
|
+
content = file_path.read_text(encoding='utf-8')
|
|
78
|
+
line_count = len(content.splitlines())
|
|
79
|
+
|
|
80
|
+
if line_count > self.MAX_FILE_LINES:
|
|
81
|
+
large_files.append({
|
|
82
|
+
'file': str(file_path.relative_to(self.project_path)),
|
|
83
|
+
'lines': line_count
|
|
84
|
+
})
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
self.result['checks']['file_length'] = {
|
|
89
|
+
'count': len(large_files),
|
|
90
|
+
'files': large_files
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if large_files:
|
|
94
|
+
for f in large_files[:5]:
|
|
95
|
+
self.result['warnings'].append(
|
|
96
|
+
f"檔案過長: {f['file']} ({f['lines']} 行)"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def check_simplified_chinese(self):
|
|
100
|
+
"""檢查簡體字"""
|
|
101
|
+
# 跳過日文專案
|
|
102
|
+
if self.project_name in self.JAPANESE_PROJECTS:
|
|
103
|
+
self.result['checks']['simplified_chinese'] = {
|
|
104
|
+
'count': 0,
|
|
105
|
+
'files': [],
|
|
106
|
+
'skipped': '日文專案'
|
|
107
|
+
}
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
issues = []
|
|
111
|
+
|
|
112
|
+
for file_path in self._get_source_files():
|
|
113
|
+
try:
|
|
114
|
+
rel_path = str(file_path.relative_to(self.project_path))
|
|
115
|
+
|
|
116
|
+
# 檢查 [pattern:簡體字] 排除
|
|
117
|
+
if self._is_pattern_ignored(rel_path, '簡體字'):
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
content = file_path.read_text(encoding='utf-8')
|
|
121
|
+
found_chars = []
|
|
122
|
+
|
|
123
|
+
for char in self.SIMPLIFIED_CHINESE:
|
|
124
|
+
if char in content:
|
|
125
|
+
found_chars.append(char)
|
|
126
|
+
|
|
127
|
+
if found_chars:
|
|
128
|
+
issues.append({
|
|
129
|
+
'file': rel_path,
|
|
130
|
+
'chars': found_chars[:5]
|
|
131
|
+
})
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
self.result['checks']['simplified_chinese'] = {
|
|
136
|
+
'count': len(issues),
|
|
137
|
+
'files': issues
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if issues:
|
|
141
|
+
for issue in issues[:3]:
|
|
142
|
+
self.result['errors'].append(
|
|
143
|
+
f"發現簡體字在 {issue['file']}: {', '.join(issue['chars'])}"
|
|
144
|
+
)
|
|
145
|
+
self.result['passed'] = False
|
|
146
|
+
|
|
147
|
+
def check_naming_conventions(self):
|
|
148
|
+
"""檢查命名規範"""
|
|
149
|
+
issues = []
|
|
150
|
+
|
|
151
|
+
for file_path in self._get_source_files():
|
|
152
|
+
name = file_path.stem
|
|
153
|
+
|
|
154
|
+
# JS/TS 檔案應該是 kebab-case 或 PascalCase
|
|
155
|
+
if file_path.suffix in ['.js', '.ts', '.jsx', '.tsx']:
|
|
156
|
+
if not self._is_valid_js_filename(name):
|
|
157
|
+
issues.append({
|
|
158
|
+
'file': str(file_path.relative_to(self.project_path)),
|
|
159
|
+
'issue': '檔名應為 kebab-case 或 PascalCase'
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
self.result['checks']['naming'] = {
|
|
163
|
+
'count': len(issues),
|
|
164
|
+
'issues': issues
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if issues:
|
|
168
|
+
for issue in issues[:3]:
|
|
169
|
+
self.result['warnings'].append(
|
|
170
|
+
f"命名問題: {issue['file']} - {issue['issue']}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def check_emoji_usage(self):
|
|
174
|
+
"""檢查程式碼中的 Emoji 使用(應改用 icon font)"""
|
|
175
|
+
# 精確的 emoji Unicode 範圍(避免誤判中文字)
|
|
176
|
+
emoji_pattern = re.compile(
|
|
177
|
+
"["
|
|
178
|
+
"\U0001F300-\U0001F5FF" # 雜項符號和象形文字
|
|
179
|
+
"\U0001F600-\U0001F64F" # 表情符號
|
|
180
|
+
"\U0001F680-\U0001F6FF" # 交通和地圖符號
|
|
181
|
+
"\U0001F900-\U0001F9FF" # 補充符號和象形文字
|
|
182
|
+
"\U0001FA00-\U0001FA6F" # 棋類符號
|
|
183
|
+
"\U0001FA70-\U0001FAFF" # 符號和象形文字擴展-A
|
|
184
|
+
"\U00002600-\U000026FF" # 雜項符號
|
|
185
|
+
"\U00002700-\U000027BF" # 裝飾符號
|
|
186
|
+
"]+",
|
|
187
|
+
flags=re.UNICODE
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
issues = []
|
|
191
|
+
|
|
192
|
+
for file_path in self._get_source_files():
|
|
193
|
+
# 檢查 JS/TS/HTML 檔案(不檢查 CSS)
|
|
194
|
+
if file_path.suffix not in ['.js', '.ts', '.jsx', '.tsx', '.html']:
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
content = file_path.read_text(encoding='utf-8')
|
|
199
|
+
matches = emoji_pattern.findall(content)
|
|
200
|
+
|
|
201
|
+
if matches:
|
|
202
|
+
unique_emojis = list(set(matches))[:5]
|
|
203
|
+
issues.append({
|
|
204
|
+
'file': str(file_path.relative_to(self.project_path)),
|
|
205
|
+
'emojis': unique_emojis,
|
|
206
|
+
'count': len(matches)
|
|
207
|
+
})
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
self.result['checks']['emoji_usage'] = {
|
|
212
|
+
'count': sum(i['count'] for i in issues) if issues else 0,
|
|
213
|
+
'files': issues
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if issues:
|
|
217
|
+
total = sum(i['count'] for i in issues)
|
|
218
|
+
self.result['warnings'].append(
|
|
219
|
+
f"程式碼中使用 Emoji: {total} 個(建議改用 icon font)"
|
|
220
|
+
)
|
|
221
|
+
for issue in issues[:3]:
|
|
222
|
+
self.result['warnings'].append(
|
|
223
|
+
f" {issue['file']}: {''.join(issue['emojis'])}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def _parse_scanignore(self):
|
|
227
|
+
"""解析 .scanignore 檔案"""
|
|
228
|
+
scanignore_file = self.project_path / '.scanignore'
|
|
229
|
+
result = {'paths': [], 'pattern_paths': []}
|
|
230
|
+
|
|
231
|
+
if not scanignore_file.exists():
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
for line in scanignore_file.read_text(encoding='utf-8').splitlines():
|
|
236
|
+
line = line.strip()
|
|
237
|
+
if not line or line.startswith('#'):
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
pattern_match = re.match(r'^\[pattern:(.+?)\]\s+(.+)$', line)
|
|
241
|
+
if pattern_match:
|
|
242
|
+
pattern_name = pattern_match.group(1).strip()
|
|
243
|
+
path_glob = pattern_match.group(2).strip()
|
|
244
|
+
result['pattern_paths'].append((pattern_name, path_glob))
|
|
245
|
+
else:
|
|
246
|
+
result['paths'].append(line)
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
def _is_scanignored(self, file_path):
|
|
253
|
+
"""檢查檔案是否被 .scanignore 全排除"""
|
|
254
|
+
rel_path = str(file_path.relative_to(self.project_path))
|
|
255
|
+
for pattern in self.scanignore['paths']:
|
|
256
|
+
# 目錄排除(以 / 結尾)
|
|
257
|
+
if pattern.endswith('/'):
|
|
258
|
+
if rel_path.startswith(pattern) or f'/{pattern}' in f'/{rel_path}':
|
|
259
|
+
return True
|
|
260
|
+
# 精確檔名比對
|
|
261
|
+
elif '/' in pattern:
|
|
262
|
+
if rel_path == pattern:
|
|
263
|
+
return True
|
|
264
|
+
else:
|
|
265
|
+
# 只比對檔名部分
|
|
266
|
+
if file_path.name == pattern:
|
|
267
|
+
return True
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
def _is_pattern_ignored(self, rel_path, pattern_name):
|
|
271
|
+
"""檢查檔案是否被 .scanignore 的特定 pattern 排除"""
|
|
272
|
+
for p_name, p_glob in self.scanignore['pattern_paths']:
|
|
273
|
+
if p_name == pattern_name:
|
|
274
|
+
if fnmatch(rel_path, p_glob):
|
|
275
|
+
return True
|
|
276
|
+
if rel_path == p_glob:
|
|
277
|
+
return True
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
def _get_source_files(self):
|
|
281
|
+
"""取得所有原始碼檔案"""
|
|
282
|
+
extensions = ['*.js', '*.ts', '*.jsx', '*.tsx', '*.py', '*.css', '*.scss', '*.html']
|
|
283
|
+
files = []
|
|
284
|
+
|
|
285
|
+
for ext in extensions:
|
|
286
|
+
for f in self.project_path.rglob(ext):
|
|
287
|
+
if any(ignore in str(f) for ignore in self.IGNORE_DIRS):
|
|
288
|
+
continue
|
|
289
|
+
if self._is_scanignored(f):
|
|
290
|
+
continue
|
|
291
|
+
files.append(f)
|
|
292
|
+
|
|
293
|
+
return files
|
|
294
|
+
|
|
295
|
+
def _is_valid_js_filename(self, name):
|
|
296
|
+
"""檢查 JS 檔名是否有效"""
|
|
297
|
+
# 允許設定檔名(含點號)
|
|
298
|
+
config_patterns = [
|
|
299
|
+
'vite.config', 'tailwind.config', 'postcss.config',
|
|
300
|
+
'eslint.config', 'prettier.config', 'jest.config',
|
|
301
|
+
'webpack.config', 'rollup.config', 'tsconfig',
|
|
302
|
+
'karma.conf', 'playwright.config', 'vitest.config',
|
|
303
|
+
'babel.config', 'nuxt.config', 'next.config',
|
|
304
|
+
'cypress.config', 'angular.json'
|
|
305
|
+
]
|
|
306
|
+
if any(name.startswith(p) for p in config_patterns):
|
|
307
|
+
return True
|
|
308
|
+
|
|
309
|
+
# kebab-case 含 dot-separated 後綴
|
|
310
|
+
# 如 xxx.component, xxx.service.spec, xxx.e2e 等
|
|
311
|
+
if re.match(r'^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)*$', name):
|
|
312
|
+
return True
|
|
313
|
+
# PascalCase
|
|
314
|
+
if re.match(r'^[A-Z][a-zA-Z0-9]*$', name):
|
|
315
|
+
return True
|
|
316
|
+
# camelCase
|
|
317
|
+
if re.match(r'^[a-z][a-zA-Z0-9]*$', name):
|
|
318
|
+
return True
|
|
319
|
+
return False
|