@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/cli/index.js
CHANGED
|
@@ -104,6 +104,7 @@ thresholds:
|
|
|
104
104
|
min-score: 70
|
|
105
105
|
max-function-length: 40
|
|
106
106
|
max-cyclomatic-complexity: 10
|
|
107
|
+
max-cognitive-complexity: 20
|
|
107
108
|
max-nesting-depth: 4
|
|
108
109
|
max-params: 5
|
|
109
110
|
|
|
@@ -128,33 +129,64 @@ import { fileURLToPath } from "url";
|
|
|
128
129
|
|
|
129
130
|
// src/parsers/diff.ts
|
|
130
131
|
import simpleGit from "simple-git";
|
|
132
|
+
var GIT_DIFF_UNIFIED = "--unified=3";
|
|
133
|
+
var SHORT_HASH_LENGTH = 7;
|
|
131
134
|
var DiffParser = class {
|
|
132
135
|
git;
|
|
133
136
|
constructor(workDir) {
|
|
134
137
|
this.git = simpleGit(workDir);
|
|
135
138
|
}
|
|
136
139
|
async getStagedFiles() {
|
|
137
|
-
const diffDetail = await this.git.diff(["--cached",
|
|
140
|
+
const diffDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
|
|
138
141
|
return this.parseDiffOutput(diffDetail);
|
|
139
142
|
}
|
|
140
143
|
async getDiffFromRef(ref) {
|
|
141
|
-
const diffDetail = await this.git.diff([ref,
|
|
144
|
+
const diffDetail = await this.git.diff([ref, GIT_DIFF_UNIFIED]);
|
|
142
145
|
return this.parseDiffOutput(diffDetail);
|
|
143
146
|
}
|
|
144
147
|
async getChangedFiles() {
|
|
145
|
-
const diffDetail = await this.git.diff([
|
|
146
|
-
const stagedDetail = await this.git.diff(["--cached",
|
|
147
|
-
const
|
|
148
|
-
|
|
148
|
+
const diffDetail = await this.git.diff([GIT_DIFF_UNIFIED]);
|
|
149
|
+
const stagedDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
|
|
150
|
+
const unstagedFiles = this.parseDiffOutput(diffDetail);
|
|
151
|
+
const stagedFiles = this.parseDiffOutput(stagedDetail);
|
|
152
|
+
return this.mergeDiffFiles(unstagedFiles, stagedFiles);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Merge two sets of diff files, deduplicating by file path.
|
|
156
|
+
* When a file appears in both, merge their hunks and combine stats.
|
|
157
|
+
*/
|
|
158
|
+
mergeDiffFiles(unstaged, staged) {
|
|
159
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
160
|
+
for (const file of unstaged) {
|
|
161
|
+
fileMap.set(file.filePath, file);
|
|
162
|
+
}
|
|
163
|
+
for (const file of staged) {
|
|
164
|
+
const existing = fileMap.get(file.filePath);
|
|
165
|
+
if (existing) {
|
|
166
|
+
fileMap.set(file.filePath, {
|
|
167
|
+
...existing,
|
|
168
|
+
// Combine additions/deletions
|
|
169
|
+
additions: existing.additions + file.additions,
|
|
170
|
+
deletions: existing.deletions + file.deletions,
|
|
171
|
+
// Merge hunks (preserve order: staged first, then unstaged)
|
|
172
|
+
hunks: [...file.hunks, ...existing.hunks],
|
|
173
|
+
// Status: if either is 'added', treat as added; otherwise keep modified
|
|
174
|
+
status: existing.status === "added" || file.status === "added" ? "added" : "modified"
|
|
175
|
+
});
|
|
176
|
+
} else {
|
|
177
|
+
fileMap.set(file.filePath, file);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return Array.from(fileMap.values());
|
|
149
181
|
}
|
|
150
182
|
async getLastCommitDiff() {
|
|
151
|
-
const diffDetail = await this.git.diff(["HEAD~1", "HEAD",
|
|
183
|
+
const diffDetail = await this.git.diff(["HEAD~1", "HEAD", GIT_DIFF_UNIFIED]);
|
|
152
184
|
return this.parseDiffOutput(diffDetail);
|
|
153
185
|
}
|
|
154
186
|
async getCurrentCommitHash() {
|
|
155
187
|
try {
|
|
156
188
|
const hash = await this.git.revparse(["HEAD"]);
|
|
157
|
-
return hash.trim().slice(0,
|
|
189
|
+
return hash.trim().slice(0, SHORT_HASH_LENGTH);
|
|
158
190
|
} catch {
|
|
159
191
|
return void 0;
|
|
160
192
|
}
|
|
@@ -180,9 +212,18 @@ var DiffParser = class {
|
|
|
180
212
|
}
|
|
181
213
|
parseFileDiff(fileDiff) {
|
|
182
214
|
const lines = fileDiff.split("\n");
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
215
|
+
let filePath = null;
|
|
216
|
+
for (const line of lines) {
|
|
217
|
+
if (line.startsWith("+++ b/")) {
|
|
218
|
+
filePath = line.slice(6);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!filePath) {
|
|
223
|
+
const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
|
|
224
|
+
if (!headerMatch) return null;
|
|
225
|
+
filePath = headerMatch[2];
|
|
226
|
+
}
|
|
186
227
|
let status = "modified";
|
|
187
228
|
let additions = 0;
|
|
188
229
|
let deletions = 0;
|
|
@@ -286,6 +327,144 @@ function t(en2, zh2) {
|
|
|
286
327
|
return isZhLocale() ? zh2 : en2;
|
|
287
328
|
}
|
|
288
329
|
|
|
330
|
+
// src/rules/brace-utils.ts
|
|
331
|
+
function countBracesInLine(line, startJ, initialDepth, inBlockComment) {
|
|
332
|
+
let depth = initialDepth;
|
|
333
|
+
let i = startJ;
|
|
334
|
+
while (i < line.length) {
|
|
335
|
+
if (inBlockComment.value) {
|
|
336
|
+
const closeIdx = line.indexOf("*/", i);
|
|
337
|
+
if (closeIdx === -1) return depth;
|
|
338
|
+
inBlockComment.value = false;
|
|
339
|
+
i = closeIdx + 2;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const ch = line[i];
|
|
343
|
+
if (ch === "/" && line[i + 1] === "/") return depth;
|
|
344
|
+
if (ch === "/" && line[i + 1] === "*") {
|
|
345
|
+
inBlockComment.value = true;
|
|
346
|
+
i += 2;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (ch === "'" || ch === '"') {
|
|
350
|
+
i = skipStringLiteral(line, i, ch);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (ch === "`") {
|
|
354
|
+
i = skipTemplateLiteral(line, i);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (ch === "/" && i > 0) {
|
|
358
|
+
const prevNonSpace = findPrevNonSpace(line, i);
|
|
359
|
+
if (prevNonSpace !== -1 && "=(!|&:,;[{?+->~%^".includes(line[prevNonSpace])) {
|
|
360
|
+
i = skipRegexpLiteral(line, i);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (ch === "{") depth++;
|
|
365
|
+
if (ch === "}") depth--;
|
|
366
|
+
i++;
|
|
367
|
+
}
|
|
368
|
+
return depth;
|
|
369
|
+
}
|
|
370
|
+
function skipStringLiteral(line, start, quote) {
|
|
371
|
+
let i = start + 1;
|
|
372
|
+
while (i < line.length) {
|
|
373
|
+
if (line[i] === "\\") {
|
|
374
|
+
i += 2;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (line[i] === quote) return i + 1;
|
|
378
|
+
i++;
|
|
379
|
+
}
|
|
380
|
+
return i;
|
|
381
|
+
}
|
|
382
|
+
function skipTemplateLiteral(line, start) {
|
|
383
|
+
let i = start + 1;
|
|
384
|
+
while (i < line.length) {
|
|
385
|
+
if (line[i] === "\\") {
|
|
386
|
+
i += 2;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (line[i] === "`") return i + 1;
|
|
390
|
+
i++;
|
|
391
|
+
}
|
|
392
|
+
return i;
|
|
393
|
+
}
|
|
394
|
+
function skipRegexpLiteral(line, start) {
|
|
395
|
+
let i = start + 1;
|
|
396
|
+
while (i < line.length) {
|
|
397
|
+
if (line[i] === "\\") {
|
|
398
|
+
i += 2;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (line[i] === "/") return i + 1;
|
|
402
|
+
i++;
|
|
403
|
+
}
|
|
404
|
+
return i;
|
|
405
|
+
}
|
|
406
|
+
function findPrevNonSpace(line, pos) {
|
|
407
|
+
for (let i = pos - 1; i >= 0; i--) {
|
|
408
|
+
if (line[i] !== " " && line[i] !== " ") return i;
|
|
409
|
+
}
|
|
410
|
+
return -1;
|
|
411
|
+
}
|
|
412
|
+
function extractBraceBlock(lines, startLineIndex, startCol) {
|
|
413
|
+
let depth = 0;
|
|
414
|
+
let started = false;
|
|
415
|
+
let bodyStart = -1;
|
|
416
|
+
const blockComment = { value: false };
|
|
417
|
+
for (let i = startLineIndex; i < lines.length; i++) {
|
|
418
|
+
const startJ = i === startLineIndex ? startCol : 0;
|
|
419
|
+
const line = lines[i];
|
|
420
|
+
let j = startJ;
|
|
421
|
+
while (j < line.length) {
|
|
422
|
+
if (blockComment.value) {
|
|
423
|
+
const closeIdx = line.indexOf("*/", j);
|
|
424
|
+
if (closeIdx === -1) {
|
|
425
|
+
j = line.length;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
blockComment.value = false;
|
|
429
|
+
j = closeIdx + 2;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const ch = line[j];
|
|
433
|
+
if (ch === "/" && line[j + 1] === "/") break;
|
|
434
|
+
if (ch === "/" && line[j + 1] === "*") {
|
|
435
|
+
blockComment.value = true;
|
|
436
|
+
j += 2;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (ch === "'" || ch === '"') {
|
|
440
|
+
j = skipStringLiteral(line, j, ch);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (ch === "`") {
|
|
444
|
+
j = skipTemplateLiteral(line, j);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (ch === "{") {
|
|
448
|
+
depth++;
|
|
449
|
+
if (!started) {
|
|
450
|
+
started = true;
|
|
451
|
+
bodyStart = i;
|
|
452
|
+
}
|
|
453
|
+
} else if (ch === "}") {
|
|
454
|
+
depth--;
|
|
455
|
+
if (started && depth === 0) {
|
|
456
|
+
return {
|
|
457
|
+
bodyLines: lines.slice(bodyStart + 1, i),
|
|
458
|
+
endLine: i
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
j++;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
|
|
289
468
|
// src/rules/builtin/unnecessary-try-catch.ts
|
|
290
469
|
var unnecessaryTryCatchRule = {
|
|
291
470
|
id: "logic/unnecessary-try-catch",
|
|
@@ -301,38 +480,53 @@ var unnecessaryTryCatchRule = {
|
|
|
301
480
|
const line = lines[i];
|
|
302
481
|
const trimmed = line.trim();
|
|
303
482
|
if (trimmed.startsWith("try") && trimmed.includes("{")) {
|
|
304
|
-
const
|
|
483
|
+
const tryCol = line.indexOf("try");
|
|
484
|
+
const tryBlock = extractBraceBlock(lines, i, tryCol);
|
|
305
485
|
if (tryBlock) {
|
|
306
|
-
const { bodyLines
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
486
|
+
const { bodyLines: tryBodyLines, endLine: tryEndLine } = tryBlock;
|
|
487
|
+
let catchLineIdx = -1;
|
|
488
|
+
for (let k = tryEndLine; k < Math.min(tryEndLine + 2, lines.length); k++) {
|
|
489
|
+
if (lines[k].includes("catch")) {
|
|
490
|
+
catchLineIdx = k;
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (catchLineIdx !== -1) {
|
|
495
|
+
const catchCol = lines[catchLineIdx].indexOf("catch");
|
|
496
|
+
const catchBlock = extractBraceBlock(lines, catchLineIdx, catchCol);
|
|
497
|
+
if (catchBlock) {
|
|
498
|
+
const { bodyLines: catchBodyLines, endLine: catchEndLine } = catchBlock;
|
|
499
|
+
const nonEmptyBody = tryBodyLines.filter((l) => l.trim().length > 0);
|
|
500
|
+
const nonEmptyCatch = catchBodyLines.filter((l) => l.trim().length > 0);
|
|
501
|
+
const isSimpleBody = nonEmptyBody.length <= 2;
|
|
502
|
+
const isGenericCatch = nonEmptyCatch.length <= 2 && nonEmptyCatch.some(
|
|
503
|
+
(l) => /console\.(log|error|warn)/.test(l) || /throw\s+(new\s+)?Error/.test(l) || l.trim() === ""
|
|
504
|
+
);
|
|
505
|
+
const bodyHasOnlyAssignments = nonEmptyBody.every(
|
|
506
|
+
(l) => /^\s*(const|let|var)\s+/.test(l) || /^\s*\w+(\.\w+)*\s*=\s*/.test(l) || /^\s*return\s+/.test(l)
|
|
507
|
+
);
|
|
508
|
+
if (isSimpleBody && isGenericCatch && bodyHasOnlyAssignments) {
|
|
509
|
+
issues.push({
|
|
510
|
+
ruleId: "logic/unnecessary-try-catch",
|
|
511
|
+
severity: "medium",
|
|
512
|
+
category: "logic",
|
|
513
|
+
file: context.filePath,
|
|
514
|
+
startLine: i + 1,
|
|
515
|
+
endLine: catchEndLine + 1,
|
|
516
|
+
message: t(
|
|
517
|
+
"Unnecessary try-catch wrapping a simple statement with generic error handling. This is likely AI-hallucinated error handling.",
|
|
518
|
+
"\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"
|
|
519
|
+
),
|
|
520
|
+
suggestion: t(
|
|
521
|
+
"Remove the try-catch block or add meaningful error recovery logic.",
|
|
522
|
+
"\u79FB\u9664 try-catch \u5757\uFF0C\u6216\u6DFB\u52A0\u6709\u610F\u4E49\u7684\u9519\u8BEF\u6062\u590D\u903B\u8F91\u3002"
|
|
523
|
+
)
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
i = catchEndLine + 1;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
333
529
|
}
|
|
334
|
-
i = endLine + 1;
|
|
335
|
-
continue;
|
|
336
530
|
}
|
|
337
531
|
}
|
|
338
532
|
i++;
|
|
@@ -340,55 +534,6 @@ var unnecessaryTryCatchRule = {
|
|
|
340
534
|
return issues;
|
|
341
535
|
}
|
|
342
536
|
};
|
|
343
|
-
function extractBlock(lines, tryLineIndex) {
|
|
344
|
-
let braceCount = 0;
|
|
345
|
-
let foundTryOpen = false;
|
|
346
|
-
let tryBodyStart = -1;
|
|
347
|
-
let tryBodyEnd = -1;
|
|
348
|
-
let catchStart = -1;
|
|
349
|
-
let catchBodyStart = -1;
|
|
350
|
-
let catchBodyEnd = -1;
|
|
351
|
-
for (let i = tryLineIndex; i < lines.length; i++) {
|
|
352
|
-
const line = lines[i];
|
|
353
|
-
for (const ch of line) {
|
|
354
|
-
if (ch === "{") {
|
|
355
|
-
braceCount++;
|
|
356
|
-
if (!foundTryOpen) {
|
|
357
|
-
foundTryOpen = true;
|
|
358
|
-
tryBodyStart = i;
|
|
359
|
-
}
|
|
360
|
-
} else if (ch === "}") {
|
|
361
|
-
braceCount--;
|
|
362
|
-
if (braceCount === 0 && tryBodyEnd === -1) {
|
|
363
|
-
tryBodyEnd = i;
|
|
364
|
-
} else if (braceCount === 0 && catchBodyEnd === -1 && catchBodyStart !== -1) {
|
|
365
|
-
catchBodyEnd = i;
|
|
366
|
-
break;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
if (tryBodyEnd !== -1 && catchStart === -1) {
|
|
371
|
-
if (line.includes("catch")) {
|
|
372
|
-
catchStart = i;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
if (catchStart !== -1 && catchBodyStart === -1 && line.includes("{")) {
|
|
376
|
-
catchBodyStart = i;
|
|
377
|
-
}
|
|
378
|
-
if (catchBodyEnd !== -1) break;
|
|
379
|
-
}
|
|
380
|
-
if (tryBodyStart === -1 || tryBodyEnd === -1 || catchBodyStart === -1 || catchBodyEnd === -1) {
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
const bodyLines = lines.slice(tryBodyStart + 1, tryBodyEnd);
|
|
384
|
-
const catchBodyLines = lines.slice(catchBodyStart + 1, catchBodyEnd);
|
|
385
|
-
return {
|
|
386
|
-
bodyLines,
|
|
387
|
-
catchStart,
|
|
388
|
-
catchBodyLines,
|
|
389
|
-
endLine: catchBodyEnd
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
537
|
|
|
393
538
|
// src/rules/builtin/over-defensive.ts
|
|
394
539
|
var overDefensiveRule = {
|
|
@@ -577,18 +722,10 @@ function detectCodeAfterReturn(context, lines, issues) {
|
|
|
577
722
|
let braceDepth = 0;
|
|
578
723
|
let lastReturnDepth = -1;
|
|
579
724
|
let lastReturnLine = -1;
|
|
725
|
+
const blockComment = { value: false };
|
|
580
726
|
for (let i = 0; i < lines.length; i++) {
|
|
581
727
|
const trimmed = lines[i].trim();
|
|
582
|
-
|
|
583
|
-
if (ch === "{") braceDepth++;
|
|
584
|
-
if (ch === "}") {
|
|
585
|
-
if (braceDepth === lastReturnDepth) {
|
|
586
|
-
lastReturnDepth = -1;
|
|
587
|
-
lastReturnLine = -1;
|
|
588
|
-
}
|
|
589
|
-
braceDepth--;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
728
|
+
braceDepth = countBracesInLine(lines[i], 0, braceDepth, blockComment);
|
|
592
729
|
if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
|
|
593
730
|
const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
|
|
594
731
|
if (endsOpen) continue;
|
|
@@ -614,6 +751,10 @@ function detectCodeAfterReturn(context, lines, issues) {
|
|
|
614
751
|
lastReturnDepth = -1;
|
|
615
752
|
lastReturnLine = -1;
|
|
616
753
|
}
|
|
754
|
+
if (braceDepth < lastReturnDepth) {
|
|
755
|
+
lastReturnDepth = -1;
|
|
756
|
+
lastReturnLine = -1;
|
|
757
|
+
}
|
|
617
758
|
}
|
|
618
759
|
}
|
|
619
760
|
function detectImmediateReassign(context, lines, issues) {
|
|
@@ -647,7 +788,21 @@ function detectImmediateReassign(context, lines, issues) {
|
|
|
647
788
|
}
|
|
648
789
|
|
|
649
790
|
// src/rules/fix-utils.ts
|
|
650
|
-
function
|
|
791
|
+
function buildLineOffsets(content) {
|
|
792
|
+
const offsets = [0];
|
|
793
|
+
for (let i = 0; i < content.length; i++) {
|
|
794
|
+
if (content[i] === "\n") {
|
|
795
|
+
offsets.push(i + 1);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return offsets;
|
|
799
|
+
}
|
|
800
|
+
function lineStartOffset(content, lineNumber, offsets) {
|
|
801
|
+
if (offsets) {
|
|
802
|
+
const idx = lineNumber - 1;
|
|
803
|
+
if (idx < 0 || idx >= offsets.length) return content.length;
|
|
804
|
+
return offsets[idx];
|
|
805
|
+
}
|
|
651
806
|
let offset = 0;
|
|
652
807
|
const lines = content.split("\n");
|
|
653
808
|
for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
|
|
@@ -655,12 +810,13 @@ function lineStartOffset(content, lineNumber) {
|
|
|
655
810
|
}
|
|
656
811
|
return offset;
|
|
657
812
|
}
|
|
658
|
-
function lineRange(content, lineNumber) {
|
|
659
|
-
const
|
|
813
|
+
function lineRange(content, lineNumber, offsets) {
|
|
814
|
+
const table = offsets ?? buildLineOffsets(content);
|
|
660
815
|
const lineIndex = lineNumber - 1;
|
|
661
|
-
if (lineIndex < 0 || lineIndex >=
|
|
662
|
-
const start =
|
|
663
|
-
const
|
|
816
|
+
if (lineIndex < 0 || lineIndex >= table.length) return [0, 0];
|
|
817
|
+
const start = table[lineIndex];
|
|
818
|
+
const nextLineStart = lineIndex + 1 < table.length ? table[lineIndex + 1] : content.length;
|
|
819
|
+
const end = lineIndex + 1 < table.length ? nextLineStart : nextLineStart;
|
|
664
820
|
return [start, end];
|
|
665
821
|
}
|
|
666
822
|
|
|
@@ -726,12 +882,12 @@ function extractFunctions(parsed) {
|
|
|
726
882
|
}
|
|
727
883
|
function visitNode(root, functions) {
|
|
728
884
|
const methodBodies = /* @__PURE__ */ new WeakSet();
|
|
729
|
-
walkAST(root, (node) => {
|
|
885
|
+
walkAST(root, (node, parent) => {
|
|
730
886
|
if (node.type === AST_NODE_TYPES.FunctionExpression && methodBodies.has(node)) {
|
|
731
887
|
return false;
|
|
732
888
|
}
|
|
733
889
|
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) {
|
|
734
|
-
const info = analyzeFunctionNode(node);
|
|
890
|
+
const info = analyzeFunctionNode(node, parent);
|
|
735
891
|
if (info) functions.push(info);
|
|
736
892
|
if (node.type === AST_NODE_TYPES.MethodDefinition) {
|
|
737
893
|
methodBodies.add(node.value);
|
|
@@ -740,7 +896,7 @@ function visitNode(root, functions) {
|
|
|
740
896
|
return;
|
|
741
897
|
});
|
|
742
898
|
}
|
|
743
|
-
function analyzeFunctionNode(node) {
|
|
899
|
+
function analyzeFunctionNode(node, parent) {
|
|
744
900
|
let name = "<anonymous>";
|
|
745
901
|
let params = [];
|
|
746
902
|
let body = null;
|
|
@@ -753,7 +909,11 @@ function analyzeFunctionNode(node) {
|
|
|
753
909
|
params = node.params;
|
|
754
910
|
body = node.body;
|
|
755
911
|
} else if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
756
|
-
|
|
912
|
+
if (parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier) {
|
|
913
|
+
name = parent.id.name;
|
|
914
|
+
} else {
|
|
915
|
+
name = "<arrow>";
|
|
916
|
+
}
|
|
757
917
|
params = node.params;
|
|
758
918
|
body = node.body;
|
|
759
919
|
} else if (node.type === AST_NODE_TYPES.MethodDefinition) {
|
|
@@ -781,6 +941,9 @@ function analyzeFunctionNode(node) {
|
|
|
781
941
|
function calculateCyclomaticComplexity(root) {
|
|
782
942
|
let complexity = 1;
|
|
783
943
|
walkAST(root, (n) => {
|
|
944
|
+
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) {
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
784
947
|
switch (n.type) {
|
|
785
948
|
case AST_NODE_TYPES.IfStatement:
|
|
786
949
|
case AST_NODE_TYPES.ConditionalExpression:
|
|
@@ -809,6 +972,9 @@ function calculateCognitiveComplexity(root) {
|
|
|
809
972
|
const depthMap = /* @__PURE__ */ new WeakMap();
|
|
810
973
|
depthMap.set(root, 0);
|
|
811
974
|
walkAST(root, (n, parent) => {
|
|
975
|
+
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) {
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
812
978
|
const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
|
|
813
979
|
const isNesting = isNestingNode(n);
|
|
814
980
|
const depth = isNesting ? parentDepth + 1 : parentDepth;
|
|
@@ -827,6 +993,9 @@ function calculateMaxNestingDepth(root) {
|
|
|
827
993
|
const depthMap = /* @__PURE__ */ new WeakMap();
|
|
828
994
|
depthMap.set(root, 0);
|
|
829
995
|
walkAST(root, (n, parent) => {
|
|
996
|
+
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) {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
830
999
|
const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
|
|
831
1000
|
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;
|
|
832
1001
|
const currentDepth = isNesting ? parentDepth + 1 : parentDepth;
|
|
@@ -934,7 +1103,24 @@ function collectDeclarationsAndReferences(root, declarations, references) {
|
|
|
934
1103
|
kind: exportedNames.has(node.id.name) ? "export" : "local"
|
|
935
1104
|
});
|
|
936
1105
|
}
|
|
937
|
-
if (node.type === AST_NODE_TYPES.
|
|
1106
|
+
if (node.type === AST_NODE_TYPES.ClassDeclaration && node.id) {
|
|
1107
|
+
declarations.set(node.id.name, {
|
|
1108
|
+
line: node.loc?.start.line ?? 0,
|
|
1109
|
+
kind: exportedNames.has(node.id.name) ? "export" : "local"
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
const declarationParentTypes = /* @__PURE__ */ new Set([
|
|
1113
|
+
"VariableDeclarator",
|
|
1114
|
+
"FunctionDeclaration",
|
|
1115
|
+
"ClassDeclaration",
|
|
1116
|
+
"MethodDefinition",
|
|
1117
|
+
"TSEnumDeclaration",
|
|
1118
|
+
"TSEnumMember",
|
|
1119
|
+
"TSTypeAliasDeclaration",
|
|
1120
|
+
"TSInterfaceDeclaration",
|
|
1121
|
+
"TSModuleDeclaration"
|
|
1122
|
+
]);
|
|
1123
|
+
if (node.type === AST_NODE_TYPES.Identifier && !declarationParentTypes.has(parentType ?? "")) {
|
|
938
1124
|
references.add(node.name);
|
|
939
1125
|
}
|
|
940
1126
|
return;
|
|
@@ -1008,16 +1194,38 @@ function stringifyCondition(node) {
|
|
|
1008
1194
|
case AST_NODE_TYPES.Literal:
|
|
1009
1195
|
return String(node.value);
|
|
1010
1196
|
case AST_NODE_TYPES.BinaryExpression:
|
|
1197
|
+
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
1011
1198
|
case AST_NODE_TYPES.LogicalExpression:
|
|
1012
1199
|
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
1013
1200
|
case AST_NODE_TYPES.UnaryExpression:
|
|
1014
1201
|
return `${node.operator}${stringifyCondition(node.argument)}`;
|
|
1015
1202
|
case AST_NODE_TYPES.MemberExpression:
|
|
1016
1203
|
return `${stringifyCondition(node.object)}.${stringifyCondition(node.property)}`;
|
|
1017
|
-
case AST_NODE_TYPES.CallExpression:
|
|
1018
|
-
|
|
1204
|
+
case AST_NODE_TYPES.CallExpression: {
|
|
1205
|
+
const args = node.arguments.map((arg) => stringifyCondition(arg)).join(", ");
|
|
1206
|
+
return `${stringifyCondition(node.callee)}(${args})`;
|
|
1207
|
+
}
|
|
1208
|
+
case AST_NODE_TYPES.ConditionalExpression:
|
|
1209
|
+
return `${stringifyCondition(node.test)} ? ${stringifyCondition(node.consequent)} : ${stringifyCondition(node.alternate)}`;
|
|
1210
|
+
case AST_NODE_TYPES.TemplateLiteral:
|
|
1211
|
+
return `\`template@${node.loc?.start.line}:${node.loc?.start.column}\``;
|
|
1212
|
+
case AST_NODE_TYPES.ArrayExpression:
|
|
1213
|
+
return `[${node.elements.map((e) => e ? stringifyCondition(e) : "empty").join(", ")}]`;
|
|
1214
|
+
case AST_NODE_TYPES.ObjectExpression:
|
|
1215
|
+
return `{obj@${node.loc?.start.line}:${node.loc?.start.column}}`;
|
|
1216
|
+
case AST_NODE_TYPES.AssignmentExpression:
|
|
1217
|
+
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
1218
|
+
case AST_NODE_TYPES.NewExpression:
|
|
1219
|
+
return `new ${stringifyCondition(node.callee)}(${node.arguments.map((a) => stringifyCondition(a)).join(", ")})`;
|
|
1220
|
+
case AST_NODE_TYPES.TSAsExpression:
|
|
1221
|
+
case AST_NODE_TYPES.TSNonNullExpression:
|
|
1222
|
+
return stringifyCondition(node.expression);
|
|
1223
|
+
case AST_NODE_TYPES.AwaitExpression:
|
|
1224
|
+
return `await ${stringifyCondition(node.argument)}`;
|
|
1225
|
+
case AST_NODE_TYPES.ChainExpression:
|
|
1226
|
+
return stringifyCondition(node.expression);
|
|
1019
1227
|
default:
|
|
1020
|
-
return `[${node.type}]`;
|
|
1228
|
+
return `[${node.type}@${node.loc?.start.line}:${node.loc?.start.column}]`;
|
|
1021
1229
|
}
|
|
1022
1230
|
}
|
|
1023
1231
|
function truncate(s, maxLen) {
|
|
@@ -1176,9 +1384,11 @@ var securityRules = [
|
|
|
1176
1384
|
const issues = [];
|
|
1177
1385
|
const lines = context.fileContent.split("\n");
|
|
1178
1386
|
for (let i = 0; i < lines.length; i++) {
|
|
1179
|
-
const
|
|
1387
|
+
const line = lines[i];
|
|
1388
|
+
const trimmed = line.trim();
|
|
1180
1389
|
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
1181
|
-
|
|
1390
|
+
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "").replace(/\/[^/]+\/[dgimsuvy]*/g, '""');
|
|
1391
|
+
if (/\.(innerHTML|outerHTML)\s*=/.test(cleaned) || /dangerouslySetInnerHTML/.test(cleaned)) {
|
|
1182
1392
|
issues.push({
|
|
1183
1393
|
ruleId: "security/dangerous-html",
|
|
1184
1394
|
severity: "medium",
|
|
@@ -1227,7 +1437,8 @@ var emptyCatchRule = {
|
|
|
1227
1437
|
const catchMatch = trimmed.match(/\bcatch\s*\(\s*(\w+)?\s*\)\s*\{/);
|
|
1228
1438
|
if (!catchMatch) continue;
|
|
1229
1439
|
const catchVarName = catchMatch[1] || "";
|
|
1230
|
-
const
|
|
1440
|
+
const catchIdx = lines[i].indexOf("catch");
|
|
1441
|
+
const blockContent = extractBraceBlock(lines, i, catchIdx);
|
|
1231
1442
|
if (!blockContent) continue;
|
|
1232
1443
|
const { bodyLines, endLine } = blockContent;
|
|
1233
1444
|
const meaningful = bodyLines.filter(
|
|
@@ -1277,37 +1488,6 @@ var emptyCatchRule = {
|
|
|
1277
1488
|
return issues;
|
|
1278
1489
|
}
|
|
1279
1490
|
};
|
|
1280
|
-
function extractCatchBody(lines, catchLineIndex) {
|
|
1281
|
-
const catchLine = lines[catchLineIndex];
|
|
1282
|
-
const catchIdx = catchLine.indexOf("catch");
|
|
1283
|
-
if (catchIdx === -1) return null;
|
|
1284
|
-
let braceCount = 0;
|
|
1285
|
-
let started = false;
|
|
1286
|
-
let bodyStart = -1;
|
|
1287
|
-
for (let i = catchLineIndex; i < lines.length; i++) {
|
|
1288
|
-
const line = lines[i];
|
|
1289
|
-
const startJ = i === catchLineIndex ? catchIdx : 0;
|
|
1290
|
-
for (let j = startJ; j < line.length; j++) {
|
|
1291
|
-
const ch = line[j];
|
|
1292
|
-
if (ch === "{") {
|
|
1293
|
-
braceCount++;
|
|
1294
|
-
if (!started) {
|
|
1295
|
-
started = true;
|
|
1296
|
-
bodyStart = i;
|
|
1297
|
-
}
|
|
1298
|
-
} else if (ch === "}") {
|
|
1299
|
-
braceCount--;
|
|
1300
|
-
if (started && braceCount === 0) {
|
|
1301
|
-
return {
|
|
1302
|
-
bodyLines: lines.slice(bodyStart + 1, i),
|
|
1303
|
-
endLine: i
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
return null;
|
|
1310
|
-
}
|
|
1311
1491
|
|
|
1312
1492
|
// src/rules/builtin/identical-branches.ts
|
|
1313
1493
|
var identicalBranchesRule = {
|
|
@@ -1654,10 +1834,25 @@ var unusedImportRule = {
|
|
|
1654
1834
|
}
|
|
1655
1835
|
return;
|
|
1656
1836
|
});
|
|
1657
|
-
const
|
|
1658
|
-
let
|
|
1659
|
-
|
|
1660
|
-
|
|
1837
|
+
const codeLines = context.fileContent.split("\n");
|
|
1838
|
+
let inBlock = false;
|
|
1839
|
+
for (const codeLine of codeLines) {
|
|
1840
|
+
const trimmedCode = codeLine.trim();
|
|
1841
|
+
if (inBlock) {
|
|
1842
|
+
if (trimmedCode.includes("*/")) inBlock = false;
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
if (trimmedCode.startsWith("/*")) {
|
|
1846
|
+
if (!trimmedCode.includes("*/")) inBlock = true;
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1849
|
+
if (trimmedCode.startsWith("//") || trimmedCode.startsWith("*")) continue;
|
|
1850
|
+
const cleaned = codeLine.replace(/\/\/.*$/, "").replace(/\/\*.*?\*\//g, "").replace(/'(?:[^'\\]|\\.)*'/g, "").replace(/"(?:[^"\\]|\\.)*"/g, "").replace(/`(?:[^`\\]|\\.)*`/g, "");
|
|
1851
|
+
const typeRefPattern = /\b([A-Z][A-Za-z0-9]*)\b/g;
|
|
1852
|
+
let match;
|
|
1853
|
+
while ((match = typeRefPattern.exec(cleaned)) !== null) {
|
|
1854
|
+
references.add(match[1]);
|
|
1855
|
+
}
|
|
1661
1856
|
}
|
|
1662
1857
|
for (const imp of imports) {
|
|
1663
1858
|
if (!references.has(imp.local)) {
|
|
@@ -1706,6 +1901,9 @@ var missingAwaitRule = {
|
|
|
1706
1901
|
const body = getFunctionBody(node);
|
|
1707
1902
|
if (!body) return;
|
|
1708
1903
|
walkAST(body, (inner, parent) => {
|
|
1904
|
+
if (inner.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1709
1907
|
if (inner !== body && isAsyncFunction(inner)) return false;
|
|
1710
1908
|
if (inner.type !== AST_NODE_TYPES.CallExpression) return;
|
|
1711
1909
|
if (parent?.type === AST_NODE_TYPES.AwaitExpression) return;
|
|
@@ -1715,6 +1913,9 @@ var missingAwaitRule = {
|
|
|
1715
1913
|
if (parent?.type === AST_NODE_TYPES.AssignmentExpression) return;
|
|
1716
1914
|
if (parent?.type === AST_NODE_TYPES.ArrayExpression) return;
|
|
1717
1915
|
if (parent?.type === AST_NODE_TYPES.CallExpression && parent !== inner) return;
|
|
1916
|
+
if (parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1718
1919
|
const callName = getCallName(inner);
|
|
1719
1920
|
if (!callName) return;
|
|
1720
1921
|
if (!asyncFuncNames.has(callName)) return;
|
|
@@ -1926,9 +2127,28 @@ var typeCoercionRule = {
|
|
|
1926
2127
|
var ALLOWED_NUMBERS = /* @__PURE__ */ new Set([
|
|
1927
2128
|
-1,
|
|
1928
2129
|
0,
|
|
2130
|
+
0.1,
|
|
2131
|
+
0.1,
|
|
2132
|
+
0.15,
|
|
2133
|
+
0.2,
|
|
2134
|
+
0.2,
|
|
2135
|
+
0.25,
|
|
2136
|
+
0.3,
|
|
2137
|
+
0.3,
|
|
2138
|
+
0.5,
|
|
1929
2139
|
1,
|
|
1930
2140
|
2,
|
|
2141
|
+
3,
|
|
2142
|
+
4,
|
|
2143
|
+
5,
|
|
1931
2144
|
10,
|
|
2145
|
+
15,
|
|
2146
|
+
20,
|
|
2147
|
+
30,
|
|
2148
|
+
40,
|
|
2149
|
+
50,
|
|
2150
|
+
70,
|
|
2151
|
+
90,
|
|
1932
2152
|
100
|
|
1933
2153
|
]);
|
|
1934
2154
|
var magicNumberRule = {
|
|
@@ -1957,6 +2177,7 @@ var magicNumberRule = {
|
|
|
1957
2177
|
if (/^\s*(export\s+)?enum\s/.test(line)) continue;
|
|
1958
2178
|
if (trimmed.startsWith("import ")) continue;
|
|
1959
2179
|
if (/^\s*return\s+[0-9]+\s*;?\s*$/.test(line)) continue;
|
|
2180
|
+
if (/^\s*['"]?[-\w]+['"]?\s*:\s*-?\d+\.?\d*(?:e[+-]?\d+)?\s*,?\s*$/.test(trimmed)) continue;
|
|
1960
2181
|
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
|
|
1961
2182
|
const numRegex = /(?<![.\w])(-?\d+\.?\d*(?:e[+-]?\d+)?)\b/gi;
|
|
1962
2183
|
let match;
|
|
@@ -2053,6 +2274,17 @@ var nestedTernaryRule = {
|
|
|
2053
2274
|
// src/rules/builtin/duplicate-string.ts
|
|
2054
2275
|
var MIN_STRING_LENGTH = 6;
|
|
2055
2276
|
var MIN_OCCURRENCES = 3;
|
|
2277
|
+
var IGNORED_LITERALS = /* @__PURE__ */ new Set([
|
|
2278
|
+
"high",
|
|
2279
|
+
"medium",
|
|
2280
|
+
"low",
|
|
2281
|
+
"info",
|
|
2282
|
+
"logic",
|
|
2283
|
+
"security",
|
|
2284
|
+
"structure",
|
|
2285
|
+
"style",
|
|
2286
|
+
"coverage"
|
|
2287
|
+
]);
|
|
2056
2288
|
var duplicateStringRule = {
|
|
2057
2289
|
id: "logic/duplicate-string",
|
|
2058
2290
|
category: "logic",
|
|
@@ -2083,6 +2315,7 @@ var duplicateStringRule = {
|
|
|
2083
2315
|
while ((match = stringRegex.exec(cleaned)) !== null) {
|
|
2084
2316
|
const value = match[2];
|
|
2085
2317
|
if (value.length < MIN_STRING_LENGTH) continue;
|
|
2318
|
+
if (IGNORED_LITERALS.has(value)) continue;
|
|
2086
2319
|
if (value.includes("${")) continue;
|
|
2087
2320
|
if (value.startsWith("http") || value.startsWith("/")) continue;
|
|
2088
2321
|
if (value.startsWith("test") || value.startsWith("mock")) continue;
|
|
@@ -2352,13 +2585,28 @@ var promiseVoidRule = {
|
|
|
2352
2585
|
/^save/,
|
|
2353
2586
|
/^load/,
|
|
2354
2587
|
/^send/,
|
|
2355
|
-
/^delete/,
|
|
2356
2588
|
/^update/,
|
|
2357
2589
|
/^create/,
|
|
2358
2590
|
/^connect/,
|
|
2359
2591
|
/^disconnect/,
|
|
2360
2592
|
/^init/
|
|
2361
2593
|
];
|
|
2594
|
+
const syncMethods = [
|
|
2595
|
+
"delete",
|
|
2596
|
+
// Map.delete(), Set.delete(), Object.delete() are synchronous
|
|
2597
|
+
"has",
|
|
2598
|
+
// Map.has(), Set.has() are synchronous
|
|
2599
|
+
"get",
|
|
2600
|
+
// Map.get() is synchronous
|
|
2601
|
+
"set",
|
|
2602
|
+
// Map.set() is synchronous (though some consider it potentially async)
|
|
2603
|
+
"keys",
|
|
2604
|
+
// Object.keys() is synchronous
|
|
2605
|
+
"values",
|
|
2606
|
+
// Object.values() is synchronous
|
|
2607
|
+
"entries"
|
|
2608
|
+
// Object.entries() is synchronous
|
|
2609
|
+
];
|
|
2362
2610
|
walkAST(ast, (node) => {
|
|
2363
2611
|
if (node.type !== AST_NODE_TYPES.ExpressionStatement) return;
|
|
2364
2612
|
const expr = node.expression;
|
|
@@ -2369,6 +2617,7 @@ var promiseVoidRule = {
|
|
|
2369
2617
|
const isKnownAsync = asyncFnNames.has(fnName);
|
|
2370
2618
|
const matchesPattern = commonAsyncPatterns.some((p) => p.test(fnName));
|
|
2371
2619
|
const endsWithAsync = fnName.endsWith("Async") || fnName.endsWith("async");
|
|
2620
|
+
if (syncMethods.includes(fnName)) return;
|
|
2372
2621
|
if (!isKnownAsync && !matchesPattern && !endsWithAsync) return;
|
|
2373
2622
|
const line = node.loc?.start.line ?? 0;
|
|
2374
2623
|
if (line === 0) return;
|
|
@@ -2729,13 +2978,20 @@ var SEVERITY_PENALTY = {
|
|
|
2729
2978
|
low: 3,
|
|
2730
2979
|
info: 0
|
|
2731
2980
|
};
|
|
2981
|
+
var DIMINISHING_FACTOR = 0.7;
|
|
2732
2982
|
function calculateDimensionScore(issues) {
|
|
2733
2983
|
let score = 100;
|
|
2984
|
+
const severityCounts = {};
|
|
2734
2985
|
for (const issue of issues) {
|
|
2735
|
-
|
|
2986
|
+
const base = SEVERITY_PENALTY[issue.severity] ?? 0;
|
|
2987
|
+
if (base === 0) continue;
|
|
2988
|
+
const n = severityCounts[issue.severity] ?? 0;
|
|
2989
|
+
severityCounts[issue.severity] = n + 1;
|
|
2990
|
+
const penalty = base * Math.pow(DIMINISHING_FACTOR, n);
|
|
2991
|
+
score -= penalty;
|
|
2736
2992
|
}
|
|
2737
2993
|
return {
|
|
2738
|
-
score: Math.max(0, Math.min(100, score)),
|
|
2994
|
+
score: Math.round(Math.max(0, Math.min(100, score)) * 10) / 10,
|
|
2739
2995
|
issues
|
|
2740
2996
|
};
|
|
2741
2997
|
}
|
|
@@ -3088,7 +3344,7 @@ var PKG_VERSION = (() => {
|
|
|
3088
3344
|
}
|
|
3089
3345
|
})();
|
|
3090
3346
|
var REPORT_SCHEMA_VERSION = "1.0.0";
|
|
3091
|
-
var FINGERPRINT_VERSION = "
|
|
3347
|
+
var FINGERPRINT_VERSION = "2";
|
|
3092
3348
|
var ScanEngine = class {
|
|
3093
3349
|
config;
|
|
3094
3350
|
diffParser;
|
|
@@ -3120,7 +3376,11 @@ var ScanEngine = class {
|
|
|
3120
3376
|
}
|
|
3121
3377
|
}
|
|
3122
3378
|
const issuesWithFingerprints = this.attachFingerprints(allIssues);
|
|
3123
|
-
const
|
|
3379
|
+
const baseline = await this.loadBaseline(options.baseline);
|
|
3380
|
+
const issuesWithLifecycle = this.attachLifecycle(issuesWithFingerprints, baseline);
|
|
3381
|
+
const fixedIssues = this.getFixedIssues(issuesWithLifecycle, baseline);
|
|
3382
|
+
const lifecycle = this.buildLifecycleSummary(issuesWithLifecycle, fixedIssues, baseline);
|
|
3383
|
+
const dimensions = this.groupByDimension(issuesWithLifecycle);
|
|
3124
3384
|
const overallScore = calculateOverallScore(dimensions, this.config.weights);
|
|
3125
3385
|
const grade = getGrade(overallScore);
|
|
3126
3386
|
const commitHash = await this.diffParser.getCurrentCommitHash();
|
|
@@ -3134,7 +3394,7 @@ var ScanEngine = class {
|
|
|
3134
3394
|
score: overallScore,
|
|
3135
3395
|
grade,
|
|
3136
3396
|
filesScanned,
|
|
3137
|
-
issuesFound:
|
|
3397
|
+
issuesFound: issuesWithLifecycle.length
|
|
3138
3398
|
},
|
|
3139
3399
|
toolHealth: {
|
|
3140
3400
|
rulesExecuted,
|
|
@@ -3147,70 +3407,75 @@ var ScanEngine = class {
|
|
|
3147
3407
|
ruleFailures
|
|
3148
3408
|
},
|
|
3149
3409
|
dimensions,
|
|
3150
|
-
issues:
|
|
3410
|
+
issues: issuesWithLifecycle.sort((a, b) => {
|
|
3151
3411
|
const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
|
|
3152
3412
|
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
3153
|
-
})
|
|
3413
|
+
}),
|
|
3414
|
+
lifecycle,
|
|
3415
|
+
fixedIssues
|
|
3154
3416
|
};
|
|
3155
3417
|
}
|
|
3156
3418
|
async scanFile(diffFile) {
|
|
3157
3419
|
if (diffFile.status === "deleted") {
|
|
3158
|
-
return {
|
|
3159
|
-
issues: [],
|
|
3160
|
-
ruleFailures: [],
|
|
3161
|
-
rulesExecuted: 0,
|
|
3162
|
-
rulesFailed: 0,
|
|
3163
|
-
scanErrors: [
|
|
3164
|
-
{
|
|
3165
|
-
type: "deleted-file",
|
|
3166
|
-
file: diffFile.filePath,
|
|
3167
|
-
message: `Skipped deleted file: ${diffFile.filePath}`
|
|
3168
|
-
}
|
|
3169
|
-
],
|
|
3170
|
-
scanned: false
|
|
3171
|
-
};
|
|
3420
|
+
return this.createSkippedResult(diffFile, "deleted-file", `Skipped deleted file: ${diffFile.filePath}`);
|
|
3172
3421
|
}
|
|
3173
3422
|
if (!this.isTsJsFile(diffFile.filePath)) {
|
|
3174
|
-
return {
|
|
3175
|
-
issues: [],
|
|
3176
|
-
ruleFailures: [],
|
|
3177
|
-
rulesExecuted: 0,
|
|
3178
|
-
rulesFailed: 0,
|
|
3179
|
-
scanErrors: [
|
|
3180
|
-
{
|
|
3181
|
-
type: "unsupported-file-type",
|
|
3182
|
-
file: diffFile.filePath,
|
|
3183
|
-
message: `Skipped unsupported file type: ${diffFile.filePath}`
|
|
3184
|
-
}
|
|
3185
|
-
],
|
|
3186
|
-
scanned: false
|
|
3187
|
-
};
|
|
3423
|
+
return this.createSkippedResult(diffFile, "unsupported-file-type", `Skipped unsupported file type: ${diffFile.filePath}`);
|
|
3188
3424
|
}
|
|
3425
|
+
const fileContent = await this.readFileContent(diffFile);
|
|
3426
|
+
if (!fileContent) {
|
|
3427
|
+
return this.createErrorResult(diffFile, "missing-file-content", `Unable to read file content for ${diffFile.filePath}`);
|
|
3428
|
+
}
|
|
3429
|
+
const addedLines = this.extractAddedLines(diffFile);
|
|
3430
|
+
const ruleResult = this.ruleEngine.runWithDiagnostics({
|
|
3431
|
+
filePath: diffFile.filePath,
|
|
3432
|
+
fileContent,
|
|
3433
|
+
addedLines
|
|
3434
|
+
});
|
|
3435
|
+
const issues = [...ruleResult.issues];
|
|
3436
|
+
issues.push(...this.runStructureAnalysis(fileContent, diffFile.filePath));
|
|
3437
|
+
issues.push(...analyzeStyle(fileContent, diffFile.filePath).issues);
|
|
3438
|
+
issues.push(...analyzeCoverage(fileContent, diffFile.filePath).issues);
|
|
3439
|
+
return {
|
|
3440
|
+
issues,
|
|
3441
|
+
ruleFailures: ruleResult.ruleFailures,
|
|
3442
|
+
rulesExecuted: ruleResult.rulesExecuted,
|
|
3443
|
+
rulesFailed: ruleResult.rulesFailed,
|
|
3444
|
+
scanErrors: [],
|
|
3445
|
+
scanned: true
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
createSkippedResult(diffFile, type, message) {
|
|
3449
|
+
return {
|
|
3450
|
+
issues: [],
|
|
3451
|
+
ruleFailures: [],
|
|
3452
|
+
rulesExecuted: 0,
|
|
3453
|
+
rulesFailed: 0,
|
|
3454
|
+
scanErrors: [{ type, file: diffFile.filePath, message }],
|
|
3455
|
+
scanned: false
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
createErrorResult(diffFile, type, message) {
|
|
3459
|
+
return {
|
|
3460
|
+
issues: [],
|
|
3461
|
+
ruleFailures: [],
|
|
3462
|
+
rulesExecuted: 0,
|
|
3463
|
+
rulesFailed: 0,
|
|
3464
|
+
scanErrors: [{ type, file: diffFile.filePath, message }],
|
|
3465
|
+
scanned: false
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3468
|
+
async readFileContent(diffFile) {
|
|
3189
3469
|
const filePath = resolve2(diffFile.filePath);
|
|
3190
|
-
let fileContent;
|
|
3191
3470
|
try {
|
|
3192
|
-
|
|
3471
|
+
return await readFile(filePath, "utf-8");
|
|
3193
3472
|
} catch {
|
|
3194
3473
|
const content = await this.diffParser.getFileContent(diffFile.filePath);
|
|
3195
|
-
|
|
3196
|
-
return {
|
|
3197
|
-
issues: [],
|
|
3198
|
-
ruleFailures: [],
|
|
3199
|
-
rulesExecuted: 0,
|
|
3200
|
-
rulesFailed: 0,
|
|
3201
|
-
scanErrors: [
|
|
3202
|
-
{
|
|
3203
|
-
type: "missing-file-content",
|
|
3204
|
-
file: diffFile.filePath,
|
|
3205
|
-
message: `Unable to read file content for ${diffFile.filePath}`
|
|
3206
|
-
}
|
|
3207
|
-
],
|
|
3208
|
-
scanned: false
|
|
3209
|
-
};
|
|
3210
|
-
}
|
|
3211
|
-
fileContent = content;
|
|
3474
|
+
return content ?? null;
|
|
3212
3475
|
}
|
|
3213
|
-
|
|
3476
|
+
}
|
|
3477
|
+
extractAddedLines(diffFile) {
|
|
3478
|
+
return diffFile.hunks.flatMap((hunk) => {
|
|
3214
3479
|
const lines = hunk.content.split("\n");
|
|
3215
3480
|
const result = [];
|
|
3216
3481
|
let currentLine = hunk.newStart;
|
|
@@ -3225,32 +3490,15 @@ var ScanEngine = class {
|
|
|
3225
3490
|
}
|
|
3226
3491
|
return result;
|
|
3227
3492
|
});
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
addedLines
|
|
3232
|
-
});
|
|
3233
|
-
const issues = [...ruleResult.issues];
|
|
3234
|
-
const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
|
|
3493
|
+
}
|
|
3494
|
+
runStructureAnalysis(fileContent, filePath) {
|
|
3495
|
+
return analyzeStructure(fileContent, filePath, {
|
|
3235
3496
|
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
3236
3497
|
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3237
3498
|
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3238
3499
|
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3239
3500
|
maxParamCount: this.config.thresholds["max-params"]
|
|
3240
|
-
});
|
|
3241
|
-
issues.push(...structureResult.issues);
|
|
3242
|
-
const styleResult = analyzeStyle(fileContent, diffFile.filePath);
|
|
3243
|
-
issues.push(...styleResult.issues);
|
|
3244
|
-
const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
|
|
3245
|
-
issues.push(...coverageResult.issues);
|
|
3246
|
-
return {
|
|
3247
|
-
issues,
|
|
3248
|
-
ruleFailures: ruleResult.ruleFailures,
|
|
3249
|
-
rulesExecuted: ruleResult.rulesExecuted,
|
|
3250
|
-
rulesFailed: ruleResult.rulesFailed,
|
|
3251
|
-
scanErrors: [],
|
|
3252
|
-
scanned: true
|
|
3253
|
-
};
|
|
3501
|
+
}).issues;
|
|
3254
3502
|
}
|
|
3255
3503
|
async getScanCandidates(options) {
|
|
3256
3504
|
const scanMode = this.getScanMode(options);
|
|
@@ -3291,7 +3539,7 @@ var ScanEngine = class {
|
|
|
3291
3539
|
}
|
|
3292
3540
|
return "changed";
|
|
3293
3541
|
}
|
|
3294
|
-
|
|
3542
|
+
getDiffFiles(options) {
|
|
3295
3543
|
if (options.staged) {
|
|
3296
3544
|
return this.diffParser.getStagedFiles();
|
|
3297
3545
|
}
|
|
@@ -3348,13 +3596,14 @@ var ScanEngine = class {
|
|
|
3348
3596
|
const occurrenceCounts = /* @__PURE__ */ new Map();
|
|
3349
3597
|
return issues.map((issue) => {
|
|
3350
3598
|
const normalizedFile = this.normalizeRelativePath(issue.file);
|
|
3351
|
-
const
|
|
3599
|
+
const contentSource = issue.codeSnippet ? issue.codeSnippet : `${issue.startLine}:${issue.endLine}`;
|
|
3600
|
+
const contentDigest = createHash("sha256").update(contentSource).digest("hex").slice(0, 16);
|
|
3352
3601
|
const baseKey = [
|
|
3353
3602
|
issue.ruleId,
|
|
3354
3603
|
normalizedFile,
|
|
3355
3604
|
issue.category,
|
|
3356
3605
|
issue.severity,
|
|
3357
|
-
|
|
3606
|
+
contentDigest
|
|
3358
3607
|
].join("|");
|
|
3359
3608
|
const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
|
|
3360
3609
|
occurrenceCounts.set(baseKey, occurrenceIndex + 1);
|
|
@@ -3372,18 +3621,110 @@ var ScanEngine = class {
|
|
|
3372
3621
|
const relativePath = relative(process.cwd(), absolutePath) || filePath;
|
|
3373
3622
|
return relativePath.split(sep).join("/");
|
|
3374
3623
|
}
|
|
3624
|
+
async loadBaseline(baselinePath) {
|
|
3625
|
+
if (!baselinePath) {
|
|
3626
|
+
return void 0;
|
|
3627
|
+
}
|
|
3628
|
+
const baselineContent = await readFile(resolve2(baselinePath), "utf-8");
|
|
3629
|
+
const parsed = JSON.parse(baselineContent);
|
|
3630
|
+
const issues = this.parseBaselineIssues(parsed.issues);
|
|
3631
|
+
return {
|
|
3632
|
+
issues,
|
|
3633
|
+
fingerprintSet: new Set(issues.map((issue) => issue.fingerprint)),
|
|
3634
|
+
commit: typeof parsed.commit === "string" ? parsed.commit : void 0,
|
|
3635
|
+
timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
|
|
3636
|
+
};
|
|
3637
|
+
}
|
|
3638
|
+
parseBaselineIssues(input) {
|
|
3639
|
+
if (!Array.isArray(input)) {
|
|
3640
|
+
return [];
|
|
3641
|
+
}
|
|
3642
|
+
return input.flatMap((item) => {
|
|
3643
|
+
const issue = this.parseBaselineIssue(item);
|
|
3644
|
+
return issue ? [issue] : [];
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
parseBaselineIssue(input) {
|
|
3648
|
+
if (!input || typeof input !== "object") {
|
|
3649
|
+
return void 0;
|
|
3650
|
+
}
|
|
3651
|
+
const issue = input;
|
|
3652
|
+
if (!this.isValidBaselineIssue(issue)) {
|
|
3653
|
+
return void 0;
|
|
3654
|
+
}
|
|
3655
|
+
return {
|
|
3656
|
+
ruleId: issue.ruleId,
|
|
3657
|
+
severity: issue.severity,
|
|
3658
|
+
category: issue.category,
|
|
3659
|
+
file: issue.file,
|
|
3660
|
+
startLine: issue.startLine,
|
|
3661
|
+
endLine: issue.endLine,
|
|
3662
|
+
message: issue.message,
|
|
3663
|
+
fingerprint: issue.fingerprint,
|
|
3664
|
+
fingerprintVersion: typeof issue.fingerprintVersion === "string" ? issue.fingerprintVersion : void 0
|
|
3665
|
+
};
|
|
3666
|
+
}
|
|
3667
|
+
isValidBaselineIssue(issue) {
|
|
3668
|
+
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";
|
|
3669
|
+
}
|
|
3670
|
+
attachLifecycle(issues, baseline) {
|
|
3671
|
+
if (!baseline) {
|
|
3672
|
+
return issues;
|
|
3673
|
+
}
|
|
3674
|
+
return issues.map((issue) => ({
|
|
3675
|
+
...issue,
|
|
3676
|
+
lifecycle: baseline.fingerprintSet.has(issue.fingerprint) ? "existing" : "new"
|
|
3677
|
+
}));
|
|
3678
|
+
}
|
|
3679
|
+
getFixedIssues(issues, baseline) {
|
|
3680
|
+
if (!baseline) {
|
|
3681
|
+
return [];
|
|
3682
|
+
}
|
|
3683
|
+
const currentFingerprints = new Set(issues.map((issue) => issue.fingerprint));
|
|
3684
|
+
return baseline.issues.filter((issue) => !currentFingerprints.has(issue.fingerprint));
|
|
3685
|
+
}
|
|
3686
|
+
buildLifecycleSummary(issues, fixedIssues, baseline) {
|
|
3687
|
+
if (!baseline) {
|
|
3688
|
+
return void 0;
|
|
3689
|
+
}
|
|
3690
|
+
let newIssues = 0;
|
|
3691
|
+
let existingIssues = 0;
|
|
3692
|
+
for (const issue of issues) {
|
|
3693
|
+
if (issue.lifecycle === "existing") {
|
|
3694
|
+
existingIssues++;
|
|
3695
|
+
} else {
|
|
3696
|
+
newIssues++;
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
return {
|
|
3700
|
+
newIssues,
|
|
3701
|
+
existingIssues,
|
|
3702
|
+
fixedIssues: fixedIssues.length,
|
|
3703
|
+
baselineUsed: true,
|
|
3704
|
+
baselineCommit: baseline.commit,
|
|
3705
|
+
baselineTimestamp: baseline.timestamp
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
isSeverity(value) {
|
|
3709
|
+
return value === "high" || value === "medium" || value === "low" || value === "info";
|
|
3710
|
+
}
|
|
3711
|
+
isRuleCategory(value) {
|
|
3712
|
+
return ["security", "logic", "structure", "style", "coverage"].includes(value);
|
|
3713
|
+
}
|
|
3375
3714
|
groupByDimension(issues) {
|
|
3376
|
-
const
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3715
|
+
const buckets = {
|
|
3716
|
+
security: [],
|
|
3717
|
+
logic: [],
|
|
3718
|
+
structure: [],
|
|
3719
|
+
style: [],
|
|
3720
|
+
coverage: []
|
|
3721
|
+
};
|
|
3722
|
+
for (const issue of issues) {
|
|
3723
|
+
buckets[issue.category]?.push(issue);
|
|
3724
|
+
}
|
|
3383
3725
|
const grouped = {};
|
|
3384
|
-
for (const cat of
|
|
3385
|
-
|
|
3386
|
-
grouped[cat] = calculateDimensionScore(catIssues);
|
|
3726
|
+
for (const cat of Object.keys(buckets)) {
|
|
3727
|
+
grouped[cat] = calculateDimensionScore(buckets[cat]);
|
|
3387
3728
|
}
|
|
3388
3729
|
return grouped;
|
|
3389
3730
|
}
|
|
@@ -3406,7 +3747,9 @@ var en = {
|
|
|
3406
3747
|
healthHeader: "Tool Health",
|
|
3407
3748
|
rulesFailed: "Failed rules: {{count}}",
|
|
3408
3749
|
filesSkipped: "Skipped files: {{count}}",
|
|
3409
|
-
filesExcluded: "Excluded files: {{count}}"
|
|
3750
|
+
filesExcluded: "Excluded files: {{count}}",
|
|
3751
|
+
lifecycleHeader: "Lifecycle",
|
|
3752
|
+
lifecycleSummary: "New: {{new}} Existing: {{existing}} Fixed: {{fixed}}"
|
|
3410
3753
|
};
|
|
3411
3754
|
var zh = {
|
|
3412
3755
|
reportTitle: "\u{1F4CA} CodeTrust \u62A5\u544A",
|
|
@@ -3422,7 +3765,9 @@ var zh = {
|
|
|
3422
3765
|
healthHeader: "\u5DE5\u5177\u5065\u5EB7\u5EA6",
|
|
3423
3766
|
rulesFailed: "\u5931\u8D25\u89C4\u5219\u6570\uFF1A{{count}}",
|
|
3424
3767
|
filesSkipped: "\u8DF3\u8FC7\u6587\u4EF6\u6570\uFF1A{{count}}",
|
|
3425
|
-
filesExcluded: "\u6392\u9664\u6587\u4EF6\u6570\uFF1A{{count}}"
|
|
3768
|
+
filesExcluded: "\u6392\u9664\u6587\u4EF6\u6570\uFF1A{{count}}",
|
|
3769
|
+
lifecycleHeader: "\u751F\u547D\u5468\u671F",
|
|
3770
|
+
lifecycleSummary: "\u65B0\u589E\uFF1A{{new}} \u5DF2\u5B58\u5728\uFF1A{{existing}} \u5DF2\u4FEE\u590D\uFF1A{{fixed}}"
|
|
3426
3771
|
};
|
|
3427
3772
|
function renderTerminalReport(report) {
|
|
3428
3773
|
const isZh = isZhLocale();
|
|
@@ -3490,6 +3835,11 @@ function renderTerminalReport(report) {
|
|
|
3490
3835
|
}
|
|
3491
3836
|
lines.push("");
|
|
3492
3837
|
}
|
|
3838
|
+
if (report.lifecycle) {
|
|
3839
|
+
lines.push(pc.bold(t2.lifecycleHeader));
|
|
3840
|
+
lines.push(` ${t2.lifecycleSummary.replace("{{new}}", String(report.lifecycle.newIssues)).replace("{{existing}}", String(report.lifecycle.existingIssues)).replace("{{fixed}}", String(report.lifecycle.fixedIssues))}`);
|
|
3841
|
+
lines.push("");
|
|
3842
|
+
}
|
|
3493
3843
|
if (report.issues.length > 0) {
|
|
3494
3844
|
lines.push(pc.bold(t2.issuesHeader.replace("{{count}}", String(report.issues.length))));
|
|
3495
3845
|
lines.push("");
|
|
@@ -3562,23 +3912,31 @@ function renderJsonReport(report) {
|
|
|
3562
3912
|
overall: report.overall,
|
|
3563
3913
|
toolHealth: report.toolHealth,
|
|
3564
3914
|
dimensions: report.dimensions,
|
|
3565
|
-
issues: report.issues
|
|
3915
|
+
issues: report.issues,
|
|
3916
|
+
lifecycle: report.lifecycle,
|
|
3917
|
+
fixedIssues: report.fixedIssues
|
|
3566
3918
|
};
|
|
3567
3919
|
return JSON.stringify(payload, null, 2);
|
|
3568
3920
|
}
|
|
3569
3921
|
|
|
3570
3922
|
// src/cli/commands/scan.ts
|
|
3571
3923
|
function createScanCommand() {
|
|
3572
|
-
const cmd = new Command("scan").description("Run the primary live trust analysis command").argument("[files...]", "Specific files to scan").option("--staged", "Scan only git staged files").option("--diff <ref>", "Scan diff against a git ref (e.g. HEAD~1, origin/main)").option("--format <format>", "Output format: terminal, json", "terminal").option("--min-score <score>", "Minimum trust score threshold", "0").action(async (files, opts) => {
|
|
3924
|
+
const cmd = new Command("scan").description("Run the primary live trust analysis command").argument("[files...]", "Specific files to scan").option("--staged", "Scan only git staged files").option("--diff <ref>", "Scan diff against a git ref (e.g. HEAD~1, origin/main)").option("--format <format>", "Output format: terminal, json", "terminal").option("--min-score <score>", "Minimum trust score threshold", "0").option("--baseline <path>", "Compare current findings against a prior CodeTrust JSON report").action(async (files, opts) => {
|
|
3573
3925
|
try {
|
|
3574
3926
|
const config = await loadConfig();
|
|
3575
3927
|
const engine = new ScanEngine(config);
|
|
3928
|
+
const parsedMinScore = parseInt(opts.minScore, 10);
|
|
3929
|
+
if (isNaN(parsedMinScore)) {
|
|
3930
|
+
console.error(`Error: Invalid --min-score value "${opts.minScore}". Must be a number.`);
|
|
3931
|
+
process.exit(1);
|
|
3932
|
+
}
|
|
3576
3933
|
const scanOptions = {
|
|
3577
3934
|
staged: opts.staged,
|
|
3578
3935
|
diff: opts.diff,
|
|
3579
3936
|
files: files.length > 0 ? files : void 0,
|
|
3580
3937
|
format: opts.format,
|
|
3581
|
-
minScore:
|
|
3938
|
+
minScore: parsedMinScore,
|
|
3939
|
+
baseline: opts.baseline
|
|
3582
3940
|
};
|
|
3583
3941
|
const report = await engine.scan(scanOptions);
|
|
3584
3942
|
if (opts.format === "json") {
|