@gulu9527/code-trust 0.2.0 → 0.2.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.md +9 -0
- package/dist/cli/index.js +305 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,15 @@ codetrust rules list
|
|
|
57
57
|
|
|
58
58
|
# Install pre-commit hook
|
|
59
59
|
codetrust hook install
|
|
60
|
+
|
|
61
|
+
# Auto-fix issues (dry-run by default)
|
|
62
|
+
codetrust fix src/
|
|
63
|
+
|
|
64
|
+
# Apply fixes
|
|
65
|
+
codetrust fix src/ --apply
|
|
66
|
+
|
|
67
|
+
# Fix only a specific rule
|
|
68
|
+
codetrust fix src/ --apply --rule logic/type-coercion
|
|
60
69
|
```
|
|
61
70
|
|
|
62
71
|
## Trust Score
|
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";
|
|
@@ -645,6 +645,24 @@ function detectImmediateReassign(context, lines, issues) {
|
|
|
645
645
|
}
|
|
646
646
|
}
|
|
647
647
|
|
|
648
|
+
// src/rules/fix-utils.ts
|
|
649
|
+
function lineStartOffset(content, lineNumber) {
|
|
650
|
+
let offset = 0;
|
|
651
|
+
const lines = content.split("\n");
|
|
652
|
+
for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
|
|
653
|
+
offset += lines[i].length + 1;
|
|
654
|
+
}
|
|
655
|
+
return offset;
|
|
656
|
+
}
|
|
657
|
+
function lineRange(content, lineNumber) {
|
|
658
|
+
const lines = content.split("\n");
|
|
659
|
+
const lineIndex = lineNumber - 1;
|
|
660
|
+
if (lineIndex < 0 || lineIndex >= lines.length) return [0, 0];
|
|
661
|
+
const start = lineStartOffset(content, lineNumber);
|
|
662
|
+
const end = start + lines[lineIndex].length + (lineIndex < lines.length - 1 ? 1 : 0);
|
|
663
|
+
return [start, end];
|
|
664
|
+
}
|
|
665
|
+
|
|
648
666
|
// src/parsers/ast.ts
|
|
649
667
|
import { parse, AST_NODE_TYPES } from "@typescript-eslint/typescript-estree";
|
|
650
668
|
|
|
@@ -827,6 +845,19 @@ var unusedVariablesRule = {
|
|
|
827
845
|
severity: "low",
|
|
828
846
|
title: "Unused variable detected",
|
|
829
847
|
description: "AI-generated code sometimes declares variables that are never used, indicating incomplete or hallucinated logic.",
|
|
848
|
+
fixable: true,
|
|
849
|
+
fix(context, issue) {
|
|
850
|
+
const lines = context.fileContent.split("\n");
|
|
851
|
+
const lineIndex = issue.startLine - 1;
|
|
852
|
+
if (lineIndex < 0 || lineIndex >= lines.length) return null;
|
|
853
|
+
const line = lines[lineIndex].trim();
|
|
854
|
+
if (/^(const|let|var)\s+\w+\s*[=:;]/.test(line) && !line.includes(",")) {
|
|
855
|
+
const [start, end] = lineRange(context.fileContent, issue.startLine);
|
|
856
|
+
if (start === end) return null;
|
|
857
|
+
return { range: [start, end], text: "" };
|
|
858
|
+
}
|
|
859
|
+
return null;
|
|
860
|
+
},
|
|
830
861
|
check(context) {
|
|
831
862
|
const issues = [];
|
|
832
863
|
let ast;
|
|
@@ -1550,6 +1581,23 @@ var unusedImportRule = {
|
|
|
1550
1581
|
severity: "low",
|
|
1551
1582
|
title: "Unused import",
|
|
1552
1583
|
description: "AI-generated code often imports modules or identifiers that are never used in the file.",
|
|
1584
|
+
fixable: true,
|
|
1585
|
+
fix(context, issue) {
|
|
1586
|
+
const lines = context.fileContent.split("\n");
|
|
1587
|
+
const lineIndex = issue.startLine - 1;
|
|
1588
|
+
if (lineIndex < 0 || lineIndex >= lines.length) return null;
|
|
1589
|
+
const line = lines[lineIndex].trim();
|
|
1590
|
+
const isSingleDefault = /^import\s+\w+\s+from\s+/.test(line);
|
|
1591
|
+
const isSingleNamed = /^import\s*\{\s*\w+\s*\}\s*from\s+/.test(line);
|
|
1592
|
+
const isSingleTypeNamed = /^import\s+type\s*\{\s*\w+\s*\}\s*from\s+/.test(line);
|
|
1593
|
+
const isSingleTypeDefault = /^import\s+type\s+\w+\s+from\s+/.test(line);
|
|
1594
|
+
if (!isSingleDefault && !isSingleNamed && !isSingleTypeNamed && !isSingleTypeDefault) {
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
const [start, end] = lineRange(context.fileContent, issue.startLine);
|
|
1598
|
+
if (start === end) return null;
|
|
1599
|
+
return { range: [start, end], text: "" };
|
|
1600
|
+
},
|
|
1553
1601
|
check(context) {
|
|
1554
1602
|
const issues = [];
|
|
1555
1603
|
let ast;
|
|
@@ -1787,6 +1835,33 @@ var typeCoercionRule = {
|
|
|
1787
1835
|
severity: "medium",
|
|
1788
1836
|
title: "Loose equality with type coercion",
|
|
1789
1837
|
description: "AI-generated code often uses == instead of ===, leading to implicit type coercion bugs.",
|
|
1838
|
+
fixable: true,
|
|
1839
|
+
fix(context, issue) {
|
|
1840
|
+
const lines = context.fileContent.split("\n");
|
|
1841
|
+
const lineIndex = issue.startLine - 1;
|
|
1842
|
+
if (lineIndex < 0 || lineIndex >= lines.length) return null;
|
|
1843
|
+
const line = lines[lineIndex];
|
|
1844
|
+
const base = lineStartOffset(context.fileContent, issue.startLine);
|
|
1845
|
+
const isNotEqual = issue.message.includes("!=");
|
|
1846
|
+
const searchOp = isNotEqual ? "!=" : "==";
|
|
1847
|
+
const replaceOp = isNotEqual ? "!==" : "===";
|
|
1848
|
+
let pos = -1;
|
|
1849
|
+
for (let j = 0; j < line.length - 1; j++) {
|
|
1850
|
+
if (line[j] === searchOp[0] && line[j + 1] === "=") {
|
|
1851
|
+
if (line[j + 2] === "=") {
|
|
1852
|
+
j += 2;
|
|
1853
|
+
continue;
|
|
1854
|
+
}
|
|
1855
|
+
if (!isNotEqual && j > 0 && (line[j - 1] === "!" || line[j - 1] === "<" || line[j - 1] === ">")) {
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
pos = j;
|
|
1859
|
+
break;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
if (pos === -1) return null;
|
|
1863
|
+
return { range: [base + pos, base + pos + searchOp.length], text: replaceOp };
|
|
1864
|
+
},
|
|
1790
1865
|
check(context) {
|
|
1791
1866
|
const issues = [];
|
|
1792
1867
|
const lines = context.fileContent.split("\n");
|
|
@@ -2041,6 +2116,12 @@ var noDebuggerRule = {
|
|
|
2041
2116
|
severity: "high",
|
|
2042
2117
|
title: "Debugger statement",
|
|
2043
2118
|
description: "Debugger statements should never be committed to production code.",
|
|
2119
|
+
fixable: true,
|
|
2120
|
+
fix(context, issue) {
|
|
2121
|
+
const [start, end] = lineRange(context.fileContent, issue.startLine);
|
|
2122
|
+
if (start === end) return null;
|
|
2123
|
+
return { range: [start, end], text: "" };
|
|
2124
|
+
},
|
|
2044
2125
|
check(context) {
|
|
2045
2126
|
const issues = [];
|
|
2046
2127
|
const lines = context.fileContent.split("\n");
|
|
@@ -3460,19 +3541,238 @@ function createHookCommand() {
|
|
|
3460
3541
|
return cmd;
|
|
3461
3542
|
}
|
|
3462
3543
|
|
|
3544
|
+
// src/cli/commands/fix.ts
|
|
3545
|
+
import { Command as Command6 } from "commander";
|
|
3546
|
+
|
|
3547
|
+
// src/core/fix-engine.ts
|
|
3548
|
+
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
3549
|
+
import pc5 from "picocolors";
|
|
3550
|
+
var FixEngine = class {
|
|
3551
|
+
ruleEngine;
|
|
3552
|
+
constructor(config) {
|
|
3553
|
+
this.ruleEngine = new RuleEngine(config);
|
|
3554
|
+
}
|
|
3555
|
+
/**
|
|
3556
|
+
* Fix issues in files. Returns results per file.
|
|
3557
|
+
* Uses text range replacement to preserve formatting.
|
|
3558
|
+
* Applies fixes iteratively (up to maxIterations) to handle cascading issues.
|
|
3559
|
+
*/
|
|
3560
|
+
async fix(options) {
|
|
3561
|
+
const results = [];
|
|
3562
|
+
const maxIter = options.maxIterations ?? 10;
|
|
3563
|
+
for (const filePath of options.files) {
|
|
3564
|
+
const result = this.fixFile(filePath, options.dryRun ?? true, options.ruleId, maxIter);
|
|
3565
|
+
if (result.applied > 0 || result.skipped > 0) {
|
|
3566
|
+
results.push(result);
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
return results;
|
|
3570
|
+
}
|
|
3571
|
+
fixFile(filePath, dryRun, ruleId, maxIter) {
|
|
3572
|
+
const result = {
|
|
3573
|
+
file: filePath,
|
|
3574
|
+
applied: 0,
|
|
3575
|
+
skipped: 0,
|
|
3576
|
+
details: []
|
|
3577
|
+
};
|
|
3578
|
+
let content;
|
|
3579
|
+
try {
|
|
3580
|
+
content = readFileSync2(filePath, "utf-8");
|
|
3581
|
+
} catch {
|
|
3582
|
+
return result;
|
|
3583
|
+
}
|
|
3584
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
3585
|
+
const context = {
|
|
3586
|
+
filePath,
|
|
3587
|
+
fileContent: content,
|
|
3588
|
+
addedLines: []
|
|
3589
|
+
};
|
|
3590
|
+
let issues = this.ruleEngine.run(context);
|
|
3591
|
+
if (ruleId) {
|
|
3592
|
+
issues = issues.filter((i) => i.ruleId === ruleId);
|
|
3593
|
+
}
|
|
3594
|
+
const fixableRules = this.ruleEngine.getRules().filter((r) => r.fixable && r.fix);
|
|
3595
|
+
const fixableRuleMap = new Map(fixableRules.map((r) => [r.id, r]));
|
|
3596
|
+
const fixesWithIssues = [];
|
|
3597
|
+
for (const issue of issues) {
|
|
3598
|
+
const rule = fixableRuleMap.get(issue.ruleId);
|
|
3599
|
+
if (!rule || !rule.fix) continue;
|
|
3600
|
+
const fix = rule.fix(context, issue);
|
|
3601
|
+
if (fix) {
|
|
3602
|
+
fixesWithIssues.push({ fix, issue });
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
if (fixesWithIssues.length === 0) break;
|
|
3606
|
+
fixesWithIssues.sort((a, b) => b.fix.range[0] - a.fix.range[0]);
|
|
3607
|
+
const nonConflicting = [];
|
|
3608
|
+
for (const item of fixesWithIssues) {
|
|
3609
|
+
const hasConflict = nonConflicting.some(
|
|
3610
|
+
(existing) => item.fix.range[0] < existing.fix.range[1] && item.fix.range[1] > existing.fix.range[0]
|
|
3611
|
+
);
|
|
3612
|
+
if (hasConflict) {
|
|
3613
|
+
result.skipped++;
|
|
3614
|
+
result.details.push({
|
|
3615
|
+
ruleId: item.issue.ruleId,
|
|
3616
|
+
line: item.issue.startLine,
|
|
3617
|
+
message: item.issue.message,
|
|
3618
|
+
status: "conflict"
|
|
3619
|
+
});
|
|
3620
|
+
} else {
|
|
3621
|
+
nonConflicting.push(item);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
if (nonConflicting.length === 0) break;
|
|
3625
|
+
let newContent = content;
|
|
3626
|
+
for (const { fix, issue } of nonConflicting) {
|
|
3627
|
+
const before = newContent.slice(0, fix.range[0]);
|
|
3628
|
+
const after = newContent.slice(fix.range[1]);
|
|
3629
|
+
newContent = before + fix.text + after;
|
|
3630
|
+
result.applied++;
|
|
3631
|
+
result.details.push({
|
|
3632
|
+
ruleId: issue.ruleId,
|
|
3633
|
+
line: issue.startLine,
|
|
3634
|
+
message: issue.message,
|
|
3635
|
+
status: "applied"
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
content = newContent;
|
|
3639
|
+
if (nonConflicting.length === 0) break;
|
|
3640
|
+
}
|
|
3641
|
+
if (!dryRun && result.applied > 0) {
|
|
3642
|
+
writeFileSync(filePath, content, "utf-8");
|
|
3643
|
+
}
|
|
3644
|
+
return result;
|
|
3645
|
+
}
|
|
3646
|
+
/**
|
|
3647
|
+
* Format fix results for terminal output.
|
|
3648
|
+
*/
|
|
3649
|
+
static formatResults(results, dryRun) {
|
|
3650
|
+
if (results.length === 0) {
|
|
3651
|
+
return pc5.green(t("No fixable issues found.", "\u672A\u53D1\u73B0\u53EF\u4FEE\u590D\u7684\u95EE\u9898\u3002"));
|
|
3652
|
+
}
|
|
3653
|
+
const lines = [];
|
|
3654
|
+
const modeLabel = dryRun ? pc5.yellow(t("[DRY RUN]", "[\u9884\u6F14\u6A21\u5F0F]")) : pc5.green(t("[APPLIED]", "[\u5DF2\u5E94\u7528]"));
|
|
3655
|
+
lines.push(`
|
|
3656
|
+
${modeLabel} ${t("Fix Results:", "\u4FEE\u590D\u7ED3\u679C\uFF1A")}
|
|
3657
|
+
`);
|
|
3658
|
+
let totalApplied = 0;
|
|
3659
|
+
let totalSkipped = 0;
|
|
3660
|
+
for (const result of results) {
|
|
3661
|
+
lines.push(pc5.bold(pc5.underline(result.file)));
|
|
3662
|
+
for (const detail of result.details) {
|
|
3663
|
+
const icon = detail.status === "applied" ? pc5.green("\u2713") : detail.status === "conflict" ? pc5.yellow("\u26A0") : pc5.dim("\u2013");
|
|
3664
|
+
const lineRef = pc5.dim(`L${detail.line}`);
|
|
3665
|
+
const ruleRef = pc5.cyan(detail.ruleId);
|
|
3666
|
+
lines.push(` ${icon} ${lineRef} ${ruleRef} ${detail.message}`);
|
|
3667
|
+
}
|
|
3668
|
+
totalApplied += result.applied;
|
|
3669
|
+
totalSkipped += result.skipped;
|
|
3670
|
+
lines.push("");
|
|
3671
|
+
}
|
|
3672
|
+
lines.push(
|
|
3673
|
+
pc5.bold(
|
|
3674
|
+
t(
|
|
3675
|
+
`${totalApplied} fix(es) ${dryRun ? "would be" : ""} applied, ${totalSkipped} skipped`,
|
|
3676
|
+
`${totalApplied} \u4E2A\u4FEE\u590D${dryRun ? "\u5C06\u88AB" : "\u5DF2"}\u5E94\u7528\uFF0C${totalSkipped} \u4E2A\u8DF3\u8FC7`
|
|
3677
|
+
)
|
|
3678
|
+
)
|
|
3679
|
+
);
|
|
3680
|
+
if (dryRun) {
|
|
3681
|
+
lines.push(
|
|
3682
|
+
pc5.dim(
|
|
3683
|
+
t(
|
|
3684
|
+
"Run without --dry-run to apply fixes.",
|
|
3685
|
+
"\u79FB\u9664 --dry-run \u4EE5\u5E94\u7528\u4FEE\u590D\u3002"
|
|
3686
|
+
)
|
|
3687
|
+
)
|
|
3688
|
+
);
|
|
3689
|
+
}
|
|
3690
|
+
return lines.join("\n");
|
|
3691
|
+
}
|
|
3692
|
+
};
|
|
3693
|
+
|
|
3694
|
+
// src/cli/commands/fix.ts
|
|
3695
|
+
import { resolve as resolve5 } from "path";
|
|
3696
|
+
import { readdirSync, statSync } from "fs";
|
|
3697
|
+
function collectFiles(dir) {
|
|
3698
|
+
const ignorePatterns = ["node_modules", "dist", ".git", "coverage", ".next", "build"];
|
|
3699
|
+
const results = [];
|
|
3700
|
+
function walk(d) {
|
|
3701
|
+
const entries = readdirSync(d, { withFileTypes: true });
|
|
3702
|
+
for (const entry of entries) {
|
|
3703
|
+
if (ignorePatterns.includes(entry.name)) continue;
|
|
3704
|
+
const fullPath = resolve5(d, entry.name);
|
|
3705
|
+
if (entry.isDirectory()) {
|
|
3706
|
+
walk(fullPath);
|
|
3707
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
3708
|
+
results.push(fullPath);
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
walk(dir);
|
|
3713
|
+
return results;
|
|
3714
|
+
}
|
|
3715
|
+
function createFixCommand() {
|
|
3716
|
+
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) => {
|
|
3717
|
+
try {
|
|
3718
|
+
const config = await loadConfig();
|
|
3719
|
+
const engine = new FixEngine(config);
|
|
3720
|
+
let targetFiles;
|
|
3721
|
+
if (files.length > 0) {
|
|
3722
|
+
targetFiles = [];
|
|
3723
|
+
for (const f of files) {
|
|
3724
|
+
const resolved = resolve5(f);
|
|
3725
|
+
try {
|
|
3726
|
+
const stat = statSync(resolved);
|
|
3727
|
+
if (stat.isDirectory()) {
|
|
3728
|
+
targetFiles.push(...collectFiles(resolved));
|
|
3729
|
+
} else {
|
|
3730
|
+
targetFiles.push(resolved);
|
|
3731
|
+
}
|
|
3732
|
+
} catch {
|
|
3733
|
+
console.error(`File not found: ${f}`);
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
} else {
|
|
3737
|
+
targetFiles = collectFiles(resolve5("src"));
|
|
3738
|
+
}
|
|
3739
|
+
if (targetFiles.length === 0) {
|
|
3740
|
+
console.log("No files to fix.");
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
const dryRun = !opts.apply;
|
|
3744
|
+
const results = await engine.fix({
|
|
3745
|
+
files: targetFiles,
|
|
3746
|
+
dryRun,
|
|
3747
|
+
ruleId: opts.rule
|
|
3748
|
+
});
|
|
3749
|
+
console.log(FixEngine.formatResults(results, dryRun));
|
|
3750
|
+
} catch (err) {
|
|
3751
|
+
if (err instanceof Error) {
|
|
3752
|
+
console.error(`Error: ${err.message}`);
|
|
3753
|
+
} else {
|
|
3754
|
+
console.error("An unexpected error occurred");
|
|
3755
|
+
}
|
|
3756
|
+
process.exit(1);
|
|
3757
|
+
}
|
|
3758
|
+
});
|
|
3759
|
+
return cmd;
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3463
3762
|
// src/cli/index.ts
|
|
3464
|
-
import { readFileSync as
|
|
3763
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3465
3764
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3466
|
-
import { dirname as dirname4, resolve as
|
|
3765
|
+
import { dirname as dirname4, resolve as resolve6 } from "path";
|
|
3467
3766
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
3468
3767
|
var __dirname2 = dirname4(__filename2);
|
|
3469
|
-
var pkg = JSON.parse(
|
|
3470
|
-
var program = new
|
|
3768
|
+
var pkg = JSON.parse(readFileSync3(resolve6(__dirname2, "../../package.json"), "utf-8"));
|
|
3769
|
+
var program = new Command7();
|
|
3471
3770
|
program.name("codetrust").description("AI code trust verification tool \u2014 verify AI-generated code with deterministic algorithms").version(pkg.version);
|
|
3472
3771
|
program.addCommand(createScanCommand());
|
|
3473
3772
|
program.addCommand(createReportCommand());
|
|
3474
3773
|
program.addCommand(createInitCommand());
|
|
3475
3774
|
program.addCommand(createRulesCommand());
|
|
3476
3775
|
program.addCommand(createHookCommand());
|
|
3776
|
+
program.addCommand(createFixCommand());
|
|
3477
3777
|
program.parse();
|
|
3478
3778
|
//# sourceMappingURL=index.js.map
|