@gulu9527/code-trust 0.1.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 +30 -8
- package/action.yml +1 -1
- package/dist/cli/index.js +1281 -134
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +929 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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;
|
|
@@ -1726,145 +1774,1025 @@ function isInsidePromiseChain(_node, parent) {
|
|
|
1726
1774
|
return false;
|
|
1727
1775
|
}
|
|
1728
1776
|
|
|
1729
|
-
// src/rules/
|
|
1730
|
-
var
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1777
|
+
// src/rules/builtin/any-type-abuse.ts
|
|
1778
|
+
var anyTypeAbuseRule = {
|
|
1779
|
+
id: "logic/any-type-abuse",
|
|
1780
|
+
category: "logic",
|
|
1781
|
+
severity: "medium",
|
|
1782
|
+
title: "Excessive any type usage",
|
|
1783
|
+
description: "AI-generated code often uses `any` type to bypass TypeScript type checking, reducing type safety.",
|
|
1784
|
+
check(context) {
|
|
1785
|
+
const issues = [];
|
|
1786
|
+
if (!context.filePath.match(/\.tsx?$/)) return issues;
|
|
1787
|
+
let ast;
|
|
1788
|
+
try {
|
|
1789
|
+
const parsed = parseCode(context.fileContent, context.filePath);
|
|
1790
|
+
ast = parsed.ast;
|
|
1791
|
+
} catch {
|
|
1792
|
+
return issues;
|
|
1793
|
+
}
|
|
1794
|
+
const lines = context.fileContent.split("\n");
|
|
1795
|
+
walkAST(ast, (node) => {
|
|
1796
|
+
if (node.type !== AST_NODE_TYPES.TSAnyKeyword) return;
|
|
1797
|
+
const line = node.loc?.start.line ?? 0;
|
|
1798
|
+
if (line === 0) return;
|
|
1799
|
+
const lineContent = lines[line - 1] ?? "";
|
|
1800
|
+
const trimmed = lineContent.trim();
|
|
1801
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) return;
|
|
1802
|
+
const parent = node.parent;
|
|
1803
|
+
if (parent?.type === AST_NODE_TYPES.TSTypeAssertion || parent?.type === AST_NODE_TYPES.TSAsExpression) {
|
|
1804
|
+
let ancestor = parent;
|
|
1805
|
+
while (ancestor) {
|
|
1806
|
+
if (ancestor.type === AST_NODE_TYPES.CatchClause) return;
|
|
1807
|
+
ancestor = ancestor.parent;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
issues.push({
|
|
1811
|
+
ruleId: "logic/any-type-abuse",
|
|
1812
|
+
severity: "medium",
|
|
1813
|
+
category: "logic",
|
|
1814
|
+
file: context.filePath,
|
|
1815
|
+
startLine: line,
|
|
1816
|
+
endLine: line,
|
|
1817
|
+
message: t(
|
|
1818
|
+
`Usage of "any" type reduces type safety.`,
|
|
1819
|
+
`\u4F7F\u7528 "any" \u7C7B\u578B\u964D\u4F4E\u4E86\u7C7B\u578B\u5B89\u5168\u6027\u3002`
|
|
1820
|
+
),
|
|
1821
|
+
suggestion: t(
|
|
1822
|
+
`Replace "any" with a specific type or "unknown" for safer type narrowing.`,
|
|
1823
|
+
`\u5C06 "any" \u66FF\u6362\u4E3A\u5177\u4F53\u7C7B\u578B\u6216\u4F7F\u7528 "unknown" \u8FDB\u884C\u66F4\u5B89\u5168\u7684\u7C7B\u578B\u6536\u7A84\u3002`
|
|
1824
|
+
)
|
|
1825
|
+
});
|
|
1826
|
+
});
|
|
1827
|
+
return issues;
|
|
1751
1828
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
// src/rules/builtin/type-coercion.ts
|
|
1832
|
+
var typeCoercionRule = {
|
|
1833
|
+
id: "logic/type-coercion",
|
|
1834
|
+
category: "logic",
|
|
1835
|
+
severity: "medium",
|
|
1836
|
+
title: "Loose equality with type coercion",
|
|
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;
|
|
1759
1860
|
}
|
|
1760
1861
|
}
|
|
1761
|
-
return
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1862
|
+
if (pos === -1) return null;
|
|
1863
|
+
return { range: [base + pos, base + pos + searchOp.length], text: replaceOp };
|
|
1864
|
+
},
|
|
1865
|
+
check(context) {
|
|
1866
|
+
const issues = [];
|
|
1867
|
+
const lines = context.fileContent.split("\n");
|
|
1868
|
+
let inBlockComment = false;
|
|
1869
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1870
|
+
const line = lines[i];
|
|
1871
|
+
const trimmed = line.trim();
|
|
1872
|
+
if (inBlockComment) {
|
|
1873
|
+
if (trimmed.includes("*/")) inBlockComment = false;
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
if (trimmed.startsWith("/*")) {
|
|
1877
|
+
if (!trimmed.includes("*/")) inBlockComment = true;
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (trimmed.startsWith("//")) continue;
|
|
1881
|
+
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
|
|
1882
|
+
const looseEqRegex = /[^!=<>]==[^=]|[^!]==[^=]|!=[^=]/g;
|
|
1883
|
+
let match;
|
|
1884
|
+
while ((match = looseEqRegex.exec(cleaned)) !== null) {
|
|
1885
|
+
const pos = match.index;
|
|
1886
|
+
const snippet = cleaned.substring(Math.max(0, pos), pos + match[0].length);
|
|
1887
|
+
if (snippet.includes("===") || snippet.includes("!==")) continue;
|
|
1888
|
+
const isNotEqual = snippet.includes("!=");
|
|
1889
|
+
const operator = isNotEqual ? "!=" : "==";
|
|
1890
|
+
const strict = isNotEqual ? "!==" : "===";
|
|
1891
|
+
const afterOp = cleaned.substring(pos + match[0].length - 1).trim();
|
|
1892
|
+
if (afterOp.startsWith("null") || afterOp.startsWith("undefined")) continue;
|
|
1893
|
+
const beforeOp = cleaned.substring(0, pos + 1).trim();
|
|
1894
|
+
if (beforeOp.endsWith("null") || beforeOp.endsWith("undefined")) continue;
|
|
1895
|
+
issues.push({
|
|
1896
|
+
ruleId: "logic/type-coercion",
|
|
1897
|
+
severity: "medium",
|
|
1898
|
+
category: "logic",
|
|
1899
|
+
file: context.filePath,
|
|
1900
|
+
startLine: i + 1,
|
|
1901
|
+
endLine: i + 1,
|
|
1902
|
+
message: t(
|
|
1903
|
+
`Loose equality "${operator}" can cause implicit type coercion.`,
|
|
1904
|
+
`\u5BBD\u677E\u7B49\u4E8E "${operator}" \u4F1A\u5BFC\u81F4\u9690\u5F0F\u7C7B\u578B\u8F6C\u6362\u3002`
|
|
1905
|
+
),
|
|
1906
|
+
suggestion: t(
|
|
1907
|
+
`Use strict equality "${strict}" instead of "${operator}".`,
|
|
1908
|
+
`\u4F7F\u7528\u4E25\u683C\u7B49\u4E8E "${strict}" \u4EE3\u66FF "${operator}"\u3002`
|
|
1909
|
+
)
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
return issues;
|
|
1773
1914
|
}
|
|
1774
1915
|
};
|
|
1775
1916
|
|
|
1776
|
-
// src/
|
|
1777
|
-
var
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
return
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1917
|
+
// src/rules/builtin/magic-number.ts
|
|
1918
|
+
var ALLOWED_NUMBERS = /* @__PURE__ */ new Set([
|
|
1919
|
+
-1,
|
|
1920
|
+
0,
|
|
1921
|
+
1,
|
|
1922
|
+
2,
|
|
1923
|
+
10,
|
|
1924
|
+
100
|
|
1925
|
+
]);
|
|
1926
|
+
var magicNumberRule = {
|
|
1927
|
+
id: "logic/magic-number",
|
|
1928
|
+
category: "logic",
|
|
1929
|
+
severity: "low",
|
|
1930
|
+
title: "Magic number",
|
|
1931
|
+
description: "AI-generated code often uses unexplained numeric literals instead of named constants.",
|
|
1932
|
+
check(context) {
|
|
1933
|
+
const issues = [];
|
|
1934
|
+
const lines = context.fileContent.split("\n");
|
|
1935
|
+
let inBlockComment = false;
|
|
1936
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1937
|
+
const line = lines[i];
|
|
1938
|
+
const trimmed = line.trim();
|
|
1939
|
+
if (inBlockComment) {
|
|
1940
|
+
if (trimmed.includes("*/")) inBlockComment = false;
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
if (trimmed.startsWith("/*")) {
|
|
1944
|
+
if (!trimmed.includes("*/")) inBlockComment = true;
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
if (trimmed.startsWith("//")) continue;
|
|
1948
|
+
if (/^\s*(export\s+)?(const|let|var|readonly)\s+[A-Z_][A-Z0-9_]*\s*[=:]/.test(line)) continue;
|
|
1949
|
+
if (/^\s*(export\s+)?enum\s/.test(line)) continue;
|
|
1950
|
+
if (trimmed.startsWith("import ")) continue;
|
|
1951
|
+
if (/^\s*return\s+[0-9]+\s*;?\s*$/.test(line)) continue;
|
|
1952
|
+
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
|
|
1953
|
+
const numRegex = /(?<![.\w])(-?\d+\.?\d*(?:e[+-]?\d+)?)\b/gi;
|
|
1954
|
+
let match;
|
|
1955
|
+
while ((match = numRegex.exec(cleaned)) !== null) {
|
|
1956
|
+
const value = parseFloat(match[1]);
|
|
1957
|
+
if (ALLOWED_NUMBERS.has(value)) continue;
|
|
1958
|
+
if (isNaN(value)) continue;
|
|
1959
|
+
const beforeChar = cleaned[match.index - 1] || "";
|
|
1960
|
+
if (beforeChar === "[") continue;
|
|
1961
|
+
if (beforeChar === "<" || beforeChar === ",") continue;
|
|
1962
|
+
issues.push({
|
|
1963
|
+
ruleId: "logic/magic-number",
|
|
1964
|
+
severity: "low",
|
|
1965
|
+
category: "logic",
|
|
1966
|
+
file: context.filePath,
|
|
1967
|
+
startLine: i + 1,
|
|
1968
|
+
endLine: i + 1,
|
|
1969
|
+
message: t(
|
|
1970
|
+
`Magic number ${match[1]} should be extracted to a named constant.`,
|
|
1971
|
+
`\u9B54\u672F\u6570\u5B57 ${match[1]} \u5E94\u63D0\u53D6\u4E3A\u547D\u540D\u5E38\u91CF\u3002`
|
|
1972
|
+
),
|
|
1973
|
+
suggestion: t(
|
|
1974
|
+
`Define a descriptive constant, e.g., const MAX_RETRIES = ${match[1]};`,
|
|
1975
|
+
`\u5B9A\u4E49\u4E00\u4E2A\u63CF\u8FF0\u6027\u5E38\u91CF\uFF0C\u4F8B\u5982 const MAX_RETRIES = ${match[1]};`
|
|
1976
|
+
)
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1827
1979
|
}
|
|
1980
|
+
return issues;
|
|
1828
1981
|
}
|
|
1829
|
-
switch (grade) {
|
|
1830
|
-
case "HIGH_TRUST":
|
|
1831
|
-
return "HIGH TRUST \u2014 Safe to merge";
|
|
1832
|
-
case "REVIEW":
|
|
1833
|
-
return "REVIEW RECOMMENDED";
|
|
1834
|
-
case "LOW_TRUST":
|
|
1835
|
-
return "LOW TRUST \u2014 Careful review needed";
|
|
1836
|
-
case "UNTRUSTED":
|
|
1837
|
-
return "UNTRUSTED \u2014 Do not merge without changes";
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
// src/analyzers/structure.ts
|
|
1842
|
-
var DEFAULT_THRESHOLDS = {
|
|
1843
|
-
maxCyclomaticComplexity: 10,
|
|
1844
|
-
maxCognitiveComplexity: 20,
|
|
1845
|
-
maxFunctionLength: 40,
|
|
1846
|
-
maxNestingDepth: 4,
|
|
1847
|
-
maxParamCount: 5
|
|
1848
1982
|
};
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1983
|
+
|
|
1984
|
+
// src/rules/builtin/nested-ternary.ts
|
|
1985
|
+
var nestedTernaryRule = {
|
|
1986
|
+
id: "logic/no-nested-ternary",
|
|
1987
|
+
category: "logic",
|
|
1988
|
+
severity: "medium",
|
|
1989
|
+
title: "Nested ternary expression",
|
|
1990
|
+
description: "AI-generated code often produces nested ternary expressions that are hard to read.",
|
|
1991
|
+
check(context) {
|
|
1992
|
+
const issues = [];
|
|
1993
|
+
let ast;
|
|
1994
|
+
try {
|
|
1995
|
+
const parsed = parseCode(context.fileContent, context.filePath);
|
|
1996
|
+
ast = parsed.ast;
|
|
1997
|
+
} catch {
|
|
1998
|
+
return issues;
|
|
1999
|
+
}
|
|
2000
|
+
const reportedLines = /* @__PURE__ */ new Set();
|
|
2001
|
+
walkAST(ast, (node) => {
|
|
2002
|
+
if (node.type !== AST_NODE_TYPES.ConditionalExpression) return;
|
|
2003
|
+
const conditional = node;
|
|
2004
|
+
const hasNestedTernary = conditional.consequent.type === AST_NODE_TYPES.ConditionalExpression || conditional.alternate.type === AST_NODE_TYPES.ConditionalExpression;
|
|
2005
|
+
if (!hasNestedTernary) return;
|
|
2006
|
+
const line = node.loc?.start.line ?? 0;
|
|
2007
|
+
if (line === 0 || reportedLines.has(line)) return;
|
|
2008
|
+
reportedLines.add(line);
|
|
2009
|
+
let depth = 1;
|
|
2010
|
+
let current = node;
|
|
2011
|
+
while (current.type === AST_NODE_TYPES.ConditionalExpression) {
|
|
2012
|
+
const cond = current;
|
|
2013
|
+
if (cond.consequent.type === AST_NODE_TYPES.ConditionalExpression) {
|
|
2014
|
+
depth++;
|
|
2015
|
+
current = cond.consequent;
|
|
2016
|
+
} else if (cond.alternate.type === AST_NODE_TYPES.ConditionalExpression) {
|
|
2017
|
+
depth++;
|
|
2018
|
+
current = cond.alternate;
|
|
2019
|
+
} else {
|
|
2020
|
+
break;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
const endLine = node.loc?.end.line ?? line;
|
|
1861
2024
|
issues.push({
|
|
1862
|
-
ruleId: "
|
|
1863
|
-
severity:
|
|
1864
|
-
category: "
|
|
1865
|
-
file: filePath,
|
|
1866
|
-
startLine:
|
|
1867
|
-
endLine
|
|
2025
|
+
ruleId: "logic/no-nested-ternary",
|
|
2026
|
+
severity: "medium",
|
|
2027
|
+
category: "logic",
|
|
2028
|
+
file: context.filePath,
|
|
2029
|
+
startLine: line,
|
|
2030
|
+
endLine,
|
|
2031
|
+
message: t(
|
|
2032
|
+
`Nested ternary expression (depth: ${depth}) reduces readability.`,
|
|
2033
|
+
`\u5D4C\u5957\u4E09\u5143\u8868\u8FBE\u5F0F\uFF08\u6DF1\u5EA6: ${depth}\uFF09\u964D\u4F4E\u4E86\u53EF\u8BFB\u6027\u3002`
|
|
2034
|
+
),
|
|
2035
|
+
suggestion: t(
|
|
2036
|
+
`Refactor into if-else statements or use a lookup object/map.`,
|
|
2037
|
+
`\u91CD\u6784\u4E3A if-else \u8BED\u53E5\u6216\u4F7F\u7528\u67E5\u627E\u5BF9\u8C61/\u6620\u5C04\u3002`
|
|
2038
|
+
)
|
|
2039
|
+
});
|
|
2040
|
+
});
|
|
2041
|
+
return issues;
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
|
|
2045
|
+
// src/rules/builtin/duplicate-string.ts
|
|
2046
|
+
var MIN_STRING_LENGTH = 6;
|
|
2047
|
+
var MIN_OCCURRENCES = 3;
|
|
2048
|
+
var duplicateStringRule = {
|
|
2049
|
+
id: "logic/duplicate-string",
|
|
2050
|
+
category: "logic",
|
|
2051
|
+
severity: "low",
|
|
2052
|
+
title: "Duplicate string literal",
|
|
2053
|
+
description: "AI-generated code often repeats the same string literal instead of extracting it into a constant.",
|
|
2054
|
+
check(context) {
|
|
2055
|
+
const issues = [];
|
|
2056
|
+
const lines = context.fileContent.split("\n");
|
|
2057
|
+
const stringMap = /* @__PURE__ */ new Map();
|
|
2058
|
+
let inBlockComment = false;
|
|
2059
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2060
|
+
const line = lines[i];
|
|
2061
|
+
const trimmed = line.trim();
|
|
2062
|
+
if (inBlockComment) {
|
|
2063
|
+
if (trimmed.includes("*/")) inBlockComment = false;
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
if (trimmed.startsWith("/*")) {
|
|
2067
|
+
if (!trimmed.includes("*/")) inBlockComment = true;
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
if (trimmed.startsWith("//")) continue;
|
|
2071
|
+
if (trimmed.startsWith("import ")) continue;
|
|
2072
|
+
const cleaned = line.replace(/\/\/.*$/, "");
|
|
2073
|
+
const stringRegex = /(['"])([^'"\\](?:(?!\1|\\).|\\.)*)\1/g;
|
|
2074
|
+
let match;
|
|
2075
|
+
while ((match = stringRegex.exec(cleaned)) !== null) {
|
|
2076
|
+
const value = match[2];
|
|
2077
|
+
if (value.length < MIN_STRING_LENGTH) continue;
|
|
2078
|
+
if (value.includes("${")) continue;
|
|
2079
|
+
if (value.startsWith("http") || value.startsWith("/")) continue;
|
|
2080
|
+
if (value.startsWith("test") || value.startsWith("mock")) continue;
|
|
2081
|
+
if (!stringMap.has(value)) {
|
|
2082
|
+
stringMap.set(value, []);
|
|
2083
|
+
}
|
|
2084
|
+
stringMap.get(value).push(i + 1);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
for (const [value, locations] of stringMap) {
|
|
2088
|
+
if (locations.length < MIN_OCCURRENCES) continue;
|
|
2089
|
+
const firstLine = locations[0];
|
|
2090
|
+
const displayValue = value.length > 30 ? value.substring(0, 30) + "..." : value;
|
|
2091
|
+
issues.push({
|
|
2092
|
+
ruleId: "logic/duplicate-string",
|
|
2093
|
+
severity: "low",
|
|
2094
|
+
category: "logic",
|
|
2095
|
+
file: context.filePath,
|
|
2096
|
+
startLine: firstLine,
|
|
2097
|
+
endLine: firstLine,
|
|
2098
|
+
message: t(
|
|
2099
|
+
`String "${displayValue}" is repeated ${locations.length} times.`,
|
|
2100
|
+
`\u5B57\u7B26\u4E32 "${displayValue}" \u91CD\u590D\u51FA\u73B0\u4E86 ${locations.length} \u6B21\u3002`
|
|
2101
|
+
),
|
|
2102
|
+
suggestion: t(
|
|
2103
|
+
`Extract to a named constant to improve maintainability.`,
|
|
2104
|
+
`\u63D0\u53D6\u4E3A\u547D\u540D\u5E38\u91CF\u4EE5\u63D0\u9AD8\u53EF\u7EF4\u62A4\u6027\u3002`
|
|
2105
|
+
)
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
return issues;
|
|
2109
|
+
}
|
|
2110
|
+
};
|
|
2111
|
+
|
|
2112
|
+
// src/rules/builtin/no-debugger.ts
|
|
2113
|
+
var noDebuggerRule = {
|
|
2114
|
+
id: "security/no-debugger",
|
|
2115
|
+
category: "security",
|
|
2116
|
+
severity: "high",
|
|
2117
|
+
title: "Debugger statement",
|
|
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
|
+
},
|
|
2125
|
+
check(context) {
|
|
2126
|
+
const issues = [];
|
|
2127
|
+
const lines = context.fileContent.split("\n");
|
|
2128
|
+
let inBlockComment = false;
|
|
2129
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2130
|
+
const line = lines[i];
|
|
2131
|
+
const trimmed = line.trim();
|
|
2132
|
+
if (inBlockComment) {
|
|
2133
|
+
if (trimmed.includes("*/")) inBlockComment = false;
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
if (trimmed.startsWith("/*")) {
|
|
2137
|
+
if (!trimmed.includes("*/")) inBlockComment = true;
|
|
2138
|
+
continue;
|
|
2139
|
+
}
|
|
2140
|
+
if (trimmed.startsWith("//")) continue;
|
|
2141
|
+
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
|
|
2142
|
+
if (/\bdebugger\b/.test(cleaned)) {
|
|
2143
|
+
issues.push({
|
|
2144
|
+
ruleId: "security/no-debugger",
|
|
2145
|
+
severity: "high",
|
|
2146
|
+
category: "security",
|
|
2147
|
+
file: context.filePath,
|
|
2148
|
+
startLine: i + 1,
|
|
2149
|
+
endLine: i + 1,
|
|
2150
|
+
message: t(
|
|
2151
|
+
`Debugger statement found. Remove before committing.`,
|
|
2152
|
+
`\u53D1\u73B0 debugger \u8BED\u53E5\u3002\u63D0\u4EA4\u524D\u8BF7\u79FB\u9664\u3002`
|
|
2153
|
+
),
|
|
2154
|
+
suggestion: t(
|
|
2155
|
+
`Remove the debugger statement.`,
|
|
2156
|
+
`\u79FB\u9664 debugger \u8BED\u53E5\u3002`
|
|
2157
|
+
)
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return issues;
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
|
|
2165
|
+
// src/rules/builtin/no-non-null-assertion.ts
|
|
2166
|
+
var noNonNullAssertionRule = {
|
|
2167
|
+
id: "logic/no-non-null-assertion",
|
|
2168
|
+
category: "logic",
|
|
2169
|
+
severity: "medium",
|
|
2170
|
+
title: "Non-null assertion operator",
|
|
2171
|
+
description: "AI-generated code often uses the ! operator to bypass TypeScript null checks, risking runtime crashes.",
|
|
2172
|
+
check(context) {
|
|
2173
|
+
const issues = [];
|
|
2174
|
+
if (!context.filePath.match(/\.tsx?$/)) return issues;
|
|
2175
|
+
let ast;
|
|
2176
|
+
try {
|
|
2177
|
+
const parsed = parseCode(context.fileContent, context.filePath);
|
|
2178
|
+
ast = parsed.ast;
|
|
2179
|
+
} catch {
|
|
2180
|
+
return issues;
|
|
2181
|
+
}
|
|
2182
|
+
walkAST(ast, (node) => {
|
|
2183
|
+
if (node.type !== AST_NODE_TYPES.TSNonNullExpression) return;
|
|
2184
|
+
const line = node.loc?.start.line ?? 0;
|
|
2185
|
+
if (line === 0) return;
|
|
2186
|
+
issues.push({
|
|
2187
|
+
ruleId: "logic/no-non-null-assertion",
|
|
2188
|
+
severity: "medium",
|
|
2189
|
+
category: "logic",
|
|
2190
|
+
file: context.filePath,
|
|
2191
|
+
startLine: line,
|
|
2192
|
+
endLine: line,
|
|
2193
|
+
message: t(
|
|
2194
|
+
`Non-null assertion (!) used \u2014 value could be null/undefined at runtime.`,
|
|
2195
|
+
`\u4F7F\u7528\u4E86\u975E\u7A7A\u65AD\u8A00 (!) \u2014 \u503C\u5728\u8FD0\u884C\u65F6\u53EF\u80FD\u4E3A null/undefined\u3002`
|
|
2196
|
+
),
|
|
2197
|
+
suggestion: t(
|
|
2198
|
+
`Use optional chaining (?.), nullish coalescing (??), or add a proper null check.`,
|
|
2199
|
+
`\u4F7F\u7528\u53EF\u9009\u94FE (?.)\u3001\u7A7A\u503C\u5408\u5E76 (??) \u6216\u6DFB\u52A0\u9002\u5F53\u7684\u7A7A\u503C\u68C0\u67E5\u3002`
|
|
2200
|
+
)
|
|
2201
|
+
});
|
|
2202
|
+
});
|
|
2203
|
+
return issues;
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
|
|
2207
|
+
// src/rules/builtin/no-self-compare.ts
|
|
2208
|
+
var noSelfCompareRule = {
|
|
2209
|
+
id: "logic/no-self-compare",
|
|
2210
|
+
category: "logic",
|
|
2211
|
+
severity: "medium",
|
|
2212
|
+
title: "Self-comparison",
|
|
2213
|
+
description: "Self-comparison (x === x) is always true (or always false for !==). Use Number.isNaN() for NaN checks.",
|
|
2214
|
+
check(context) {
|
|
2215
|
+
const issues = [];
|
|
2216
|
+
let ast;
|
|
2217
|
+
try {
|
|
2218
|
+
const parsed = parseCode(context.fileContent, context.filePath);
|
|
2219
|
+
ast = parsed.ast;
|
|
2220
|
+
} catch {
|
|
2221
|
+
return issues;
|
|
2222
|
+
}
|
|
2223
|
+
walkAST(ast, (node) => {
|
|
2224
|
+
if (node.type !== AST_NODE_TYPES.BinaryExpression) return;
|
|
2225
|
+
const binExpr = node;
|
|
2226
|
+
const op = binExpr.operator;
|
|
2227
|
+
if (!["===", "!==", "==", "!="].includes(op)) return;
|
|
2228
|
+
const left = serializeNode(binExpr.left);
|
|
2229
|
+
const right = serializeNode(binExpr.right);
|
|
2230
|
+
if (left === null || right === null || left !== right) return;
|
|
2231
|
+
const line = node.loc?.start.line ?? 0;
|
|
2232
|
+
if (line === 0) return;
|
|
2233
|
+
issues.push({
|
|
2234
|
+
ruleId: "logic/no-self-compare",
|
|
2235
|
+
severity: "medium",
|
|
2236
|
+
category: "logic",
|
|
2237
|
+
file: context.filePath,
|
|
2238
|
+
startLine: line,
|
|
2239
|
+
endLine: line,
|
|
2240
|
+
message: t(
|
|
2241
|
+
`Self-comparison "${left} ${op} ${right}" is always ${op.includes("!") ? "false" : "true"}.`,
|
|
2242
|
+
`\u81EA\u6BD4\u8F83 "${left} ${op} ${right}" \u59CB\u7EC8\u4E3A ${op.includes("!") ? "false" : "true"}\u3002`
|
|
2243
|
+
),
|
|
2244
|
+
suggestion: t(
|
|
2245
|
+
`If checking for NaN, use Number.isNaN(${left}) instead.`,
|
|
2246
|
+
`\u5982\u9700\u68C0\u67E5 NaN\uFF0C\u8BF7\u4F7F\u7528 Number.isNaN(${left})\u3002`
|
|
2247
|
+
)
|
|
2248
|
+
});
|
|
2249
|
+
});
|
|
2250
|
+
return issues;
|
|
2251
|
+
}
|
|
2252
|
+
};
|
|
2253
|
+
function serializeNode(node) {
|
|
2254
|
+
if (node.type === AST_NODE_TYPES.Identifier) {
|
|
2255
|
+
return node.name;
|
|
2256
|
+
}
|
|
2257
|
+
if (node.type === AST_NODE_TYPES.MemberExpression && !node.computed) {
|
|
2258
|
+
const obj = serializeNode(node.object);
|
|
2259
|
+
const prop = node.property.name;
|
|
2260
|
+
if (obj && prop) return `${obj}.${prop}`;
|
|
2261
|
+
}
|
|
2262
|
+
return null;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// src/rules/builtin/no-return-assign.ts
|
|
2266
|
+
var noReturnAssignRule = {
|
|
2267
|
+
id: "logic/no-return-assign",
|
|
2268
|
+
category: "logic",
|
|
2269
|
+
severity: "medium",
|
|
2270
|
+
title: "Assignment in return statement",
|
|
2271
|
+
description: "AI-generated code sometimes uses assignment (=) instead of comparison (===) in return statements.",
|
|
2272
|
+
check(context) {
|
|
2273
|
+
const issues = [];
|
|
2274
|
+
let ast;
|
|
2275
|
+
try {
|
|
2276
|
+
const parsed = parseCode(context.fileContent, context.filePath);
|
|
2277
|
+
ast = parsed.ast;
|
|
2278
|
+
} catch {
|
|
2279
|
+
return issues;
|
|
2280
|
+
}
|
|
2281
|
+
walkAST(ast, (node) => {
|
|
2282
|
+
if (node.type !== AST_NODE_TYPES.ReturnStatement) return;
|
|
2283
|
+
const returnStmt = node;
|
|
2284
|
+
if (!returnStmt.argument) return;
|
|
2285
|
+
if (returnStmt.argument.type === AST_NODE_TYPES.AssignmentExpression) {
|
|
2286
|
+
const assignExpr = returnStmt.argument;
|
|
2287
|
+
if (assignExpr.operator !== "=") return;
|
|
2288
|
+
const line = node.loc?.start.line ?? 0;
|
|
2289
|
+
if (line === 0) return;
|
|
2290
|
+
issues.push({
|
|
2291
|
+
ruleId: "logic/no-return-assign",
|
|
2292
|
+
severity: "medium",
|
|
2293
|
+
category: "logic",
|
|
2294
|
+
file: context.filePath,
|
|
2295
|
+
startLine: line,
|
|
2296
|
+
endLine: line,
|
|
2297
|
+
message: t(
|
|
2298
|
+
`Assignment in return statement \u2014 did you mean to use === instead of =?`,
|
|
2299
|
+
`return \u8BED\u53E5\u4E2D\u4F7F\u7528\u4E86\u8D4B\u503C \u2014 \u662F\u5426\u5E94\u8BE5\u4F7F\u7528 === \u800C\u975E =\uFF1F`
|
|
2300
|
+
),
|
|
2301
|
+
suggestion: t(
|
|
2302
|
+
`If comparison was intended, use === instead of =. If assignment is intentional, extract it to a separate line.`,
|
|
2303
|
+
`\u5982\u679C\u610F\u56FE\u662F\u6BD4\u8F83\uFF0C\u8BF7\u4F7F\u7528 === \u4EE3\u66FF =\u3002\u5982\u679C\u786E\u5B9E\u9700\u8981\u8D4B\u503C\uFF0C\u8BF7\u63D0\u53D6\u5230\u5355\u72EC\u7684\u884C\u3002`
|
|
2304
|
+
)
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
return issues;
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2311
|
+
|
|
2312
|
+
// src/rules/builtin/promise-void.ts
|
|
2313
|
+
var promiseVoidRule = {
|
|
2314
|
+
id: "logic/promise-void",
|
|
2315
|
+
category: "logic",
|
|
2316
|
+
severity: "medium",
|
|
2317
|
+
title: "Floating promise (not awaited or returned)",
|
|
2318
|
+
description: "AI-generated code often calls async functions without await, causing unhandled rejections.",
|
|
2319
|
+
check(context) {
|
|
2320
|
+
const issues = [];
|
|
2321
|
+
let ast;
|
|
2322
|
+
try {
|
|
2323
|
+
const parsed = parseCode(context.fileContent, context.filePath);
|
|
2324
|
+
ast = parsed.ast;
|
|
2325
|
+
} catch {
|
|
2326
|
+
return issues;
|
|
2327
|
+
}
|
|
2328
|
+
const asyncFnNames = /* @__PURE__ */ new Set();
|
|
2329
|
+
walkAST(ast, (node) => {
|
|
2330
|
+
if (node.type === AST_NODE_TYPES.FunctionDeclaration && node.async && node.id) {
|
|
2331
|
+
asyncFnNames.add(node.id.name);
|
|
2332
|
+
}
|
|
2333
|
+
if (node.type === AST_NODE_TYPES.VariableDeclarator && node.id.type === AST_NODE_TYPES.Identifier) {
|
|
2334
|
+
const init = node.init;
|
|
2335
|
+
if (init && (init.type === AST_NODE_TYPES.ArrowFunctionExpression || init.type === AST_NODE_TYPES.FunctionExpression) && init.async) {
|
|
2336
|
+
asyncFnNames.add(
|
|
2337
|
+
node.id.name
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
});
|
|
2342
|
+
const commonAsyncPatterns = [
|
|
2343
|
+
/^fetch$/,
|
|
2344
|
+
/^save/,
|
|
2345
|
+
/^load/,
|
|
2346
|
+
/^send/,
|
|
2347
|
+
/^delete/,
|
|
2348
|
+
/^update/,
|
|
2349
|
+
/^create/,
|
|
2350
|
+
/^connect/,
|
|
2351
|
+
/^disconnect/,
|
|
2352
|
+
/^init/
|
|
2353
|
+
];
|
|
2354
|
+
walkAST(ast, (node) => {
|
|
2355
|
+
if (node.type !== AST_NODE_TYPES.ExpressionStatement) return;
|
|
2356
|
+
const expr = node.expression;
|
|
2357
|
+
if (expr.type !== AST_NODE_TYPES.CallExpression) return;
|
|
2358
|
+
const callExpr = expr;
|
|
2359
|
+
const fnName = getCallName2(callExpr);
|
|
2360
|
+
if (!fnName) return;
|
|
2361
|
+
const isKnownAsync = asyncFnNames.has(fnName);
|
|
2362
|
+
const matchesPattern = commonAsyncPatterns.some((p) => p.test(fnName));
|
|
2363
|
+
const endsWithAsync = fnName.endsWith("Async") || fnName.endsWith("async");
|
|
2364
|
+
if (!isKnownAsync && !matchesPattern && !endsWithAsync) return;
|
|
2365
|
+
const line = node.loc?.start.line ?? 0;
|
|
2366
|
+
if (line === 0) return;
|
|
2367
|
+
issues.push({
|
|
2368
|
+
ruleId: "logic/promise-void",
|
|
2369
|
+
severity: "medium",
|
|
2370
|
+
category: "logic",
|
|
2371
|
+
file: context.filePath,
|
|
2372
|
+
startLine: line,
|
|
2373
|
+
endLine: node.loc?.end.line ?? line,
|
|
2374
|
+
message: t(
|
|
2375
|
+
`Call to "${fnName}()" appears to be a floating promise (not awaited or returned).`,
|
|
2376
|
+
`\u8C03\u7528 "${fnName}()" \u7591\u4F3C\u6D6E\u52A8 Promise\uFF08\u672A await \u6216 return\uFF09\u3002`
|
|
2377
|
+
),
|
|
2378
|
+
suggestion: t(
|
|
2379
|
+
`Add "await" before the call, assign the result, or use "void ${fnName}()" to explicitly discard.`,
|
|
2380
|
+
`\u5728\u8C03\u7528\u524D\u6DFB\u52A0 "await"\uFF0C\u8D4B\u503C\u7ED9\u53D8\u91CF\uFF0C\u6216\u4F7F\u7528 "void ${fnName}()" \u663E\u5F0F\u4E22\u5F03\u3002`
|
|
2381
|
+
)
|
|
2382
|
+
});
|
|
2383
|
+
});
|
|
2384
|
+
return issues;
|
|
2385
|
+
}
|
|
2386
|
+
};
|
|
2387
|
+
function getCallName2(callExpr) {
|
|
2388
|
+
const callee = callExpr.callee;
|
|
2389
|
+
if (callee.type === AST_NODE_TYPES.Identifier) {
|
|
2390
|
+
return callee.name;
|
|
2391
|
+
}
|
|
2392
|
+
if (callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES.Identifier) {
|
|
2393
|
+
return callee.property.name;
|
|
2394
|
+
}
|
|
2395
|
+
return null;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// src/rules/builtin/no-reassign-param.ts
|
|
2399
|
+
var noReassignParamRule = {
|
|
2400
|
+
id: "logic/no-reassign-param",
|
|
2401
|
+
category: "logic",
|
|
2402
|
+
severity: "low",
|
|
2403
|
+
title: "Parameter reassignment",
|
|
2404
|
+
description: "AI-generated code often reassigns function parameters, creating confusing side-effect patterns.",
|
|
2405
|
+
check(context) {
|
|
2406
|
+
const issues = [];
|
|
2407
|
+
let ast;
|
|
2408
|
+
try {
|
|
2409
|
+
const parsed = parseCode(context.fileContent, context.filePath);
|
|
2410
|
+
ast = parsed.ast;
|
|
2411
|
+
} catch {
|
|
2412
|
+
return issues;
|
|
2413
|
+
}
|
|
2414
|
+
walkAST(ast, (node) => {
|
|
2415
|
+
const isFn = node.type === AST_NODE_TYPES.FunctionDeclaration || node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression || node.type === AST_NODE_TYPES.MethodDefinition;
|
|
2416
|
+
if (!isFn) return;
|
|
2417
|
+
let params = [];
|
|
2418
|
+
if (node.type === AST_NODE_TYPES.MethodDefinition) {
|
|
2419
|
+
const method = node;
|
|
2420
|
+
if (method.value && "params" in method.value) {
|
|
2421
|
+
params = method.value.params;
|
|
2422
|
+
}
|
|
2423
|
+
} else {
|
|
2424
|
+
params = node.params;
|
|
2425
|
+
}
|
|
2426
|
+
const paramNames = /* @__PURE__ */ new Set();
|
|
2427
|
+
for (const param of params) {
|
|
2428
|
+
if (param.type === AST_NODE_TYPES.Identifier) {
|
|
2429
|
+
paramNames.add(param.name);
|
|
2430
|
+
}
|
|
2431
|
+
if (param.type === AST_NODE_TYPES.AssignmentPattern) {
|
|
2432
|
+
const left = param.left;
|
|
2433
|
+
if (left.type === AST_NODE_TYPES.Identifier) {
|
|
2434
|
+
paramNames.add(left.name);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
if (paramNames.size === 0) return;
|
|
2439
|
+
let body = null;
|
|
2440
|
+
if (node.type === AST_NODE_TYPES.MethodDefinition) {
|
|
2441
|
+
body = node.value;
|
|
2442
|
+
} else {
|
|
2443
|
+
body = node;
|
|
2444
|
+
}
|
|
2445
|
+
if (!body || !("body" in body)) return;
|
|
2446
|
+
const fnBody = body.body;
|
|
2447
|
+
if (!fnBody) return;
|
|
2448
|
+
const reportedParams = /* @__PURE__ */ new Set();
|
|
2449
|
+
walkAST(fnBody, (innerNode) => {
|
|
2450
|
+
if (innerNode.type !== AST_NODE_TYPES.AssignmentExpression) return;
|
|
2451
|
+
const assignExpr = innerNode;
|
|
2452
|
+
if (assignExpr.left.type !== AST_NODE_TYPES.Identifier) return;
|
|
2453
|
+
const name = assignExpr.left.name;
|
|
2454
|
+
if (!paramNames.has(name) || reportedParams.has(name)) return;
|
|
2455
|
+
reportedParams.add(name);
|
|
2456
|
+
const line = innerNode.loc?.start.line ?? 0;
|
|
2457
|
+
if (line === 0) return;
|
|
2458
|
+
issues.push({
|
|
2459
|
+
ruleId: "logic/no-reassign-param",
|
|
2460
|
+
severity: "low",
|
|
2461
|
+
category: "logic",
|
|
2462
|
+
file: context.filePath,
|
|
2463
|
+
startLine: line,
|
|
2464
|
+
endLine: line,
|
|
2465
|
+
message: t(
|
|
2466
|
+
`Parameter "${name}" is reassigned. This can cause confusion and lose the original value.`,
|
|
2467
|
+
`\u53C2\u6570 "${name}" \u88AB\u91CD\u65B0\u8D4B\u503C\u3002\u8FD9\u53EF\u80FD\u9020\u6210\u6DF7\u6DC6\u5E76\u4E22\u5931\u539F\u59CB\u503C\u3002`
|
|
2468
|
+
),
|
|
2469
|
+
suggestion: t(
|
|
2470
|
+
`Use a local variable instead: const local${name.charAt(0).toUpperCase() + name.slice(1)} = ...`,
|
|
2471
|
+
`\u4F7F\u7528\u5C40\u90E8\u53D8\u91CF\u4EE3\u66FF\uFF1Aconst local${name.charAt(0).toUpperCase() + name.slice(1)} = ...`
|
|
2472
|
+
)
|
|
2473
|
+
});
|
|
2474
|
+
});
|
|
2475
|
+
});
|
|
2476
|
+
return issues;
|
|
2477
|
+
}
|
|
2478
|
+
};
|
|
2479
|
+
|
|
2480
|
+
// src/rules/builtin/no-async-without-await.ts
|
|
2481
|
+
var noAsyncWithoutAwaitRule = {
|
|
2482
|
+
id: "logic/no-async-without-await",
|
|
2483
|
+
category: "logic",
|
|
2484
|
+
severity: "low",
|
|
2485
|
+
title: "Async function without await",
|
|
2486
|
+
description: "AI-generated code often marks functions as async without using await, adding unnecessary Promise wrapping.",
|
|
2487
|
+
check(context) {
|
|
2488
|
+
const issues = [];
|
|
2489
|
+
let ast;
|
|
2490
|
+
try {
|
|
2491
|
+
const parsed = parseCode(context.fileContent, context.filePath);
|
|
2492
|
+
ast = parsed.ast;
|
|
2493
|
+
} catch {
|
|
2494
|
+
return issues;
|
|
2495
|
+
}
|
|
2496
|
+
walkAST(ast, (node) => {
|
|
2497
|
+
const isAsyncFn = (node.type === AST_NODE_TYPES.FunctionDeclaration || node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression) && node.async;
|
|
2498
|
+
if (!isAsyncFn) return;
|
|
2499
|
+
const fnNode = node;
|
|
2500
|
+
const body = fnNode.body;
|
|
2501
|
+
if (!body) return;
|
|
2502
|
+
let hasAwait = false;
|
|
2503
|
+
walkAST(body, (innerNode) => {
|
|
2504
|
+
if (innerNode !== body && (innerNode.type === AST_NODE_TYPES.FunctionDeclaration || innerNode.type === AST_NODE_TYPES.FunctionExpression || innerNode.type === AST_NODE_TYPES.ArrowFunctionExpression) && innerNode.async) {
|
|
2505
|
+
return false;
|
|
2506
|
+
}
|
|
2507
|
+
if (innerNode.type === AST_NODE_TYPES.AwaitExpression) {
|
|
2508
|
+
hasAwait = true;
|
|
2509
|
+
return false;
|
|
2510
|
+
}
|
|
2511
|
+
if (innerNode.type === AST_NODE_TYPES.ForOfStatement && innerNode.await) {
|
|
2512
|
+
hasAwait = true;
|
|
2513
|
+
return false;
|
|
2514
|
+
}
|
|
2515
|
+
return;
|
|
2516
|
+
});
|
|
2517
|
+
if (hasAwait) return false;
|
|
2518
|
+
const line = node.loc?.start.line ?? 0;
|
|
2519
|
+
if (line === 0) return;
|
|
2520
|
+
let fnName = "<anonymous>";
|
|
2521
|
+
if (fnNode.type === AST_NODE_TYPES.FunctionDeclaration && fnNode.id) {
|
|
2522
|
+
fnName = fnNode.id.name;
|
|
2523
|
+
}
|
|
2524
|
+
issues.push({
|
|
2525
|
+
ruleId: "logic/no-async-without-await",
|
|
2526
|
+
severity: "low",
|
|
2527
|
+
category: "logic",
|
|
2528
|
+
file: context.filePath,
|
|
2529
|
+
startLine: line,
|
|
2530
|
+
endLine: node.loc?.end.line ?? line,
|
|
2531
|
+
message: t(
|
|
2532
|
+
`Async function "${fnName}" does not use await.`,
|
|
2533
|
+
`\u5F02\u6B65\u51FD\u6570 "${fnName}" \u5185\u90E8\u6CA1\u6709\u4F7F\u7528 await\u3002`
|
|
2534
|
+
),
|
|
2535
|
+
suggestion: t(
|
|
2536
|
+
`Remove the async keyword if this function doesn't need to be asynchronous.`,
|
|
2537
|
+
`\u5982\u679C\u6B64\u51FD\u6570\u4E0D\u9700\u8981\u5F02\u6B65\u884C\u4E3A\uFF0C\u8BF7\u79FB\u9664 async \u5173\u952E\u5B57\u3002`
|
|
2538
|
+
)
|
|
2539
|
+
});
|
|
2540
|
+
return false;
|
|
2541
|
+
});
|
|
2542
|
+
return issues;
|
|
2543
|
+
}
|
|
2544
|
+
};
|
|
2545
|
+
|
|
2546
|
+
// src/rules/builtin/no-useless-constructor.ts
|
|
2547
|
+
var noUselessConstructorRule = {
|
|
2548
|
+
id: "logic/no-useless-constructor",
|
|
2549
|
+
category: "logic",
|
|
2550
|
+
severity: "low",
|
|
2551
|
+
title: "Useless constructor",
|
|
2552
|
+
description: "AI-generated code often produces constructors that only call super() or are completely empty.",
|
|
2553
|
+
check(context) {
|
|
2554
|
+
const issues = [];
|
|
2555
|
+
let ast;
|
|
2556
|
+
try {
|
|
2557
|
+
const parsed = parseCode(context.fileContent, context.filePath);
|
|
2558
|
+
ast = parsed.ast;
|
|
2559
|
+
} catch {
|
|
2560
|
+
return issues;
|
|
2561
|
+
}
|
|
2562
|
+
walkAST(ast, (node) => {
|
|
2563
|
+
if (node.type !== AST_NODE_TYPES.ClassDeclaration && node.type !== AST_NODE_TYPES.ClassExpression) return;
|
|
2564
|
+
const classNode = node;
|
|
2565
|
+
const hasSuper = classNode.superClass !== null && classNode.superClass !== void 0;
|
|
2566
|
+
const constructor = classNode.body.body.find(
|
|
2567
|
+
(member) => member.type === AST_NODE_TYPES.MethodDefinition && member.kind === "constructor"
|
|
2568
|
+
);
|
|
2569
|
+
if (!constructor) return;
|
|
2570
|
+
const ctorValue = constructor.value;
|
|
2571
|
+
if (!ctorValue.body) return;
|
|
2572
|
+
const bodyStatements = ctorValue.body.body;
|
|
2573
|
+
if (bodyStatements.length === 0) {
|
|
2574
|
+
const line = constructor.loc?.start.line ?? 0;
|
|
2575
|
+
if (line === 0) return;
|
|
2576
|
+
issues.push({
|
|
2577
|
+
ruleId: "logic/no-useless-constructor",
|
|
2578
|
+
severity: "low",
|
|
2579
|
+
category: "logic",
|
|
2580
|
+
file: context.filePath,
|
|
2581
|
+
startLine: line,
|
|
2582
|
+
endLine: constructor.loc?.end.line ?? line,
|
|
2583
|
+
message: t(
|
|
2584
|
+
`Empty constructor is unnecessary.`,
|
|
2585
|
+
`\u7A7A\u6784\u9020\u51FD\u6570\u662F\u4E0D\u5FC5\u8981\u7684\u3002`
|
|
2586
|
+
),
|
|
2587
|
+
suggestion: t(
|
|
2588
|
+
`Remove the empty constructor \u2014 JavaScript provides a default one.`,
|
|
2589
|
+
`\u79FB\u9664\u7A7A\u6784\u9020\u51FD\u6570 \u2014 JavaScript \u4F1A\u81EA\u52A8\u63D0\u4F9B\u9ED8\u8BA4\u6784\u9020\u51FD\u6570\u3002`
|
|
2590
|
+
)
|
|
2591
|
+
});
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
if (hasSuper && bodyStatements.length === 1) {
|
|
2595
|
+
const stmt = bodyStatements[0];
|
|
2596
|
+
if (stmt.type !== AST_NODE_TYPES.ExpressionStatement) return;
|
|
2597
|
+
const expr = stmt.expression;
|
|
2598
|
+
if (expr.type !== AST_NODE_TYPES.CallExpression) return;
|
|
2599
|
+
if (expr.callee.type !== AST_NODE_TYPES.Super) return;
|
|
2600
|
+
const ctorParams = ctorValue.params;
|
|
2601
|
+
const superArgs = expr.arguments;
|
|
2602
|
+
if (ctorParams.length === superArgs.length) {
|
|
2603
|
+
let allMatch = true;
|
|
2604
|
+
for (let i = 0; i < ctorParams.length; i++) {
|
|
2605
|
+
const param = ctorParams[i];
|
|
2606
|
+
const arg = superArgs[i];
|
|
2607
|
+
if (param.type === AST_NODE_TYPES.Identifier && arg.type === AST_NODE_TYPES.Identifier && param.name === arg.name) {
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
if (param.type === AST_NODE_TYPES.TSParameterProperty && param.parameter.type === AST_NODE_TYPES.Identifier) {
|
|
2611
|
+
allMatch = false;
|
|
2612
|
+
break;
|
|
2613
|
+
}
|
|
2614
|
+
allMatch = false;
|
|
2615
|
+
break;
|
|
2616
|
+
}
|
|
2617
|
+
if (allMatch) {
|
|
2618
|
+
const line = constructor.loc?.start.line ?? 0;
|
|
2619
|
+
if (line === 0) return;
|
|
2620
|
+
issues.push({
|
|
2621
|
+
ruleId: "logic/no-useless-constructor",
|
|
2622
|
+
severity: "low",
|
|
2623
|
+
category: "logic",
|
|
2624
|
+
file: context.filePath,
|
|
2625
|
+
startLine: line,
|
|
2626
|
+
endLine: constructor.loc?.end.line ?? line,
|
|
2627
|
+
message: t(
|
|
2628
|
+
`Constructor only calls super() with the same arguments \u2014 it is unnecessary.`,
|
|
2629
|
+
`\u6784\u9020\u51FD\u6570\u4EC5\u8C03\u7528 super() \u5E76\u4F20\u9012\u76F8\u540C\u53C2\u6570 \u2014 \u8FD9\u662F\u4E0D\u5FC5\u8981\u7684\u3002`
|
|
2630
|
+
),
|
|
2631
|
+
suggestion: t(
|
|
2632
|
+
`Remove the constructor \u2014 JavaScript automatically calls super() with the same arguments.`,
|
|
2633
|
+
`\u79FB\u9664\u6784\u9020\u51FD\u6570 \u2014 JavaScript \u4F1A\u81EA\u52A8\u7528\u76F8\u540C\u53C2\u6570\u8C03\u7528 super()\u3002`
|
|
2634
|
+
)
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
});
|
|
2640
|
+
return issues;
|
|
2641
|
+
}
|
|
2642
|
+
};
|
|
2643
|
+
|
|
2644
|
+
// src/rules/engine.ts
|
|
2645
|
+
var BUILTIN_RULES = [
|
|
2646
|
+
unnecessaryTryCatchRule,
|
|
2647
|
+
overDefensiveRule,
|
|
2648
|
+
deadLogicRule,
|
|
2649
|
+
unusedVariablesRule,
|
|
2650
|
+
duplicateConditionRule,
|
|
2651
|
+
...securityRules,
|
|
2652
|
+
emptyCatchRule,
|
|
2653
|
+
identicalBranchesRule,
|
|
2654
|
+
redundantElseRule,
|
|
2655
|
+
consoleInCodeRule,
|
|
2656
|
+
phantomImportRule,
|
|
2657
|
+
unusedImportRule,
|
|
2658
|
+
missingAwaitRule,
|
|
2659
|
+
anyTypeAbuseRule,
|
|
2660
|
+
typeCoercionRule,
|
|
2661
|
+
magicNumberRule,
|
|
2662
|
+
nestedTernaryRule,
|
|
2663
|
+
duplicateStringRule,
|
|
2664
|
+
noDebuggerRule,
|
|
2665
|
+
noNonNullAssertionRule,
|
|
2666
|
+
noSelfCompareRule,
|
|
2667
|
+
noReturnAssignRule,
|
|
2668
|
+
promiseVoidRule,
|
|
2669
|
+
noReassignParamRule,
|
|
2670
|
+
noAsyncWithoutAwaitRule,
|
|
2671
|
+
noUselessConstructorRule
|
|
2672
|
+
];
|
|
2673
|
+
var RuleEngine = class {
|
|
2674
|
+
rules;
|
|
2675
|
+
constructor(config) {
|
|
2676
|
+
this.rules = BUILTIN_RULES.filter(
|
|
2677
|
+
(rule) => !config.rules.disabled.includes(rule.id)
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
run(context) {
|
|
2681
|
+
const allIssues = [];
|
|
2682
|
+
for (const rule of this.rules) {
|
|
2683
|
+
try {
|
|
2684
|
+
const issues = rule.check(context);
|
|
2685
|
+
allIssues.push(...issues);
|
|
2686
|
+
} catch (_err) {
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
return allIssues;
|
|
2690
|
+
}
|
|
2691
|
+
getRules() {
|
|
2692
|
+
return [...this.rules];
|
|
2693
|
+
}
|
|
2694
|
+
listRules() {
|
|
2695
|
+
return BUILTIN_RULES.map((r) => ({
|
|
2696
|
+
id: r.id,
|
|
2697
|
+
category: r.category,
|
|
2698
|
+
severity: r.severity,
|
|
2699
|
+
title: r.title
|
|
2700
|
+
}));
|
|
2701
|
+
}
|
|
2702
|
+
};
|
|
2703
|
+
|
|
2704
|
+
// src/core/scorer.ts
|
|
2705
|
+
var SEVERITY_PENALTY = {
|
|
2706
|
+
high: 15,
|
|
2707
|
+
medium: 8,
|
|
2708
|
+
low: 3,
|
|
2709
|
+
info: 0
|
|
2710
|
+
};
|
|
2711
|
+
function calculateDimensionScore(issues) {
|
|
2712
|
+
let score = 100;
|
|
2713
|
+
for (const issue of issues) {
|
|
2714
|
+
score -= SEVERITY_PENALTY[issue.severity] ?? 0;
|
|
2715
|
+
}
|
|
2716
|
+
return {
|
|
2717
|
+
score: Math.max(0, Math.min(100, score)),
|
|
2718
|
+
issues
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
function calculateOverallScore(dimensions, weights) {
|
|
2722
|
+
const score = dimensions.security.score * weights.security + dimensions.logic.score * weights.logic + dimensions.structure.score * weights.structure + dimensions.style.score * weights.style + dimensions.coverage.score * weights.coverage;
|
|
2723
|
+
return Math.round(Math.max(0, Math.min(100, score)));
|
|
2724
|
+
}
|
|
2725
|
+
function getGrade(score) {
|
|
2726
|
+
if (score >= 90) return "HIGH_TRUST";
|
|
2727
|
+
if (score >= 70) return "REVIEW";
|
|
2728
|
+
if (score >= 50) return "LOW_TRUST";
|
|
2729
|
+
return "UNTRUSTED";
|
|
2730
|
+
}
|
|
2731
|
+
function getGradeEmoji(grade) {
|
|
2732
|
+
switch (grade) {
|
|
2733
|
+
case "HIGH_TRUST":
|
|
2734
|
+
return "\u2705";
|
|
2735
|
+
case "REVIEW":
|
|
2736
|
+
return "\u26A0\uFE0F";
|
|
2737
|
+
case "LOW_TRUST":
|
|
2738
|
+
return "\u26A0\uFE0F";
|
|
2739
|
+
case "UNTRUSTED":
|
|
2740
|
+
return "\u274C";
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
function getGradeLabel(grade) {
|
|
2744
|
+
const isZh = isZhLocale();
|
|
2745
|
+
if (isZh) {
|
|
2746
|
+
switch (grade) {
|
|
2747
|
+
case "HIGH_TRUST":
|
|
2748
|
+
return "\u9AD8\u4FE1\u4EFB \u2014 \u53EF\u5B89\u5168\u5408\u5E76";
|
|
2749
|
+
case "REVIEW":
|
|
2750
|
+
return "\u5EFA\u8BAE\u5BA1\u67E5";
|
|
2751
|
+
case "LOW_TRUST":
|
|
2752
|
+
return "\u4F4E\u4FE1\u4EFB \u2014 \u9700\u4ED4\u7EC6\u5BA1\u67E5";
|
|
2753
|
+
case "UNTRUSTED":
|
|
2754
|
+
return "\u4E0D\u53EF\u4FE1 \u2014 \u4E0D\u5E94\u5408\u5E76";
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
switch (grade) {
|
|
2758
|
+
case "HIGH_TRUST":
|
|
2759
|
+
return "HIGH TRUST \u2014 Safe to merge";
|
|
2760
|
+
case "REVIEW":
|
|
2761
|
+
return "REVIEW RECOMMENDED";
|
|
2762
|
+
case "LOW_TRUST":
|
|
2763
|
+
return "LOW TRUST \u2014 Careful review needed";
|
|
2764
|
+
case "UNTRUSTED":
|
|
2765
|
+
return "UNTRUSTED \u2014 Do not merge without changes";
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
// src/analyzers/structure.ts
|
|
2770
|
+
var DEFAULT_THRESHOLDS = {
|
|
2771
|
+
maxCyclomaticComplexity: 10,
|
|
2772
|
+
maxCognitiveComplexity: 20,
|
|
2773
|
+
maxFunctionLength: 40,
|
|
2774
|
+
maxNestingDepth: 4,
|
|
2775
|
+
maxParamCount: 5
|
|
2776
|
+
};
|
|
2777
|
+
function analyzeStructure(code, filePath, thresholds = {}) {
|
|
2778
|
+
const t_ = { ...DEFAULT_THRESHOLDS, ...thresholds };
|
|
2779
|
+
const issues = [];
|
|
2780
|
+
let parsed;
|
|
2781
|
+
try {
|
|
2782
|
+
parsed = parseCode(code, filePath);
|
|
2783
|
+
} catch {
|
|
2784
|
+
return { functions: [], issues: [] };
|
|
2785
|
+
}
|
|
2786
|
+
const functions = extractFunctions(parsed);
|
|
2787
|
+
for (const fn of functions) {
|
|
2788
|
+
if (fn.cyclomaticComplexity > t_.maxCyclomaticComplexity) {
|
|
2789
|
+
issues.push({
|
|
2790
|
+
ruleId: "structure/high-cyclomatic-complexity",
|
|
2791
|
+
severity: fn.cyclomaticComplexity > t_.maxCyclomaticComplexity * 2 ? "high" : "medium",
|
|
2792
|
+
category: "structure",
|
|
2793
|
+
file: filePath,
|
|
2794
|
+
startLine: fn.startLine,
|
|
2795
|
+
endLine: fn.endLine,
|
|
1868
2796
|
message: t(
|
|
1869
2797
|
`Function "${fn.name}" has cyclomatic complexity of ${fn.cyclomaticComplexity} (threshold: ${t_.maxCyclomaticComplexity}).`,
|
|
1870
2798
|
`\u51FD\u6570 "${fn.name}" \u7684\u5708\u590D\u6742\u5EA6\u4E3A ${fn.cyclomaticComplexity}\uFF08\u9608\u503C\uFF1A${t_.maxCyclomaticComplexity}\uFF09\u3002`
|
|
@@ -2613,19 +3541,238 @@ function createHookCommand() {
|
|
|
2613
3541
|
return cmd;
|
|
2614
3542
|
}
|
|
2615
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
|
+
|
|
2616
3762
|
// src/cli/index.ts
|
|
2617
|
-
import { readFileSync as
|
|
3763
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2618
3764
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2619
|
-
import { dirname as dirname4, resolve as
|
|
3765
|
+
import { dirname as dirname4, resolve as resolve6 } from "path";
|
|
2620
3766
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2621
3767
|
var __dirname2 = dirname4(__filename2);
|
|
2622
|
-
var pkg = JSON.parse(
|
|
2623
|
-
var program = new
|
|
3768
|
+
var pkg = JSON.parse(readFileSync3(resolve6(__dirname2, "../../package.json"), "utf-8"));
|
|
3769
|
+
var program = new Command7();
|
|
2624
3770
|
program.name("codetrust").description("AI code trust verification tool \u2014 verify AI-generated code with deterministic algorithms").version(pkg.version);
|
|
2625
3771
|
program.addCommand(createScanCommand());
|
|
2626
3772
|
program.addCommand(createReportCommand());
|
|
2627
3773
|
program.addCommand(createInitCommand());
|
|
2628
3774
|
program.addCommand(createRulesCommand());
|
|
2629
3775
|
program.addCommand(createHookCommand());
|
|
3776
|
+
program.addCommand(createFixCommand());
|
|
2630
3777
|
program.parse();
|
|
2631
3778
|
//# sourceMappingURL=index.js.map
|