@gulu9527/code-trust 0.2.0 → 0.3.0
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 +51 -4
- package/dist/cli/index.js +644 -129
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +56 -2
- package/dist/index.js +372 -108
- 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/cli/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command7 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/cli/commands/scan.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -120,9 +120,10 @@ detection:
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// src/core/engine.ts
|
|
123
|
+
import { createHash } from "crypto";
|
|
123
124
|
import { readFile } from "fs/promises";
|
|
124
|
-
import { readFileSync
|
|
125
|
-
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
125
|
+
import { readFileSync } from "fs";
|
|
126
|
+
import { resolve as resolve2, dirname as dirname3, relative, sep } from "path";
|
|
126
127
|
import { fileURLToPath } from "url";
|
|
127
128
|
|
|
128
129
|
// src/parsers/diff.ts
|
|
@@ -589,7 +590,7 @@ function detectCodeAfterReturn(context, lines, issues) {
|
|
|
589
590
|
}
|
|
590
591
|
}
|
|
591
592
|
if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
|
|
592
|
-
const endsOpen = /[{(
|
|
593
|
+
const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
|
|
593
594
|
if (endsOpen) continue;
|
|
594
595
|
lastReturnDepth = braceDepth;
|
|
595
596
|
lastReturnLine = i;
|
|
@@ -645,6 +646,24 @@ function detectImmediateReassign(context, lines, issues) {
|
|
|
645
646
|
}
|
|
646
647
|
}
|
|
647
648
|
|
|
649
|
+
// src/rules/fix-utils.ts
|
|
650
|
+
function lineStartOffset(content, lineNumber) {
|
|
651
|
+
let offset = 0;
|
|
652
|
+
const lines = content.split("\n");
|
|
653
|
+
for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
|
|
654
|
+
offset += lines[i].length + 1;
|
|
655
|
+
}
|
|
656
|
+
return offset;
|
|
657
|
+
}
|
|
658
|
+
function lineRange(content, lineNumber) {
|
|
659
|
+
const lines = content.split("\n");
|
|
660
|
+
const lineIndex = lineNumber - 1;
|
|
661
|
+
if (lineIndex < 0 || lineIndex >= lines.length) return [0, 0];
|
|
662
|
+
const start = lineStartOffset(content, lineNumber);
|
|
663
|
+
const end = start + lines[lineIndex].length + (lineIndex < lines.length - 1 ? 1 : 0);
|
|
664
|
+
return [start, end];
|
|
665
|
+
}
|
|
666
|
+
|
|
648
667
|
// src/parsers/ast.ts
|
|
649
668
|
import { parse, AST_NODE_TYPES } from "@typescript-eslint/typescript-estree";
|
|
650
669
|
|
|
@@ -827,6 +846,19 @@ var unusedVariablesRule = {
|
|
|
827
846
|
severity: "low",
|
|
828
847
|
title: "Unused variable detected",
|
|
829
848
|
description: "AI-generated code sometimes declares variables that are never used, indicating incomplete or hallucinated logic.",
|
|
849
|
+
fixable: true,
|
|
850
|
+
fix(context, issue) {
|
|
851
|
+
const lines = context.fileContent.split("\n");
|
|
852
|
+
const lineIndex = issue.startLine - 1;
|
|
853
|
+
if (lineIndex < 0 || lineIndex >= lines.length) return null;
|
|
854
|
+
const line = lines[lineIndex].trim();
|
|
855
|
+
if (/^(const|let|var)\s+\w+\s*[=:;]/.test(line) && !line.includes(",")) {
|
|
856
|
+
const [start, end] = lineRange(context.fileContent, issue.startLine);
|
|
857
|
+
if (start === end) return null;
|
|
858
|
+
return { range: [start, end], text: "" };
|
|
859
|
+
}
|
|
860
|
+
return null;
|
|
861
|
+
},
|
|
830
862
|
check(context) {
|
|
831
863
|
const issues = [];
|
|
832
864
|
let ast;
|
|
@@ -993,6 +1025,10 @@ function truncate(s, maxLen) {
|
|
|
993
1025
|
}
|
|
994
1026
|
|
|
995
1027
|
// src/rules/builtin/security.ts
|
|
1028
|
+
var isCommentLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("*");
|
|
1029
|
+
var stripQuotedStrings = (line) => line.replace(/'[^'\\]*(?:\\.[^'\\]*)*'/g, "''").replace(/"[^"\\]*(?:\\.[^"\\]*)*"/g, '""').replace(/`[^`\\]*(?:\\.[^`\\]*)*`/g, "``");
|
|
1030
|
+
var isPatternDefinitionLine = (line) => /\bpattern\s*:\s*\/.*\/[dgimsuvy]*/.test(line);
|
|
1031
|
+
var hasQueryContext = (line) => /\b(query|sql|statement|stmt)\b/i.test(line) || /\.(query|execute|run|prepare)\s*\(/i.test(line);
|
|
996
1032
|
var securityRules = [
|
|
997
1033
|
{
|
|
998
1034
|
id: "security/hardcoded-secret",
|
|
@@ -1005,7 +1041,7 @@ var securityRules = [
|
|
|
1005
1041
|
const lines = context.fileContent.split("\n");
|
|
1006
1042
|
const secretPatterns = [
|
|
1007
1043
|
// API keys / tokens
|
|
1008
|
-
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_
|
|
1044
|
+
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_-]{16,}['"`]/i, label: "API key" },
|
|
1009
1045
|
{ pattern: /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{8,}['"`]/i, label: "secret/password" },
|
|
1010
1046
|
// AWS
|
|
1011
1047
|
{ pattern: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
|
|
@@ -1058,15 +1094,17 @@ var securityRules = [
|
|
|
1058
1094
|
const issues = [];
|
|
1059
1095
|
const lines = context.fileContent.split("\n");
|
|
1060
1096
|
for (let i = 0; i < lines.length; i++) {
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1097
|
+
const line = lines[i];
|
|
1098
|
+
const trimmed = line.trim();
|
|
1099
|
+
if (isCommentLine(trimmed) || isPatternDefinitionLine(line)) continue;
|
|
1100
|
+
const sanitizedLine = stripQuotedStrings(line);
|
|
1063
1101
|
const evalPatterns = [
|
|
1064
|
-
{ pattern: /\beval\s*\(/, label: "eval()" },
|
|
1065
|
-
{ pattern: /new\s+Function\s*\(/, label: "new Function()" },
|
|
1066
|
-
{ pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string" }
|
|
1102
|
+
{ pattern: /\beval\s*\(/, label: "eval()", source: sanitizedLine },
|
|
1103
|
+
{ pattern: /new\s+Function\s*\(/, label: "new Function()", source: sanitizedLine },
|
|
1104
|
+
{ pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string", source: line }
|
|
1067
1105
|
];
|
|
1068
|
-
for (const { pattern, label } of evalPatterns) {
|
|
1069
|
-
if (pattern.test(
|
|
1106
|
+
for (const { pattern, label, source } of evalPatterns) {
|
|
1107
|
+
if (pattern.test(source)) {
|
|
1070
1108
|
issues.push({
|
|
1071
1109
|
ruleId: "security/eval-usage",
|
|
1072
1110
|
severity: "high",
|
|
@@ -1099,29 +1137,30 @@ var securityRules = [
|
|
|
1099
1137
|
check(context) {
|
|
1100
1138
|
const issues = [];
|
|
1101
1139
|
const lines = context.fileContent.split("\n");
|
|
1140
|
+
const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
|
|
1102
1141
|
for (let i = 0; i < lines.length; i++) {
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
}
|
|
1142
|
+
const line = lines[i];
|
|
1143
|
+
const trimmed = line.trim();
|
|
1144
|
+
if (isCommentLine(trimmed)) continue;
|
|
1145
|
+
const hasSqlKeyword = sqlKeywords.test(line);
|
|
1146
|
+
const hasInterpolation = /\$\{[^}]+\}/.test(line) || /['"]\s*\+\s*\w+/.test(line);
|
|
1147
|
+
if (hasSqlKeyword && hasInterpolation && hasQueryContext(line)) {
|
|
1148
|
+
issues.push({
|
|
1149
|
+
ruleId: "security/sql-injection",
|
|
1150
|
+
severity: "high",
|
|
1151
|
+
category: "security",
|
|
1152
|
+
file: context.filePath,
|
|
1153
|
+
startLine: i + 1,
|
|
1154
|
+
endLine: i + 1,
|
|
1155
|
+
message: t(
|
|
1156
|
+
"Potential SQL injection \u2014 string interpolation in SQL query.",
|
|
1157
|
+
"\u6F5C\u5728\u7684 SQL \u6CE8\u5165 \u2014 SQL \u67E5\u8BE2\u4E2D\u4F7F\u7528\u4E86\u5B57\u7B26\u4E32\u63D2\u503C\u3002"
|
|
1158
|
+
),
|
|
1159
|
+
suggestion: t(
|
|
1160
|
+
"Use parameterized queries or prepared statements instead.",
|
|
1161
|
+
"\u8BF7\u6539\u7528\u53C2\u6570\u5316\u67E5\u8BE2\u6216\u9884\u7F16\u8BD1\u8BED\u53E5\u3002"
|
|
1162
|
+
)
|
|
1163
|
+
});
|
|
1125
1164
|
}
|
|
1126
1165
|
}
|
|
1127
1166
|
return issues;
|
|
@@ -1550,6 +1589,23 @@ var unusedImportRule = {
|
|
|
1550
1589
|
severity: "low",
|
|
1551
1590
|
title: "Unused import",
|
|
1552
1591
|
description: "AI-generated code often imports modules or identifiers that are never used in the file.",
|
|
1592
|
+
fixable: true,
|
|
1593
|
+
fix(context, issue) {
|
|
1594
|
+
const lines = context.fileContent.split("\n");
|
|
1595
|
+
const lineIndex = issue.startLine - 1;
|
|
1596
|
+
if (lineIndex < 0 || lineIndex >= lines.length) return null;
|
|
1597
|
+
const line = lines[lineIndex].trim();
|
|
1598
|
+
const isSingleDefault = /^import\s+\w+\s+from\s+/.test(line);
|
|
1599
|
+
const isSingleNamed = /^import\s*\{\s*\w+\s*\}\s*from\s+/.test(line);
|
|
1600
|
+
const isSingleTypeNamed = /^import\s+type\s*\{\s*\w+\s*\}\s*from\s+/.test(line);
|
|
1601
|
+
const isSingleTypeDefault = /^import\s+type\s+\w+\s+from\s+/.test(line);
|
|
1602
|
+
if (!isSingleDefault && !isSingleNamed && !isSingleTypeNamed && !isSingleTypeDefault) {
|
|
1603
|
+
return null;
|
|
1604
|
+
}
|
|
1605
|
+
const [start, end] = lineRange(context.fileContent, issue.startLine);
|
|
1606
|
+
if (start === end) return null;
|
|
1607
|
+
return { range: [start, end], text: "" };
|
|
1608
|
+
},
|
|
1553
1609
|
check(context) {
|
|
1554
1610
|
const issues = [];
|
|
1555
1611
|
let ast;
|
|
@@ -1787,6 +1843,33 @@ var typeCoercionRule = {
|
|
|
1787
1843
|
severity: "medium",
|
|
1788
1844
|
title: "Loose equality with type coercion",
|
|
1789
1845
|
description: "AI-generated code often uses == instead of ===, leading to implicit type coercion bugs.",
|
|
1846
|
+
fixable: true,
|
|
1847
|
+
fix(context, issue) {
|
|
1848
|
+
const lines = context.fileContent.split("\n");
|
|
1849
|
+
const lineIndex = issue.startLine - 1;
|
|
1850
|
+
if (lineIndex < 0 || lineIndex >= lines.length) return null;
|
|
1851
|
+
const line = lines[lineIndex];
|
|
1852
|
+
const base = lineStartOffset(context.fileContent, issue.startLine);
|
|
1853
|
+
const isNotEqual = issue.message.includes("!=");
|
|
1854
|
+
const searchOp = isNotEqual ? "!=" : "==";
|
|
1855
|
+
const replaceOp = isNotEqual ? "!==" : "===";
|
|
1856
|
+
let pos = -1;
|
|
1857
|
+
for (let j = 0; j < line.length - 1; j++) {
|
|
1858
|
+
if (line[j] === searchOp[0] && line[j + 1] === "=") {
|
|
1859
|
+
if (line[j + 2] === "=") {
|
|
1860
|
+
j += 2;
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
if (!isNotEqual && j > 0 && (line[j - 1] === "!" || line[j - 1] === "<" || line[j - 1] === ">")) {
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
pos = j;
|
|
1867
|
+
break;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
if (pos === -1) return null;
|
|
1871
|
+
return { range: [base + pos, base + pos + searchOp.length], text: replaceOp };
|
|
1872
|
+
},
|
|
1790
1873
|
check(context) {
|
|
1791
1874
|
const issues = [];
|
|
1792
1875
|
const lines = context.fileContent.split("\n");
|
|
@@ -2041,6 +2124,12 @@ var noDebuggerRule = {
|
|
|
2041
2124
|
severity: "high",
|
|
2042
2125
|
title: "Debugger statement",
|
|
2043
2126
|
description: "Debugger statements should never be committed to production code.",
|
|
2127
|
+
fixable: true,
|
|
2128
|
+
fix(context, issue) {
|
|
2129
|
+
const [start, end] = lineRange(context.fileContent, issue.startLine);
|
|
2130
|
+
if (start === end) return null;
|
|
2131
|
+
return { range: [start, end], text: "" };
|
|
2132
|
+
},
|
|
2044
2133
|
check(context) {
|
|
2045
2134
|
const issues = [];
|
|
2046
2135
|
const lines = context.fileContent.split("\n");
|
|
@@ -2355,12 +2444,7 @@ var noReassignParamRule = {
|
|
|
2355
2444
|
}
|
|
2356
2445
|
}
|
|
2357
2446
|
if (paramNames.size === 0) return;
|
|
2358
|
-
|
|
2359
|
-
if (node.type === AST_NODE_TYPES.MethodDefinition) {
|
|
2360
|
-
body = node.value;
|
|
2361
|
-
} else {
|
|
2362
|
-
body = node;
|
|
2363
|
-
}
|
|
2447
|
+
const body = node.type === AST_NODE_TYPES.MethodDefinition ? node.value : node;
|
|
2364
2448
|
if (!body || !("body" in body)) return;
|
|
2365
2449
|
const fnBody = body.body;
|
|
2366
2450
|
if (!fnBody) return;
|
|
@@ -2597,15 +2681,33 @@ var RuleEngine = class {
|
|
|
2597
2681
|
);
|
|
2598
2682
|
}
|
|
2599
2683
|
run(context) {
|
|
2684
|
+
return this.runWithDiagnostics(context).issues;
|
|
2685
|
+
}
|
|
2686
|
+
runWithDiagnostics(context) {
|
|
2600
2687
|
const allIssues = [];
|
|
2688
|
+
const ruleFailures = [];
|
|
2689
|
+
let rulesExecuted = 0;
|
|
2690
|
+
let rulesFailed = 0;
|
|
2601
2691
|
for (const rule of this.rules) {
|
|
2692
|
+
rulesExecuted++;
|
|
2602
2693
|
try {
|
|
2603
2694
|
const issues = rule.check(context);
|
|
2604
2695
|
allIssues.push(...issues);
|
|
2605
|
-
} catch (
|
|
2696
|
+
} catch (err) {
|
|
2697
|
+
rulesFailed++;
|
|
2698
|
+
ruleFailures.push({
|
|
2699
|
+
ruleId: rule.id,
|
|
2700
|
+
file: context.filePath,
|
|
2701
|
+
message: err instanceof Error ? err.message : "Unknown rule execution failure"
|
|
2702
|
+
});
|
|
2606
2703
|
}
|
|
2607
2704
|
}
|
|
2608
|
-
return
|
|
2705
|
+
return {
|
|
2706
|
+
issues: allIssues,
|
|
2707
|
+
rulesExecuted,
|
|
2708
|
+
rulesFailed,
|
|
2709
|
+
ruleFailures
|
|
2710
|
+
};
|
|
2609
2711
|
}
|
|
2610
2712
|
getRules() {
|
|
2611
2713
|
return [...this.rules];
|
|
@@ -2985,6 +3087,8 @@ var PKG_VERSION = (() => {
|
|
|
2985
3087
|
return "0.1.0";
|
|
2986
3088
|
}
|
|
2987
3089
|
})();
|
|
3090
|
+
var REPORT_SCHEMA_VERSION = "1.0.0";
|
|
3091
|
+
var FINGERPRINT_VERSION = "1";
|
|
2988
3092
|
var ScanEngine = class {
|
|
2989
3093
|
config;
|
|
2990
3094
|
diffParser;
|
|
@@ -2995,80 +3099,198 @@ var ScanEngine = class {
|
|
|
2995
3099
|
this.ruleEngine = new RuleEngine(config);
|
|
2996
3100
|
}
|
|
2997
3101
|
async scan(options) {
|
|
2998
|
-
const
|
|
3102
|
+
const selection = await this.getScanCandidates(options);
|
|
2999
3103
|
const allIssues = [];
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
if (line.startsWith("+")) {
|
|
3021
|
-
result.push({ lineNumber: currentLine, content: line.slice(1) });
|
|
3022
|
-
currentLine++;
|
|
3023
|
-
} else if (line.startsWith("-")) {
|
|
3024
|
-
} else {
|
|
3025
|
-
currentLine++;
|
|
3026
|
-
}
|
|
3027
|
-
}
|
|
3028
|
-
return result;
|
|
3029
|
-
});
|
|
3030
|
-
const issues = this.ruleEngine.run({
|
|
3031
|
-
filePath: diffFile.filePath,
|
|
3032
|
-
fileContent,
|
|
3033
|
-
addedLines
|
|
3034
|
-
});
|
|
3035
|
-
allIssues.push(...issues);
|
|
3036
|
-
if (this.isTsJsFile(diffFile.filePath)) {
|
|
3037
|
-
const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
|
|
3038
|
-
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
3039
|
-
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3040
|
-
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3041
|
-
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3042
|
-
maxParamCount: this.config.thresholds["max-params"]
|
|
3043
|
-
});
|
|
3044
|
-
allIssues.push(...structureResult.issues);
|
|
3045
|
-
const styleResult = analyzeStyle(fileContent, diffFile.filePath);
|
|
3046
|
-
allIssues.push(...styleResult.issues);
|
|
3047
|
-
const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
|
|
3048
|
-
allIssues.push(...coverageResult.issues);
|
|
3049
|
-
}
|
|
3050
|
-
}
|
|
3051
|
-
const dimensions = this.groupByDimension(allIssues);
|
|
3104
|
+
const scanErrors = [];
|
|
3105
|
+
const ruleFailures = [];
|
|
3106
|
+
let rulesExecuted = 0;
|
|
3107
|
+
let rulesFailed = 0;
|
|
3108
|
+
let filesScanned = 0;
|
|
3109
|
+
const results = await Promise.all(
|
|
3110
|
+
selection.candidates.map((diffFile) => this.scanFile(diffFile))
|
|
3111
|
+
);
|
|
3112
|
+
for (const result of results) {
|
|
3113
|
+
allIssues.push(...result.issues);
|
|
3114
|
+
ruleFailures.push(...result.ruleFailures);
|
|
3115
|
+
scanErrors.push(...result.scanErrors);
|
|
3116
|
+
rulesExecuted += result.rulesExecuted;
|
|
3117
|
+
rulesFailed += result.rulesFailed;
|
|
3118
|
+
if (result.scanned) {
|
|
3119
|
+
filesScanned++;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
const issuesWithFingerprints = this.attachFingerprints(allIssues);
|
|
3123
|
+
const dimensions = this.groupByDimension(issuesWithFingerprints);
|
|
3052
3124
|
const overallScore = calculateOverallScore(dimensions, this.config.weights);
|
|
3053
3125
|
const grade = getGrade(overallScore);
|
|
3054
3126
|
const commitHash = await this.diffParser.getCurrentCommitHash();
|
|
3055
3127
|
return {
|
|
3128
|
+
schemaVersion: REPORT_SCHEMA_VERSION,
|
|
3056
3129
|
version: PKG_VERSION,
|
|
3057
3130
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3058
3131
|
commit: commitHash,
|
|
3132
|
+
scanMode: selection.scanMode,
|
|
3059
3133
|
overall: {
|
|
3060
3134
|
score: overallScore,
|
|
3061
3135
|
grade,
|
|
3062
|
-
filesScanned
|
|
3063
|
-
issuesFound:
|
|
3136
|
+
filesScanned,
|
|
3137
|
+
issuesFound: issuesWithFingerprints.length
|
|
3138
|
+
},
|
|
3139
|
+
toolHealth: {
|
|
3140
|
+
rulesExecuted,
|
|
3141
|
+
rulesFailed,
|
|
3142
|
+
filesConsidered: selection.filesConsidered,
|
|
3143
|
+
filesScanned,
|
|
3144
|
+
filesExcluded: selection.filesExcluded,
|
|
3145
|
+
filesSkipped: scanErrors.length,
|
|
3146
|
+
scanErrors,
|
|
3147
|
+
ruleFailures
|
|
3064
3148
|
},
|
|
3065
3149
|
dimensions,
|
|
3066
|
-
issues:
|
|
3150
|
+
issues: issuesWithFingerprints.sort((a, b) => {
|
|
3067
3151
|
const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
|
|
3068
3152
|
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
3069
3153
|
})
|
|
3070
3154
|
};
|
|
3071
3155
|
}
|
|
3156
|
+
async scanFile(diffFile) {
|
|
3157
|
+
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
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
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
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
const filePath = resolve2(diffFile.filePath);
|
|
3190
|
+
let fileContent;
|
|
3191
|
+
try {
|
|
3192
|
+
fileContent = await readFile(filePath, "utf-8");
|
|
3193
|
+
} catch {
|
|
3194
|
+
const content = await this.diffParser.getFileContent(diffFile.filePath);
|
|
3195
|
+
if (!content) {
|
|
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;
|
|
3212
|
+
}
|
|
3213
|
+
const addedLines = diffFile.hunks.flatMap((hunk) => {
|
|
3214
|
+
const lines = hunk.content.split("\n");
|
|
3215
|
+
const result = [];
|
|
3216
|
+
let currentLine = hunk.newStart;
|
|
3217
|
+
for (const line of lines) {
|
|
3218
|
+
if (line.startsWith("+")) {
|
|
3219
|
+
result.push({ lineNumber: currentLine, content: line.slice(1) });
|
|
3220
|
+
currentLine++;
|
|
3221
|
+
} else if (line.startsWith("-")) {
|
|
3222
|
+
} else {
|
|
3223
|
+
currentLine++;
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
return result;
|
|
3227
|
+
});
|
|
3228
|
+
const ruleResult = this.ruleEngine.runWithDiagnostics({
|
|
3229
|
+
filePath: diffFile.filePath,
|
|
3230
|
+
fileContent,
|
|
3231
|
+
addedLines
|
|
3232
|
+
});
|
|
3233
|
+
const issues = [...ruleResult.issues];
|
|
3234
|
+
const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
|
|
3235
|
+
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
3236
|
+
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3237
|
+
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3238
|
+
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3239
|
+
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
|
+
};
|
|
3254
|
+
}
|
|
3255
|
+
async getScanCandidates(options) {
|
|
3256
|
+
const scanMode = this.getScanMode(options);
|
|
3257
|
+
const candidates = await this.getDiffFiles(options);
|
|
3258
|
+
if (scanMode === "files") {
|
|
3259
|
+
return {
|
|
3260
|
+
scanMode,
|
|
3261
|
+
candidates,
|
|
3262
|
+
filesConsidered: options.files?.length ?? candidates.length,
|
|
3263
|
+
filesExcluded: (options.files?.length ?? candidates.length) - candidates.length
|
|
3264
|
+
};
|
|
3265
|
+
}
|
|
3266
|
+
const filteredCandidates = [];
|
|
3267
|
+
let filesExcluded = 0;
|
|
3268
|
+
for (const candidate of candidates) {
|
|
3269
|
+
if (this.shouldIncludeFile(candidate.filePath)) {
|
|
3270
|
+
filteredCandidates.push(candidate);
|
|
3271
|
+
} else {
|
|
3272
|
+
filesExcluded++;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
return {
|
|
3276
|
+
scanMode,
|
|
3277
|
+
candidates: filteredCandidates,
|
|
3278
|
+
filesConsidered: candidates.length,
|
|
3279
|
+
filesExcluded
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
getScanMode(options) {
|
|
3283
|
+
if (options.staged) {
|
|
3284
|
+
return "staged";
|
|
3285
|
+
}
|
|
3286
|
+
if (options.diff) {
|
|
3287
|
+
return "diff";
|
|
3288
|
+
}
|
|
3289
|
+
if (options.files && options.files.length > 0) {
|
|
3290
|
+
return "files";
|
|
3291
|
+
}
|
|
3292
|
+
return "changed";
|
|
3293
|
+
}
|
|
3072
3294
|
async getDiffFiles(options) {
|
|
3073
3295
|
if (options.staged) {
|
|
3074
3296
|
return this.diffParser.getStagedFiles();
|
|
@@ -3077,14 +3299,10 @@ var ScanEngine = class {
|
|
|
3077
3299
|
return this.diffParser.getDiffFromRef(options.diff);
|
|
3078
3300
|
}
|
|
3079
3301
|
if (options.files && options.files.length > 0) {
|
|
3302
|
+
const includedFiles = options.files.filter((filePath) => this.shouldIncludeFile(filePath));
|
|
3080
3303
|
return Promise.all(
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
try {
|
|
3084
|
-
content = await readFile(resolve2(filePath), "utf-8");
|
|
3085
|
-
} catch {
|
|
3086
|
-
}
|
|
3087
|
-
return {
|
|
3304
|
+
includedFiles.map(
|
|
3305
|
+
(filePath) => readFile(resolve2(filePath), "utf-8").catch(() => "").then((content) => ({
|
|
3088
3306
|
filePath,
|
|
3089
3307
|
status: "modified",
|
|
3090
3308
|
additions: content.split("\n").length,
|
|
@@ -3096,18 +3314,64 @@ var ScanEngine = class {
|
|
|
3096
3314
|
oldLines: 0,
|
|
3097
3315
|
newStart: 1,
|
|
3098
3316
|
newLines: content.split("\n").length,
|
|
3099
|
-
content: content.split("\n").map((
|
|
3317
|
+
content: content.split("\n").map((line) => "+" + line).join("\n")
|
|
3100
3318
|
}
|
|
3101
3319
|
]
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3320
|
+
}))
|
|
3321
|
+
)
|
|
3104
3322
|
);
|
|
3105
3323
|
}
|
|
3106
3324
|
return this.diffParser.getChangedFiles();
|
|
3107
3325
|
}
|
|
3326
|
+
shouldIncludeFile(filePath) {
|
|
3327
|
+
const normalizedPath = filePath.split(sep).join("/");
|
|
3328
|
+
const includePatterns = this.config.include.length > 0 ? this.config.include : ["**/*"];
|
|
3329
|
+
const included = includePatterns.some((pattern) => this.matchesPattern(normalizedPath, pattern));
|
|
3330
|
+
if (!included) {
|
|
3331
|
+
return false;
|
|
3332
|
+
}
|
|
3333
|
+
return !this.config.exclude.some((pattern) => this.matchesPattern(normalizedPath, pattern));
|
|
3334
|
+
}
|
|
3335
|
+
matchesPattern(filePath, pattern) {
|
|
3336
|
+
let regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
3337
|
+
regexPattern = regexPattern.replace(/\*\*\//g, "::DOUBLE_DIR::");
|
|
3338
|
+
regexPattern = regexPattern.replace(/\*\*/g, "::DOUBLE_STAR::");
|
|
3339
|
+
regexPattern = regexPattern.replace(/\*/g, "[^/]*");
|
|
3340
|
+
regexPattern = regexPattern.replace(/::DOUBLE_DIR::/g, "(?:.*/)?");
|
|
3341
|
+
regexPattern = regexPattern.replace(/::DOUBLE_STAR::/g, ".*");
|
|
3342
|
+
return new RegExp(`^${regexPattern}$`).test(filePath);
|
|
3343
|
+
}
|
|
3108
3344
|
isTsJsFile(filePath) {
|
|
3109
3345
|
return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
|
|
3110
3346
|
}
|
|
3347
|
+
attachFingerprints(issues) {
|
|
3348
|
+
const occurrenceCounts = /* @__PURE__ */ new Map();
|
|
3349
|
+
return issues.map((issue) => {
|
|
3350
|
+
const normalizedFile = this.normalizeRelativePath(issue.file);
|
|
3351
|
+
const locationComponent = `${issue.startLine}:${issue.endLine}`;
|
|
3352
|
+
const baseKey = [
|
|
3353
|
+
issue.ruleId,
|
|
3354
|
+
normalizedFile,
|
|
3355
|
+
issue.category,
|
|
3356
|
+
issue.severity,
|
|
3357
|
+
locationComponent
|
|
3358
|
+
].join("|");
|
|
3359
|
+
const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
|
|
3360
|
+
occurrenceCounts.set(baseKey, occurrenceIndex + 1);
|
|
3361
|
+
const fingerprint = createHash("sha256").update(`${FINGERPRINT_VERSION}|${baseKey}|${occurrenceIndex}`).digest("hex");
|
|
3362
|
+
return {
|
|
3363
|
+
...issue,
|
|
3364
|
+
file: normalizedFile,
|
|
3365
|
+
fingerprint,
|
|
3366
|
+
fingerprintVersion: FINGERPRINT_VERSION
|
|
3367
|
+
};
|
|
3368
|
+
});
|
|
3369
|
+
}
|
|
3370
|
+
normalizeRelativePath(filePath) {
|
|
3371
|
+
const absolutePath = resolve2(filePath);
|
|
3372
|
+
const relativePath = relative(process.cwd(), absolutePath) || filePath;
|
|
3373
|
+
return relativePath.split(sep).join("/");
|
|
3374
|
+
}
|
|
3111
3375
|
groupByDimension(issues) {
|
|
3112
3376
|
const categories = [
|
|
3113
3377
|
"security",
|
|
@@ -3118,7 +3382,7 @@ var ScanEngine = class {
|
|
|
3118
3382
|
];
|
|
3119
3383
|
const grouped = {};
|
|
3120
3384
|
for (const cat of categories) {
|
|
3121
|
-
const catIssues = issues.filter((
|
|
3385
|
+
const catIssues = issues.filter((issue) => issue.category === cat);
|
|
3122
3386
|
grouped[cat] = calculateDimensionScore(catIssues);
|
|
3123
3387
|
}
|
|
3124
3388
|
return grouped;
|
|
@@ -3138,7 +3402,11 @@ var en = {
|
|
|
3138
3402
|
issuesFound: "{{count}} issue(s) found",
|
|
3139
3403
|
issuesHeader: "Issues ({{count}}):",
|
|
3140
3404
|
noIssuesFound: "No issues found! \u{1F389}",
|
|
3141
|
-
scanned: "Scanned {{count}} file(s)"
|
|
3405
|
+
scanned: "Scanned {{count}} file(s)",
|
|
3406
|
+
healthHeader: "Tool Health",
|
|
3407
|
+
rulesFailed: "Failed rules: {{count}}",
|
|
3408
|
+
filesSkipped: "Skipped files: {{count}}",
|
|
3409
|
+
filesExcluded: "Excluded files: {{count}}"
|
|
3142
3410
|
};
|
|
3143
3411
|
var zh = {
|
|
3144
3412
|
reportTitle: "\u{1F4CA} CodeTrust \u62A5\u544A",
|
|
@@ -3150,7 +3418,11 @@ var zh = {
|
|
|
3150
3418
|
issuesFound: "\u53D1\u73B0 {{count}} \u4E2A\u95EE\u9898",
|
|
3151
3419
|
issuesHeader: "\u95EE\u9898\u5217\u8868 ({{count}}):",
|
|
3152
3420
|
noIssuesFound: "\u672A\u53D1\u73B0\u95EE\u9898! \u{1F389}",
|
|
3153
|
-
scanned: "\u626B\u63CF\u4E86 {{count}} \u4E2A\u6587\u4EF6"
|
|
3421
|
+
scanned: "\u626B\u63CF\u4E86 {{count}} \u4E2A\u6587\u4EF6",
|
|
3422
|
+
healthHeader: "\u5DE5\u5177\u5065\u5EB7\u5EA6",
|
|
3423
|
+
rulesFailed: "\u5931\u8D25\u89C4\u5219\u6570\uFF1A{{count}}",
|
|
3424
|
+
filesSkipped: "\u8DF3\u8FC7\u6587\u4EF6\u6570\uFF1A{{count}}",
|
|
3425
|
+
filesExcluded: "\u6392\u9664\u6587\u4EF6\u6570\uFF1A{{count}}"
|
|
3154
3426
|
};
|
|
3155
3427
|
function renderTerminalReport(report) {
|
|
3156
3428
|
const isZh = isZhLocale();
|
|
@@ -3192,19 +3464,32 @@ function renderTerminalReport(report) {
|
|
|
3192
3464
|
};
|
|
3193
3465
|
const dims = ["security", "logic", "structure", "style", "coverage"];
|
|
3194
3466
|
for (const dim of dims) {
|
|
3195
|
-
const
|
|
3196
|
-
const dimEmoji =
|
|
3197
|
-
const color = getScoreColor(
|
|
3198
|
-
const issueCount =
|
|
3467
|
+
const dimension = report.dimensions[dim];
|
|
3468
|
+
const dimEmoji = dimension.score >= 80 ? "\u2705" : dimension.score >= 60 ? "\u26A0\uFE0F" : "\u274C";
|
|
3469
|
+
const color = getScoreColor(dimension.score);
|
|
3470
|
+
const issueCount = dimension.issues.length;
|
|
3199
3471
|
const detail = issueCount === 0 ? pc.green(t2.noIssues) : t2.issuesFound.replace("{{count}}", String(issueCount));
|
|
3200
3472
|
table.push([
|
|
3201
3473
|
`${dimEmoji} ${dimLabels[dim]}`,
|
|
3202
|
-
color(String(
|
|
3474
|
+
color(String(dimension.score)),
|
|
3203
3475
|
detail
|
|
3204
3476
|
]);
|
|
3205
3477
|
}
|
|
3206
3478
|
lines.push(table.toString());
|
|
3207
3479
|
lines.push("");
|
|
3480
|
+
if (report.toolHealth.rulesFailed > 0 || report.toolHealth.filesSkipped > 0 || report.toolHealth.filesExcluded > 0) {
|
|
3481
|
+
lines.push(pc.bold(t2.healthHeader));
|
|
3482
|
+
if (report.toolHealth.rulesFailed > 0) {
|
|
3483
|
+
lines.push(pc.yellow(` ${t2.rulesFailed.replace("{{count}}", String(report.toolHealth.rulesFailed))}`));
|
|
3484
|
+
}
|
|
3485
|
+
if (report.toolHealth.filesSkipped > 0) {
|
|
3486
|
+
lines.push(pc.yellow(` ${t2.filesSkipped.replace("{{count}}", String(report.toolHealth.filesSkipped))}`));
|
|
3487
|
+
}
|
|
3488
|
+
if (report.toolHealth.filesExcluded > 0) {
|
|
3489
|
+
lines.push(pc.dim(` ${t2.filesExcluded.replace("{{count}}", String(report.toolHealth.filesExcluded))}`));
|
|
3490
|
+
}
|
|
3491
|
+
lines.push("");
|
|
3492
|
+
}
|
|
3208
3493
|
if (report.issues.length > 0) {
|
|
3209
3494
|
lines.push(pc.bold(t2.issuesHeader.replace("{{count}}", String(report.issues.length))));
|
|
3210
3495
|
lines.push("");
|
|
@@ -3268,12 +3553,23 @@ function getScoreColor(score) {
|
|
|
3268
3553
|
|
|
3269
3554
|
// src/cli/output/json.ts
|
|
3270
3555
|
function renderJsonReport(report) {
|
|
3271
|
-
|
|
3556
|
+
const payload = {
|
|
3557
|
+
schemaVersion: report.schemaVersion,
|
|
3558
|
+
version: report.version,
|
|
3559
|
+
timestamp: report.timestamp,
|
|
3560
|
+
commit: report.commit,
|
|
3561
|
+
scanMode: report.scanMode,
|
|
3562
|
+
overall: report.overall,
|
|
3563
|
+
toolHealth: report.toolHealth,
|
|
3564
|
+
dimensions: report.dimensions,
|
|
3565
|
+
issues: report.issues
|
|
3566
|
+
};
|
|
3567
|
+
return JSON.stringify(payload, null, 2);
|
|
3272
3568
|
}
|
|
3273
3569
|
|
|
3274
3570
|
// src/cli/commands/scan.ts
|
|
3275
3571
|
function createScanCommand() {
|
|
3276
|
-
const cmd = new Command("scan").description("
|
|
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) => {
|
|
3277
3573
|
try {
|
|
3278
3574
|
const config = await loadConfig();
|
|
3279
3575
|
const engine = new ScanEngine(config);
|
|
@@ -3308,7 +3604,7 @@ function createScanCommand() {
|
|
|
3308
3604
|
// src/cli/commands/report.ts
|
|
3309
3605
|
import { Command as Command2 } from "commander";
|
|
3310
3606
|
function createReportCommand() {
|
|
3311
|
-
const cmd = new Command2("report").description("
|
|
3607
|
+
const cmd = new Command2("report").description("Render a report for a diff-based scan (transitional wrapper around scan)").option("--json", "Output as JSON").option("--diff <ref>", "Diff against a git ref for report presentation", "HEAD~1").action(async (opts) => {
|
|
3312
3608
|
try {
|
|
3313
3609
|
const config = await loadConfig();
|
|
3314
3610
|
const engine = new ScanEngine(config);
|
|
@@ -3332,14 +3628,14 @@ function createReportCommand() {
|
|
|
3332
3628
|
|
|
3333
3629
|
// src/cli/commands/init.ts
|
|
3334
3630
|
import { writeFile } from "fs/promises";
|
|
3335
|
-
import { existsSync as
|
|
3631
|
+
import { existsSync as existsSync3 } from "fs";
|
|
3336
3632
|
import { resolve as resolve3 } from "path";
|
|
3337
3633
|
import { Command as Command3 } from "commander";
|
|
3338
3634
|
import pc2 from "picocolors";
|
|
3339
3635
|
function createInitCommand() {
|
|
3340
3636
|
const cmd = new Command3("init").description("Initialize CodeTrust configuration file").action(async () => {
|
|
3341
3637
|
const configPath = resolve3(".codetrust.yml");
|
|
3342
|
-
if (
|
|
3638
|
+
if (existsSync3(configPath)) {
|
|
3343
3639
|
console.log(pc2.yellow("\u26A0\uFE0F .codetrust.yml already exists. Skipping."));
|
|
3344
3640
|
return;
|
|
3345
3641
|
}
|
|
@@ -3413,7 +3709,7 @@ function formatSeverity2(severity) {
|
|
|
3413
3709
|
|
|
3414
3710
|
// src/cli/commands/hook.ts
|
|
3415
3711
|
import { writeFile as writeFile2, chmod, mkdir } from "fs/promises";
|
|
3416
|
-
import { existsSync as
|
|
3712
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3417
3713
|
import { resolve as resolve4, join as join2 } from "path";
|
|
3418
3714
|
import { Command as Command5 } from "commander";
|
|
3419
3715
|
import pc4 from "picocolors";
|
|
@@ -3430,17 +3726,17 @@ function createHookCommand() {
|
|
|
3430
3726
|
const cmd = new Command5("hook").description("Manage git hooks");
|
|
3431
3727
|
cmd.command("install").description("Install pre-commit hook").action(async () => {
|
|
3432
3728
|
const gitDir = resolve4(".git");
|
|
3433
|
-
if (!
|
|
3729
|
+
if (!existsSync4(gitDir)) {
|
|
3434
3730
|
console.error(pc4.red("Error: Not a git repository."));
|
|
3435
3731
|
process.exit(1);
|
|
3436
3732
|
}
|
|
3437
3733
|
const hooksDir = join2(gitDir, "hooks");
|
|
3438
3734
|
const hookPath = join2(hooksDir, "pre-commit");
|
|
3439
3735
|
try {
|
|
3440
|
-
if (!
|
|
3736
|
+
if (!existsSync4(hooksDir)) {
|
|
3441
3737
|
await mkdir(hooksDir, { recursive: true });
|
|
3442
3738
|
}
|
|
3443
|
-
if (
|
|
3739
|
+
if (existsSync4(hookPath)) {
|
|
3444
3740
|
console.log(pc4.yellow("\u26A0\uFE0F pre-commit hook already exists. Skipping."));
|
|
3445
3741
|
console.log(pc4.dim(" Remove .git/hooks/pre-commit to reinstall."));
|
|
3446
3742
|
return;
|
|
@@ -3460,19 +3756,238 @@ function createHookCommand() {
|
|
|
3460
3756
|
return cmd;
|
|
3461
3757
|
}
|
|
3462
3758
|
|
|
3759
|
+
// src/cli/commands/fix.ts
|
|
3760
|
+
import { Command as Command6 } from "commander";
|
|
3761
|
+
|
|
3762
|
+
// src/core/fix-engine.ts
|
|
3763
|
+
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
3764
|
+
import pc5 from "picocolors";
|
|
3765
|
+
var FixEngine = class {
|
|
3766
|
+
ruleEngine;
|
|
3767
|
+
constructor(config) {
|
|
3768
|
+
this.ruleEngine = new RuleEngine(config);
|
|
3769
|
+
}
|
|
3770
|
+
/**
|
|
3771
|
+
* Fix issues in files. Returns results per file.
|
|
3772
|
+
* Uses text range replacement to preserve formatting.
|
|
3773
|
+
* Applies fixes iteratively (up to maxIterations) to handle cascading issues.
|
|
3774
|
+
*/
|
|
3775
|
+
async fix(options) {
|
|
3776
|
+
const results = [];
|
|
3777
|
+
const maxIter = options.maxIterations ?? 10;
|
|
3778
|
+
for (const filePath of options.files) {
|
|
3779
|
+
const result = this.fixFile(filePath, options.dryRun ?? true, options.ruleId, maxIter);
|
|
3780
|
+
if (result.applied > 0 || result.skipped > 0) {
|
|
3781
|
+
results.push(result);
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
return results;
|
|
3785
|
+
}
|
|
3786
|
+
fixFile(filePath, dryRun, ruleId, maxIter) {
|
|
3787
|
+
const result = {
|
|
3788
|
+
file: filePath,
|
|
3789
|
+
applied: 0,
|
|
3790
|
+
skipped: 0,
|
|
3791
|
+
details: []
|
|
3792
|
+
};
|
|
3793
|
+
let content;
|
|
3794
|
+
try {
|
|
3795
|
+
content = readFileSync2(filePath, "utf-8");
|
|
3796
|
+
} catch {
|
|
3797
|
+
return result;
|
|
3798
|
+
}
|
|
3799
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
3800
|
+
const context = {
|
|
3801
|
+
filePath,
|
|
3802
|
+
fileContent: content,
|
|
3803
|
+
addedLines: []
|
|
3804
|
+
};
|
|
3805
|
+
let issues = this.ruleEngine.run(context);
|
|
3806
|
+
if (ruleId) {
|
|
3807
|
+
issues = issues.filter((i) => i.ruleId === ruleId);
|
|
3808
|
+
}
|
|
3809
|
+
const fixableRules = this.ruleEngine.getRules().filter((r) => r.fixable && r.fix);
|
|
3810
|
+
const fixableRuleMap = new Map(fixableRules.map((r) => [r.id, r]));
|
|
3811
|
+
const fixesWithIssues = [];
|
|
3812
|
+
for (const issue of issues) {
|
|
3813
|
+
const rule = fixableRuleMap.get(issue.ruleId);
|
|
3814
|
+
if (!rule || !rule.fix) continue;
|
|
3815
|
+
const fix = rule.fix(context, issue);
|
|
3816
|
+
if (fix) {
|
|
3817
|
+
fixesWithIssues.push({ fix, issue });
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
if (fixesWithIssues.length === 0) break;
|
|
3821
|
+
fixesWithIssues.sort((a, b) => b.fix.range[0] - a.fix.range[0]);
|
|
3822
|
+
const nonConflicting = [];
|
|
3823
|
+
for (const item of fixesWithIssues) {
|
|
3824
|
+
const hasConflict = nonConflicting.some(
|
|
3825
|
+
(existing) => item.fix.range[0] < existing.fix.range[1] && item.fix.range[1] > existing.fix.range[0]
|
|
3826
|
+
);
|
|
3827
|
+
if (hasConflict) {
|
|
3828
|
+
result.skipped++;
|
|
3829
|
+
result.details.push({
|
|
3830
|
+
ruleId: item.issue.ruleId,
|
|
3831
|
+
line: item.issue.startLine,
|
|
3832
|
+
message: item.issue.message,
|
|
3833
|
+
status: "conflict"
|
|
3834
|
+
});
|
|
3835
|
+
} else {
|
|
3836
|
+
nonConflicting.push(item);
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
if (nonConflicting.length === 0) break;
|
|
3840
|
+
let newContent = content;
|
|
3841
|
+
for (const { fix, issue } of nonConflicting) {
|
|
3842
|
+
const before = newContent.slice(0, fix.range[0]);
|
|
3843
|
+
const after = newContent.slice(fix.range[1]);
|
|
3844
|
+
newContent = before + fix.text + after;
|
|
3845
|
+
result.applied++;
|
|
3846
|
+
result.details.push({
|
|
3847
|
+
ruleId: issue.ruleId,
|
|
3848
|
+
line: issue.startLine,
|
|
3849
|
+
message: issue.message,
|
|
3850
|
+
status: "applied"
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
content = newContent;
|
|
3854
|
+
if (nonConflicting.length === 0) break;
|
|
3855
|
+
}
|
|
3856
|
+
if (!dryRun && result.applied > 0) {
|
|
3857
|
+
writeFileSync(filePath, content, "utf-8");
|
|
3858
|
+
}
|
|
3859
|
+
return result;
|
|
3860
|
+
}
|
|
3861
|
+
/**
|
|
3862
|
+
* Format fix results for terminal output.
|
|
3863
|
+
*/
|
|
3864
|
+
static formatResults(results, dryRun) {
|
|
3865
|
+
if (results.length === 0) {
|
|
3866
|
+
return pc5.green(t("No fixable issues found.", "\u672A\u53D1\u73B0\u53EF\u4FEE\u590D\u7684\u95EE\u9898\u3002"));
|
|
3867
|
+
}
|
|
3868
|
+
const lines = [];
|
|
3869
|
+
const modeLabel = dryRun ? pc5.yellow(t("[DRY RUN]", "[\u9884\u6F14\u6A21\u5F0F]")) : pc5.green(t("[APPLIED]", "[\u5DF2\u5E94\u7528]"));
|
|
3870
|
+
lines.push(`
|
|
3871
|
+
${modeLabel} ${t("Fix Results:", "\u4FEE\u590D\u7ED3\u679C\uFF1A")}
|
|
3872
|
+
`);
|
|
3873
|
+
let totalApplied = 0;
|
|
3874
|
+
let totalSkipped = 0;
|
|
3875
|
+
for (const result of results) {
|
|
3876
|
+
lines.push(pc5.bold(pc5.underline(result.file)));
|
|
3877
|
+
for (const detail of result.details) {
|
|
3878
|
+
const icon = detail.status === "applied" ? pc5.green("\u2713") : detail.status === "conflict" ? pc5.yellow("\u26A0") : pc5.dim("\u2013");
|
|
3879
|
+
const lineRef = pc5.dim(`L${detail.line}`);
|
|
3880
|
+
const ruleRef = pc5.cyan(detail.ruleId);
|
|
3881
|
+
lines.push(` ${icon} ${lineRef} ${ruleRef} ${detail.message}`);
|
|
3882
|
+
}
|
|
3883
|
+
totalApplied += result.applied;
|
|
3884
|
+
totalSkipped += result.skipped;
|
|
3885
|
+
lines.push("");
|
|
3886
|
+
}
|
|
3887
|
+
lines.push(
|
|
3888
|
+
pc5.bold(
|
|
3889
|
+
t(
|
|
3890
|
+
`${totalApplied} fix(es) ${dryRun ? "would be" : ""} applied, ${totalSkipped} skipped`,
|
|
3891
|
+
`${totalApplied} \u4E2A\u4FEE\u590D${dryRun ? "\u5C06\u88AB" : "\u5DF2"}\u5E94\u7528\uFF0C${totalSkipped} \u4E2A\u8DF3\u8FC7`
|
|
3892
|
+
)
|
|
3893
|
+
)
|
|
3894
|
+
);
|
|
3895
|
+
if (dryRun) {
|
|
3896
|
+
lines.push(
|
|
3897
|
+
pc5.dim(
|
|
3898
|
+
t(
|
|
3899
|
+
"Run without --dry-run to apply fixes.",
|
|
3900
|
+
"\u79FB\u9664 --dry-run \u4EE5\u5E94\u7528\u4FEE\u590D\u3002"
|
|
3901
|
+
)
|
|
3902
|
+
)
|
|
3903
|
+
);
|
|
3904
|
+
}
|
|
3905
|
+
return lines.join("\n");
|
|
3906
|
+
}
|
|
3907
|
+
};
|
|
3908
|
+
|
|
3909
|
+
// src/cli/commands/fix.ts
|
|
3910
|
+
import { resolve as resolve5 } from "path";
|
|
3911
|
+
import { readdirSync, statSync } from "fs";
|
|
3912
|
+
function collectFiles(dir) {
|
|
3913
|
+
const ignorePatterns = ["node_modules", "dist", ".git", "coverage", ".next", "build"];
|
|
3914
|
+
const results = [];
|
|
3915
|
+
function walk(d) {
|
|
3916
|
+
const entries = readdirSync(d, { withFileTypes: true });
|
|
3917
|
+
for (const entry of entries) {
|
|
3918
|
+
if (ignorePatterns.includes(entry.name)) continue;
|
|
3919
|
+
const fullPath = resolve5(d, entry.name);
|
|
3920
|
+
if (entry.isDirectory()) {
|
|
3921
|
+
walk(fullPath);
|
|
3922
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
3923
|
+
results.push(fullPath);
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
walk(dir);
|
|
3928
|
+
return results;
|
|
3929
|
+
}
|
|
3930
|
+
function createFixCommand() {
|
|
3931
|
+
const cmd = new Command6("fix").description("Auto-fix issues in source files").argument("[files...]", "Specific files or directories to fix (default: src/)").option("--dry-run", "Preview fixes without applying them (default)", true).option("--apply", "Actually apply the fixes").option("--rule <ruleId>", "Only fix issues from a specific rule").action(async (files, opts) => {
|
|
3932
|
+
try {
|
|
3933
|
+
const config = await loadConfig();
|
|
3934
|
+
const engine = new FixEngine(config);
|
|
3935
|
+
let targetFiles;
|
|
3936
|
+
if (files.length > 0) {
|
|
3937
|
+
targetFiles = [];
|
|
3938
|
+
for (const f of files) {
|
|
3939
|
+
const resolved = resolve5(f);
|
|
3940
|
+
try {
|
|
3941
|
+
const stat = statSync(resolved);
|
|
3942
|
+
if (stat.isDirectory()) {
|
|
3943
|
+
targetFiles.push(...collectFiles(resolved));
|
|
3944
|
+
} else {
|
|
3945
|
+
targetFiles.push(resolved);
|
|
3946
|
+
}
|
|
3947
|
+
} catch {
|
|
3948
|
+
console.error(`File not found: ${f}`);
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
} else {
|
|
3952
|
+
targetFiles = collectFiles(resolve5("src"));
|
|
3953
|
+
}
|
|
3954
|
+
if (targetFiles.length === 0) {
|
|
3955
|
+
console.log("No files to fix.");
|
|
3956
|
+
return;
|
|
3957
|
+
}
|
|
3958
|
+
const dryRun = !opts.apply;
|
|
3959
|
+
const results = await engine.fix({
|
|
3960
|
+
files: targetFiles,
|
|
3961
|
+
dryRun,
|
|
3962
|
+
ruleId: opts.rule
|
|
3963
|
+
});
|
|
3964
|
+
console.log(FixEngine.formatResults(results, dryRun));
|
|
3965
|
+
} catch (err) {
|
|
3966
|
+
if (err instanceof Error) {
|
|
3967
|
+
console.error(`Error: ${err.message}`);
|
|
3968
|
+
} else {
|
|
3969
|
+
console.error("An unexpected error occurred");
|
|
3970
|
+
}
|
|
3971
|
+
process.exit(1);
|
|
3972
|
+
}
|
|
3973
|
+
});
|
|
3974
|
+
return cmd;
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3463
3977
|
// src/cli/index.ts
|
|
3464
|
-
import { readFileSync as
|
|
3978
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3465
3979
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3466
|
-
import { dirname as dirname4, resolve as
|
|
3980
|
+
import { dirname as dirname4, resolve as resolve6 } from "path";
|
|
3467
3981
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
3468
3982
|
var __dirname2 = dirname4(__filename2);
|
|
3469
|
-
var pkg = JSON.parse(
|
|
3470
|
-
var program = new
|
|
3983
|
+
var pkg = JSON.parse(readFileSync3(resolve6(__dirname2, "../../package.json"), "utf-8"));
|
|
3984
|
+
var program = new Command7();
|
|
3471
3985
|
program.name("codetrust").description("AI code trust verification tool \u2014 verify AI-generated code with deterministic algorithms").version(pkg.version);
|
|
3472
3986
|
program.addCommand(createScanCommand());
|
|
3473
3987
|
program.addCommand(createReportCommand());
|
|
3474
3988
|
program.addCommand(createInitCommand());
|
|
3475
3989
|
program.addCommand(createRulesCommand());
|
|
3476
3990
|
program.addCommand(createHookCommand());
|
|
3991
|
+
program.addCommand(createFixCommand());
|
|
3477
3992
|
program.parse();
|
|
3478
3993
|
//# sourceMappingURL=index.js.map
|