@gulu9527/code-trust 0.3.0 → 0.3.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/README-CN.md +15 -0
- package/README.md +15 -0
- package/action.yml +5 -1
- package/dist/cli/index.js +605 -247
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +43 -0
- package/dist/index.js +583 -242
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,33 +7,64 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
|
|
8
8
|
// src/parsers/diff.ts
|
|
9
9
|
import simpleGit from "simple-git";
|
|
10
|
+
var GIT_DIFF_UNIFIED = "--unified=3";
|
|
11
|
+
var SHORT_HASH_LENGTH = 7;
|
|
10
12
|
var DiffParser = class {
|
|
11
13
|
git;
|
|
12
14
|
constructor(workDir) {
|
|
13
15
|
this.git = simpleGit(workDir);
|
|
14
16
|
}
|
|
15
17
|
async getStagedFiles() {
|
|
16
|
-
const diffDetail = await this.git.diff(["--cached",
|
|
18
|
+
const diffDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
|
|
17
19
|
return this.parseDiffOutput(diffDetail);
|
|
18
20
|
}
|
|
19
21
|
async getDiffFromRef(ref) {
|
|
20
|
-
const diffDetail = await this.git.diff([ref,
|
|
22
|
+
const diffDetail = await this.git.diff([ref, GIT_DIFF_UNIFIED]);
|
|
21
23
|
return this.parseDiffOutput(diffDetail);
|
|
22
24
|
}
|
|
23
25
|
async getChangedFiles() {
|
|
24
|
-
const diffDetail = await this.git.diff([
|
|
25
|
-
const stagedDetail = await this.git.diff(["--cached",
|
|
26
|
-
const
|
|
27
|
-
|
|
26
|
+
const diffDetail = await this.git.diff([GIT_DIFF_UNIFIED]);
|
|
27
|
+
const stagedDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
|
|
28
|
+
const unstagedFiles = this.parseDiffOutput(diffDetail);
|
|
29
|
+
const stagedFiles = this.parseDiffOutput(stagedDetail);
|
|
30
|
+
return this.mergeDiffFiles(unstagedFiles, stagedFiles);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Merge two sets of diff files, deduplicating by file path.
|
|
34
|
+
* When a file appears in both, merge their hunks and combine stats.
|
|
35
|
+
*/
|
|
36
|
+
mergeDiffFiles(unstaged, staged) {
|
|
37
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const file of unstaged) {
|
|
39
|
+
fileMap.set(file.filePath, file);
|
|
40
|
+
}
|
|
41
|
+
for (const file of staged) {
|
|
42
|
+
const existing = fileMap.get(file.filePath);
|
|
43
|
+
if (existing) {
|
|
44
|
+
fileMap.set(file.filePath, {
|
|
45
|
+
...existing,
|
|
46
|
+
// Combine additions/deletions
|
|
47
|
+
additions: existing.additions + file.additions,
|
|
48
|
+
deletions: existing.deletions + file.deletions,
|
|
49
|
+
// Merge hunks (preserve order: staged first, then unstaged)
|
|
50
|
+
hunks: [...file.hunks, ...existing.hunks],
|
|
51
|
+
// Status: if either is 'added', treat as added; otherwise keep modified
|
|
52
|
+
status: existing.status === "added" || file.status === "added" ? "added" : "modified"
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
fileMap.set(file.filePath, file);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return Array.from(fileMap.values());
|
|
28
59
|
}
|
|
29
60
|
async getLastCommitDiff() {
|
|
30
|
-
const diffDetail = await this.git.diff(["HEAD~1", "HEAD",
|
|
61
|
+
const diffDetail = await this.git.diff(["HEAD~1", "HEAD", GIT_DIFF_UNIFIED]);
|
|
31
62
|
return this.parseDiffOutput(diffDetail);
|
|
32
63
|
}
|
|
33
64
|
async getCurrentCommitHash() {
|
|
34
65
|
try {
|
|
35
66
|
const hash = await this.git.revparse(["HEAD"]);
|
|
36
|
-
return hash.trim().slice(0,
|
|
67
|
+
return hash.trim().slice(0, SHORT_HASH_LENGTH);
|
|
37
68
|
} catch {
|
|
38
69
|
return void 0;
|
|
39
70
|
}
|
|
@@ -59,9 +90,18 @@ var DiffParser = class {
|
|
|
59
90
|
}
|
|
60
91
|
parseFileDiff(fileDiff) {
|
|
61
92
|
const lines = fileDiff.split("\n");
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
93
|
+
let filePath = null;
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
if (line.startsWith("+++ b/")) {
|
|
96
|
+
filePath = line.slice(6);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!filePath) {
|
|
101
|
+
const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
|
|
102
|
+
if (!headerMatch) return null;
|
|
103
|
+
filePath = headerMatch[2];
|
|
104
|
+
}
|
|
65
105
|
let status = "modified";
|
|
66
106
|
let additions = 0;
|
|
67
107
|
let deletions = 0;
|
|
@@ -165,6 +205,144 @@ function t(en, zh) {
|
|
|
165
205
|
return isZhLocale() ? zh : en;
|
|
166
206
|
}
|
|
167
207
|
|
|
208
|
+
// src/rules/brace-utils.ts
|
|
209
|
+
function countBracesInLine(line, startJ, initialDepth, inBlockComment) {
|
|
210
|
+
let depth = initialDepth;
|
|
211
|
+
let i = startJ;
|
|
212
|
+
while (i < line.length) {
|
|
213
|
+
if (inBlockComment.value) {
|
|
214
|
+
const closeIdx = line.indexOf("*/", i);
|
|
215
|
+
if (closeIdx === -1) return depth;
|
|
216
|
+
inBlockComment.value = false;
|
|
217
|
+
i = closeIdx + 2;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const ch = line[i];
|
|
221
|
+
if (ch === "/" && line[i + 1] === "/") return depth;
|
|
222
|
+
if (ch === "/" && line[i + 1] === "*") {
|
|
223
|
+
inBlockComment.value = true;
|
|
224
|
+
i += 2;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (ch === "'" || ch === '"') {
|
|
228
|
+
i = skipStringLiteral(line, i, ch);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (ch === "`") {
|
|
232
|
+
i = skipTemplateLiteral(line, i);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (ch === "/" && i > 0) {
|
|
236
|
+
const prevNonSpace = findPrevNonSpace(line, i);
|
|
237
|
+
if (prevNonSpace !== -1 && "=(!|&:,;[{?+->~%^".includes(line[prevNonSpace])) {
|
|
238
|
+
i = skipRegexpLiteral(line, i);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (ch === "{") depth++;
|
|
243
|
+
if (ch === "}") depth--;
|
|
244
|
+
i++;
|
|
245
|
+
}
|
|
246
|
+
return depth;
|
|
247
|
+
}
|
|
248
|
+
function skipStringLiteral(line, start, quote) {
|
|
249
|
+
let i = start + 1;
|
|
250
|
+
while (i < line.length) {
|
|
251
|
+
if (line[i] === "\\") {
|
|
252
|
+
i += 2;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (line[i] === quote) return i + 1;
|
|
256
|
+
i++;
|
|
257
|
+
}
|
|
258
|
+
return i;
|
|
259
|
+
}
|
|
260
|
+
function skipTemplateLiteral(line, start) {
|
|
261
|
+
let i = start + 1;
|
|
262
|
+
while (i < line.length) {
|
|
263
|
+
if (line[i] === "\\") {
|
|
264
|
+
i += 2;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (line[i] === "`") return i + 1;
|
|
268
|
+
i++;
|
|
269
|
+
}
|
|
270
|
+
return i;
|
|
271
|
+
}
|
|
272
|
+
function skipRegexpLiteral(line, start) {
|
|
273
|
+
let i = start + 1;
|
|
274
|
+
while (i < line.length) {
|
|
275
|
+
if (line[i] === "\\") {
|
|
276
|
+
i += 2;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (line[i] === "/") return i + 1;
|
|
280
|
+
i++;
|
|
281
|
+
}
|
|
282
|
+
return i;
|
|
283
|
+
}
|
|
284
|
+
function findPrevNonSpace(line, pos) {
|
|
285
|
+
for (let i = pos - 1; i >= 0; i--) {
|
|
286
|
+
if (line[i] !== " " && line[i] !== " ") return i;
|
|
287
|
+
}
|
|
288
|
+
return -1;
|
|
289
|
+
}
|
|
290
|
+
function extractBraceBlock(lines, startLineIndex, startCol) {
|
|
291
|
+
let depth = 0;
|
|
292
|
+
let started = false;
|
|
293
|
+
let bodyStart = -1;
|
|
294
|
+
const blockComment = { value: false };
|
|
295
|
+
for (let i = startLineIndex; i < lines.length; i++) {
|
|
296
|
+
const startJ = i === startLineIndex ? startCol : 0;
|
|
297
|
+
const line = lines[i];
|
|
298
|
+
let j = startJ;
|
|
299
|
+
while (j < line.length) {
|
|
300
|
+
if (blockComment.value) {
|
|
301
|
+
const closeIdx = line.indexOf("*/", j);
|
|
302
|
+
if (closeIdx === -1) {
|
|
303
|
+
j = line.length;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
blockComment.value = false;
|
|
307
|
+
j = closeIdx + 2;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const ch = line[j];
|
|
311
|
+
if (ch === "/" && line[j + 1] === "/") break;
|
|
312
|
+
if (ch === "/" && line[j + 1] === "*") {
|
|
313
|
+
blockComment.value = true;
|
|
314
|
+
j += 2;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (ch === "'" || ch === '"') {
|
|
318
|
+
j = skipStringLiteral(line, j, ch);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (ch === "`") {
|
|
322
|
+
j = skipTemplateLiteral(line, j);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (ch === "{") {
|
|
326
|
+
depth++;
|
|
327
|
+
if (!started) {
|
|
328
|
+
started = true;
|
|
329
|
+
bodyStart = i;
|
|
330
|
+
}
|
|
331
|
+
} else if (ch === "}") {
|
|
332
|
+
depth--;
|
|
333
|
+
if (started && depth === 0) {
|
|
334
|
+
return {
|
|
335
|
+
bodyLines: lines.slice(bodyStart + 1, i),
|
|
336
|
+
endLine: i
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
j++;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
|
|
168
346
|
// src/rules/builtin/unnecessary-try-catch.ts
|
|
169
347
|
var unnecessaryTryCatchRule = {
|
|
170
348
|
id: "logic/unnecessary-try-catch",
|
|
@@ -180,38 +358,53 @@ var unnecessaryTryCatchRule = {
|
|
|
180
358
|
const line = lines[i];
|
|
181
359
|
const trimmed = line.trim();
|
|
182
360
|
if (trimmed.startsWith("try") && trimmed.includes("{")) {
|
|
183
|
-
const
|
|
361
|
+
const tryCol = line.indexOf("try");
|
|
362
|
+
const tryBlock = extractBraceBlock(lines, i, tryCol);
|
|
184
363
|
if (tryBlock) {
|
|
185
|
-
const { bodyLines
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
364
|
+
const { bodyLines: tryBodyLines, endLine: tryEndLine } = tryBlock;
|
|
365
|
+
let catchLineIdx = -1;
|
|
366
|
+
for (let k = tryEndLine; k < Math.min(tryEndLine + 2, lines.length); k++) {
|
|
367
|
+
if (lines[k].includes("catch")) {
|
|
368
|
+
catchLineIdx = k;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (catchLineIdx !== -1) {
|
|
373
|
+
const catchCol = lines[catchLineIdx].indexOf("catch");
|
|
374
|
+
const catchBlock = extractBraceBlock(lines, catchLineIdx, catchCol);
|
|
375
|
+
if (catchBlock) {
|
|
376
|
+
const { bodyLines: catchBodyLines, endLine: catchEndLine } = catchBlock;
|
|
377
|
+
const nonEmptyBody = tryBodyLines.filter((l) => l.trim().length > 0);
|
|
378
|
+
const nonEmptyCatch = catchBodyLines.filter((l) => l.trim().length > 0);
|
|
379
|
+
const isSimpleBody = nonEmptyBody.length <= 2;
|
|
380
|
+
const isGenericCatch = nonEmptyCatch.length <= 2 && nonEmptyCatch.some(
|
|
381
|
+
(l) => /console\.(log|error|warn)/.test(l) || /throw\s+(new\s+)?Error/.test(l) || l.trim() === ""
|
|
382
|
+
);
|
|
383
|
+
const bodyHasOnlyAssignments = nonEmptyBody.every(
|
|
384
|
+
(l) => /^\s*(const|let|var)\s+/.test(l) || /^\s*\w+(\.\w+)*\s*=\s*/.test(l) || /^\s*return\s+/.test(l)
|
|
385
|
+
);
|
|
386
|
+
if (isSimpleBody && isGenericCatch && bodyHasOnlyAssignments) {
|
|
387
|
+
issues.push({
|
|
388
|
+
ruleId: "logic/unnecessary-try-catch",
|
|
389
|
+
severity: "medium",
|
|
390
|
+
category: "logic",
|
|
391
|
+
file: context.filePath,
|
|
392
|
+
startLine: i + 1,
|
|
393
|
+
endLine: catchEndLine + 1,
|
|
394
|
+
message: t(
|
|
395
|
+
"Unnecessary try-catch wrapping a simple statement with generic error handling. This is likely AI-hallucinated error handling.",
|
|
396
|
+
"\u4E0D\u5FC5\u8981\u7684 try-catch \u5305\u88F9\u4E86\u7B80\u5355\u8BED\u53E5\uFF0Ccatch \u4E2D\u53EA\u6709\u901A\u7528\u7684\u9519\u8BEF\u65E5\u5FD7\u3002\u8FD9\u5F88\u53EF\u80FD\u662F AI \u5E7B\u89C9\u751F\u6210\u7684\u9519\u8BEF\u5904\u7406\u3002"
|
|
397
|
+
),
|
|
398
|
+
suggestion: t(
|
|
399
|
+
"Remove the try-catch block or add meaningful error recovery logic.",
|
|
400
|
+
"\u79FB\u9664 try-catch \u5757\uFF0C\u6216\u6DFB\u52A0\u6709\u610F\u4E49\u7684\u9519\u8BEF\u6062\u590D\u903B\u8F91\u3002"
|
|
401
|
+
)
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
i = catchEndLine + 1;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
212
407
|
}
|
|
213
|
-
i = endLine + 1;
|
|
214
|
-
continue;
|
|
215
408
|
}
|
|
216
409
|
}
|
|
217
410
|
i++;
|
|
@@ -219,55 +412,6 @@ var unnecessaryTryCatchRule = {
|
|
|
219
412
|
return issues;
|
|
220
413
|
}
|
|
221
414
|
};
|
|
222
|
-
function extractBlock(lines, tryLineIndex) {
|
|
223
|
-
let braceCount = 0;
|
|
224
|
-
let foundTryOpen = false;
|
|
225
|
-
let tryBodyStart = -1;
|
|
226
|
-
let tryBodyEnd = -1;
|
|
227
|
-
let catchStart = -1;
|
|
228
|
-
let catchBodyStart = -1;
|
|
229
|
-
let catchBodyEnd = -1;
|
|
230
|
-
for (let i = tryLineIndex; i < lines.length; i++) {
|
|
231
|
-
const line = lines[i];
|
|
232
|
-
for (const ch of line) {
|
|
233
|
-
if (ch === "{") {
|
|
234
|
-
braceCount++;
|
|
235
|
-
if (!foundTryOpen) {
|
|
236
|
-
foundTryOpen = true;
|
|
237
|
-
tryBodyStart = i;
|
|
238
|
-
}
|
|
239
|
-
} else if (ch === "}") {
|
|
240
|
-
braceCount--;
|
|
241
|
-
if (braceCount === 0 && tryBodyEnd === -1) {
|
|
242
|
-
tryBodyEnd = i;
|
|
243
|
-
} else if (braceCount === 0 && catchBodyEnd === -1 && catchBodyStart !== -1) {
|
|
244
|
-
catchBodyEnd = i;
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
if (tryBodyEnd !== -1 && catchStart === -1) {
|
|
250
|
-
if (line.includes("catch")) {
|
|
251
|
-
catchStart = i;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if (catchStart !== -1 && catchBodyStart === -1 && line.includes("{")) {
|
|
255
|
-
catchBodyStart = i;
|
|
256
|
-
}
|
|
257
|
-
if (catchBodyEnd !== -1) break;
|
|
258
|
-
}
|
|
259
|
-
if (tryBodyStart === -1 || tryBodyEnd === -1 || catchBodyStart === -1 || catchBodyEnd === -1) {
|
|
260
|
-
return null;
|
|
261
|
-
}
|
|
262
|
-
const bodyLines = lines.slice(tryBodyStart + 1, tryBodyEnd);
|
|
263
|
-
const catchBodyLines = lines.slice(catchBodyStart + 1, catchBodyEnd);
|
|
264
|
-
return {
|
|
265
|
-
bodyLines,
|
|
266
|
-
catchStart,
|
|
267
|
-
catchBodyLines,
|
|
268
|
-
endLine: catchBodyEnd
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
415
|
|
|
272
416
|
// src/rules/builtin/over-defensive.ts
|
|
273
417
|
var overDefensiveRule = {
|
|
@@ -456,18 +600,10 @@ function detectCodeAfterReturn(context, lines, issues) {
|
|
|
456
600
|
let braceDepth = 0;
|
|
457
601
|
let lastReturnDepth = -1;
|
|
458
602
|
let lastReturnLine = -1;
|
|
603
|
+
const blockComment = { value: false };
|
|
459
604
|
for (let i = 0; i < lines.length; i++) {
|
|
460
605
|
const trimmed = lines[i].trim();
|
|
461
|
-
|
|
462
|
-
if (ch === "{") braceDepth++;
|
|
463
|
-
if (ch === "}") {
|
|
464
|
-
if (braceDepth === lastReturnDepth) {
|
|
465
|
-
lastReturnDepth = -1;
|
|
466
|
-
lastReturnLine = -1;
|
|
467
|
-
}
|
|
468
|
-
braceDepth--;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
606
|
+
braceDepth = countBracesInLine(lines[i], 0, braceDepth, blockComment);
|
|
471
607
|
if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
|
|
472
608
|
const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
|
|
473
609
|
if (endsOpen) continue;
|
|
@@ -493,6 +629,10 @@ function detectCodeAfterReturn(context, lines, issues) {
|
|
|
493
629
|
lastReturnDepth = -1;
|
|
494
630
|
lastReturnLine = -1;
|
|
495
631
|
}
|
|
632
|
+
if (braceDepth < lastReturnDepth) {
|
|
633
|
+
lastReturnDepth = -1;
|
|
634
|
+
lastReturnLine = -1;
|
|
635
|
+
}
|
|
496
636
|
}
|
|
497
637
|
}
|
|
498
638
|
function detectImmediateReassign(context, lines, issues) {
|
|
@@ -526,7 +666,21 @@ function detectImmediateReassign(context, lines, issues) {
|
|
|
526
666
|
}
|
|
527
667
|
|
|
528
668
|
// src/rules/fix-utils.ts
|
|
529
|
-
function
|
|
669
|
+
function buildLineOffsets(content) {
|
|
670
|
+
const offsets = [0];
|
|
671
|
+
for (let i = 0; i < content.length; i++) {
|
|
672
|
+
if (content[i] === "\n") {
|
|
673
|
+
offsets.push(i + 1);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return offsets;
|
|
677
|
+
}
|
|
678
|
+
function lineStartOffset(content, lineNumber, offsets) {
|
|
679
|
+
if (offsets) {
|
|
680
|
+
const idx = lineNumber - 1;
|
|
681
|
+
if (idx < 0 || idx >= offsets.length) return content.length;
|
|
682
|
+
return offsets[idx];
|
|
683
|
+
}
|
|
530
684
|
let offset = 0;
|
|
531
685
|
const lines = content.split("\n");
|
|
532
686
|
for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
|
|
@@ -534,12 +688,13 @@ function lineStartOffset(content, lineNumber) {
|
|
|
534
688
|
}
|
|
535
689
|
return offset;
|
|
536
690
|
}
|
|
537
|
-
function lineRange(content, lineNumber) {
|
|
538
|
-
const
|
|
691
|
+
function lineRange(content, lineNumber, offsets) {
|
|
692
|
+
const table = offsets ?? buildLineOffsets(content);
|
|
539
693
|
const lineIndex = lineNumber - 1;
|
|
540
|
-
if (lineIndex < 0 || lineIndex >=
|
|
541
|
-
const start =
|
|
542
|
-
const
|
|
694
|
+
if (lineIndex < 0 || lineIndex >= table.length) return [0, 0];
|
|
695
|
+
const start = table[lineIndex];
|
|
696
|
+
const nextLineStart = lineIndex + 1 < table.length ? table[lineIndex + 1] : content.length;
|
|
697
|
+
const end = lineIndex + 1 < table.length ? nextLineStart : nextLineStart;
|
|
543
698
|
return [start, end];
|
|
544
699
|
}
|
|
545
700
|
|
|
@@ -605,12 +760,12 @@ function extractFunctions(parsed) {
|
|
|
605
760
|
}
|
|
606
761
|
function visitNode(root, functions) {
|
|
607
762
|
const methodBodies = /* @__PURE__ */ new WeakSet();
|
|
608
|
-
walkAST(root, (node) => {
|
|
763
|
+
walkAST(root, (node, parent) => {
|
|
609
764
|
if (node.type === AST_NODE_TYPES.FunctionExpression && methodBodies.has(node)) {
|
|
610
765
|
return false;
|
|
611
766
|
}
|
|
612
767
|
if (node.type === AST_NODE_TYPES.FunctionDeclaration || node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression || node.type === AST_NODE_TYPES.MethodDefinition) {
|
|
613
|
-
const info = analyzeFunctionNode(node);
|
|
768
|
+
const info = analyzeFunctionNode(node, parent);
|
|
614
769
|
if (info) functions.push(info);
|
|
615
770
|
if (node.type === AST_NODE_TYPES.MethodDefinition) {
|
|
616
771
|
methodBodies.add(node.value);
|
|
@@ -619,7 +774,7 @@ function visitNode(root, functions) {
|
|
|
619
774
|
return;
|
|
620
775
|
});
|
|
621
776
|
}
|
|
622
|
-
function analyzeFunctionNode(node) {
|
|
777
|
+
function analyzeFunctionNode(node, parent) {
|
|
623
778
|
let name = "<anonymous>";
|
|
624
779
|
let params = [];
|
|
625
780
|
let body = null;
|
|
@@ -632,7 +787,11 @@ function analyzeFunctionNode(node) {
|
|
|
632
787
|
params = node.params;
|
|
633
788
|
body = node.body;
|
|
634
789
|
} else if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
635
|
-
|
|
790
|
+
if (parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier) {
|
|
791
|
+
name = parent.id.name;
|
|
792
|
+
} else {
|
|
793
|
+
name = "<arrow>";
|
|
794
|
+
}
|
|
636
795
|
params = node.params;
|
|
637
796
|
body = node.body;
|
|
638
797
|
} else if (node.type === AST_NODE_TYPES.MethodDefinition) {
|
|
@@ -660,6 +819,9 @@ function analyzeFunctionNode(node) {
|
|
|
660
819
|
function calculateCyclomaticComplexity(root) {
|
|
661
820
|
let complexity = 1;
|
|
662
821
|
walkAST(root, (n) => {
|
|
822
|
+
if (n.type === AST_NODE_TYPES.FunctionDeclaration || n.type === AST_NODE_TYPES.FunctionExpression || n.type === AST_NODE_TYPES.ArrowFunctionExpression || n.type === AST_NODE_TYPES.MethodDefinition) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
663
825
|
switch (n.type) {
|
|
664
826
|
case AST_NODE_TYPES.IfStatement:
|
|
665
827
|
case AST_NODE_TYPES.ConditionalExpression:
|
|
@@ -688,6 +850,9 @@ function calculateCognitiveComplexity(root) {
|
|
|
688
850
|
const depthMap = /* @__PURE__ */ new WeakMap();
|
|
689
851
|
depthMap.set(root, 0);
|
|
690
852
|
walkAST(root, (n, parent) => {
|
|
853
|
+
if (n.type === AST_NODE_TYPES.FunctionDeclaration || n.type === AST_NODE_TYPES.FunctionExpression || n.type === AST_NODE_TYPES.ArrowFunctionExpression || n.type === AST_NODE_TYPES.MethodDefinition) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
691
856
|
const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
|
|
692
857
|
const isNesting = isNestingNode(n);
|
|
693
858
|
const depth = isNesting ? parentDepth + 1 : parentDepth;
|
|
@@ -706,6 +871,9 @@ function calculateMaxNestingDepth(root) {
|
|
|
706
871
|
const depthMap = /* @__PURE__ */ new WeakMap();
|
|
707
872
|
depthMap.set(root, 0);
|
|
708
873
|
walkAST(root, (n, parent) => {
|
|
874
|
+
if (n.type === AST_NODE_TYPES.FunctionDeclaration || n.type === AST_NODE_TYPES.FunctionExpression || n.type === AST_NODE_TYPES.ArrowFunctionExpression || n.type === AST_NODE_TYPES.MethodDefinition) {
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
709
877
|
const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
|
|
710
878
|
const isNesting = n.type === AST_NODE_TYPES.IfStatement || n.type === AST_NODE_TYPES.ForStatement || n.type === AST_NODE_TYPES.ForInStatement || n.type === AST_NODE_TYPES.ForOfStatement || n.type === AST_NODE_TYPES.WhileStatement || n.type === AST_NODE_TYPES.DoWhileStatement || n.type === AST_NODE_TYPES.SwitchStatement || n.type === AST_NODE_TYPES.TryStatement;
|
|
711
879
|
const currentDepth = isNesting ? parentDepth + 1 : parentDepth;
|
|
@@ -813,7 +981,24 @@ function collectDeclarationsAndReferences(root, declarations, references) {
|
|
|
813
981
|
kind: exportedNames.has(node.id.name) ? "export" : "local"
|
|
814
982
|
});
|
|
815
983
|
}
|
|
816
|
-
if (node.type === AST_NODE_TYPES.
|
|
984
|
+
if (node.type === AST_NODE_TYPES.ClassDeclaration && node.id) {
|
|
985
|
+
declarations.set(node.id.name, {
|
|
986
|
+
line: node.loc?.start.line ?? 0,
|
|
987
|
+
kind: exportedNames.has(node.id.name) ? "export" : "local"
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
const declarationParentTypes = /* @__PURE__ */ new Set([
|
|
991
|
+
"VariableDeclarator",
|
|
992
|
+
"FunctionDeclaration",
|
|
993
|
+
"ClassDeclaration",
|
|
994
|
+
"MethodDefinition",
|
|
995
|
+
"TSEnumDeclaration",
|
|
996
|
+
"TSEnumMember",
|
|
997
|
+
"TSTypeAliasDeclaration",
|
|
998
|
+
"TSInterfaceDeclaration",
|
|
999
|
+
"TSModuleDeclaration"
|
|
1000
|
+
]);
|
|
1001
|
+
if (node.type === AST_NODE_TYPES.Identifier && !declarationParentTypes.has(parentType ?? "")) {
|
|
817
1002
|
references.add(node.name);
|
|
818
1003
|
}
|
|
819
1004
|
return;
|
|
@@ -887,16 +1072,38 @@ function stringifyCondition(node) {
|
|
|
887
1072
|
case AST_NODE_TYPES.Literal:
|
|
888
1073
|
return String(node.value);
|
|
889
1074
|
case AST_NODE_TYPES.BinaryExpression:
|
|
1075
|
+
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
890
1076
|
case AST_NODE_TYPES.LogicalExpression:
|
|
891
1077
|
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
892
1078
|
case AST_NODE_TYPES.UnaryExpression:
|
|
893
1079
|
return `${node.operator}${stringifyCondition(node.argument)}`;
|
|
894
1080
|
case AST_NODE_TYPES.MemberExpression:
|
|
895
1081
|
return `${stringifyCondition(node.object)}.${stringifyCondition(node.property)}`;
|
|
896
|
-
case AST_NODE_TYPES.CallExpression:
|
|
897
|
-
|
|
1082
|
+
case AST_NODE_TYPES.CallExpression: {
|
|
1083
|
+
const args = node.arguments.map((arg) => stringifyCondition(arg)).join(", ");
|
|
1084
|
+
return `${stringifyCondition(node.callee)}(${args})`;
|
|
1085
|
+
}
|
|
1086
|
+
case AST_NODE_TYPES.ConditionalExpression:
|
|
1087
|
+
return `${stringifyCondition(node.test)} ? ${stringifyCondition(node.consequent)} : ${stringifyCondition(node.alternate)}`;
|
|
1088
|
+
case AST_NODE_TYPES.TemplateLiteral:
|
|
1089
|
+
return `\`template@${node.loc?.start.line}:${node.loc?.start.column}\``;
|
|
1090
|
+
case AST_NODE_TYPES.ArrayExpression:
|
|
1091
|
+
return `[${node.elements.map((e) => e ? stringifyCondition(e) : "empty").join(", ")}]`;
|
|
1092
|
+
case AST_NODE_TYPES.ObjectExpression:
|
|
1093
|
+
return `{obj@${node.loc?.start.line}:${node.loc?.start.column}}`;
|
|
1094
|
+
case AST_NODE_TYPES.AssignmentExpression:
|
|
1095
|
+
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
1096
|
+
case AST_NODE_TYPES.NewExpression:
|
|
1097
|
+
return `new ${stringifyCondition(node.callee)}(${node.arguments.map((a) => stringifyCondition(a)).join(", ")})`;
|
|
1098
|
+
case AST_NODE_TYPES.TSAsExpression:
|
|
1099
|
+
case AST_NODE_TYPES.TSNonNullExpression:
|
|
1100
|
+
return stringifyCondition(node.expression);
|
|
1101
|
+
case AST_NODE_TYPES.AwaitExpression:
|
|
1102
|
+
return `await ${stringifyCondition(node.argument)}`;
|
|
1103
|
+
case AST_NODE_TYPES.ChainExpression:
|
|
1104
|
+
return stringifyCondition(node.expression);
|
|
898
1105
|
default:
|
|
899
|
-
return `[${node.type}]`;
|
|
1106
|
+
return `[${node.type}@${node.loc?.start.line}:${node.loc?.start.column}]`;
|
|
900
1107
|
}
|
|
901
1108
|
}
|
|
902
1109
|
function truncate(s, maxLen) {
|
|
@@ -1055,9 +1262,11 @@ var securityRules = [
|
|
|
1055
1262
|
const issues = [];
|
|
1056
1263
|
const lines = context.fileContent.split("\n");
|
|
1057
1264
|
for (let i = 0; i < lines.length; i++) {
|
|
1058
|
-
const
|
|
1265
|
+
const line = lines[i];
|
|
1266
|
+
const trimmed = line.trim();
|
|
1059
1267
|
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
1060
|
-
|
|
1268
|
+
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "").replace(/\/[^/]+\/[dgimsuvy]*/g, '""');
|
|
1269
|
+
if (/\.(innerHTML|outerHTML)\s*=/.test(cleaned) || /dangerouslySetInnerHTML/.test(cleaned)) {
|
|
1061
1270
|
issues.push({
|
|
1062
1271
|
ruleId: "security/dangerous-html",
|
|
1063
1272
|
severity: "medium",
|
|
@@ -1106,7 +1315,8 @@ var emptyCatchRule = {
|
|
|
1106
1315
|
const catchMatch = trimmed.match(/\bcatch\s*\(\s*(\w+)?\s*\)\s*\{/);
|
|
1107
1316
|
if (!catchMatch) continue;
|
|
1108
1317
|
const catchVarName = catchMatch[1] || "";
|
|
1109
|
-
const
|
|
1318
|
+
const catchIdx = lines[i].indexOf("catch");
|
|
1319
|
+
const blockContent = extractBraceBlock(lines, i, catchIdx);
|
|
1110
1320
|
if (!blockContent) continue;
|
|
1111
1321
|
const { bodyLines, endLine } = blockContent;
|
|
1112
1322
|
const meaningful = bodyLines.filter(
|
|
@@ -1156,37 +1366,6 @@ var emptyCatchRule = {
|
|
|
1156
1366
|
return issues;
|
|
1157
1367
|
}
|
|
1158
1368
|
};
|
|
1159
|
-
function extractCatchBody(lines, catchLineIndex) {
|
|
1160
|
-
const catchLine = lines[catchLineIndex];
|
|
1161
|
-
const catchIdx = catchLine.indexOf("catch");
|
|
1162
|
-
if (catchIdx === -1) return null;
|
|
1163
|
-
let braceCount = 0;
|
|
1164
|
-
let started = false;
|
|
1165
|
-
let bodyStart = -1;
|
|
1166
|
-
for (let i = catchLineIndex; i < lines.length; i++) {
|
|
1167
|
-
const line = lines[i];
|
|
1168
|
-
const startJ = i === catchLineIndex ? catchIdx : 0;
|
|
1169
|
-
for (let j = startJ; j < line.length; j++) {
|
|
1170
|
-
const ch = line[j];
|
|
1171
|
-
if (ch === "{") {
|
|
1172
|
-
braceCount++;
|
|
1173
|
-
if (!started) {
|
|
1174
|
-
started = true;
|
|
1175
|
-
bodyStart = i;
|
|
1176
|
-
}
|
|
1177
|
-
} else if (ch === "}") {
|
|
1178
|
-
braceCount--;
|
|
1179
|
-
if (started && braceCount === 0) {
|
|
1180
|
-
return {
|
|
1181
|
-
bodyLines: lines.slice(bodyStart + 1, i),
|
|
1182
|
-
endLine: i
|
|
1183
|
-
};
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
return null;
|
|
1189
|
-
}
|
|
1190
1369
|
|
|
1191
1370
|
// src/rules/builtin/identical-branches.ts
|
|
1192
1371
|
var identicalBranchesRule = {
|
|
@@ -1533,10 +1712,25 @@ var unusedImportRule = {
|
|
|
1533
1712
|
}
|
|
1534
1713
|
return;
|
|
1535
1714
|
});
|
|
1536
|
-
const
|
|
1537
|
-
let
|
|
1538
|
-
|
|
1539
|
-
|
|
1715
|
+
const codeLines = context.fileContent.split("\n");
|
|
1716
|
+
let inBlock = false;
|
|
1717
|
+
for (const codeLine of codeLines) {
|
|
1718
|
+
const trimmedCode = codeLine.trim();
|
|
1719
|
+
if (inBlock) {
|
|
1720
|
+
if (trimmedCode.includes("*/")) inBlock = false;
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
if (trimmedCode.startsWith("/*")) {
|
|
1724
|
+
if (!trimmedCode.includes("*/")) inBlock = true;
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
if (trimmedCode.startsWith("//") || trimmedCode.startsWith("*")) continue;
|
|
1728
|
+
const cleaned = codeLine.replace(/\/\/.*$/, "").replace(/\/\*.*?\*\//g, "").replace(/'(?:[^'\\]|\\.)*'/g, "").replace(/"(?:[^"\\]|\\.)*"/g, "").replace(/`(?:[^`\\]|\\.)*`/g, "");
|
|
1729
|
+
const typeRefPattern = /\b([A-Z][A-Za-z0-9]*)\b/g;
|
|
1730
|
+
let match;
|
|
1731
|
+
while ((match = typeRefPattern.exec(cleaned)) !== null) {
|
|
1732
|
+
references.add(match[1]);
|
|
1733
|
+
}
|
|
1540
1734
|
}
|
|
1541
1735
|
for (const imp of imports) {
|
|
1542
1736
|
if (!references.has(imp.local)) {
|
|
@@ -1585,6 +1779,9 @@ var missingAwaitRule = {
|
|
|
1585
1779
|
const body = getFunctionBody(node);
|
|
1586
1780
|
if (!body) return;
|
|
1587
1781
|
walkAST(body, (inner, parent) => {
|
|
1782
|
+
if (inner.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1588
1785
|
if (inner !== body && isAsyncFunction(inner)) return false;
|
|
1589
1786
|
if (inner.type !== AST_NODE_TYPES.CallExpression) return;
|
|
1590
1787
|
if (parent?.type === AST_NODE_TYPES.AwaitExpression) return;
|
|
@@ -1594,6 +1791,9 @@ var missingAwaitRule = {
|
|
|
1594
1791
|
if (parent?.type === AST_NODE_TYPES.AssignmentExpression) return;
|
|
1595
1792
|
if (parent?.type === AST_NODE_TYPES.ArrayExpression) return;
|
|
1596
1793
|
if (parent?.type === AST_NODE_TYPES.CallExpression && parent !== inner) return;
|
|
1794
|
+
if (parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1597
1797
|
const callName = getCallName(inner);
|
|
1598
1798
|
if (!callName) return;
|
|
1599
1799
|
if (!asyncFuncNames.has(callName)) return;
|
|
@@ -1805,9 +2005,28 @@ var typeCoercionRule = {
|
|
|
1805
2005
|
var ALLOWED_NUMBERS = /* @__PURE__ */ new Set([
|
|
1806
2006
|
-1,
|
|
1807
2007
|
0,
|
|
2008
|
+
0.1,
|
|
2009
|
+
0.1,
|
|
2010
|
+
0.15,
|
|
2011
|
+
0.2,
|
|
2012
|
+
0.2,
|
|
2013
|
+
0.25,
|
|
2014
|
+
0.3,
|
|
2015
|
+
0.3,
|
|
2016
|
+
0.5,
|
|
1808
2017
|
1,
|
|
1809
2018
|
2,
|
|
2019
|
+
3,
|
|
2020
|
+
4,
|
|
2021
|
+
5,
|
|
1810
2022
|
10,
|
|
2023
|
+
15,
|
|
2024
|
+
20,
|
|
2025
|
+
30,
|
|
2026
|
+
40,
|
|
2027
|
+
50,
|
|
2028
|
+
70,
|
|
2029
|
+
90,
|
|
1811
2030
|
100
|
|
1812
2031
|
]);
|
|
1813
2032
|
var magicNumberRule = {
|
|
@@ -1836,6 +2055,7 @@ var magicNumberRule = {
|
|
|
1836
2055
|
if (/^\s*(export\s+)?enum\s/.test(line)) continue;
|
|
1837
2056
|
if (trimmed.startsWith("import ")) continue;
|
|
1838
2057
|
if (/^\s*return\s+[0-9]+\s*;?\s*$/.test(line)) continue;
|
|
2058
|
+
if (/^\s*['"]?[-\w]+['"]?\s*:\s*-?\d+\.?\d*(?:e[+-]?\d+)?\s*,?\s*$/.test(trimmed)) continue;
|
|
1839
2059
|
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
|
|
1840
2060
|
const numRegex = /(?<![.\w])(-?\d+\.?\d*(?:e[+-]?\d+)?)\b/gi;
|
|
1841
2061
|
let match;
|
|
@@ -1932,6 +2152,17 @@ var nestedTernaryRule = {
|
|
|
1932
2152
|
// src/rules/builtin/duplicate-string.ts
|
|
1933
2153
|
var MIN_STRING_LENGTH = 6;
|
|
1934
2154
|
var MIN_OCCURRENCES = 3;
|
|
2155
|
+
var IGNORED_LITERALS = /* @__PURE__ */ new Set([
|
|
2156
|
+
"high",
|
|
2157
|
+
"medium",
|
|
2158
|
+
"low",
|
|
2159
|
+
"info",
|
|
2160
|
+
"logic",
|
|
2161
|
+
"security",
|
|
2162
|
+
"structure",
|
|
2163
|
+
"style",
|
|
2164
|
+
"coverage"
|
|
2165
|
+
]);
|
|
1935
2166
|
var duplicateStringRule = {
|
|
1936
2167
|
id: "logic/duplicate-string",
|
|
1937
2168
|
category: "logic",
|
|
@@ -1962,6 +2193,7 @@ var duplicateStringRule = {
|
|
|
1962
2193
|
while ((match = stringRegex.exec(cleaned)) !== null) {
|
|
1963
2194
|
const value = match[2];
|
|
1964
2195
|
if (value.length < MIN_STRING_LENGTH) continue;
|
|
2196
|
+
if (IGNORED_LITERALS.has(value)) continue;
|
|
1965
2197
|
if (value.includes("${")) continue;
|
|
1966
2198
|
if (value.startsWith("http") || value.startsWith("/")) continue;
|
|
1967
2199
|
if (value.startsWith("test") || value.startsWith("mock")) continue;
|
|
@@ -2231,13 +2463,28 @@ var promiseVoidRule = {
|
|
|
2231
2463
|
/^save/,
|
|
2232
2464
|
/^load/,
|
|
2233
2465
|
/^send/,
|
|
2234
|
-
/^delete/,
|
|
2235
2466
|
/^update/,
|
|
2236
2467
|
/^create/,
|
|
2237
2468
|
/^connect/,
|
|
2238
2469
|
/^disconnect/,
|
|
2239
2470
|
/^init/
|
|
2240
2471
|
];
|
|
2472
|
+
const syncMethods = [
|
|
2473
|
+
"delete",
|
|
2474
|
+
// Map.delete(), Set.delete(), Object.delete() are synchronous
|
|
2475
|
+
"has",
|
|
2476
|
+
// Map.has(), Set.has() are synchronous
|
|
2477
|
+
"get",
|
|
2478
|
+
// Map.get() is synchronous
|
|
2479
|
+
"set",
|
|
2480
|
+
// Map.set() is synchronous (though some consider it potentially async)
|
|
2481
|
+
"keys",
|
|
2482
|
+
// Object.keys() is synchronous
|
|
2483
|
+
"values",
|
|
2484
|
+
// Object.values() is synchronous
|
|
2485
|
+
"entries"
|
|
2486
|
+
// Object.entries() is synchronous
|
|
2487
|
+
];
|
|
2241
2488
|
walkAST(ast, (node) => {
|
|
2242
2489
|
if (node.type !== AST_NODE_TYPES.ExpressionStatement) return;
|
|
2243
2490
|
const expr = node.expression;
|
|
@@ -2248,6 +2495,7 @@ var promiseVoidRule = {
|
|
|
2248
2495
|
const isKnownAsync = asyncFnNames.has(fnName);
|
|
2249
2496
|
const matchesPattern = commonAsyncPatterns.some((p) => p.test(fnName));
|
|
2250
2497
|
const endsWithAsync = fnName.endsWith("Async") || fnName.endsWith("async");
|
|
2498
|
+
if (syncMethods.includes(fnName)) return;
|
|
2251
2499
|
if (!isKnownAsync && !matchesPattern && !endsWithAsync) return;
|
|
2252
2500
|
const line = node.loc?.start.line ?? 0;
|
|
2253
2501
|
if (line === 0) return;
|
|
@@ -2608,13 +2856,20 @@ var SEVERITY_PENALTY = {
|
|
|
2608
2856
|
low: 3,
|
|
2609
2857
|
info: 0
|
|
2610
2858
|
};
|
|
2859
|
+
var DIMINISHING_FACTOR = 0.7;
|
|
2611
2860
|
function calculateDimensionScore(issues) {
|
|
2612
2861
|
let score = 100;
|
|
2862
|
+
const severityCounts = {};
|
|
2613
2863
|
for (const issue of issues) {
|
|
2614
|
-
|
|
2864
|
+
const base = SEVERITY_PENALTY[issue.severity] ?? 0;
|
|
2865
|
+
if (base === 0) continue;
|
|
2866
|
+
const n = severityCounts[issue.severity] ?? 0;
|
|
2867
|
+
severityCounts[issue.severity] = n + 1;
|
|
2868
|
+
const penalty = base * Math.pow(DIMINISHING_FACTOR, n);
|
|
2869
|
+
score -= penalty;
|
|
2615
2870
|
}
|
|
2616
2871
|
return {
|
|
2617
|
-
score: Math.max(0, Math.min(100, score)),
|
|
2872
|
+
score: Math.round(Math.max(0, Math.min(100, score)) * 10) / 10,
|
|
2618
2873
|
issues
|
|
2619
2874
|
};
|
|
2620
2875
|
}
|
|
@@ -2967,7 +3222,7 @@ var PKG_VERSION = (() => {
|
|
|
2967
3222
|
}
|
|
2968
3223
|
})();
|
|
2969
3224
|
var REPORT_SCHEMA_VERSION = "1.0.0";
|
|
2970
|
-
var FINGERPRINT_VERSION = "
|
|
3225
|
+
var FINGERPRINT_VERSION = "2";
|
|
2971
3226
|
var ScanEngine = class {
|
|
2972
3227
|
config;
|
|
2973
3228
|
diffParser;
|
|
@@ -2999,7 +3254,11 @@ var ScanEngine = class {
|
|
|
2999
3254
|
}
|
|
3000
3255
|
}
|
|
3001
3256
|
const issuesWithFingerprints = this.attachFingerprints(allIssues);
|
|
3002
|
-
const
|
|
3257
|
+
const baseline = await this.loadBaseline(options.baseline);
|
|
3258
|
+
const issuesWithLifecycle = this.attachLifecycle(issuesWithFingerprints, baseline);
|
|
3259
|
+
const fixedIssues = this.getFixedIssues(issuesWithLifecycle, baseline);
|
|
3260
|
+
const lifecycle = this.buildLifecycleSummary(issuesWithLifecycle, fixedIssues, baseline);
|
|
3261
|
+
const dimensions = this.groupByDimension(issuesWithLifecycle);
|
|
3003
3262
|
const overallScore = calculateOverallScore(dimensions, this.config.weights);
|
|
3004
3263
|
const grade = getGrade(overallScore);
|
|
3005
3264
|
const commitHash = await this.diffParser.getCurrentCommitHash();
|
|
@@ -3013,7 +3272,7 @@ var ScanEngine = class {
|
|
|
3013
3272
|
score: overallScore,
|
|
3014
3273
|
grade,
|
|
3015
3274
|
filesScanned,
|
|
3016
|
-
issuesFound:
|
|
3275
|
+
issuesFound: issuesWithLifecycle.length
|
|
3017
3276
|
},
|
|
3018
3277
|
toolHealth: {
|
|
3019
3278
|
rulesExecuted,
|
|
@@ -3026,70 +3285,75 @@ var ScanEngine = class {
|
|
|
3026
3285
|
ruleFailures
|
|
3027
3286
|
},
|
|
3028
3287
|
dimensions,
|
|
3029
|
-
issues:
|
|
3288
|
+
issues: issuesWithLifecycle.sort((a, b) => {
|
|
3030
3289
|
const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
|
|
3031
3290
|
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
3032
|
-
})
|
|
3291
|
+
}),
|
|
3292
|
+
lifecycle,
|
|
3293
|
+
fixedIssues
|
|
3033
3294
|
};
|
|
3034
3295
|
}
|
|
3035
3296
|
async scanFile(diffFile) {
|
|
3036
3297
|
if (diffFile.status === "deleted") {
|
|
3037
|
-
return {
|
|
3038
|
-
issues: [],
|
|
3039
|
-
ruleFailures: [],
|
|
3040
|
-
rulesExecuted: 0,
|
|
3041
|
-
rulesFailed: 0,
|
|
3042
|
-
scanErrors: [
|
|
3043
|
-
{
|
|
3044
|
-
type: "deleted-file",
|
|
3045
|
-
file: diffFile.filePath,
|
|
3046
|
-
message: `Skipped deleted file: ${diffFile.filePath}`
|
|
3047
|
-
}
|
|
3048
|
-
],
|
|
3049
|
-
scanned: false
|
|
3050
|
-
};
|
|
3298
|
+
return this.createSkippedResult(diffFile, "deleted-file", `Skipped deleted file: ${diffFile.filePath}`);
|
|
3051
3299
|
}
|
|
3052
3300
|
if (!this.isTsJsFile(diffFile.filePath)) {
|
|
3053
|
-
return {
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
scanErrors: [
|
|
3059
|
-
{
|
|
3060
|
-
type: "unsupported-file-type",
|
|
3061
|
-
file: diffFile.filePath,
|
|
3062
|
-
message: `Skipped unsupported file type: ${diffFile.filePath}`
|
|
3063
|
-
}
|
|
3064
|
-
],
|
|
3065
|
-
scanned: false
|
|
3066
|
-
};
|
|
3301
|
+
return this.createSkippedResult(diffFile, "unsupported-file-type", `Skipped unsupported file type: ${diffFile.filePath}`);
|
|
3302
|
+
}
|
|
3303
|
+
const fileContent = await this.readFileContent(diffFile);
|
|
3304
|
+
if (!fileContent) {
|
|
3305
|
+
return this.createErrorResult(diffFile, "missing-file-content", `Unable to read file content for ${diffFile.filePath}`);
|
|
3067
3306
|
}
|
|
3307
|
+
const addedLines = this.extractAddedLines(diffFile);
|
|
3308
|
+
const ruleResult = this.ruleEngine.runWithDiagnostics({
|
|
3309
|
+
filePath: diffFile.filePath,
|
|
3310
|
+
fileContent,
|
|
3311
|
+
addedLines
|
|
3312
|
+
});
|
|
3313
|
+
const issues = [...ruleResult.issues];
|
|
3314
|
+
issues.push(...this.runStructureAnalysis(fileContent, diffFile.filePath));
|
|
3315
|
+
issues.push(...analyzeStyle(fileContent, diffFile.filePath).issues);
|
|
3316
|
+
issues.push(...analyzeCoverage(fileContent, diffFile.filePath).issues);
|
|
3317
|
+
return {
|
|
3318
|
+
issues,
|
|
3319
|
+
ruleFailures: ruleResult.ruleFailures,
|
|
3320
|
+
rulesExecuted: ruleResult.rulesExecuted,
|
|
3321
|
+
rulesFailed: ruleResult.rulesFailed,
|
|
3322
|
+
scanErrors: [],
|
|
3323
|
+
scanned: true
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
createSkippedResult(diffFile, type, message) {
|
|
3327
|
+
return {
|
|
3328
|
+
issues: [],
|
|
3329
|
+
ruleFailures: [],
|
|
3330
|
+
rulesExecuted: 0,
|
|
3331
|
+
rulesFailed: 0,
|
|
3332
|
+
scanErrors: [{ type, file: diffFile.filePath, message }],
|
|
3333
|
+
scanned: false
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
createErrorResult(diffFile, type, message) {
|
|
3337
|
+
return {
|
|
3338
|
+
issues: [],
|
|
3339
|
+
ruleFailures: [],
|
|
3340
|
+
rulesExecuted: 0,
|
|
3341
|
+
rulesFailed: 0,
|
|
3342
|
+
scanErrors: [{ type, file: diffFile.filePath, message }],
|
|
3343
|
+
scanned: false
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
async readFileContent(diffFile) {
|
|
3068
3347
|
const filePath = resolve2(diffFile.filePath);
|
|
3069
|
-
let fileContent;
|
|
3070
3348
|
try {
|
|
3071
|
-
|
|
3349
|
+
return await readFile(filePath, "utf-8");
|
|
3072
3350
|
} catch {
|
|
3073
3351
|
const content = await this.diffParser.getFileContent(diffFile.filePath);
|
|
3074
|
-
|
|
3075
|
-
return {
|
|
3076
|
-
issues: [],
|
|
3077
|
-
ruleFailures: [],
|
|
3078
|
-
rulesExecuted: 0,
|
|
3079
|
-
rulesFailed: 0,
|
|
3080
|
-
scanErrors: [
|
|
3081
|
-
{
|
|
3082
|
-
type: "missing-file-content",
|
|
3083
|
-
file: diffFile.filePath,
|
|
3084
|
-
message: `Unable to read file content for ${diffFile.filePath}`
|
|
3085
|
-
}
|
|
3086
|
-
],
|
|
3087
|
-
scanned: false
|
|
3088
|
-
};
|
|
3089
|
-
}
|
|
3090
|
-
fileContent = content;
|
|
3352
|
+
return content ?? null;
|
|
3091
3353
|
}
|
|
3092
|
-
|
|
3354
|
+
}
|
|
3355
|
+
extractAddedLines(diffFile) {
|
|
3356
|
+
return diffFile.hunks.flatMap((hunk) => {
|
|
3093
3357
|
const lines = hunk.content.split("\n");
|
|
3094
3358
|
const result = [];
|
|
3095
3359
|
let currentLine = hunk.newStart;
|
|
@@ -3104,32 +3368,15 @@ var ScanEngine = class {
|
|
|
3104
3368
|
}
|
|
3105
3369
|
return result;
|
|
3106
3370
|
});
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
addedLines
|
|
3111
|
-
});
|
|
3112
|
-
const issues = [...ruleResult.issues];
|
|
3113
|
-
const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
|
|
3371
|
+
}
|
|
3372
|
+
runStructureAnalysis(fileContent, filePath) {
|
|
3373
|
+
return analyzeStructure(fileContent, filePath, {
|
|
3114
3374
|
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
3115
3375
|
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3116
3376
|
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3117
3377
|
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3118
3378
|
maxParamCount: this.config.thresholds["max-params"]
|
|
3119
|
-
});
|
|
3120
|
-
issues.push(...structureResult.issues);
|
|
3121
|
-
const styleResult = analyzeStyle(fileContent, diffFile.filePath);
|
|
3122
|
-
issues.push(...styleResult.issues);
|
|
3123
|
-
const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
|
|
3124
|
-
issues.push(...coverageResult.issues);
|
|
3125
|
-
return {
|
|
3126
|
-
issues,
|
|
3127
|
-
ruleFailures: ruleResult.ruleFailures,
|
|
3128
|
-
rulesExecuted: ruleResult.rulesExecuted,
|
|
3129
|
-
rulesFailed: ruleResult.rulesFailed,
|
|
3130
|
-
scanErrors: [],
|
|
3131
|
-
scanned: true
|
|
3132
|
-
};
|
|
3379
|
+
}).issues;
|
|
3133
3380
|
}
|
|
3134
3381
|
async getScanCandidates(options) {
|
|
3135
3382
|
const scanMode = this.getScanMode(options);
|
|
@@ -3170,7 +3417,7 @@ var ScanEngine = class {
|
|
|
3170
3417
|
}
|
|
3171
3418
|
return "changed";
|
|
3172
3419
|
}
|
|
3173
|
-
|
|
3420
|
+
getDiffFiles(options) {
|
|
3174
3421
|
if (options.staged) {
|
|
3175
3422
|
return this.diffParser.getStagedFiles();
|
|
3176
3423
|
}
|
|
@@ -3227,13 +3474,14 @@ var ScanEngine = class {
|
|
|
3227
3474
|
const occurrenceCounts = /* @__PURE__ */ new Map();
|
|
3228
3475
|
return issues.map((issue) => {
|
|
3229
3476
|
const normalizedFile = this.normalizeRelativePath(issue.file);
|
|
3230
|
-
const
|
|
3477
|
+
const contentSource = issue.codeSnippet ? issue.codeSnippet : `${issue.startLine}:${issue.endLine}`;
|
|
3478
|
+
const contentDigest = createHash("sha256").update(contentSource).digest("hex").slice(0, 16);
|
|
3231
3479
|
const baseKey = [
|
|
3232
3480
|
issue.ruleId,
|
|
3233
3481
|
normalizedFile,
|
|
3234
3482
|
issue.category,
|
|
3235
3483
|
issue.severity,
|
|
3236
|
-
|
|
3484
|
+
contentDigest
|
|
3237
3485
|
].join("|");
|
|
3238
3486
|
const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
|
|
3239
3487
|
occurrenceCounts.set(baseKey, occurrenceIndex + 1);
|
|
@@ -3251,18 +3499,110 @@ var ScanEngine = class {
|
|
|
3251
3499
|
const relativePath = relative(process.cwd(), absolutePath) || filePath;
|
|
3252
3500
|
return relativePath.split(sep).join("/");
|
|
3253
3501
|
}
|
|
3502
|
+
async loadBaseline(baselinePath) {
|
|
3503
|
+
if (!baselinePath) {
|
|
3504
|
+
return void 0;
|
|
3505
|
+
}
|
|
3506
|
+
const baselineContent = await readFile(resolve2(baselinePath), "utf-8");
|
|
3507
|
+
const parsed = JSON.parse(baselineContent);
|
|
3508
|
+
const issues = this.parseBaselineIssues(parsed.issues);
|
|
3509
|
+
return {
|
|
3510
|
+
issues,
|
|
3511
|
+
fingerprintSet: new Set(issues.map((issue) => issue.fingerprint)),
|
|
3512
|
+
commit: typeof parsed.commit === "string" ? parsed.commit : void 0,
|
|
3513
|
+
timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3516
|
+
parseBaselineIssues(input) {
|
|
3517
|
+
if (!Array.isArray(input)) {
|
|
3518
|
+
return [];
|
|
3519
|
+
}
|
|
3520
|
+
return input.flatMap((item) => {
|
|
3521
|
+
const issue = this.parseBaselineIssue(item);
|
|
3522
|
+
return issue ? [issue] : [];
|
|
3523
|
+
});
|
|
3524
|
+
}
|
|
3525
|
+
parseBaselineIssue(input) {
|
|
3526
|
+
if (!input || typeof input !== "object") {
|
|
3527
|
+
return void 0;
|
|
3528
|
+
}
|
|
3529
|
+
const issue = input;
|
|
3530
|
+
if (!this.isValidBaselineIssue(issue)) {
|
|
3531
|
+
return void 0;
|
|
3532
|
+
}
|
|
3533
|
+
return {
|
|
3534
|
+
ruleId: issue.ruleId,
|
|
3535
|
+
severity: issue.severity,
|
|
3536
|
+
category: issue.category,
|
|
3537
|
+
file: issue.file,
|
|
3538
|
+
startLine: issue.startLine,
|
|
3539
|
+
endLine: issue.endLine,
|
|
3540
|
+
message: issue.message,
|
|
3541
|
+
fingerprint: issue.fingerprint,
|
|
3542
|
+
fingerprintVersion: typeof issue.fingerprintVersion === "string" ? issue.fingerprintVersion : void 0
|
|
3543
|
+
};
|
|
3544
|
+
}
|
|
3545
|
+
isValidBaselineIssue(issue) {
|
|
3546
|
+
return typeof issue.ruleId === "string" && this.isSeverity(issue.severity) && this.isRuleCategory(issue.category) && typeof issue.file === "string" && typeof issue.startLine === "number" && typeof issue.endLine === "number" && typeof issue.message === "string" && typeof issue.fingerprint === "string";
|
|
3547
|
+
}
|
|
3548
|
+
attachLifecycle(issues, baseline) {
|
|
3549
|
+
if (!baseline) {
|
|
3550
|
+
return issues;
|
|
3551
|
+
}
|
|
3552
|
+
return issues.map((issue) => ({
|
|
3553
|
+
...issue,
|
|
3554
|
+
lifecycle: baseline.fingerprintSet.has(issue.fingerprint) ? "existing" : "new"
|
|
3555
|
+
}));
|
|
3556
|
+
}
|
|
3557
|
+
getFixedIssues(issues, baseline) {
|
|
3558
|
+
if (!baseline) {
|
|
3559
|
+
return [];
|
|
3560
|
+
}
|
|
3561
|
+
const currentFingerprints = new Set(issues.map((issue) => issue.fingerprint));
|
|
3562
|
+
return baseline.issues.filter((issue) => !currentFingerprints.has(issue.fingerprint));
|
|
3563
|
+
}
|
|
3564
|
+
buildLifecycleSummary(issues, fixedIssues, baseline) {
|
|
3565
|
+
if (!baseline) {
|
|
3566
|
+
return void 0;
|
|
3567
|
+
}
|
|
3568
|
+
let newIssues = 0;
|
|
3569
|
+
let existingIssues = 0;
|
|
3570
|
+
for (const issue of issues) {
|
|
3571
|
+
if (issue.lifecycle === "existing") {
|
|
3572
|
+
existingIssues++;
|
|
3573
|
+
} else {
|
|
3574
|
+
newIssues++;
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
return {
|
|
3578
|
+
newIssues,
|
|
3579
|
+
existingIssues,
|
|
3580
|
+
fixedIssues: fixedIssues.length,
|
|
3581
|
+
baselineUsed: true,
|
|
3582
|
+
baselineCommit: baseline.commit,
|
|
3583
|
+
baselineTimestamp: baseline.timestamp
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
isSeverity(value) {
|
|
3587
|
+
return value === "high" || value === "medium" || value === "low" || value === "info";
|
|
3588
|
+
}
|
|
3589
|
+
isRuleCategory(value) {
|
|
3590
|
+
return ["security", "logic", "structure", "style", "coverage"].includes(value);
|
|
3591
|
+
}
|
|
3254
3592
|
groupByDimension(issues) {
|
|
3255
|
-
const
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3593
|
+
const buckets = {
|
|
3594
|
+
security: [],
|
|
3595
|
+
logic: [],
|
|
3596
|
+
structure: [],
|
|
3597
|
+
style: [],
|
|
3598
|
+
coverage: []
|
|
3599
|
+
};
|
|
3600
|
+
for (const issue of issues) {
|
|
3601
|
+
buckets[issue.category]?.push(issue);
|
|
3602
|
+
}
|
|
3262
3603
|
const grouped = {};
|
|
3263
|
-
for (const cat of
|
|
3264
|
-
|
|
3265
|
-
grouped[cat] = calculateDimensionScore(catIssues);
|
|
3604
|
+
for (const cat of Object.keys(buckets)) {
|
|
3605
|
+
grouped[cat] = calculateDimensionScore(buckets[cat]);
|
|
3266
3606
|
}
|
|
3267
3607
|
return grouped;
|
|
3268
3608
|
}
|
|
@@ -3366,6 +3706,7 @@ thresholds:
|
|
|
3366
3706
|
min-score: 70
|
|
3367
3707
|
max-function-length: 40
|
|
3368
3708
|
max-cyclomatic-complexity: 10
|
|
3709
|
+
max-cognitive-complexity: 20
|
|
3369
3710
|
max-nesting-depth: 4
|
|
3370
3711
|
max-params: 5
|
|
3371
3712
|
|