kcode-pi 0.1.5 → 0.1.7
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.
- package/README.md +35 -2
- package/dist/cli/kcode.d.ts +1 -0
- package/dist/cli/kcode.js +27 -4
- package/package.json +1 -1
- package/src/cli/kcode.ts +29 -4
- package/src/official/kingdee-skills.ts +60 -13
- package/src/rules/checker.ts +143 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/SKILL.md +2 -2
- package/vendor/kingdee-skills/ok-cosmic/SKILL.md +52 -101
- package/vendor/kingdee-skills/ok-cosmic/agents/openai.yaml +4 -4
- package/vendor/kingdee-skills/ok-cosmic/manifest.json +21 -20
- package/vendor/kingdee-skills/ok-cosmic/ok-cosmic-intro.html +1 -1
- package/vendor/kingdee-skills/ok-cosmic/rules/a-layer-rules.json +1 -1
- package/vendor/kingdee-skills/ok-cosmic/rules/anti-patterns.md +2 -2
- package/vendor/kingdee-skills/ok-cosmic/rules/coding-preferences.md +4 -4
- package/vendor/kingdee-skills/ok-cosmic/rules/constraints.md +3 -3
- package/vendor/kingdee-skills/ok-cosmic/rules/decision-matrix.md +8 -8
- package/vendor/kingdee-skills/ok-cosmic/rules/intent-routing.md +1 -1
- package/vendor/kingdee-skills/ok-cosmic/rules/post-check.md +19 -18
- package/vendor/kingdee-skills/ok-ksql/SKILL.md +9 -9
- package/vendor/kingdee-skills/ok-ksql/manifest.json +2 -1
- package/vendor/kingdee-skills/ok-ksql/references/ksql-datafix.md +2 -2
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/pattern-matcher.py +0 -336
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/review-score-calculator.py +0 -121
- package/vendor/kingdee-skills/ok-cosmic/CHANGELOG.md +0 -295
- package/vendor/kingdee-skills/ok-cosmic/README.md +0 -460
- package/vendor/kingdee-skills/ok-cosmic/requirements.txt +0 -2
- package/vendor/kingdee-skills/ok-cosmic/scripts/config_loader.py +0 -204
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-api-knowledge.py +0 -910
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-basedata-query.py +0 -359
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-config-check.py +0 -181
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-extpoints-query.py +0 -389
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-form-metadata.py +0 -856
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-check.py +0 -262
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-lint.py +0 -293
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/__init__.py +0 -2
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/base.py +0 -393
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/resource_check.py +0 -176
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/scene_check.py +0 -375
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/style_check.py +0 -434
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/verify_check.py +0 -36
- package/vendor/kingdee-skills/ok-cosmic/scripts/route_client.py +0 -186
- package/vendor/kingdee-skills/ok-cosmic/scripts/script_utils.py +0 -40
- package/vendor/kingdee-skills/ok-cosmic/scripts/sqlite_cache.py +0 -142
- package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-commons.jar +0 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-features.jar +0 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/setup-mac.sh +0 -18
- package/vendor/kingdee-skills/ok-cosmic/setup/setup-windows.bat +0 -53
- package/vendor/kingdee-skills/ok-cosmic/setup/setup.jar +0 -0
- package/vendor/kingdee-skills/ok-ksql/scripts/ksql_lint.py +0 -363
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""编码偏好检查 (STYLE-*) — 来源: coding-preferences.md"""
|
|
3
|
-
|
|
4
|
-
import re
|
|
5
|
-
from typing import List
|
|
6
|
-
|
|
7
|
-
from .base import (
|
|
8
|
-
LintIssue,
|
|
9
|
-
Severity,
|
|
10
|
-
analyze_java_context,
|
|
11
|
-
analyze_plugin_context,
|
|
12
|
-
classify_lines,
|
|
13
|
-
code_for_structure,
|
|
14
|
-
detect_plugin_type,
|
|
15
|
-
looks_like_loop_header,
|
|
16
|
-
strip_line_comment,
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# 编码偏好规则列表
|
|
21
|
-
STYLE_RULES = [
|
|
22
|
-
{
|
|
23
|
-
"pattern": r"\bStringUtils\s*\.\s*(isBlank|isNotBlank|isEmpty|isNotEmpty|equals)\b",
|
|
24
|
-
"rule_id": "STYLE-001",
|
|
25
|
-
"severity": Severity.WARNING,
|
|
26
|
-
"message": "字符串判空应使用 CharSequenceUtils 而非 StringUtils",
|
|
27
|
-
"fix_hint": "使用 CharSequenceUtils.isBlank() / isNotBlank()",
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"pattern": r"(?:!=\s*null\s*&&\s*!\w+\.isEmpty\(\))|(?:==\s*null\s*\|\|\s*\w+\.isEmpty\(\))",
|
|
31
|
-
"rule_id": "STYLE-002",
|
|
32
|
-
"severity": Severity.WARNING,
|
|
33
|
-
"message": "手写 null/isEmpty 判空应使用工具类封装",
|
|
34
|
-
"fix_hint": "字符串用 CharSequenceUtils.isBlank() / isNotBlank();集合用 CollectionUtils.isEmpty() / isNotEmpty()",
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"pattern": r"\bOperationServiceHelper\s*\.\s*(save|submit|audit)\b",
|
|
38
|
-
"rule_id": "STYLE-003",
|
|
39
|
-
"severity": Severity.WARNING,
|
|
40
|
-
"message": "不应散落调用 OperationServiceHelper,缺少错误聚合",
|
|
41
|
-
"fix_hint": "使用 OpUtils.executeOperateOrThrow() 或 OperateChain",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"pattern": r"new\s+PushArgs\s*\(",
|
|
45
|
-
"rule_id": "STYLE-004",
|
|
46
|
-
"severity": Severity.WARNING,
|
|
47
|
-
"message": "不应手拼 PushArgs,有封装可用",
|
|
48
|
-
"fix_hint": "使用 BotpUtils 封装下推逻辑",
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
"pattern": r"new\s+DrawArgs\s*\(",
|
|
52
|
-
"rule_id": "STYLE-005",
|
|
53
|
-
"severity": Severity.WARNING,
|
|
54
|
-
"message": "不应手拼 DrawArgs,有封装可用",
|
|
55
|
-
"fix_hint": "使用 BotpUtils 封装选单逻辑",
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
"pattern": r"\.\s*get\(\s*\"[\w]+\.[\w.]+\"",
|
|
59
|
-
"rule_id": "STYLE-006",
|
|
60
|
-
"severity": Severity.WARNING,
|
|
61
|
-
"message": "不应直接深链式 .get(\"a.b.c\"),中间节点为空时会抛异常",
|
|
62
|
-
"fix_hint": "使用 DynamicObjectUtils.nullSafeGet(dynamicObject, \"field\")",
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
"pattern": r"\bAttachmentServiceHelper\s*\.",
|
|
66
|
-
"rule_id": "STYLE-007",
|
|
67
|
-
"severity": Severity.WARNING,
|
|
68
|
-
"message": "附件处理应优先使用 AttachmentUtils 封装",
|
|
69
|
-
"fix_hint": "使用 AttachmentUtils 和 uploader",
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"pattern": r"\bQueryServiceHelper\s*\.\s*queryOne\s*\([^)]*\)\s*(?:!=|==)\s*null",
|
|
73
|
-
"rule_id": "STYLE-008",
|
|
74
|
-
"severity": Severity.WARNING,
|
|
75
|
-
"message": "判断数据是否存在时,优先使用 QueryServiceHelper.exists(...)",
|
|
76
|
-
"fix_hint": "将 queryOne(...) != null / == null 改为 QueryServiceHelper.exists(...)",
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
"pattern": r"\bprintStackTrace\s*\(",
|
|
80
|
-
"rule_id": "STYLE-009",
|
|
81
|
-
"severity": Severity.ERROR,
|
|
82
|
-
"message": "不要直接调用 printStackTrace(),应使用统一日志框架",
|
|
83
|
-
"fix_hint": "改为 logger.error(\"错误分析描述\", e) 或插件内的 log.error(...)",
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
"pattern": r"\bthrow\s+new\s+(?:(?:java\.lang\.)?RuntimeException|(?:java\.lang\.)?IllegalArgumentException|(?:java\.lang\.)?IllegalStateException)\b",
|
|
87
|
-
"rule_id": "STYLE-018",
|
|
88
|
-
"severity": Severity.ERROR,
|
|
89
|
-
"message": "业务异常应统一使用 KDBizException,不要直接抛 RuntimeException/IllegalArgumentException/IllegalStateException",
|
|
90
|
-
"fix_hint": "改为 throw new KDBizException(new ErrorCode(...));若为包装异常,保留原始 cause",
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
"pattern": r"\bnew\s+Thread\s*\(",
|
|
94
|
-
"rule_id": "STYLE-019",
|
|
95
|
-
"severity": Severity.WARNING,
|
|
96
|
-
"message": "禁止直接 new Thread(),绕开了平台线程池的统一监控与资源回收",
|
|
97
|
-
"fix_hint": "使用 kd.bos.threads.ThreadPools.newXxx() 或 ThreadPools.executeOnceXxx()",
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
"pattern": r"\bExecutors\s*\.\s*(newFixedThreadPool|newCachedThreadPool|newSingleThreadExecutor|newScheduledThreadPool)\b",
|
|
101
|
-
"rule_id": "STYLE-020",
|
|
102
|
-
"severity": Severity.WARNING,
|
|
103
|
-
"message": "禁止直接使用 JDK Executors 创建线程池,无法遵守平台线程治理约束",
|
|
104
|
-
"fix_hint": "使用 kd.bos.threads.ThreadPools.newXxx() 或 ThreadPools.executeOnceXxx()",
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
"pattern": r"\bSerializationUtils\s*\.\s*toJsonString\s*\([^)]*\b(args|e|event|evt|dataEntity|dataEntities|view)\b",
|
|
108
|
-
"rule_id": "STYLE-021",
|
|
109
|
-
"severity": Severity.WARNING,
|
|
110
|
-
"message": "不要对页面对象/事件对象/数据对象整包 JSON 序列化打印,成本高且可能打印大对象",
|
|
111
|
-
"fix_hint": "只按需提取关键字段打印,如 log.info(\"billNo={}\", data.getString(\"billno\"))",
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
"pattern": r"\bBusinessDataServiceHelper\s*\.\s*load\s*\([^,]+,\s*(?:EntityUtils\.getMainEntityType|BusinessDataServiceHelper\.newDynamicObject|EntityMetadataCache\.getDataEntityType)",
|
|
115
|
-
"rule_id": "STYLE-025",
|
|
116
|
-
"severity": Severity.WARNING,
|
|
117
|
-
"message": "加载实体时应指定必要字段,避免全量加载",
|
|
118
|
-
"fix_hint": "使用 BusinessDataServiceHelper.load(entityName, selectFields, filters) 指定查询字段",
|
|
119
|
-
},
|
|
120
|
-
]
|
|
121
|
-
|
|
122
|
-
# 主键/id 判空检测(STYLE-026)
|
|
123
|
-
# 检测模式:xxxId/xxxPk 变量与 null/0/0L 比较,或 .get("id"/"xxx_id") 与 null 比较
|
|
124
|
-
PK_NULL_CHECK_PATTERNS = [
|
|
125
|
-
# xxxId == null / xxxId != null / xxxPk == null / xxxPk != null
|
|
126
|
-
re.compile(r'\b\w*(?:[Ii]d|[Pp]k)\s*(?:==|!=)\s*null\b'),
|
|
127
|
-
# xxxId == 0L / xxxId == 0 / xxxId <= 0L / xxxId <= 0 / xxxId < 1
|
|
128
|
-
re.compile(r'\b\w*(?:[Ii]d|[Pp]k)\s*(?:==|<=|<)\s*(?:0L?|1)\b'),
|
|
129
|
-
# .get("id") == null / .get("id") != null
|
|
130
|
-
re.compile(r'\.\s*get\s*\(\s*"\w*[Ii]d"\s*\)\s*(?:==|!=)\s*null'),
|
|
131
|
-
]
|
|
132
|
-
|
|
133
|
-
# BigDecimal 原生运算检测(STYLE-027)
|
|
134
|
-
# 检测加减乘除 + compareTo 比较
|
|
135
|
-
BIGDECIMAL_RAW_PATTERNS = [
|
|
136
|
-
# 链式 .add/.subtract/.multiply/.divide + BigDecimal/new 上下文
|
|
137
|
-
re.compile(r'\.\s*(?:add|subtract|multiply|divide)\s*\(\s*(?:new\s+BigDecimal|BigDecimal\.)'),
|
|
138
|
-
# .divide(..., RoundingMode) —— 有 RoundingMode 参数必定是 BigDecimal 除法
|
|
139
|
-
re.compile(r'\.\s*divide\s*\([^)]*RoundingMode'),
|
|
140
|
-
# .compareTo(BigDecimal → BigDecimalUtils.equals/largeThan
|
|
141
|
-
re.compile(r'\.\s*compareTo\s*\(\s*BigDecimal'),
|
|
142
|
-
]
|
|
143
|
-
|
|
144
|
-
# QFilter 字符串运算符检测(P0:编译通过但运行时崩溃)
|
|
145
|
-
QFILTER_STRING_OP_PATTERN = re.compile(
|
|
146
|
-
r'new\s+QFilter\s*\([^,]+,\s*"[^"]*"\s*,'
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
SQL_CONCAT_PATTERN = re.compile(
|
|
150
|
-
r'("[^"]*\b(select|insert|update|delete|from|where)\b[^"]*"\s*\+)'
|
|
151
|
-
r'|(\+\s*"[^"]*\b(from|where|and|or|order\s+by|group\s+by)\b[^"]*")',
|
|
152
|
-
re.IGNORECASE,
|
|
153
|
-
)
|
|
154
|
-
SQL_DIALECT_PATTERN = re.compile(
|
|
155
|
-
r'"[^"]*\b(select|insert|update|delete)\b[^"]*\b(limit|rownum|nvl|isnull|ifnull|top\s+\d+)\b[^"]*"',
|
|
156
|
-
re.IGNORECASE,
|
|
157
|
-
)
|
|
158
|
-
CHINESE_CONCAT_PATTERN = re.compile(
|
|
159
|
-
r'("[^"]*[\u4e00-\u9fff][^"]*"\s*\+)|(\+\s*"[^"]*[\u4e00-\u9fff][^"]*")'
|
|
160
|
-
)
|
|
161
|
-
LOOP_UPDATE_VIEW_PATTERN = re.compile(r"\bupdateView\s*\(")
|
|
162
|
-
LOOP_DB_PATTERN = re.compile(
|
|
163
|
-
r"\b(BusinessDataServiceHelper|QueryServiceHelper|SaveServiceHelper)\s*\.\s*"
|
|
164
|
-
r"(load|loadSingle|loadSingleFromCache|loadFromCache|query|queryOne|queryDataSet|exists?|save|update)\b"
|
|
165
|
-
)
|
|
166
|
-
LOOP_DB_HELPER_PATTERN = re.compile(r"\b(BusinessDataServiceHelper|QueryServiceHelper|SaveServiceHelper)\b")
|
|
167
|
-
LOOP_DB_METHOD_PATTERN = re.compile(
|
|
168
|
-
r"\.\s*(load|loadSingle|loadSingleFromCache|loadFromCache|query|queryOne|queryDataSet|exists?|save|update)\b"
|
|
169
|
-
)
|
|
170
|
-
LOOP_REDIS_PATTERN = re.compile(r"\b(RedisTemplate|StringRedisTemplate|Jedis|Redisson|redisTemplate|redisClient)\b")
|
|
171
|
-
LOOP_ORM_CREATE_PATTERN = re.compile(r"\bORM\s*\.\s*create\s*\(")
|
|
172
|
-
LOOP_DISPATCH_PATTERN = re.compile(r"\bDispatchServiceHelper\s*\.\s*invoke\w*\s*\(")
|
|
173
|
-
QUERY_RESULT_DECL_PATTERN = re.compile(
|
|
174
|
-
r"\b(?:DynamicObjectCollection|var)\s+([A-Za-z_]\w*)\s*=\s*QueryServiceHelper\s*\.\s*query\b"
|
|
175
|
-
)
|
|
176
|
-
QUERY_RESULT_HELPER_DECL_PATTERN = re.compile(
|
|
177
|
-
r"\b(?:DynamicObjectCollection|var)\s+([A-Za-z_]\w*)\s*=\s*QueryServiceHelper\b"
|
|
178
|
-
)
|
|
179
|
-
QUERY_RESULT_METHOD_PATTERN = re.compile(r"\.\s*query\b")
|
|
180
|
-
QUERY_RESULT_GET_ALIAS_PATTERN = re.compile(
|
|
181
|
-
r"\b(?:DynamicObject|var)\s+([A-Za-z_]\w*)\s*=\s*([A-Za-z_]\w*)\s*\.\s*get\s*\("
|
|
182
|
-
)
|
|
183
|
-
ENHANCED_FOR_PATTERN = re.compile(
|
|
184
|
-
r"\bfor\s*\(\s*(?:final\s+)?(?:DynamicObject|var)\s+([A-Za-z_]\w*)\s*:\s*([A-Za-z_]\w*)\s*\)"
|
|
185
|
-
)
|
|
186
|
-
SAVE_SERVICE_PATTERN = re.compile(r"\bSaveServiceHelper\s*\.\s*(save|update)\s*\(")
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def check(filepath: str, lines: List[str]) -> List[LintIssue]:
|
|
190
|
-
"""执行编码偏好检查,返回问题列表。"""
|
|
191
|
-
issues: List[LintIssue] = []
|
|
192
|
-
_, loop_context = analyze_java_context(lines)
|
|
193
|
-
plugin_context = analyze_plugin_context(lines)
|
|
194
|
-
skip_lines = classify_lines(lines)
|
|
195
|
-
query_result_vars: dict[str, int] = {}
|
|
196
|
-
query_result_entity_vars: dict[str, int] = {}
|
|
197
|
-
active_query_loop_vars: dict[str, int] = {}
|
|
198
|
-
single_line_query_loop_vars: dict[str, int] = {}
|
|
199
|
-
brace_depth = 0
|
|
200
|
-
|
|
201
|
-
for i, line in enumerate(lines):
|
|
202
|
-
lineno = i + 1
|
|
203
|
-
code_line = code_for_structure(line)
|
|
204
|
-
raw_code_line = strip_line_comment(line)
|
|
205
|
-
prev_code_line = code_for_structure(lines[i - 1]) if i > 0 else ""
|
|
206
|
-
current_depth = brace_depth
|
|
207
|
-
active_query_loop_vars = {
|
|
208
|
-
var_name: depth
|
|
209
|
-
for var_name, depth in active_query_loop_vars.items()
|
|
210
|
-
if current_depth >= depth
|
|
211
|
-
}
|
|
212
|
-
single_line_query_loop_vars = {
|
|
213
|
-
var_name: target_line
|
|
214
|
-
for var_name, target_line in single_line_query_loop_vars.items()
|
|
215
|
-
if lineno <= target_line
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
decl_match = QUERY_RESULT_DECL_PATTERN.search(code_line)
|
|
219
|
-
if decl_match:
|
|
220
|
-
query_result_vars.setdefault(decl_match.group(1), lineno)
|
|
221
|
-
elif QUERY_RESULT_METHOD_PATTERN.search(code_line):
|
|
222
|
-
helper_decl_match = QUERY_RESULT_HELPER_DECL_PATTERN.search(prev_code_line)
|
|
223
|
-
if helper_decl_match:
|
|
224
|
-
query_result_vars.setdefault(helper_decl_match.group(1), lineno - 1)
|
|
225
|
-
|
|
226
|
-
alias_match = QUERY_RESULT_GET_ALIAS_PATTERN.search(code_line)
|
|
227
|
-
if alias_match and alias_match.group(2) in query_result_vars:
|
|
228
|
-
query_result_entity_vars.setdefault(alias_match.group(1), query_result_vars[alias_match.group(2)])
|
|
229
|
-
|
|
230
|
-
for_match = ENHANCED_FOR_PATTERN.search(code_line)
|
|
231
|
-
if for_match and for_match.group(2) in query_result_vars:
|
|
232
|
-
loop_var = for_match.group(1)
|
|
233
|
-
if re.search(rf"\b{re.escape(loop_var)}\s*\.\s*set\s*\(", code_line):
|
|
234
|
-
query_result_entity_vars.setdefault(loop_var, query_result_vars[for_match.group(2)])
|
|
235
|
-
elif "{" in code_line:
|
|
236
|
-
active_query_loop_vars[loop_var] = current_depth + 1
|
|
237
|
-
else:
|
|
238
|
-
single_line_query_loop_vars[loop_var] = lineno + 1
|
|
239
|
-
|
|
240
|
-
if skip_lines[i]:
|
|
241
|
-
brace_depth += code_line.count("{") - code_line.count("}")
|
|
242
|
-
continue
|
|
243
|
-
|
|
244
|
-
loop_line = loop_context[i] or looks_like_loop_header(line)
|
|
245
|
-
for rule in STYLE_RULES:
|
|
246
|
-
exclude = rule.get("exclude_pattern")
|
|
247
|
-
match_source = raw_code_line if rule["rule_id"] == "STYLE-006" else code_line
|
|
248
|
-
if exclude and re.search(exclude, match_source):
|
|
249
|
-
continue
|
|
250
|
-
if re.search(rule["pattern"], match_source):
|
|
251
|
-
issues.append(LintIssue(
|
|
252
|
-
file=filepath, line=lineno,
|
|
253
|
-
severity=rule["severity"],
|
|
254
|
-
rule_id=rule["rule_id"],
|
|
255
|
-
message=rule["message"],
|
|
256
|
-
fix_hint=rule["fix_hint"],
|
|
257
|
-
source_line=line.strip(),
|
|
258
|
-
))
|
|
259
|
-
|
|
260
|
-
if plugin_context[i] == "op" and re.search(r"\ballFields\s*\(", code_line):
|
|
261
|
-
issues.append(LintIssue(
|
|
262
|
-
file=filepath, line=lineno,
|
|
263
|
-
severity=Severity.WARNING,
|
|
264
|
-
rule_id="STYLE-010",
|
|
265
|
-
message="操作插件里除非字段很多,否则不要直接使用 allFields()",
|
|
266
|
-
fix_hint="按实际业务显式准备字段,优先 e.getFieldKeys().add(...) 或 entryFields(...)",
|
|
267
|
-
source_line=line.strip(),
|
|
268
|
-
))
|
|
269
|
-
|
|
270
|
-
if SQL_CONCAT_PATTERN.search(line):
|
|
271
|
-
# 降低误报:移除所有字符串字面量后,检查 + 附近是否有变量/标识符
|
|
272
|
-
# 纯字面量拼接(如 "Please select" + " an option")不构成 SQL 注入风险
|
|
273
|
-
line_without_strings = re.sub(r'"[^"]*"', '', line)
|
|
274
|
-
has_var_concat = bool(re.search(r'\w\s*\+|\+\s*\w', line_without_strings))
|
|
275
|
-
if has_var_concat:
|
|
276
|
-
issues.append(LintIssue(
|
|
277
|
-
file=filepath, line=lineno,
|
|
278
|
-
severity=Severity.ERROR,
|
|
279
|
-
rule_id="STYLE-011",
|
|
280
|
-
message="SQL/KSQL 传参不要通过字符串拼接构造",
|
|
281
|
-
fix_hint="改为参数化查询或平台查询构造,避免手拼 SQL 条件",
|
|
282
|
-
source_line=line.strip(),
|
|
283
|
-
))
|
|
284
|
-
|
|
285
|
-
if SQL_DIALECT_PATTERN.search(line):
|
|
286
|
-
issues.append(LintIssue(
|
|
287
|
-
file=filepath, line=lineno,
|
|
288
|
-
severity=Severity.ERROR,
|
|
289
|
-
rule_id="STYLE-012",
|
|
290
|
-
message="检测到可能的数据库方言 SQL 关键字,跨库兼容性差",
|
|
291
|
-
fix_hint="改为 KSQL 或平台查询接口,避免 limit/rownum/nvl/isnull/top 等方言写法",
|
|
292
|
-
source_line=line.strip(),
|
|
293
|
-
))
|
|
294
|
-
|
|
295
|
-
# STYLE-024: QFilter 使用字符串运算符代替 QCP 枚举(编译通过但运行时崩溃)
|
|
296
|
-
if QFILTER_STRING_OP_PATTERN.search(raw_code_line):
|
|
297
|
-
issues.append(LintIssue(
|
|
298
|
-
file=filepath, line=lineno,
|
|
299
|
-
severity=Severity.ERROR,
|
|
300
|
-
rule_id="STYLE-024",
|
|
301
|
-
message="QFilter 第二个参数必须使用 QCP 枚举,不能用字符串(编译通过但运行时崩溃)",
|
|
302
|
-
fix_hint='\u5c06 new QFilter(field, "=", value) \u6539\u4e3a new QFilter(field, QCP.equals, value)\uff1b\u5e38\u7528\u679a\u4e3e: QCP.equals / not_equals / large_equals / less_equals / like / in',
|
|
303
|
-
source_line=line.strip(),
|
|
304
|
-
))
|
|
305
|
-
|
|
306
|
-
# STYLE-027: BigDecimal 原生运算,应优先使用 BigDecimalUtils
|
|
307
|
-
if 'BigDecimalUtils' not in code_line:
|
|
308
|
-
for bd_pat in BIGDECIMAL_RAW_PATTERNS:
|
|
309
|
-
if bd_pat.search(code_line):
|
|
310
|
-
issues.append(LintIssue(
|
|
311
|
-
file=filepath, line=lineno,
|
|
312
|
-
severity=Severity.WARNING,
|
|
313
|
-
rule_id="STYLE-027",
|
|
314
|
-
message="BigDecimal 原生运算应优先使用 BigDecimalUtils 工具方法",
|
|
315
|
-
fix_hint="使用 BigDecimalUtils.valueOf() / add() / subtract() / multiply() / divide() / largeThanZero() / nullToZero() 等",
|
|
316
|
-
source_line=line.strip(),
|
|
317
|
-
))
|
|
318
|
-
break
|
|
319
|
-
|
|
320
|
-
# STYLE-026: 主键/id 用 == null / != null / == 0L 等方式判空
|
|
321
|
-
if not any(kw in code_line for kw in ('isEmptyPk', 'isNotEmptyPk')):
|
|
322
|
-
# 前两个 pattern 检测变量名,用 code_line;第三个 pattern 检测 .get("id"),用 raw_code_line(保留字符串字面量)
|
|
323
|
-
pk_sources = [code_line, code_line, raw_code_line]
|
|
324
|
-
for pk_pat, src in zip(PK_NULL_CHECK_PATTERNS, pk_sources):
|
|
325
|
-
if pk_pat.search(src):
|
|
326
|
-
issues.append(LintIssue(
|
|
327
|
-
file=filepath, line=lineno,
|
|
328
|
-
severity=Severity.WARNING,
|
|
329
|
-
rule_id="STYLE-026",
|
|
330
|
-
message="主键/id 判空不应直接用 == null / != null / == 0L,苍穹主键默认值为 0L",
|
|
331
|
-
fix_hint="使用 EntityUtils.isEmptyPk(pk) 或 EntityUtils.isNotEmptyPk(pk),兼容 null 和 0L",
|
|
332
|
-
source_line=line.strip(),
|
|
333
|
-
))
|
|
334
|
-
break
|
|
335
|
-
|
|
336
|
-
if "ResManager.loadKDString" not in line and "String.format" not in line and CHINESE_CONCAT_PATTERN.search(line):
|
|
337
|
-
issues.append(LintIssue(
|
|
338
|
-
file=filepath, line=lineno,
|
|
339
|
-
severity=Severity.INFO,
|
|
340
|
-
rule_id="STYLE-013",
|
|
341
|
-
message="中文提示语存在拼接,后续多语言翻译时语序容易失真",
|
|
342
|
-
fix_hint="使用完整句模板 + String.format(ResManager.loadKDString(...), ...)",
|
|
343
|
-
source_line=line.strip(),
|
|
344
|
-
))
|
|
345
|
-
|
|
346
|
-
if loop_line and LOOP_UPDATE_VIEW_PATTERN.search(code_line):
|
|
347
|
-
issues.append(LintIssue(
|
|
348
|
-
file=filepath, line=lineno,
|
|
349
|
-
severity=Severity.ERROR,
|
|
350
|
-
rule_id="STYLE-014",
|
|
351
|
-
message="禁止在循环中调用 updateView(),界面刷新成本高",
|
|
352
|
-
fix_hint="循环结束后统一执行局部刷新",
|
|
353
|
-
source_line=line.strip(),
|
|
354
|
-
))
|
|
355
|
-
|
|
356
|
-
loop_db_hit = LOOP_DB_PATTERN.search(code_line) or (
|
|
357
|
-
LOOP_DB_METHOD_PATTERN.search(code_line) and LOOP_DB_HELPER_PATTERN.search(prev_code_line)
|
|
358
|
-
)
|
|
359
|
-
if loop_line and loop_db_hit:
|
|
360
|
-
issues.append(LintIssue(
|
|
361
|
-
file=filepath, line=lineno,
|
|
362
|
-
severity=Severity.ERROR,
|
|
363
|
-
rule_id="STYLE-015",
|
|
364
|
-
message="在循环中访问数据库(包括 BusinessDataServiceHelper.loadSingle(...) 等),存在明显的 N+1 查询风险",
|
|
365
|
-
fix_hint="先看 skills/ok-cosmic/assets/snippets/query/BatchQuerySample.java,按“分组 key -> 批量查询 -> 本地映射”改写,避免循环里逐条查库",
|
|
366
|
-
source_line=line.strip(),
|
|
367
|
-
))
|
|
368
|
-
|
|
369
|
-
if loop_line and LOOP_REDIS_PATTERN.search(code_line):
|
|
370
|
-
issues.append(LintIssue(
|
|
371
|
-
file=filepath, line=lineno,
|
|
372
|
-
severity=Severity.ERROR,
|
|
373
|
-
rule_id="STYLE-016",
|
|
374
|
-
message="禁止在循环中频繁访问 Redis,容易造成缓存压力",
|
|
375
|
-
fix_hint="减少访问次数,优先批量读取或增加本地缓存",
|
|
376
|
-
source_line=line.strip(),
|
|
377
|
-
))
|
|
378
|
-
|
|
379
|
-
if loop_line and LOOP_ORM_CREATE_PATTERN.search(code_line):
|
|
380
|
-
issues.append(LintIssue(
|
|
381
|
-
file=filepath, line=lineno,
|
|
382
|
-
severity=Severity.ERROR,
|
|
383
|
-
rule_id="STYLE-022",
|
|
384
|
-
message="禁止在循环中频繁调用 ORM.create(),容易造成性能问题",
|
|
385
|
-
fix_hint="先批量组织数据,按批次处理",
|
|
386
|
-
source_line=line.strip(),
|
|
387
|
-
))
|
|
388
|
-
|
|
389
|
-
if loop_line and LOOP_DISPATCH_PATTERN.search(code_line):
|
|
390
|
-
issues.append(LintIssue(
|
|
391
|
-
file=filepath, line=lineno,
|
|
392
|
-
severity=Severity.ERROR,
|
|
393
|
-
rule_id="STYLE-023",
|
|
394
|
-
message="禁止在循环中调用 DispatchServiceHelper.invoke*(),远程调用放进循环容易放大时延和失败面",
|
|
395
|
-
fix_hint="合并调用、批量调用或先聚合参数",
|
|
396
|
-
source_line=line.strip(),
|
|
397
|
-
))
|
|
398
|
-
|
|
399
|
-
query_update_hit = False
|
|
400
|
-
query_update_var = ""
|
|
401
|
-
if SAVE_SERVICE_PATTERN.search(code_line):
|
|
402
|
-
for var_name in list(query_result_vars) + list(query_result_entity_vars) + list(active_query_loop_vars) + list(single_line_query_loop_vars):
|
|
403
|
-
if re.search(rf"\b{re.escape(var_name)}\b", code_line):
|
|
404
|
-
query_update_hit = True
|
|
405
|
-
query_update_var = var_name
|
|
406
|
-
break
|
|
407
|
-
|
|
408
|
-
if not query_update_hit:
|
|
409
|
-
for var_name in list(query_result_entity_vars) + list(active_query_loop_vars) + list(single_line_query_loop_vars):
|
|
410
|
-
if re.search(rf"\b{re.escape(var_name)}\s*\.\s*set\s*\(", code_line):
|
|
411
|
-
query_update_hit = True
|
|
412
|
-
query_update_var = var_name
|
|
413
|
-
break
|
|
414
|
-
|
|
415
|
-
if not query_update_hit:
|
|
416
|
-
for var_name in query_result_vars:
|
|
417
|
-
if re.search(rf"\b{re.escape(var_name)}\s*\.\s*get\s*\([^)]*\)\s*\.\s*set\s*\(", code_line):
|
|
418
|
-
query_update_hit = True
|
|
419
|
-
query_update_var = var_name
|
|
420
|
-
break
|
|
421
|
-
|
|
422
|
-
if query_update_hit:
|
|
423
|
-
issues.append(LintIssue(
|
|
424
|
-
file=filepath, line=lineno,
|
|
425
|
-
severity=Severity.WARNING,
|
|
426
|
-
rule_id="STYLE-017",
|
|
427
|
-
message="QueryServiceHelper.query(...) 返回的是扁平查询结果,不应直接当成可更新实体使用",
|
|
428
|
-
fix_hint="先看 skills/ok-cosmic/assets/snippets/query/BatchQuerySample.java 的“query -> id -> load -> update”桥接样例,先查 id,再 load 实体包后更新保存",
|
|
429
|
-
source_line=line.strip(),
|
|
430
|
-
))
|
|
431
|
-
|
|
432
|
-
brace_depth += code_line.count("{") - code_line.count("}")
|
|
433
|
-
|
|
434
|
-
return issues
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""验证来源注释检查 (VERIFY-*) — 来源: coding-preferences.md C 层治理项(仅严格模式生效)"""
|
|
3
|
-
|
|
4
|
-
import re
|
|
5
|
-
from typing import List
|
|
6
|
-
|
|
7
|
-
from .base import LintIssue, Severity
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
OVERRIDE_PATTERN = re.compile(r"@Override")
|
|
11
|
-
VERIFY_PATTERN = re.compile(r"//\s*验证来源:")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def check(filepath: str, lines: List[str]) -> List[LintIssue]:
|
|
15
|
-
"""执行 @Override 验证来源注释检查,返回 C 层治理建议。"""
|
|
16
|
-
issues: List[LintIssue] = []
|
|
17
|
-
|
|
18
|
-
for i, line in enumerate(lines):
|
|
19
|
-
if OVERRIDE_PATTERN.search(line):
|
|
20
|
-
# 检查上方 15 行内是否有验证来源注释(兼容被 JavaDoc 隔开的场景)
|
|
21
|
-
has_verify = False
|
|
22
|
-
for j in range(max(0, i - 15), i):
|
|
23
|
-
if VERIFY_PATTERN.search(lines[j]):
|
|
24
|
-
has_verify = True
|
|
25
|
-
break
|
|
26
|
-
if not has_verify:
|
|
27
|
-
issues.append(LintIssue(
|
|
28
|
-
file=filepath, line=i + 1,
|
|
29
|
-
severity=Severity.INFO,
|
|
30
|
-
rule_id="VERIFY-001",
|
|
31
|
-
message="@Override 方法缺少验证来源注释",
|
|
32
|
-
fix_hint="在 @Override 上方添加: // 验证来源: scripts/cosmic-api-knowledge.py detail [ClassName]",
|
|
33
|
-
source_line=line.strip(),
|
|
34
|
-
))
|
|
35
|
-
|
|
36
|
-
return issues
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# SPDX-License-Identifier: NOASSERTION
|
|
3
|
-
"""Shared runtime/route HTTP client helpers for ok-cosmic scripts."""
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import ssl
|
|
8
|
-
import sys
|
|
9
|
-
import urllib.error
|
|
10
|
-
import urllib.parse
|
|
11
|
-
import urllib.request
|
|
12
|
-
from typing import Any, Callable, Dict, Optional
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
DEFAULT_ROUTE_TIMEOUT_SECONDS = 10.0
|
|
16
|
-
ROUTE_PAYLOAD_KEYS = ("data", "result", "respData", "response")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def append_query_param(url: str, key: str, value: str) -> str:
|
|
20
|
-
"""Append a query parameter unless the key already exists."""
|
|
21
|
-
if not url or not key or not value:
|
|
22
|
-
return url
|
|
23
|
-
parsed = urllib.parse.urlsplit(url)
|
|
24
|
-
query_pairs = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
|
|
25
|
-
if any(k == key for k, _ in query_pairs):
|
|
26
|
-
return url
|
|
27
|
-
query_pairs.append((key, value))
|
|
28
|
-
return urllib.parse.urlunsplit(
|
|
29
|
-
(
|
|
30
|
-
parsed.scheme,
|
|
31
|
-
parsed.netloc,
|
|
32
|
-
parsed.path,
|
|
33
|
-
urllib.parse.urlencode(query_pairs),
|
|
34
|
-
parsed.fragment,
|
|
35
|
-
)
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def parse_bool(value: Any, default: bool = False) -> bool:
|
|
40
|
-
"""Parse bool-like JSON/env values."""
|
|
41
|
-
if value is None:
|
|
42
|
-
return default
|
|
43
|
-
if isinstance(value, bool):
|
|
44
|
-
return value
|
|
45
|
-
if isinstance(value, (int, float)):
|
|
46
|
-
return value != 0
|
|
47
|
-
if isinstance(value, str):
|
|
48
|
-
normalized = value.strip().lower()
|
|
49
|
-
if normalized in {"1", "true", "yes", "y", "on"}:
|
|
50
|
-
return True
|
|
51
|
-
if normalized in {"0", "false", "no", "n", "off"}:
|
|
52
|
-
return False
|
|
53
|
-
return bool(value)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _first_non_empty(*values: Any) -> str:
|
|
57
|
-
for value in values:
|
|
58
|
-
text = str(value or "").strip()
|
|
59
|
-
if text:
|
|
60
|
-
return text
|
|
61
|
-
return ""
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class RouteClient:
|
|
65
|
-
"""Small JSON POST client for Cosmic unified runtime/route APIs."""
|
|
66
|
-
|
|
67
|
-
def __init__(
|
|
68
|
-
self,
|
|
69
|
-
route_config: Optional[Dict[str, Any]] = None,
|
|
70
|
-
*,
|
|
71
|
-
api_url: Optional[str] = None,
|
|
72
|
-
debug: bool = False,
|
|
73
|
-
default_timeout: float = DEFAULT_ROUTE_TIMEOUT_SECONDS,
|
|
74
|
-
missing_message: Optional[str] = None,
|
|
75
|
-
):
|
|
76
|
-
if not isinstance(route_config, dict):
|
|
77
|
-
route_config = {}
|
|
78
|
-
|
|
79
|
-
self.debug = debug
|
|
80
|
-
self.api_url = _first_non_empty(
|
|
81
|
-
api_url,
|
|
82
|
-
route_config.get("apiUrl"),
|
|
83
|
-
os.getenv("COSMIC_ROUTE_API"),
|
|
84
|
-
os.getenv("COSMIC_RUNTIME_ROUTE_API"),
|
|
85
|
-
)
|
|
86
|
-
open_api_sign = _first_non_empty(
|
|
87
|
-
route_config.get("openApiSign"),
|
|
88
|
-
route_config.get("openapiSign"),
|
|
89
|
-
os.getenv("COSMIC_ROUTE_OPEN_API_SIGN"),
|
|
90
|
-
os.getenv("COSMIC_OPEN_API_SIGN"),
|
|
91
|
-
)
|
|
92
|
-
if open_api_sign:
|
|
93
|
-
self.api_url = append_query_param(self.api_url, "openApiSign", open_api_sign)
|
|
94
|
-
|
|
95
|
-
self.api_token = _first_non_empty(
|
|
96
|
-
route_config.get("apiToken"),
|
|
97
|
-
route_config.get("token"),
|
|
98
|
-
os.getenv("COSMIC_ROUTE_TOKEN"),
|
|
99
|
-
)
|
|
100
|
-
self.timeout = float(
|
|
101
|
-
route_config.get("timeoutSeconds")
|
|
102
|
-
or os.getenv("COSMIC_ROUTE_TIMEOUT")
|
|
103
|
-
or default_timeout
|
|
104
|
-
)
|
|
105
|
-
self.skip_ssl_verify = parse_bool(route_config.get("skipSslVerify"), default=True)
|
|
106
|
-
self.missing_message = missing_message or (
|
|
107
|
-
"未配置统一路由 API。请在 ok-cosmic.json 的 route.apiUrl 中配置统一路由,"
|
|
108
|
-
"或设置 COSMIC_ROUTE_API / COSMIC_RUNTIME_ROUTE_API 环境变量。"
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
def _log_debug(self, msg: str) -> None:
|
|
112
|
-
if self.debug:
|
|
113
|
-
print(f" (DEBUG) {msg}", file=sys.stderr)
|
|
114
|
-
|
|
115
|
-
def post(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
116
|
-
"""POST JSON payload and return the decoded JSON object."""
|
|
117
|
-
if not self.api_url:
|
|
118
|
-
raise RuntimeError(self.missing_message)
|
|
119
|
-
|
|
120
|
-
headers = {"Content-Type": "application/json"}
|
|
121
|
-
if self.api_token:
|
|
122
|
-
headers["Authorization"] = f"Bearer {self.api_token}"
|
|
123
|
-
|
|
124
|
-
req_body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
125
|
-
self._log_debug(f"POST {self.api_url}")
|
|
126
|
-
self._log_debug(f"payload={json.dumps(payload, ensure_ascii=False)}")
|
|
127
|
-
req = urllib.request.Request(
|
|
128
|
-
self.api_url,
|
|
129
|
-
data=req_body,
|
|
130
|
-
headers=headers,
|
|
131
|
-
method="POST",
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
try:
|
|
135
|
-
ssl_context = ssl.create_default_context()
|
|
136
|
-
if self.skip_ssl_verify:
|
|
137
|
-
ssl_context.check_hostname = False
|
|
138
|
-
ssl_context.verify_mode = ssl.CERT_NONE
|
|
139
|
-
with urllib.request.urlopen(req, timeout=self.timeout, context=ssl_context) as resp:
|
|
140
|
-
raw = json.loads(resp.read().decode("utf-8", errors="replace"))
|
|
141
|
-
if not isinstance(raw, dict):
|
|
142
|
-
raise RuntimeError("远程接口返回的根对象不是 JSON Object。")
|
|
143
|
-
return raw
|
|
144
|
-
except urllib.error.HTTPError as e:
|
|
145
|
-
body = ""
|
|
146
|
-
try:
|
|
147
|
-
body = e.read().decode("utf-8", errors="replace").strip()
|
|
148
|
-
except Exception:
|
|
149
|
-
pass
|
|
150
|
-
detail = f": {body}" if body else f": {e.reason}"
|
|
151
|
-
raise RuntimeError(f"HTTP {e.code}{detail}") from e
|
|
152
|
-
except urllib.error.URLError as e:
|
|
153
|
-
raise RuntimeError(f"Network error: {e.reason}") from e
|
|
154
|
-
except json.JSONDecodeError as e:
|
|
155
|
-
raise RuntimeError(f"响应 JSON 解析失败: {e}") from e
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def unwrap_route_payload(data: Any, payload_matcher: Callable[[Any], bool]) -> Any:
|
|
159
|
-
"""Unwrap common route wrapper keys and return the domain payload."""
|
|
160
|
-
if not isinstance(data, dict):
|
|
161
|
-
return data
|
|
162
|
-
if payload_matcher(data):
|
|
163
|
-
return data
|
|
164
|
-
for key in ROUTE_PAYLOAD_KEYS:
|
|
165
|
-
value = data.get(key)
|
|
166
|
-
if payload_matcher(value):
|
|
167
|
-
return value
|
|
168
|
-
return data
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def unwrap_route_raw(raw: Dict[str, Any], payload_matcher: Callable[[Any], bool]) -> Dict[str, Any]:
|
|
172
|
-
"""Unwrap one level of route envelope while preserving the original root shape."""
|
|
173
|
-
data = raw.get("data")
|
|
174
|
-
if not isinstance(data, dict):
|
|
175
|
-
return raw
|
|
176
|
-
if "status" in data and "data" in data:
|
|
177
|
-
return data
|
|
178
|
-
if payload_matcher(data):
|
|
179
|
-
return raw
|
|
180
|
-
for key in ROUTE_PAYLOAD_KEYS:
|
|
181
|
-
value = data.get(key)
|
|
182
|
-
if payload_matcher(value):
|
|
183
|
-
cloned = dict(raw)
|
|
184
|
-
cloned["data"] = value
|
|
185
|
-
return cloned
|
|
186
|
-
return raw
|