@gulu9527/code-trust 0.2.1 → 0.3.1
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 +256 -0
- package/README.md +42 -4
- package/dist/cli/index.js +548 -138
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +89 -2
- package/dist/index.js +489 -123
- package/dist/index.js.map +1 -1
- package/docs/codetrust-deep-research-report-zh-en.md +802 -0
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -1,38 +1,70 @@
|
|
|
1
1
|
// src/core/engine.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
2
3
|
import { readFile } from "fs/promises";
|
|
3
|
-
import { readFileSync
|
|
4
|
-
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { resolve as resolve2, dirname as dirname3, relative, sep } from "path";
|
|
5
6
|
import { fileURLToPath } from "url";
|
|
6
7
|
|
|
7
8
|
// src/parsers/diff.ts
|
|
8
9
|
import simpleGit from "simple-git";
|
|
10
|
+
var GIT_DIFF_UNIFIED = "--unified=3";
|
|
11
|
+
var SHORT_HASH_LENGTH = 7;
|
|
9
12
|
var DiffParser = class {
|
|
10
13
|
git;
|
|
11
14
|
constructor(workDir) {
|
|
12
15
|
this.git = simpleGit(workDir);
|
|
13
16
|
}
|
|
14
17
|
async getStagedFiles() {
|
|
15
|
-
const diffDetail = await this.git.diff(["--cached",
|
|
18
|
+
const diffDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
|
|
16
19
|
return this.parseDiffOutput(diffDetail);
|
|
17
20
|
}
|
|
18
21
|
async getDiffFromRef(ref) {
|
|
19
|
-
const diffDetail = await this.git.diff([ref,
|
|
22
|
+
const diffDetail = await this.git.diff([ref, GIT_DIFF_UNIFIED]);
|
|
20
23
|
return this.parseDiffOutput(diffDetail);
|
|
21
24
|
}
|
|
22
25
|
async getChangedFiles() {
|
|
23
|
-
const diffDetail = await this.git.diff([
|
|
24
|
-
const stagedDetail = await this.git.diff(["--cached",
|
|
25
|
-
const
|
|
26
|
-
|
|
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());
|
|
27
59
|
}
|
|
28
60
|
async getLastCommitDiff() {
|
|
29
|
-
const diffDetail = await this.git.diff(["HEAD~1", "HEAD",
|
|
61
|
+
const diffDetail = await this.git.diff(["HEAD~1", "HEAD", GIT_DIFF_UNIFIED]);
|
|
30
62
|
return this.parseDiffOutput(diffDetail);
|
|
31
63
|
}
|
|
32
64
|
async getCurrentCommitHash() {
|
|
33
65
|
try {
|
|
34
66
|
const hash = await this.git.revparse(["HEAD"]);
|
|
35
|
-
return hash.trim().slice(0,
|
|
67
|
+
return hash.trim().slice(0, SHORT_HASH_LENGTH);
|
|
36
68
|
} catch {
|
|
37
69
|
return void 0;
|
|
38
70
|
}
|
|
@@ -468,7 +500,7 @@ function detectCodeAfterReturn(context, lines, issues) {
|
|
|
468
500
|
}
|
|
469
501
|
}
|
|
470
502
|
if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
|
|
471
|
-
const endsOpen = /[{(
|
|
503
|
+
const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
|
|
472
504
|
if (endsOpen) continue;
|
|
473
505
|
lastReturnDepth = braceDepth;
|
|
474
506
|
lastReturnLine = i;
|
|
@@ -659,6 +691,9 @@ function analyzeFunctionNode(node) {
|
|
|
659
691
|
function calculateCyclomaticComplexity(root) {
|
|
660
692
|
let complexity = 1;
|
|
661
693
|
walkAST(root, (n) => {
|
|
694
|
+
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) {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
662
697
|
switch (n.type) {
|
|
663
698
|
case AST_NODE_TYPES.IfStatement:
|
|
664
699
|
case AST_NODE_TYPES.ConditionalExpression:
|
|
@@ -687,6 +722,9 @@ function calculateCognitiveComplexity(root) {
|
|
|
687
722
|
const depthMap = /* @__PURE__ */ new WeakMap();
|
|
688
723
|
depthMap.set(root, 0);
|
|
689
724
|
walkAST(root, (n, parent) => {
|
|
725
|
+
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) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
690
728
|
const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
|
|
691
729
|
const isNesting = isNestingNode(n);
|
|
692
730
|
const depth = isNesting ? parentDepth + 1 : parentDepth;
|
|
@@ -705,6 +743,9 @@ function calculateMaxNestingDepth(root) {
|
|
|
705
743
|
const depthMap = /* @__PURE__ */ new WeakMap();
|
|
706
744
|
depthMap.set(root, 0);
|
|
707
745
|
walkAST(root, (n, parent) => {
|
|
746
|
+
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) {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
708
749
|
const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
|
|
709
750
|
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;
|
|
710
751
|
const currentDepth = isNesting ? parentDepth + 1 : parentDepth;
|
|
@@ -886,14 +927,19 @@ function stringifyCondition(node) {
|
|
|
886
927
|
case AST_NODE_TYPES.Literal:
|
|
887
928
|
return String(node.value);
|
|
888
929
|
case AST_NODE_TYPES.BinaryExpression:
|
|
930
|
+
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
889
931
|
case AST_NODE_TYPES.LogicalExpression:
|
|
890
932
|
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
891
933
|
case AST_NODE_TYPES.UnaryExpression:
|
|
892
934
|
return `${node.operator}${stringifyCondition(node.argument)}`;
|
|
893
935
|
case AST_NODE_TYPES.MemberExpression:
|
|
894
936
|
return `${stringifyCondition(node.object)}.${stringifyCondition(node.property)}`;
|
|
895
|
-
case AST_NODE_TYPES.CallExpression:
|
|
896
|
-
|
|
937
|
+
case AST_NODE_TYPES.CallExpression: {
|
|
938
|
+
const args = node.arguments.map((arg) => stringifyCondition(arg)).join(", ");
|
|
939
|
+
return `${stringifyCondition(node.callee)}(${args})`;
|
|
940
|
+
}
|
|
941
|
+
case AST_NODE_TYPES.ConditionalExpression:
|
|
942
|
+
return `${stringifyCondition(node.test)} ? ${stringifyCondition(node.consequent)} : ${stringifyCondition(node.alternate)}`;
|
|
897
943
|
default:
|
|
898
944
|
return `[${node.type}]`;
|
|
899
945
|
}
|
|
@@ -903,6 +949,10 @@ function truncate(s, maxLen) {
|
|
|
903
949
|
}
|
|
904
950
|
|
|
905
951
|
// src/rules/builtin/security.ts
|
|
952
|
+
var isCommentLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("*");
|
|
953
|
+
var stripQuotedStrings = (line) => line.replace(/'[^'\\]*(?:\\.[^'\\]*)*'/g, "''").replace(/"[^"\\]*(?:\\.[^"\\]*)*"/g, '""').replace(/`[^`\\]*(?:\\.[^`\\]*)*`/g, "``");
|
|
954
|
+
var isPatternDefinitionLine = (line) => /\bpattern\s*:\s*\/.*\/[dgimsuvy]*/.test(line);
|
|
955
|
+
var hasQueryContext = (line) => /\b(query|sql|statement|stmt)\b/i.test(line) || /\.(query|execute|run|prepare)\s*\(/i.test(line);
|
|
906
956
|
var securityRules = [
|
|
907
957
|
{
|
|
908
958
|
id: "security/hardcoded-secret",
|
|
@@ -915,7 +965,7 @@ var securityRules = [
|
|
|
915
965
|
const lines = context.fileContent.split("\n");
|
|
916
966
|
const secretPatterns = [
|
|
917
967
|
// API keys / tokens
|
|
918
|
-
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_
|
|
968
|
+
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_-]{16,}['"`]/i, label: "API key" },
|
|
919
969
|
{ pattern: /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{8,}['"`]/i, label: "secret/password" },
|
|
920
970
|
// AWS
|
|
921
971
|
{ pattern: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
|
|
@@ -968,15 +1018,17 @@ var securityRules = [
|
|
|
968
1018
|
const issues = [];
|
|
969
1019
|
const lines = context.fileContent.split("\n");
|
|
970
1020
|
for (let i = 0; i < lines.length; i++) {
|
|
971
|
-
const
|
|
972
|
-
|
|
1021
|
+
const line = lines[i];
|
|
1022
|
+
const trimmed = line.trim();
|
|
1023
|
+
if (isCommentLine(trimmed) || isPatternDefinitionLine(line)) continue;
|
|
1024
|
+
const sanitizedLine = stripQuotedStrings(line);
|
|
973
1025
|
const evalPatterns = [
|
|
974
|
-
{ pattern: /\beval\s*\(/, label: "eval()" },
|
|
975
|
-
{ pattern: /new\s+Function\s*\(/, label: "new Function()" },
|
|
976
|
-
{ pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string" }
|
|
1026
|
+
{ pattern: /\beval\s*\(/, label: "eval()", source: sanitizedLine },
|
|
1027
|
+
{ pattern: /new\s+Function\s*\(/, label: "new Function()", source: sanitizedLine },
|
|
1028
|
+
{ pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string", source: line }
|
|
977
1029
|
];
|
|
978
|
-
for (const { pattern, label } of evalPatterns) {
|
|
979
|
-
if (pattern.test(
|
|
1030
|
+
for (const { pattern, label, source } of evalPatterns) {
|
|
1031
|
+
if (pattern.test(source)) {
|
|
980
1032
|
issues.push({
|
|
981
1033
|
ruleId: "security/eval-usage",
|
|
982
1034
|
severity: "high",
|
|
@@ -1009,29 +1061,30 @@ var securityRules = [
|
|
|
1009
1061
|
check(context) {
|
|
1010
1062
|
const issues = [];
|
|
1011
1063
|
const lines = context.fileContent.split("\n");
|
|
1064
|
+
const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
|
|
1012
1065
|
for (let i = 0; i < lines.length; i++) {
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
}
|
|
1066
|
+
const line = lines[i];
|
|
1067
|
+
const trimmed = line.trim();
|
|
1068
|
+
if (isCommentLine(trimmed)) continue;
|
|
1069
|
+
const hasSqlKeyword = sqlKeywords.test(line);
|
|
1070
|
+
const hasInterpolation = /\$\{[^}]+\}/.test(line) || /['"]\s*\+\s*\w+/.test(line);
|
|
1071
|
+
if (hasSqlKeyword && hasInterpolation && hasQueryContext(line)) {
|
|
1072
|
+
issues.push({
|
|
1073
|
+
ruleId: "security/sql-injection",
|
|
1074
|
+
severity: "high",
|
|
1075
|
+
category: "security",
|
|
1076
|
+
file: context.filePath,
|
|
1077
|
+
startLine: i + 1,
|
|
1078
|
+
endLine: i + 1,
|
|
1079
|
+
message: t(
|
|
1080
|
+
"Potential SQL injection \u2014 string interpolation in SQL query.",
|
|
1081
|
+
"\u6F5C\u5728\u7684 SQL \u6CE8\u5165 \u2014 SQL \u67E5\u8BE2\u4E2D\u4F7F\u7528\u4E86\u5B57\u7B26\u4E32\u63D2\u503C\u3002"
|
|
1082
|
+
),
|
|
1083
|
+
suggestion: t(
|
|
1084
|
+
"Use parameterized queries or prepared statements instead.",
|
|
1085
|
+
"\u8BF7\u6539\u7528\u53C2\u6570\u5316\u67E5\u8BE2\u6216\u9884\u7F16\u8BD1\u8BED\u53E5\u3002"
|
|
1086
|
+
)
|
|
1087
|
+
});
|
|
1035
1088
|
}
|
|
1036
1089
|
}
|
|
1037
1090
|
return issues;
|
|
@@ -1047,9 +1100,11 @@ var securityRules = [
|
|
|
1047
1100
|
const issues = [];
|
|
1048
1101
|
const lines = context.fileContent.split("\n");
|
|
1049
1102
|
for (let i = 0; i < lines.length; i++) {
|
|
1050
|
-
const
|
|
1103
|
+
const line = lines[i];
|
|
1104
|
+
const trimmed = line.trim();
|
|
1051
1105
|
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
1052
|
-
|
|
1106
|
+
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "").replace(/\/[^/]+\/[dgimsuvy]*/g, '""');
|
|
1107
|
+
if (/\.(innerHTML|outerHTML)\s*=/.test(cleaned) || /dangerouslySetInnerHTML/.test(cleaned)) {
|
|
1053
1108
|
issues.push({
|
|
1054
1109
|
ruleId: "security/dangerous-html",
|
|
1055
1110
|
severity: "medium",
|
|
@@ -1577,6 +1632,9 @@ var missingAwaitRule = {
|
|
|
1577
1632
|
const body = getFunctionBody(node);
|
|
1578
1633
|
if (!body) return;
|
|
1579
1634
|
walkAST(body, (inner, parent) => {
|
|
1635
|
+
if (inner.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1580
1638
|
if (inner !== body && isAsyncFunction(inner)) return false;
|
|
1581
1639
|
if (inner.type !== AST_NODE_TYPES.CallExpression) return;
|
|
1582
1640
|
if (parent?.type === AST_NODE_TYPES.AwaitExpression) return;
|
|
@@ -1586,6 +1644,9 @@ var missingAwaitRule = {
|
|
|
1586
1644
|
if (parent?.type === AST_NODE_TYPES.AssignmentExpression) return;
|
|
1587
1645
|
if (parent?.type === AST_NODE_TYPES.ArrayExpression) return;
|
|
1588
1646
|
if (parent?.type === AST_NODE_TYPES.CallExpression && parent !== inner) return;
|
|
1647
|
+
if (parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1589
1650
|
const callName = getCallName(inner);
|
|
1590
1651
|
if (!callName) return;
|
|
1591
1652
|
if (!asyncFuncNames.has(callName)) return;
|
|
@@ -1797,9 +1858,28 @@ var typeCoercionRule = {
|
|
|
1797
1858
|
var ALLOWED_NUMBERS = /* @__PURE__ */ new Set([
|
|
1798
1859
|
-1,
|
|
1799
1860
|
0,
|
|
1861
|
+
0.1,
|
|
1862
|
+
0.1,
|
|
1863
|
+
0.15,
|
|
1864
|
+
0.2,
|
|
1865
|
+
0.2,
|
|
1866
|
+
0.25,
|
|
1867
|
+
0.3,
|
|
1868
|
+
0.3,
|
|
1869
|
+
0.5,
|
|
1800
1870
|
1,
|
|
1801
1871
|
2,
|
|
1872
|
+
3,
|
|
1873
|
+
4,
|
|
1874
|
+
5,
|
|
1802
1875
|
10,
|
|
1876
|
+
15,
|
|
1877
|
+
20,
|
|
1878
|
+
30,
|
|
1879
|
+
40,
|
|
1880
|
+
50,
|
|
1881
|
+
70,
|
|
1882
|
+
90,
|
|
1803
1883
|
100
|
|
1804
1884
|
]);
|
|
1805
1885
|
var magicNumberRule = {
|
|
@@ -1828,6 +1908,7 @@ var magicNumberRule = {
|
|
|
1828
1908
|
if (/^\s*(export\s+)?enum\s/.test(line)) continue;
|
|
1829
1909
|
if (trimmed.startsWith("import ")) continue;
|
|
1830
1910
|
if (/^\s*return\s+[0-9]+\s*;?\s*$/.test(line)) continue;
|
|
1911
|
+
if (/^\s*['"]?[-\w]+['"]?\s*:\s*-?\d+\.?\d*(?:e[+-]?\d+)?\s*,?\s*$/.test(trimmed)) continue;
|
|
1831
1912
|
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
|
|
1832
1913
|
const numRegex = /(?<![.\w])(-?\d+\.?\d*(?:e[+-]?\d+)?)\b/gi;
|
|
1833
1914
|
let match;
|
|
@@ -1924,6 +2005,17 @@ var nestedTernaryRule = {
|
|
|
1924
2005
|
// src/rules/builtin/duplicate-string.ts
|
|
1925
2006
|
var MIN_STRING_LENGTH = 6;
|
|
1926
2007
|
var MIN_OCCURRENCES = 3;
|
|
2008
|
+
var IGNORED_LITERALS = /* @__PURE__ */ new Set([
|
|
2009
|
+
"high",
|
|
2010
|
+
"medium",
|
|
2011
|
+
"low",
|
|
2012
|
+
"info",
|
|
2013
|
+
"logic",
|
|
2014
|
+
"security",
|
|
2015
|
+
"structure",
|
|
2016
|
+
"style",
|
|
2017
|
+
"coverage"
|
|
2018
|
+
]);
|
|
1927
2019
|
var duplicateStringRule = {
|
|
1928
2020
|
id: "logic/duplicate-string",
|
|
1929
2021
|
category: "logic",
|
|
@@ -1954,6 +2046,7 @@ var duplicateStringRule = {
|
|
|
1954
2046
|
while ((match = stringRegex.exec(cleaned)) !== null) {
|
|
1955
2047
|
const value = match[2];
|
|
1956
2048
|
if (value.length < MIN_STRING_LENGTH) continue;
|
|
2049
|
+
if (IGNORED_LITERALS.has(value)) continue;
|
|
1957
2050
|
if (value.includes("${")) continue;
|
|
1958
2051
|
if (value.startsWith("http") || value.startsWith("/")) continue;
|
|
1959
2052
|
if (value.startsWith("test") || value.startsWith("mock")) continue;
|
|
@@ -2223,13 +2316,28 @@ var promiseVoidRule = {
|
|
|
2223
2316
|
/^save/,
|
|
2224
2317
|
/^load/,
|
|
2225
2318
|
/^send/,
|
|
2226
|
-
/^delete/,
|
|
2227
2319
|
/^update/,
|
|
2228
2320
|
/^create/,
|
|
2229
2321
|
/^connect/,
|
|
2230
2322
|
/^disconnect/,
|
|
2231
2323
|
/^init/
|
|
2232
2324
|
];
|
|
2325
|
+
const syncMethods = [
|
|
2326
|
+
"delete",
|
|
2327
|
+
// Map.delete(), Set.delete(), Object.delete() are synchronous
|
|
2328
|
+
"has",
|
|
2329
|
+
// Map.has(), Set.has() are synchronous
|
|
2330
|
+
"get",
|
|
2331
|
+
// Map.get() is synchronous
|
|
2332
|
+
"set",
|
|
2333
|
+
// Map.set() is synchronous (though some consider it potentially async)
|
|
2334
|
+
"keys",
|
|
2335
|
+
// Object.keys() is synchronous
|
|
2336
|
+
"values",
|
|
2337
|
+
// Object.values() is synchronous
|
|
2338
|
+
"entries"
|
|
2339
|
+
// Object.entries() is synchronous
|
|
2340
|
+
];
|
|
2233
2341
|
walkAST(ast, (node) => {
|
|
2234
2342
|
if (node.type !== AST_NODE_TYPES.ExpressionStatement) return;
|
|
2235
2343
|
const expr = node.expression;
|
|
@@ -2240,6 +2348,7 @@ var promiseVoidRule = {
|
|
|
2240
2348
|
const isKnownAsync = asyncFnNames.has(fnName);
|
|
2241
2349
|
const matchesPattern = commonAsyncPatterns.some((p) => p.test(fnName));
|
|
2242
2350
|
const endsWithAsync = fnName.endsWith("Async") || fnName.endsWith("async");
|
|
2351
|
+
if (syncMethods.includes(fnName)) return;
|
|
2243
2352
|
if (!isKnownAsync && !matchesPattern && !endsWithAsync) return;
|
|
2244
2353
|
const line = node.loc?.start.line ?? 0;
|
|
2245
2354
|
if (line === 0) return;
|
|
@@ -2315,12 +2424,7 @@ var noReassignParamRule = {
|
|
|
2315
2424
|
}
|
|
2316
2425
|
}
|
|
2317
2426
|
if (paramNames.size === 0) return;
|
|
2318
|
-
|
|
2319
|
-
if (node.type === AST_NODE_TYPES.MethodDefinition) {
|
|
2320
|
-
body = node.value;
|
|
2321
|
-
} else {
|
|
2322
|
-
body = node;
|
|
2323
|
-
}
|
|
2427
|
+
const body = node.type === AST_NODE_TYPES.MethodDefinition ? node.value : node;
|
|
2324
2428
|
if (!body || !("body" in body)) return;
|
|
2325
2429
|
const fnBody = body.body;
|
|
2326
2430
|
if (!fnBody) return;
|
|
@@ -2557,15 +2661,33 @@ var RuleEngine = class {
|
|
|
2557
2661
|
);
|
|
2558
2662
|
}
|
|
2559
2663
|
run(context) {
|
|
2664
|
+
return this.runWithDiagnostics(context).issues;
|
|
2665
|
+
}
|
|
2666
|
+
runWithDiagnostics(context) {
|
|
2560
2667
|
const allIssues = [];
|
|
2668
|
+
const ruleFailures = [];
|
|
2669
|
+
let rulesExecuted = 0;
|
|
2670
|
+
let rulesFailed = 0;
|
|
2561
2671
|
for (const rule of this.rules) {
|
|
2672
|
+
rulesExecuted++;
|
|
2562
2673
|
try {
|
|
2563
2674
|
const issues = rule.check(context);
|
|
2564
2675
|
allIssues.push(...issues);
|
|
2565
|
-
} catch (
|
|
2676
|
+
} catch (err) {
|
|
2677
|
+
rulesFailed++;
|
|
2678
|
+
ruleFailures.push({
|
|
2679
|
+
ruleId: rule.id,
|
|
2680
|
+
file: context.filePath,
|
|
2681
|
+
message: err instanceof Error ? err.message : "Unknown rule execution failure"
|
|
2682
|
+
});
|
|
2566
2683
|
}
|
|
2567
2684
|
}
|
|
2568
|
-
return
|
|
2685
|
+
return {
|
|
2686
|
+
issues: allIssues,
|
|
2687
|
+
rulesExecuted,
|
|
2688
|
+
rulesFailed,
|
|
2689
|
+
ruleFailures
|
|
2690
|
+
};
|
|
2569
2691
|
}
|
|
2570
2692
|
getRules() {
|
|
2571
2693
|
return [...this.rules];
|
|
@@ -2945,6 +3067,8 @@ var PKG_VERSION = (() => {
|
|
|
2945
3067
|
return "0.1.0";
|
|
2946
3068
|
}
|
|
2947
3069
|
})();
|
|
3070
|
+
var REPORT_SCHEMA_VERSION = "1.0.0";
|
|
3071
|
+
var FINGERPRINT_VERSION = "1";
|
|
2948
3072
|
var ScanEngine = class {
|
|
2949
3073
|
config;
|
|
2950
3074
|
diffParser;
|
|
@@ -2955,81 +3079,191 @@ var ScanEngine = class {
|
|
|
2955
3079
|
this.ruleEngine = new RuleEngine(config);
|
|
2956
3080
|
}
|
|
2957
3081
|
async scan(options) {
|
|
2958
|
-
const
|
|
3082
|
+
const selection = await this.getScanCandidates(options);
|
|
2959
3083
|
const allIssues = [];
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
} else {
|
|
2985
|
-
currentLine++;
|
|
2986
|
-
}
|
|
2987
|
-
}
|
|
2988
|
-
return result;
|
|
2989
|
-
});
|
|
2990
|
-
const issues = this.ruleEngine.run({
|
|
2991
|
-
filePath: diffFile.filePath,
|
|
2992
|
-
fileContent,
|
|
2993
|
-
addedLines
|
|
2994
|
-
});
|
|
2995
|
-
allIssues.push(...issues);
|
|
2996
|
-
if (this.isTsJsFile(diffFile.filePath)) {
|
|
2997
|
-
const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
|
|
2998
|
-
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
2999
|
-
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3000
|
-
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3001
|
-
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3002
|
-
maxParamCount: this.config.thresholds["max-params"]
|
|
3003
|
-
});
|
|
3004
|
-
allIssues.push(...structureResult.issues);
|
|
3005
|
-
const styleResult = analyzeStyle(fileContent, diffFile.filePath);
|
|
3006
|
-
allIssues.push(...styleResult.issues);
|
|
3007
|
-
const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
|
|
3008
|
-
allIssues.push(...coverageResult.issues);
|
|
3009
|
-
}
|
|
3010
|
-
}
|
|
3011
|
-
const dimensions = this.groupByDimension(allIssues);
|
|
3084
|
+
const scanErrors = [];
|
|
3085
|
+
const ruleFailures = [];
|
|
3086
|
+
let rulesExecuted = 0;
|
|
3087
|
+
let rulesFailed = 0;
|
|
3088
|
+
let filesScanned = 0;
|
|
3089
|
+
const results = await Promise.all(
|
|
3090
|
+
selection.candidates.map((diffFile) => this.scanFile(diffFile))
|
|
3091
|
+
);
|
|
3092
|
+
for (const result of results) {
|
|
3093
|
+
allIssues.push(...result.issues);
|
|
3094
|
+
ruleFailures.push(...result.ruleFailures);
|
|
3095
|
+
scanErrors.push(...result.scanErrors);
|
|
3096
|
+
rulesExecuted += result.rulesExecuted;
|
|
3097
|
+
rulesFailed += result.rulesFailed;
|
|
3098
|
+
if (result.scanned) {
|
|
3099
|
+
filesScanned++;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
const issuesWithFingerprints = this.attachFingerprints(allIssues);
|
|
3103
|
+
const baseline = await this.loadBaseline(options.baseline);
|
|
3104
|
+
const issuesWithLifecycle = this.attachLifecycle(issuesWithFingerprints, baseline);
|
|
3105
|
+
const fixedIssues = this.getFixedIssues(issuesWithLifecycle, baseline);
|
|
3106
|
+
const lifecycle = this.buildLifecycleSummary(issuesWithLifecycle, fixedIssues, baseline);
|
|
3107
|
+
const dimensions = this.groupByDimension(issuesWithLifecycle);
|
|
3012
3108
|
const overallScore = calculateOverallScore(dimensions, this.config.weights);
|
|
3013
3109
|
const grade = getGrade(overallScore);
|
|
3014
3110
|
const commitHash = await this.diffParser.getCurrentCommitHash();
|
|
3015
3111
|
return {
|
|
3112
|
+
schemaVersion: REPORT_SCHEMA_VERSION,
|
|
3016
3113
|
version: PKG_VERSION,
|
|
3017
3114
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3018
3115
|
commit: commitHash,
|
|
3116
|
+
scanMode: selection.scanMode,
|
|
3019
3117
|
overall: {
|
|
3020
3118
|
score: overallScore,
|
|
3021
3119
|
grade,
|
|
3022
|
-
filesScanned
|
|
3023
|
-
issuesFound:
|
|
3120
|
+
filesScanned,
|
|
3121
|
+
issuesFound: issuesWithLifecycle.length
|
|
3122
|
+
},
|
|
3123
|
+
toolHealth: {
|
|
3124
|
+
rulesExecuted,
|
|
3125
|
+
rulesFailed,
|
|
3126
|
+
filesConsidered: selection.filesConsidered,
|
|
3127
|
+
filesScanned,
|
|
3128
|
+
filesExcluded: selection.filesExcluded,
|
|
3129
|
+
filesSkipped: scanErrors.length,
|
|
3130
|
+
scanErrors,
|
|
3131
|
+
ruleFailures
|
|
3024
3132
|
},
|
|
3025
3133
|
dimensions,
|
|
3026
|
-
issues:
|
|
3134
|
+
issues: issuesWithLifecycle.sort((a, b) => {
|
|
3027
3135
|
const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
|
|
3028
3136
|
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
3029
|
-
})
|
|
3137
|
+
}),
|
|
3138
|
+
lifecycle,
|
|
3139
|
+
fixedIssues
|
|
3030
3140
|
};
|
|
3031
3141
|
}
|
|
3032
|
-
async
|
|
3142
|
+
async scanFile(diffFile) {
|
|
3143
|
+
if (diffFile.status === "deleted") {
|
|
3144
|
+
return this.createSkippedResult(diffFile, "deleted-file", `Skipped deleted file: ${diffFile.filePath}`);
|
|
3145
|
+
}
|
|
3146
|
+
if (!this.isTsJsFile(diffFile.filePath)) {
|
|
3147
|
+
return this.createSkippedResult(diffFile, "unsupported-file-type", `Skipped unsupported file type: ${diffFile.filePath}`);
|
|
3148
|
+
}
|
|
3149
|
+
const fileContent = await this.readFileContent(diffFile);
|
|
3150
|
+
if (!fileContent) {
|
|
3151
|
+
return this.createErrorResult(diffFile, "missing-file-content", `Unable to read file content for ${diffFile.filePath}`);
|
|
3152
|
+
}
|
|
3153
|
+
const addedLines = this.extractAddedLines(diffFile);
|
|
3154
|
+
const ruleResult = this.ruleEngine.runWithDiagnostics({
|
|
3155
|
+
filePath: diffFile.filePath,
|
|
3156
|
+
fileContent,
|
|
3157
|
+
addedLines
|
|
3158
|
+
});
|
|
3159
|
+
const issues = [...ruleResult.issues];
|
|
3160
|
+
issues.push(...this.runStructureAnalysis(fileContent, diffFile.filePath));
|
|
3161
|
+
issues.push(...analyzeStyle(fileContent, diffFile.filePath).issues);
|
|
3162
|
+
issues.push(...analyzeCoverage(fileContent, diffFile.filePath).issues);
|
|
3163
|
+
return {
|
|
3164
|
+
issues,
|
|
3165
|
+
ruleFailures: ruleResult.ruleFailures,
|
|
3166
|
+
rulesExecuted: ruleResult.rulesExecuted,
|
|
3167
|
+
rulesFailed: ruleResult.rulesFailed,
|
|
3168
|
+
scanErrors: [],
|
|
3169
|
+
scanned: true
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
createSkippedResult(diffFile, type, message) {
|
|
3173
|
+
return {
|
|
3174
|
+
issues: [],
|
|
3175
|
+
ruleFailures: [],
|
|
3176
|
+
rulesExecuted: 0,
|
|
3177
|
+
rulesFailed: 0,
|
|
3178
|
+
scanErrors: [{ type, file: diffFile.filePath, message }],
|
|
3179
|
+
scanned: false
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
createErrorResult(diffFile, type, message) {
|
|
3183
|
+
return {
|
|
3184
|
+
issues: [],
|
|
3185
|
+
ruleFailures: [],
|
|
3186
|
+
rulesExecuted: 0,
|
|
3187
|
+
rulesFailed: 0,
|
|
3188
|
+
scanErrors: [{ type, file: diffFile.filePath, message }],
|
|
3189
|
+
scanned: false
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
async readFileContent(diffFile) {
|
|
3193
|
+
const filePath = resolve2(diffFile.filePath);
|
|
3194
|
+
try {
|
|
3195
|
+
return await readFile(filePath, "utf-8");
|
|
3196
|
+
} catch {
|
|
3197
|
+
const content = await this.diffParser.getFileContent(diffFile.filePath);
|
|
3198
|
+
return content ?? null;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
extractAddedLines(diffFile) {
|
|
3202
|
+
return diffFile.hunks.flatMap((hunk) => {
|
|
3203
|
+
const lines = hunk.content.split("\n");
|
|
3204
|
+
const result = [];
|
|
3205
|
+
let currentLine = hunk.newStart;
|
|
3206
|
+
for (const line of lines) {
|
|
3207
|
+
if (line.startsWith("+")) {
|
|
3208
|
+
result.push({ lineNumber: currentLine, content: line.slice(1) });
|
|
3209
|
+
currentLine++;
|
|
3210
|
+
} else if (line.startsWith("-")) {
|
|
3211
|
+
} else {
|
|
3212
|
+
currentLine++;
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
return result;
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
runStructureAnalysis(fileContent, filePath) {
|
|
3219
|
+
return analyzeStructure(fileContent, filePath, {
|
|
3220
|
+
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
3221
|
+
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3222
|
+
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3223
|
+
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3224
|
+
maxParamCount: this.config.thresholds["max-params"]
|
|
3225
|
+
}).issues;
|
|
3226
|
+
}
|
|
3227
|
+
async getScanCandidates(options) {
|
|
3228
|
+
const scanMode = this.getScanMode(options);
|
|
3229
|
+
const candidates = await this.getDiffFiles(options);
|
|
3230
|
+
if (scanMode === "files") {
|
|
3231
|
+
return {
|
|
3232
|
+
scanMode,
|
|
3233
|
+
candidates,
|
|
3234
|
+
filesConsidered: options.files?.length ?? candidates.length,
|
|
3235
|
+
filesExcluded: (options.files?.length ?? candidates.length) - candidates.length
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
const filteredCandidates = [];
|
|
3239
|
+
let filesExcluded = 0;
|
|
3240
|
+
for (const candidate of candidates) {
|
|
3241
|
+
if (this.shouldIncludeFile(candidate.filePath)) {
|
|
3242
|
+
filteredCandidates.push(candidate);
|
|
3243
|
+
} else {
|
|
3244
|
+
filesExcluded++;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
return {
|
|
3248
|
+
scanMode,
|
|
3249
|
+
candidates: filteredCandidates,
|
|
3250
|
+
filesConsidered: candidates.length,
|
|
3251
|
+
filesExcluded
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
getScanMode(options) {
|
|
3255
|
+
if (options.staged) {
|
|
3256
|
+
return "staged";
|
|
3257
|
+
}
|
|
3258
|
+
if (options.diff) {
|
|
3259
|
+
return "diff";
|
|
3260
|
+
}
|
|
3261
|
+
if (options.files && options.files.length > 0) {
|
|
3262
|
+
return "files";
|
|
3263
|
+
}
|
|
3264
|
+
return "changed";
|
|
3265
|
+
}
|
|
3266
|
+
getDiffFiles(options) {
|
|
3033
3267
|
if (options.staged) {
|
|
3034
3268
|
return this.diffParser.getStagedFiles();
|
|
3035
3269
|
}
|
|
@@ -3037,14 +3271,10 @@ var ScanEngine = class {
|
|
|
3037
3271
|
return this.diffParser.getDiffFromRef(options.diff);
|
|
3038
3272
|
}
|
|
3039
3273
|
if (options.files && options.files.length > 0) {
|
|
3274
|
+
const includedFiles = options.files.filter((filePath) => this.shouldIncludeFile(filePath));
|
|
3040
3275
|
return Promise.all(
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
try {
|
|
3044
|
-
content = await readFile(resolve2(filePath), "utf-8");
|
|
3045
|
-
} catch {
|
|
3046
|
-
}
|
|
3047
|
-
return {
|
|
3276
|
+
includedFiles.map(
|
|
3277
|
+
(filePath) => readFile(resolve2(filePath), "utf-8").catch(() => "").then((content) => ({
|
|
3048
3278
|
filePath,
|
|
3049
3279
|
status: "modified",
|
|
3050
3280
|
additions: content.split("\n").length,
|
|
@@ -3056,18 +3286,154 @@ var ScanEngine = class {
|
|
|
3056
3286
|
oldLines: 0,
|
|
3057
3287
|
newStart: 1,
|
|
3058
3288
|
newLines: content.split("\n").length,
|
|
3059
|
-
content: content.split("\n").map((
|
|
3289
|
+
content: content.split("\n").map((line) => "+" + line).join("\n")
|
|
3060
3290
|
}
|
|
3061
3291
|
]
|
|
3062
|
-
}
|
|
3063
|
-
|
|
3292
|
+
}))
|
|
3293
|
+
)
|
|
3064
3294
|
);
|
|
3065
3295
|
}
|
|
3066
3296
|
return this.diffParser.getChangedFiles();
|
|
3067
3297
|
}
|
|
3298
|
+
shouldIncludeFile(filePath) {
|
|
3299
|
+
const normalizedPath = filePath.split(sep).join("/");
|
|
3300
|
+
const includePatterns = this.config.include.length > 0 ? this.config.include : ["**/*"];
|
|
3301
|
+
const included = includePatterns.some((pattern) => this.matchesPattern(normalizedPath, pattern));
|
|
3302
|
+
if (!included) {
|
|
3303
|
+
return false;
|
|
3304
|
+
}
|
|
3305
|
+
return !this.config.exclude.some((pattern) => this.matchesPattern(normalizedPath, pattern));
|
|
3306
|
+
}
|
|
3307
|
+
matchesPattern(filePath, pattern) {
|
|
3308
|
+
let regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
3309
|
+
regexPattern = regexPattern.replace(/\*\*\//g, "::DOUBLE_DIR::");
|
|
3310
|
+
regexPattern = regexPattern.replace(/\*\*/g, "::DOUBLE_STAR::");
|
|
3311
|
+
regexPattern = regexPattern.replace(/\*/g, "[^/]*");
|
|
3312
|
+
regexPattern = regexPattern.replace(/::DOUBLE_DIR::/g, "(?:.*/)?");
|
|
3313
|
+
regexPattern = regexPattern.replace(/::DOUBLE_STAR::/g, ".*");
|
|
3314
|
+
return new RegExp(`^${regexPattern}$`).test(filePath);
|
|
3315
|
+
}
|
|
3068
3316
|
isTsJsFile(filePath) {
|
|
3069
3317
|
return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
|
|
3070
3318
|
}
|
|
3319
|
+
attachFingerprints(issues) {
|
|
3320
|
+
const occurrenceCounts = /* @__PURE__ */ new Map();
|
|
3321
|
+
return issues.map((issue) => {
|
|
3322
|
+
const normalizedFile = this.normalizeRelativePath(issue.file);
|
|
3323
|
+
const locationComponent = `${issue.startLine}:${issue.endLine}`;
|
|
3324
|
+
const baseKey = [
|
|
3325
|
+
issue.ruleId,
|
|
3326
|
+
normalizedFile,
|
|
3327
|
+
issue.category,
|
|
3328
|
+
issue.severity,
|
|
3329
|
+
locationComponent
|
|
3330
|
+
].join("|");
|
|
3331
|
+
const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
|
|
3332
|
+
occurrenceCounts.set(baseKey, occurrenceIndex + 1);
|
|
3333
|
+
const fingerprint = createHash("sha256").update(`${FINGERPRINT_VERSION}|${baseKey}|${occurrenceIndex}`).digest("hex");
|
|
3334
|
+
return {
|
|
3335
|
+
...issue,
|
|
3336
|
+
file: normalizedFile,
|
|
3337
|
+
fingerprint,
|
|
3338
|
+
fingerprintVersion: FINGERPRINT_VERSION
|
|
3339
|
+
};
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
normalizeRelativePath(filePath) {
|
|
3343
|
+
const absolutePath = resolve2(filePath);
|
|
3344
|
+
const relativePath = relative(process.cwd(), absolutePath) || filePath;
|
|
3345
|
+
return relativePath.split(sep).join("/");
|
|
3346
|
+
}
|
|
3347
|
+
async loadBaseline(baselinePath) {
|
|
3348
|
+
if (!baselinePath) {
|
|
3349
|
+
return void 0;
|
|
3350
|
+
}
|
|
3351
|
+
const baselineContent = await readFile(resolve2(baselinePath), "utf-8");
|
|
3352
|
+
const parsed = JSON.parse(baselineContent);
|
|
3353
|
+
const issues = this.parseBaselineIssues(parsed.issues);
|
|
3354
|
+
return {
|
|
3355
|
+
issues,
|
|
3356
|
+
fingerprintSet: new Set(issues.map((issue) => issue.fingerprint)),
|
|
3357
|
+
commit: typeof parsed.commit === "string" ? parsed.commit : void 0,
|
|
3358
|
+
timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
parseBaselineIssues(input) {
|
|
3362
|
+
if (!Array.isArray(input)) {
|
|
3363
|
+
return [];
|
|
3364
|
+
}
|
|
3365
|
+
return input.flatMap((item) => {
|
|
3366
|
+
const issue = this.parseBaselineIssue(item);
|
|
3367
|
+
return issue ? [issue] : [];
|
|
3368
|
+
});
|
|
3369
|
+
}
|
|
3370
|
+
parseBaselineIssue(input) {
|
|
3371
|
+
if (!input || typeof input !== "object") {
|
|
3372
|
+
return void 0;
|
|
3373
|
+
}
|
|
3374
|
+
const issue = input;
|
|
3375
|
+
if (!this.isValidBaselineIssue(issue)) {
|
|
3376
|
+
return void 0;
|
|
3377
|
+
}
|
|
3378
|
+
return {
|
|
3379
|
+
ruleId: issue.ruleId,
|
|
3380
|
+
severity: issue.severity,
|
|
3381
|
+
category: issue.category,
|
|
3382
|
+
file: issue.file,
|
|
3383
|
+
startLine: issue.startLine,
|
|
3384
|
+
endLine: issue.endLine,
|
|
3385
|
+
message: issue.message,
|
|
3386
|
+
fingerprint: issue.fingerprint,
|
|
3387
|
+
fingerprintVersion: typeof issue.fingerprintVersion === "string" ? issue.fingerprintVersion : void 0
|
|
3388
|
+
};
|
|
3389
|
+
}
|
|
3390
|
+
isValidBaselineIssue(issue) {
|
|
3391
|
+
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";
|
|
3392
|
+
}
|
|
3393
|
+
attachLifecycle(issues, baseline) {
|
|
3394
|
+
if (!baseline) {
|
|
3395
|
+
return issues;
|
|
3396
|
+
}
|
|
3397
|
+
return issues.map((issue) => ({
|
|
3398
|
+
...issue,
|
|
3399
|
+
lifecycle: baseline.fingerprintSet.has(issue.fingerprint) ? "existing" : "new"
|
|
3400
|
+
}));
|
|
3401
|
+
}
|
|
3402
|
+
getFixedIssues(issues, baseline) {
|
|
3403
|
+
if (!baseline) {
|
|
3404
|
+
return [];
|
|
3405
|
+
}
|
|
3406
|
+
const currentFingerprints = new Set(issues.map((issue) => issue.fingerprint));
|
|
3407
|
+
return baseline.issues.filter((issue) => !currentFingerprints.has(issue.fingerprint));
|
|
3408
|
+
}
|
|
3409
|
+
buildLifecycleSummary(issues, fixedIssues, baseline) {
|
|
3410
|
+
if (!baseline) {
|
|
3411
|
+
return void 0;
|
|
3412
|
+
}
|
|
3413
|
+
let newIssues = 0;
|
|
3414
|
+
let existingIssues = 0;
|
|
3415
|
+
for (const issue of issues) {
|
|
3416
|
+
if (issue.lifecycle === "existing") {
|
|
3417
|
+
existingIssues++;
|
|
3418
|
+
} else {
|
|
3419
|
+
newIssues++;
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
return {
|
|
3423
|
+
newIssues,
|
|
3424
|
+
existingIssues,
|
|
3425
|
+
fixedIssues: fixedIssues.length,
|
|
3426
|
+
baselineUsed: true,
|
|
3427
|
+
baselineCommit: baseline.commit,
|
|
3428
|
+
baselineTimestamp: baseline.timestamp
|
|
3429
|
+
};
|
|
3430
|
+
}
|
|
3431
|
+
isSeverity(value) {
|
|
3432
|
+
return value === "high" || value === "medium" || value === "low" || value === "info";
|
|
3433
|
+
}
|
|
3434
|
+
isRuleCategory(value) {
|
|
3435
|
+
return ["security", "logic", "structure", "style", "coverage"].includes(value);
|
|
3436
|
+
}
|
|
3071
3437
|
groupByDimension(issues) {
|
|
3072
3438
|
const categories = [
|
|
3073
3439
|
"security",
|
|
@@ -3078,7 +3444,7 @@ var ScanEngine = class {
|
|
|
3078
3444
|
];
|
|
3079
3445
|
const grouped = {};
|
|
3080
3446
|
for (const cat of categories) {
|
|
3081
|
-
const catIssues = issues.filter((
|
|
3447
|
+
const catIssues = issues.filter((issue) => issue.category === cat);
|
|
3082
3448
|
grouped[cat] = calculateDimensionScore(catIssues);
|
|
3083
3449
|
}
|
|
3084
3450
|
return grouped;
|