@gulu9527/code-trust 0.3.1 → 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/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
 
@@ -211,9 +212,18 @@ var DiffParser = class {
211
212
  }
212
213
  parseFileDiff(fileDiff) {
213
214
  const lines = fileDiff.split("\n");
214
- const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
215
- if (!headerMatch) return null;
216
- const filePath = headerMatch[2];
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
+ }
217
227
  let status = "modified";
218
228
  let additions = 0;
219
229
  let deletions = 0;
@@ -317,6 +327,144 @@ function t(en2, zh2) {
317
327
  return isZhLocale() ? zh2 : en2;
318
328
  }
319
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
+
320
468
  // src/rules/builtin/unnecessary-try-catch.ts
321
469
  var unnecessaryTryCatchRule = {
322
470
  id: "logic/unnecessary-try-catch",
@@ -332,38 +480,53 @@ var unnecessaryTryCatchRule = {
332
480
  const line = lines[i];
333
481
  const trimmed = line.trim();
334
482
  if (trimmed.startsWith("try") && trimmed.includes("{")) {
335
- const tryBlock = extractBlock(lines, i);
483
+ const tryCol = line.indexOf("try");
484
+ const tryBlock = extractBraceBlock(lines, i, tryCol);
336
485
  if (tryBlock) {
337
- const { bodyLines, catchBodyLines, endLine } = tryBlock;
338
- const nonEmptyBody = bodyLines.filter((l) => l.trim().length > 0);
339
- const nonEmptyCatch = catchBodyLines.filter((l) => l.trim().length > 0);
340
- const isSimpleBody = nonEmptyBody.length <= 2;
341
- const isGenericCatch = nonEmptyCatch.length <= 2 && nonEmptyCatch.some(
342
- (l) => /console\.(log|error|warn)/.test(l) || /throw\s+(new\s+)?Error/.test(l) || l.trim() === ""
343
- );
344
- const bodyHasOnlyAssignments = nonEmptyBody.every(
345
- (l) => /^\s*(const|let|var)\s+/.test(l) || /^\s*\w+(\.\w+)*\s*=\s*/.test(l) || /^\s*return\s+/.test(l)
346
- );
347
- if (isSimpleBody && isGenericCatch && bodyHasOnlyAssignments) {
348
- issues.push({
349
- ruleId: "logic/unnecessary-try-catch",
350
- severity: "medium",
351
- category: "logic",
352
- file: context.filePath,
353
- startLine: i + 1,
354
- endLine: endLine + 1,
355
- message: t(
356
- "Unnecessary try-catch wrapping a simple statement with generic error handling. This is likely AI-hallucinated error handling.",
357
- "\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"
358
- ),
359
- suggestion: t(
360
- "Remove the try-catch block or add meaningful error recovery logic.",
361
- "\u79FB\u9664 try-catch \u5757\uFF0C\u6216\u6DFB\u52A0\u6709\u610F\u4E49\u7684\u9519\u8BEF\u6062\u590D\u903B\u8F91\u3002"
362
- )
363
- });
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
+ }
364
529
  }
365
- i = endLine + 1;
366
- continue;
367
530
  }
368
531
  }
369
532
  i++;
@@ -371,55 +534,6 @@ var unnecessaryTryCatchRule = {
371
534
  return issues;
372
535
  }
373
536
  };
374
- function extractBlock(lines, tryLineIndex) {
375
- let braceCount = 0;
376
- let foundTryOpen = false;
377
- let tryBodyStart = -1;
378
- let tryBodyEnd = -1;
379
- let catchStart = -1;
380
- let catchBodyStart = -1;
381
- let catchBodyEnd = -1;
382
- for (let i = tryLineIndex; i < lines.length; i++) {
383
- const line = lines[i];
384
- for (const ch of line) {
385
- if (ch === "{") {
386
- braceCount++;
387
- if (!foundTryOpen) {
388
- foundTryOpen = true;
389
- tryBodyStart = i;
390
- }
391
- } else if (ch === "}") {
392
- braceCount--;
393
- if (braceCount === 0 && tryBodyEnd === -1) {
394
- tryBodyEnd = i;
395
- } else if (braceCount === 0 && catchBodyEnd === -1 && catchBodyStart !== -1) {
396
- catchBodyEnd = i;
397
- break;
398
- }
399
- }
400
- }
401
- if (tryBodyEnd !== -1 && catchStart === -1) {
402
- if (line.includes("catch")) {
403
- catchStart = i;
404
- }
405
- }
406
- if (catchStart !== -1 && catchBodyStart === -1 && line.includes("{")) {
407
- catchBodyStart = i;
408
- }
409
- if (catchBodyEnd !== -1) break;
410
- }
411
- if (tryBodyStart === -1 || tryBodyEnd === -1 || catchBodyStart === -1 || catchBodyEnd === -1) {
412
- return null;
413
- }
414
- const bodyLines = lines.slice(tryBodyStart + 1, tryBodyEnd);
415
- const catchBodyLines = lines.slice(catchBodyStart + 1, catchBodyEnd);
416
- return {
417
- bodyLines,
418
- catchStart,
419
- catchBodyLines,
420
- endLine: catchBodyEnd
421
- };
422
- }
423
537
 
424
538
  // src/rules/builtin/over-defensive.ts
425
539
  var overDefensiveRule = {
@@ -608,18 +722,10 @@ function detectCodeAfterReturn(context, lines, issues) {
608
722
  let braceDepth = 0;
609
723
  let lastReturnDepth = -1;
610
724
  let lastReturnLine = -1;
725
+ const blockComment = { value: false };
611
726
  for (let i = 0; i < lines.length; i++) {
612
727
  const trimmed = lines[i].trim();
613
- for (const ch of trimmed) {
614
- if (ch === "{") braceDepth++;
615
- if (ch === "}") {
616
- if (braceDepth === lastReturnDepth) {
617
- lastReturnDepth = -1;
618
- lastReturnLine = -1;
619
- }
620
- braceDepth--;
621
- }
622
- }
728
+ braceDepth = countBracesInLine(lines[i], 0, braceDepth, blockComment);
623
729
  if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
624
730
  const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
625
731
  if (endsOpen) continue;
@@ -645,6 +751,10 @@ function detectCodeAfterReturn(context, lines, issues) {
645
751
  lastReturnDepth = -1;
646
752
  lastReturnLine = -1;
647
753
  }
754
+ if (braceDepth < lastReturnDepth) {
755
+ lastReturnDepth = -1;
756
+ lastReturnLine = -1;
757
+ }
648
758
  }
649
759
  }
650
760
  function detectImmediateReassign(context, lines, issues) {
@@ -678,7 +788,21 @@ function detectImmediateReassign(context, lines, issues) {
678
788
  }
679
789
 
680
790
  // src/rules/fix-utils.ts
681
- function lineStartOffset(content, lineNumber) {
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
+ }
682
806
  let offset = 0;
683
807
  const lines = content.split("\n");
684
808
  for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
@@ -686,12 +810,13 @@ function lineStartOffset(content, lineNumber) {
686
810
  }
687
811
  return offset;
688
812
  }
689
- function lineRange(content, lineNumber) {
690
- const lines = content.split("\n");
813
+ function lineRange(content, lineNumber, offsets) {
814
+ const table = offsets ?? buildLineOffsets(content);
691
815
  const lineIndex = lineNumber - 1;
692
- if (lineIndex < 0 || lineIndex >= lines.length) return [0, 0];
693
- const start = lineStartOffset(content, lineNumber);
694
- const end = start + lines[lineIndex].length + (lineIndex < lines.length - 1 ? 1 : 0);
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;
695
820
  return [start, end];
696
821
  }
697
822
 
@@ -757,12 +882,12 @@ function extractFunctions(parsed) {
757
882
  }
758
883
  function visitNode(root, functions) {
759
884
  const methodBodies = /* @__PURE__ */ new WeakSet();
760
- walkAST(root, (node) => {
885
+ walkAST(root, (node, parent) => {
761
886
  if (node.type === AST_NODE_TYPES.FunctionExpression && methodBodies.has(node)) {
762
887
  return false;
763
888
  }
764
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) {
765
- const info = analyzeFunctionNode(node);
890
+ const info = analyzeFunctionNode(node, parent);
766
891
  if (info) functions.push(info);
767
892
  if (node.type === AST_NODE_TYPES.MethodDefinition) {
768
893
  methodBodies.add(node.value);
@@ -771,7 +896,7 @@ function visitNode(root, functions) {
771
896
  return;
772
897
  });
773
898
  }
774
- function analyzeFunctionNode(node) {
899
+ function analyzeFunctionNode(node, parent) {
775
900
  let name = "<anonymous>";
776
901
  let params = [];
777
902
  let body = null;
@@ -784,7 +909,11 @@ function analyzeFunctionNode(node) {
784
909
  params = node.params;
785
910
  body = node.body;
786
911
  } else if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) {
787
- name = "<arrow>";
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
+ }
788
917
  params = node.params;
789
918
  body = node.body;
790
919
  } else if (node.type === AST_NODE_TYPES.MethodDefinition) {
@@ -974,7 +1103,24 @@ function collectDeclarationsAndReferences(root, declarations, references) {
974
1103
  kind: exportedNames.has(node.id.name) ? "export" : "local"
975
1104
  });
976
1105
  }
977
- if (node.type === AST_NODE_TYPES.Identifier && parentType !== "VariableDeclarator" && parentType !== "FunctionDeclaration") {
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 ?? "")) {
978
1124
  references.add(node.name);
979
1125
  }
980
1126
  return;
@@ -1061,8 +1207,25 @@ function stringifyCondition(node) {
1061
1207
  }
1062
1208
  case AST_NODE_TYPES.ConditionalExpression:
1063
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);
1064
1227
  default:
1065
- return `[${node.type}]`;
1228
+ return `[${node.type}@${node.loc?.start.line}:${node.loc?.start.column}]`;
1066
1229
  }
1067
1230
  }
1068
1231
  function truncate(s, maxLen) {
@@ -1274,7 +1437,8 @@ var emptyCatchRule = {
1274
1437
  const catchMatch = trimmed.match(/\bcatch\s*\(\s*(\w+)?\s*\)\s*\{/);
1275
1438
  if (!catchMatch) continue;
1276
1439
  const catchVarName = catchMatch[1] || "";
1277
- const blockContent = extractCatchBody(lines, i);
1440
+ const catchIdx = lines[i].indexOf("catch");
1441
+ const blockContent = extractBraceBlock(lines, i, catchIdx);
1278
1442
  if (!blockContent) continue;
1279
1443
  const { bodyLines, endLine } = blockContent;
1280
1444
  const meaningful = bodyLines.filter(
@@ -1324,37 +1488,6 @@ var emptyCatchRule = {
1324
1488
  return issues;
1325
1489
  }
1326
1490
  };
1327
- function extractCatchBody(lines, catchLineIndex) {
1328
- const catchLine = lines[catchLineIndex];
1329
- const catchIdx = catchLine.indexOf("catch");
1330
- if (catchIdx === -1) return null;
1331
- let braceCount = 0;
1332
- let started = false;
1333
- let bodyStart = -1;
1334
- for (let i = catchLineIndex; i < lines.length; i++) {
1335
- const line = lines[i];
1336
- const startJ = i === catchLineIndex ? catchIdx : 0;
1337
- for (let j = startJ; j < line.length; j++) {
1338
- const ch = line[j];
1339
- if (ch === "{") {
1340
- braceCount++;
1341
- if (!started) {
1342
- started = true;
1343
- bodyStart = i;
1344
- }
1345
- } else if (ch === "}") {
1346
- braceCount--;
1347
- if (started && braceCount === 0) {
1348
- return {
1349
- bodyLines: lines.slice(bodyStart + 1, i),
1350
- endLine: i
1351
- };
1352
- }
1353
- }
1354
- }
1355
- }
1356
- return null;
1357
- }
1358
1491
 
1359
1492
  // src/rules/builtin/identical-branches.ts
1360
1493
  var identicalBranchesRule = {
@@ -1701,10 +1834,25 @@ var unusedImportRule = {
1701
1834
  }
1702
1835
  return;
1703
1836
  });
1704
- const typeRefPattern = /\b([A-Z][A-Za-z0-9]*)\b/g;
1705
- let match;
1706
- while ((match = typeRefPattern.exec(context.fileContent)) !== null) {
1707
- references.add(match[1]);
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
+ }
1708
1856
  }
1709
1857
  for (const imp of imports) {
1710
1858
  if (!references.has(imp.local)) {
@@ -2830,13 +2978,20 @@ var SEVERITY_PENALTY = {
2830
2978
  low: 3,
2831
2979
  info: 0
2832
2980
  };
2981
+ var DIMINISHING_FACTOR = 0.7;
2833
2982
  function calculateDimensionScore(issues) {
2834
2983
  let score = 100;
2984
+ const severityCounts = {};
2835
2985
  for (const issue of issues) {
2836
- score -= SEVERITY_PENALTY[issue.severity] ?? 0;
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;
2837
2992
  }
2838
2993
  return {
2839
- score: Math.max(0, Math.min(100, score)),
2994
+ score: Math.round(Math.max(0, Math.min(100, score)) * 10) / 10,
2840
2995
  issues
2841
2996
  };
2842
2997
  }
@@ -3189,7 +3344,7 @@ var PKG_VERSION = (() => {
3189
3344
  }
3190
3345
  })();
3191
3346
  var REPORT_SCHEMA_VERSION = "1.0.0";
3192
- var FINGERPRINT_VERSION = "1";
3347
+ var FINGERPRINT_VERSION = "2";
3193
3348
  var ScanEngine = class {
3194
3349
  config;
3195
3350
  diffParser;
@@ -3441,13 +3596,14 @@ var ScanEngine = class {
3441
3596
  const occurrenceCounts = /* @__PURE__ */ new Map();
3442
3597
  return issues.map((issue) => {
3443
3598
  const normalizedFile = this.normalizeRelativePath(issue.file);
3444
- const locationComponent = `${issue.startLine}:${issue.endLine}`;
3599
+ const contentSource = issue.codeSnippet ? issue.codeSnippet : `${issue.startLine}:${issue.endLine}`;
3600
+ const contentDigest = createHash("sha256").update(contentSource).digest("hex").slice(0, 16);
3445
3601
  const baseKey = [
3446
3602
  issue.ruleId,
3447
3603
  normalizedFile,
3448
3604
  issue.category,
3449
3605
  issue.severity,
3450
- locationComponent
3606
+ contentDigest
3451
3607
  ].join("|");
3452
3608
  const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
3453
3609
  occurrenceCounts.set(baseKey, occurrenceIndex + 1);
@@ -3556,17 +3712,19 @@ var ScanEngine = class {
3556
3712
  return ["security", "logic", "structure", "style", "coverage"].includes(value);
3557
3713
  }
3558
3714
  groupByDimension(issues) {
3559
- const categories = [
3560
- "security",
3561
- "logic",
3562
- "structure",
3563
- "style",
3564
- "coverage"
3565
- ];
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
+ }
3566
3725
  const grouped = {};
3567
- for (const cat of categories) {
3568
- const catIssues = issues.filter((issue) => issue.category === cat);
3569
- grouped[cat] = calculateDimensionScore(catIssues);
3726
+ for (const cat of Object.keys(buckets)) {
3727
+ grouped[cat] = calculateDimensionScore(buckets[cat]);
3570
3728
  }
3571
3729
  return grouped;
3572
3730
  }
@@ -3767,12 +3925,17 @@ function createScanCommand() {
3767
3925
  try {
3768
3926
  const config = await loadConfig();
3769
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
+ }
3770
3933
  const scanOptions = {
3771
3934
  staged: opts.staged,
3772
3935
  diff: opts.diff,
3773
3936
  files: files.length > 0 ? files : void 0,
3774
3937
  format: opts.format,
3775
- minScore: parseInt(opts.minScore, 10),
3938
+ minScore: parsedMinScore,
3776
3939
  baseline: opts.baseline
3777
3940
  };
3778
3941
  const report = await engine.scan(scanOptions);