kcode-pi 0.1.0 → 0.1.2
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/dist/cli/kcode.js +26 -6
- package/package.json +2 -1
- package/src/cli/kcode.ts +219 -0
- package/src/cli/main.ts +10 -0
- package/src/harness/artifacts.ts +94 -0
- package/src/harness/format.ts +30 -0
- package/src/harness/gates.ts +136 -0
- package/src/harness/paths.ts +23 -0
- package/src/harness/state.ts +117 -0
- package/src/harness/types.ts +42 -0
- package/src/knowledge/format.ts +48 -0
- package/src/knowledge/loader.ts +147 -0
- package/src/knowledge/search.ts +118 -0
- package/src/knowledge/types.ts +64 -0
- package/src/official/kingdee-skills.ts +230 -0
- package/src/product/profile.ts +115 -0
- package/src/rules/checker.ts +612 -0
- package/src/tools/build-debug.ts +214 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
export const CHECK_TYPES = [
|
|
2
|
+
"magic_value",
|
|
3
|
+
"naming",
|
|
4
|
+
"loop_db",
|
|
5
|
+
"exception",
|
|
6
|
+
"import",
|
|
7
|
+
"lifecycle",
|
|
8
|
+
"transaction",
|
|
9
|
+
"resource",
|
|
10
|
+
"security",
|
|
11
|
+
"performance",
|
|
12
|
+
"threading",
|
|
13
|
+
] as const;
|
|
14
|
+
export type CheckType = (typeof CHECK_TYPES)[number];
|
|
15
|
+
|
|
16
|
+
export const CHECK_SEVERITIES = ["error", "warning", "info"] as const;
|
|
17
|
+
export type CheckSeverity = (typeof CHECK_SEVERITIES)[number];
|
|
18
|
+
|
|
19
|
+
export interface CheckResult {
|
|
20
|
+
line: number;
|
|
21
|
+
column: number;
|
|
22
|
+
type: CheckType;
|
|
23
|
+
severity: CheckSeverity;
|
|
24
|
+
message: string;
|
|
25
|
+
rule: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type CheckLanguage = "java" | "csharp";
|
|
29
|
+
|
|
30
|
+
const ALLOWED_NUMBERS = new Set(["0", "1", "-1", "2", "10", "100"]);
|
|
31
|
+
|
|
32
|
+
const DB_KEYWORDS = [
|
|
33
|
+
"BusinessDataServiceHelper",
|
|
34
|
+
"QueryServiceHelper",
|
|
35
|
+
"SaveServiceHelper",
|
|
36
|
+
"OperationServiceHelper",
|
|
37
|
+
"DispatchServiceHelper",
|
|
38
|
+
"ORM.create",
|
|
39
|
+
"DB.",
|
|
40
|
+
"loadSingle",
|
|
41
|
+
"loadInBatch",
|
|
42
|
+
"executeQuery",
|
|
43
|
+
"executeSql",
|
|
44
|
+
"queryDataSet",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export function checkCode(code: string, language: CheckLanguage = "java"): CheckResult[] {
|
|
48
|
+
const lines = code.split("\n");
|
|
49
|
+
const results = [
|
|
50
|
+
...checkMagicValues(lines),
|
|
51
|
+
...checkNaming(lines, language),
|
|
52
|
+
...checkLoopDb(lines),
|
|
53
|
+
...checkExceptionHandling(lines),
|
|
54
|
+
...checkCosmicReviewerRules(lines),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
return results.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function checkCosmicReviewerRules(lines: string[]): CheckResult[] {
|
|
61
|
+
return [
|
|
62
|
+
...checkLifecycleMisuse(lines),
|
|
63
|
+
...checkTransactionMisuse(lines),
|
|
64
|
+
...checkResourceHandling(lines),
|
|
65
|
+
...checkSecurityPatterns(lines),
|
|
66
|
+
...checkThreadingPatterns(lines),
|
|
67
|
+
...checkUiPerformance(lines),
|
|
68
|
+
...checkLoggingAndDiagnostics(lines),
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function checkLifecycleMisuse(lines: string[]): CheckResult[] {
|
|
73
|
+
const results: CheckResult[] = [];
|
|
74
|
+
const initializeRanges = findMethodRanges(lines, "initialize");
|
|
75
|
+
const beforeBindRanges = findMethodRanges(lines, "beforeBindData");
|
|
76
|
+
const afterBindRanges = findMethodRanges(lines, "afterBindData");
|
|
77
|
+
const afterOperationRanges = findMethodRanges(lines, "afterExecuteOperationTransaction");
|
|
78
|
+
|
|
79
|
+
for (const range of initializeRanges) {
|
|
80
|
+
forEachLineInRange(lines, range, (line, index) => {
|
|
81
|
+
if (/addItemClickListener\s*\(/.test(line)) {
|
|
82
|
+
results.push(makeResult(index, line, "addItemClickListener", "lifecycle", "error", "P0:initialize 中注册监听器,可能导致监听生命周期错误;请移到 registerListener", "p0-initialize-listener"));
|
|
83
|
+
}
|
|
84
|
+
if (/getView\s*\(\)\s*\.\s*(setVisible|setEnable|updateView|showConfirm|showTipNotification)\s*\(/.test(line)) {
|
|
85
|
+
results.push(makeResult(index, line, "getView", "lifecycle", "error", "P0:initialize 中操作 UI 控件,控件状态可能不生效;请移到 afterBindData 等合适阶段", "p0-initialize-ui"));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const range of [...beforeBindRanges, ...afterBindRanges]) {
|
|
91
|
+
forEachLineInRange(lines, range, (line, index) => {
|
|
92
|
+
if (/(getModel\s*\(\)\s*\.\s*)?setValue\s*\(/.test(line)) {
|
|
93
|
+
results.push(makeResult(index, line, "setValue", "lifecycle", "error", "P0:数据绑定阶段修改模型数据,可能破坏绑定流程;默认值应放到 afterCreateNewData 等阶段", "p0-binddata-setvalue"));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const range of afterOperationRanges) {
|
|
99
|
+
forEachLineInRange(lines, range, (line, index) => {
|
|
100
|
+
if (/getOperationResult\s*\(/.test(line)) {
|
|
101
|
+
results.push(makeResult(index, line, "getOperationResult", "lifecycle", "error", "P0:afterExecuteOperationTransaction 的参数不应调用 getOperationResult,请确认事件参数类型", "p0-after-operation-result"));
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function checkTransactionMisuse(lines: string[]): CheckResult[] {
|
|
110
|
+
const results: CheckResult[] = [];
|
|
111
|
+
const ranges = [...findMethodRanges(lines, "beforeExecuteOperationTransaction"), ...findMethodRanges(lines, "afterExecuteOperationTransaction")];
|
|
112
|
+
|
|
113
|
+
for (const range of ranges) {
|
|
114
|
+
forEachLineInRange(lines, range, (line, index) => {
|
|
115
|
+
if (/SaveServiceHelper\s*\.\s*(save|update)\s*\(/.test(line)) {
|
|
116
|
+
results.push(makeResult(index, line, "SaveServiceHelper", "transaction", "error", "P0:操作事务钩子中独立保存会破坏事务一致性;应直接修改平台传入的数据实体", "p0-transaction-save"));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function checkResourceHandling(lines: string[]): CheckResult[] {
|
|
125
|
+
const results: CheckResult[] = [];
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < lines.length; i++) {
|
|
128
|
+
const line = lines[i];
|
|
129
|
+
if (isCommentLine(line)) continue;
|
|
130
|
+
if (!/\bDataSet\b\s+\w+\s*=/.test(line)) continue;
|
|
131
|
+
if (/try\s*\([^)]*\bDataSet\b/.test(line)) continue;
|
|
132
|
+
if (nearbyLineHas(lines, i, -2, /\btry\s*\(/)) continue;
|
|
133
|
+
|
|
134
|
+
results.push(makeResult(i, line, "DataSet", "resource", "error", "P0:DataSet 创建后未明显使用 try-with-resources 关闭,可能导致连接泄漏", "p0-dataset-not-closed"));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return results;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function checkSecurityPatterns(lines: string[]): CheckResult[] {
|
|
141
|
+
const results: CheckResult[] = [];
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < lines.length; i++) {
|
|
144
|
+
const line = lines[i];
|
|
145
|
+
if (isCommentLine(line)) continue;
|
|
146
|
+
|
|
147
|
+
if (/\bStatement\b/.test(line) && !/\bPreparedStatement\b/.test(line)) {
|
|
148
|
+
results.push(makeResult(i, line, "Statement", "security", "error", "P0:使用 Statement 存在 SQL 注入风险;请改用参数化查询或 QFilter", "p0-raw-statement"));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (/"\s*(SELECT|UPDATE|DELETE|INSERT)\b[^"]*"\s*\+|\+\s*"\s*(WHERE|AND|OR|SET)\b/i.test(line)) {
|
|
152
|
+
results.push(makeResult(i, line, "+", "security", "error", "P0:SQL 字符串拼接存在注入风险;请改用参数化查询或 QFilter", "p0-sql-concat"));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (/<!DOCTYPE|<!ENTITY|\bSYSTEM\b/.test(line)) {
|
|
156
|
+
results.push(makeResult(i, line, "XML", "security", "error", "P0:XML 外部实体相关内容可能导致 XXE 风险,请禁用外部实体", "p0-xxe"));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (/(password|passwd|secret|token|ak|sk)\s*=\s*"[^"]{4,}"/i.test(line)) {
|
|
160
|
+
results.push(makeResult(i, line, "=", "security", "error", "P0:疑似敏感信息硬编码,请改为安全配置或密文存储", "p0-secret-hardcode"));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return results;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function checkThreadingPatterns(lines: string[]): CheckResult[] {
|
|
168
|
+
const results: CheckResult[] = [];
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < lines.length; i++) {
|
|
171
|
+
const line = lines[i];
|
|
172
|
+
if (isCommentLine(line)) continue;
|
|
173
|
+
if (/new\s+Thread\s*\(|Executors\s*\.\s*new\w+/.test(line)) {
|
|
174
|
+
results.push(makeResult(i, line, "Thread", "threading", "error", "P0:使用 JDK 原生线程会绕过平台线程管理;请使用苍穹 ThreadPools", "p0-native-thread"));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return results;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function checkUiPerformance(lines: string[]): CheckResult[] {
|
|
182
|
+
const results: CheckResult[] = [];
|
|
183
|
+
let beginInitCount = 0;
|
|
184
|
+
let endInitCount = 0;
|
|
185
|
+
|
|
186
|
+
for (let i = 0; i < lines.length; i++) {
|
|
187
|
+
const line = lines[i];
|
|
188
|
+
if (isCommentLine(line)) continue;
|
|
189
|
+
|
|
190
|
+
if (/beginInit\s*\(/.test(line)) beginInitCount++;
|
|
191
|
+
if (/endInit\s*\(/.test(line)) endInitCount++;
|
|
192
|
+
|
|
193
|
+
if (isInsideLoop(lines, i) && /updateView\s*\(/.test(line)) {
|
|
194
|
+
results.push(makeResult(i, line, "updateView", "performance", "warning", "P1:循环内刷新视图会造成界面卡顿;应在循环外统一刷新", "p1-loop-update-view"));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (isInsideLoop(lines, i) && /getFieldIndex\s*\(/.test(line)) {
|
|
198
|
+
results.push(makeResult(i, line, "getFieldIndex", "performance", "warning", "P1:循环内重复获取 FieldIndex 会造成性能损耗;应在循环外缓存", "p1-loop-field-index"));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (beginInitCount > endInitCount) {
|
|
203
|
+
const index = lines.findIndex((line) => /beginInit\s*\(/.test(line));
|
|
204
|
+
if (index >= 0) {
|
|
205
|
+
results.push(makeResult(index, lines[index], "beginInit", "performance", "warning", "P1:beginInit/endInit 数量不成对,异常时可能导致表单行为异常", "p1-unpaired-begin-init"));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return results;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function checkLoggingAndDiagnostics(lines: string[]): CheckResult[] {
|
|
213
|
+
const results: CheckResult[] = [];
|
|
214
|
+
|
|
215
|
+
for (let i = 0; i < lines.length; i++) {
|
|
216
|
+
const line = lines[i];
|
|
217
|
+
if (isCommentLine(line)) continue;
|
|
218
|
+
|
|
219
|
+
if (/System\s*\.\s*out\s*\.\s*println\s*\(/.test(line)) {
|
|
220
|
+
results.push(makeResult(i, line, "System.out.println", "exception", "warning", "P1:生产代码不应使用 System.out.println,请使用平台日志", "p1-system-out"));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (/\.printStackTrace\s*\(/.test(line)) {
|
|
224
|
+
results.push(makeResult(i, line, "printStackTrace", "exception", "warning", "P2:不要直接 printStackTrace,请使用 logger.error 并传入异常对象", "p2-print-stack-trace"));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return results;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function checkMagicValues(lines: string[]): CheckResult[] {
|
|
232
|
+
const results: CheckResult[] = [];
|
|
233
|
+
|
|
234
|
+
for (let i = 0; i < lines.length; i++) {
|
|
235
|
+
const line = lines[i];
|
|
236
|
+
const lineNum = i + 1;
|
|
237
|
+
if (isCommentLine(line)) continue;
|
|
238
|
+
|
|
239
|
+
const numberPattern = /new\s+BigDecimal\s*\(\s*(\d+)\s*\)|(?<!\w)(\d{2,})(?!\w)/g;
|
|
240
|
+
for (const match of line.matchAll(numberPattern)) {
|
|
241
|
+
const value = match[1] || match[2];
|
|
242
|
+
if (value && !ALLOWED_NUMBERS.has(value)) {
|
|
243
|
+
results.push({
|
|
244
|
+
line: lineNum,
|
|
245
|
+
column: match.index ?? 0,
|
|
246
|
+
type: "magic_value",
|
|
247
|
+
severity: "warning",
|
|
248
|
+
message: `检测到硬编码数字 ${value},建议使用常量或配置`,
|
|
249
|
+
rule: "magic-number",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const stringPattern = /(?<![\\])"([^"\\]*(?:\\.[^"\\]*)*)"/g;
|
|
255
|
+
for (const match of line.matchAll(stringPattern)) {
|
|
256
|
+
const value = match[1];
|
|
257
|
+
if (
|
|
258
|
+
value.length > 1 &&
|
|
259
|
+
!value.startsWith(" ") &&
|
|
260
|
+
!value.includes("{") &&
|
|
261
|
+
!value.includes("%") &&
|
|
262
|
+
!isCommonString(value) &&
|
|
263
|
+
!isLogMessage(line) &&
|
|
264
|
+
!isAnnotation(line) &&
|
|
265
|
+
isBusinessConstant(value)
|
|
266
|
+
) {
|
|
267
|
+
results.push({
|
|
268
|
+
line: lineNum,
|
|
269
|
+
column: match.index ?? 0,
|
|
270
|
+
type: "magic_value",
|
|
271
|
+
severity: "warning",
|
|
272
|
+
message: `检测到可能的业务常量 "${value}",建议定义为常量`,
|
|
273
|
+
rule: "magic-string",
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return results;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function checkNaming(lines: string[], language: CheckLanguage): CheckResult[] {
|
|
283
|
+
const results: CheckResult[] = [];
|
|
284
|
+
|
|
285
|
+
for (let i = 0; i < lines.length; i++) {
|
|
286
|
+
const line = lines[i];
|
|
287
|
+
const lineNum = i + 1;
|
|
288
|
+
if (isCommentLine(line)) continue;
|
|
289
|
+
|
|
290
|
+
if (language === "java") {
|
|
291
|
+
const classMatch = line.match(/\bclass\s+([A-Za-z_]\w*)/);
|
|
292
|
+
if (classMatch) {
|
|
293
|
+
const className = classMatch[1];
|
|
294
|
+
if (!isPascalCase(className) && !className.startsWith("Abstract") && !className.startsWith("I")) {
|
|
295
|
+
results.push({
|
|
296
|
+
line: lineNum,
|
|
297
|
+
column: line.indexOf(className),
|
|
298
|
+
type: "naming",
|
|
299
|
+
severity: "error",
|
|
300
|
+
message: `类名 "${className}" 应使用 PascalCase 命名`,
|
|
301
|
+
rule: "class-naming",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const methodMatch = line.match(/\b(?:public|protected|private)\s+\w+(?:<[^>]+>)?\s+(\w+)\s*\(/);
|
|
307
|
+
if (methodMatch) {
|
|
308
|
+
const methodName = methodMatch[1];
|
|
309
|
+
if (
|
|
310
|
+
!isPascalCase(methodName) &&
|
|
311
|
+
!isCamelCase(methodName) &&
|
|
312
|
+
!methodName.startsWith("get") &&
|
|
313
|
+
!methodName.startsWith("set") &&
|
|
314
|
+
!methodName.startsWith("is") &&
|
|
315
|
+
!methodName.startsWith("has") &&
|
|
316
|
+
methodName !== "main"
|
|
317
|
+
) {
|
|
318
|
+
results.push({
|
|
319
|
+
line: lineNum,
|
|
320
|
+
column: line.indexOf(methodName),
|
|
321
|
+
type: "naming",
|
|
322
|
+
severity: "warning",
|
|
323
|
+
message: `方法名 "${methodName}" 应使用 camelCase 命名`,
|
|
324
|
+
rule: "method-naming",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const constantMatch = line.match(/\b(?:static\s+final|final\s+static)\s+\w+\s+(\w+)\s*=/);
|
|
330
|
+
if (constantMatch) {
|
|
331
|
+
const constantName = constantMatch[1];
|
|
332
|
+
if (!isUpperSnakeCase(constantName)) {
|
|
333
|
+
results.push({
|
|
334
|
+
line: lineNum,
|
|
335
|
+
column: line.indexOf(constantName),
|
|
336
|
+
type: "naming",
|
|
337
|
+
severity: "warning",
|
|
338
|
+
message: `常量 "${constantName}" 应使用 UPPER_SNAKE_CASE 命名`,
|
|
339
|
+
rule: "constant-naming",
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return results;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function checkLoopDb(lines: string[]): CheckResult[] {
|
|
350
|
+
const results: CheckResult[] = [];
|
|
351
|
+
let inLoop = false;
|
|
352
|
+
let loopBraceCount = 0;
|
|
353
|
+
|
|
354
|
+
for (let i = 0; i < lines.length; i++) {
|
|
355
|
+
const line = lines[i];
|
|
356
|
+
const trimmed = line.trim();
|
|
357
|
+
|
|
358
|
+
if (/\b(for|while)\s*\(/.test(trimmed)) {
|
|
359
|
+
inLoop = true;
|
|
360
|
+
loopBraceCount = 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!inLoop) continue;
|
|
364
|
+
|
|
365
|
+
for (const char of line) {
|
|
366
|
+
if (char === "{") loopBraceCount++;
|
|
367
|
+
if (char === "}") loopBraceCount--;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
for (const keyword of DB_KEYWORDS) {
|
|
371
|
+
if (line.includes(keyword)) {
|
|
372
|
+
results.push({
|
|
373
|
+
line: i + 1,
|
|
374
|
+
column: line.indexOf(keyword),
|
|
375
|
+
type: "loop_db",
|
|
376
|
+
severity: "error",
|
|
377
|
+
message: `在循环中调用 DB 操作 "${keyword}",可能导致性能问题`,
|
|
378
|
+
rule: "loop-db-query",
|
|
379
|
+
});
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (loopBraceCount <= 0 && trimmed.includes("}")) {
|
|
385
|
+
inLoop = false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return results;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function checkExceptionHandling(lines: string[]): CheckResult[] {
|
|
393
|
+
const results: CheckResult[] = [];
|
|
394
|
+
|
|
395
|
+
for (let i = 0; i < lines.length; i++) {
|
|
396
|
+
const line = lines[i];
|
|
397
|
+
const trimmed = line.trim();
|
|
398
|
+
if (!/\bcatch\s*\(/.test(trimmed)) continue;
|
|
399
|
+
|
|
400
|
+
let j = i + 1;
|
|
401
|
+
let hasContent = false;
|
|
402
|
+
let braceCount = 0;
|
|
403
|
+
let catchStarted = false;
|
|
404
|
+
|
|
405
|
+
if (trimmed.includes("{")) {
|
|
406
|
+
catchStarted = true;
|
|
407
|
+
braceCount += (trimmed.match(/{/g) || []).length;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
while (j < lines.length) {
|
|
411
|
+
const catchLine = lines[j].trim();
|
|
412
|
+
if (!catchStarted && catchLine.includes("{")) catchStarted = true;
|
|
413
|
+
if (catchStarted) {
|
|
414
|
+
braceCount += (catchLine.match(/{/g) || []).length;
|
|
415
|
+
braceCount -= (catchLine.match(/}/g) || []).length;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (
|
|
419
|
+
catchStarted &&
|
|
420
|
+
catchLine !== "{" &&
|
|
421
|
+
catchLine !== "}" &&
|
|
422
|
+
catchLine !== "" &&
|
|
423
|
+
!catchLine.startsWith("//") &&
|
|
424
|
+
!catchLine.startsWith("*") &&
|
|
425
|
+
!catchLine.startsWith("/*")
|
|
426
|
+
) {
|
|
427
|
+
hasContent = true;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (braceCount <= 0 && catchStarted) break;
|
|
432
|
+
j++;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!hasContent) {
|
|
436
|
+
results.push({
|
|
437
|
+
line: i + 1,
|
|
438
|
+
column: line.indexOf("catch"),
|
|
439
|
+
type: "exception",
|
|
440
|
+
severity: "error",
|
|
441
|
+
message: "空的 catch 块,应记录日志或处理异常",
|
|
442
|
+
rule: "empty-catch",
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return results;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function formatCheckResults(results: CheckResult[]): string {
|
|
451
|
+
if (results.length === 0) return "金蝶代码检查通过,未发现问题。";
|
|
452
|
+
|
|
453
|
+
const errors = results.filter((result) => result.severity === "error");
|
|
454
|
+
const warnings = results.filter((result) => result.severity === "warning");
|
|
455
|
+
const infos = results.filter((result) => result.severity === "info");
|
|
456
|
+
|
|
457
|
+
const lines = [
|
|
458
|
+
`金蝶代码检查发现 ${results.length} 个问题:`,
|
|
459
|
+
`错误:${errors.length}`,
|
|
460
|
+
`警告:${warnings.length}`,
|
|
461
|
+
`提示:${infos.length}`,
|
|
462
|
+
"",
|
|
463
|
+
];
|
|
464
|
+
|
|
465
|
+
for (const result of results) {
|
|
466
|
+
lines.push(`- 第 ${result.line} 行,第 ${result.column + 1} 列:[${result.severity}] ${result.type} ${result.message} (${result.rule})`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return lines.join("\n");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
interface LineRange {
|
|
473
|
+
start: number;
|
|
474
|
+
end: number;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function makeResult(
|
|
478
|
+
index: number,
|
|
479
|
+
line: string,
|
|
480
|
+
token: string,
|
|
481
|
+
type: CheckType,
|
|
482
|
+
severity: CheckSeverity,
|
|
483
|
+
message: string,
|
|
484
|
+
rule: string,
|
|
485
|
+
): CheckResult {
|
|
486
|
+
return {
|
|
487
|
+
line: index + 1,
|
|
488
|
+
column: Math.max(0, line.indexOf(token)),
|
|
489
|
+
type,
|
|
490
|
+
severity,
|
|
491
|
+
message,
|
|
492
|
+
rule,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function findMethodRanges(lines: string[], methodName: string): LineRange[] {
|
|
497
|
+
const ranges: LineRange[] = [];
|
|
498
|
+
const pattern = new RegExp(`\\b${methodName}\\s*\\(`);
|
|
499
|
+
|
|
500
|
+
for (let i = 0; i < lines.length; i++) {
|
|
501
|
+
if (!pattern.test(lines[i])) continue;
|
|
502
|
+
const end = findBlockEnd(lines, i);
|
|
503
|
+
ranges.push({ start: i, end });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return ranges;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function findBlockEnd(lines: string[], start: number): number {
|
|
510
|
+
let braceCount = 0;
|
|
511
|
+
let seenOpen = false;
|
|
512
|
+
|
|
513
|
+
for (let i = start; i < lines.length; i++) {
|
|
514
|
+
for (const char of lines[i]) {
|
|
515
|
+
if (char === "{") {
|
|
516
|
+
seenOpen = true;
|
|
517
|
+
braceCount++;
|
|
518
|
+
}
|
|
519
|
+
if (char === "}") braceCount--;
|
|
520
|
+
}
|
|
521
|
+
if (seenOpen && braceCount <= 0) return i;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return start;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function forEachLineInRange(lines: string[], range: LineRange, fn: (line: string, index: number) => void): void {
|
|
528
|
+
for (let i = range.start; i <= range.end; i++) {
|
|
529
|
+
if (!isCommentLine(lines[i])) fn(lines[i], i);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function nearbyLineHas(lines: string[], index: number, offsetStart: number, pattern: RegExp): boolean {
|
|
534
|
+
const start = Math.max(0, index + offsetStart);
|
|
535
|
+
for (let i = start; i <= index; i++) {
|
|
536
|
+
if (pattern.test(lines[i])) return true;
|
|
537
|
+
}
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function isInsideLoop(lines: string[], index: number): boolean {
|
|
542
|
+
for (let i = index; i >= Math.max(0, index - 20); i--) {
|
|
543
|
+
if (/\b(for|while)\s*\(/.test(lines[i])) {
|
|
544
|
+
const end = findBlockEnd(lines, i);
|
|
545
|
+
return index <= end;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function isCommentLine(line: string): boolean {
|
|
552
|
+
const trimmed = line.trim();
|
|
553
|
+
return trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function isPascalCase(name: string): boolean {
|
|
557
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(name);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function isCamelCase(name: string): boolean {
|
|
561
|
+
return /^[a-z][a-zA-Z0-9]*$/.test(name);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function isUpperSnakeCase(name: string): boolean {
|
|
565
|
+
return /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/.test(name);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function isCommonString(value: string): boolean {
|
|
569
|
+
return [
|
|
570
|
+
"UTF-8",
|
|
571
|
+
"GBK",
|
|
572
|
+
"ISO-8859-1",
|
|
573
|
+
"ASCII",
|
|
574
|
+
"true",
|
|
575
|
+
"false",
|
|
576
|
+
"null",
|
|
577
|
+
"undefined",
|
|
578
|
+
"GET",
|
|
579
|
+
"POST",
|
|
580
|
+
"PUT",
|
|
581
|
+
"DELETE",
|
|
582
|
+
"JSON",
|
|
583
|
+
"XML",
|
|
584
|
+
"HTML",
|
|
585
|
+
"TEXT",
|
|
586
|
+
"application/json",
|
|
587
|
+
"text/html",
|
|
588
|
+
"yyyy-MM-dd",
|
|
589
|
+
"yyyy-MM-dd HH:mm:ss",
|
|
590
|
+
].includes(value);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function isLogMessage(line: string): boolean {
|
|
594
|
+
return (
|
|
595
|
+
/\b(log|logger|LOG)\s*\.\s*(debug|info|warn|error|trace)\s*\(/.test(line) ||
|
|
596
|
+
/\b(System\s*\.\s*out\s*\.\s*println)\s*\(/.test(line) ||
|
|
597
|
+
/\b(console\s*\.\s*log)\s*\(/.test(line)
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function isAnnotation(line: string): boolean {
|
|
602
|
+
return line.trim().startsWith("@") || /@\w+\s*\(/.test(line);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function isBusinessConstant(value: string): boolean {
|
|
606
|
+
return (
|
|
607
|
+
/^[A-Z][A-Z_]+$/.test(value) ||
|
|
608
|
+
/^[A-Z]+_[A-Z]+$/.test(value) ||
|
|
609
|
+
/^(DRAFT|SUBMIT|APPROVE|CLOSE|VOID|AUDIT)/.test(value) ||
|
|
610
|
+
/^(SUCCESS|FAIL|ERROR|PENDING)/.test(value)
|
|
611
|
+
);
|
|
612
|
+
}
|