aislop 0.9.4 → 0.9.5
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 +50 -3
- package/dist/cli.js +1034 -62
- package/dist/{version-C45P3Q1N.js → engine-info-DCvIfZ0f.js} +1 -5
- package/dist/index.d.ts +6 -0
- package/dist/index.js +758 -51
- package/dist/{json-CXiEvR_M.js → json-CZU3lEfE.js} +2 -1
- package/dist/mcp.js +653 -39
- package/dist/sarif-CZVuavf_.js +61 -0
- package/dist/sarif-Cneulb6L.js +60 -0
- package/dist/version-ls3wZmOU.js +5 -0
- package/package.json +3 -2
- package/scripts/gen-config-schema.mjs +35 -0
- /package/dist/{expo-doctor-Bz0LZhQ6.js → expo-doctor-BcIkOte5.js} +0 -0
- /package/dist/{generic-BrcWMW7E.js → generic-D_T4cUaC.js} +0 -0
- /package/dist/{typecheck-XJMuCczG.js → typecheck-DQSzG8fX.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { n as
|
|
1
|
+
import { n as getEngineLabel, t as ENGINE_INFO } from "./engine-info-DCvIfZ0f.js";
|
|
2
2
|
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { t as APP_VERSION } from "./version-ls3wZmOU.js";
|
|
4
|
+
import { r as runGenericLinter, t as fixRubyLint } from "./generic-D_T4cUaC.js";
|
|
5
|
+
import { n as runExpoDoctor } from "./expo-doctor-BcIkOte5.js";
|
|
5
6
|
import { createRequire, isBuiltin } from "node:module";
|
|
6
7
|
import fs from "node:fs";
|
|
7
8
|
import path from "node:path";
|
|
@@ -13,8 +14,8 @@ import { spawnSync } from "node:child_process";
|
|
|
13
14
|
import micromatch from "micromatch";
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
15
16
|
import { performance } from "node:perf_hooks";
|
|
16
|
-
import os from "node:os";
|
|
17
17
|
import ts from "typescript";
|
|
18
|
+
import os from "node:os";
|
|
18
19
|
import { randomUUID } from "node:crypto";
|
|
19
20
|
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
20
21
|
|
|
@@ -67,7 +68,8 @@ const DEFAULT_CONFIG = {
|
|
|
67
68
|
failBelow: 70,
|
|
68
69
|
format: "json"
|
|
69
70
|
},
|
|
70
|
-
telemetry: { enabled: true }
|
|
71
|
+
telemetry: { enabled: true },
|
|
72
|
+
rules: {}
|
|
71
73
|
};
|
|
72
74
|
const GITHUB_WORKFLOW_DIR = ".github/workflows";
|
|
73
75
|
const GITHUB_WORKFLOW_FILE = "aislop.yml";
|
|
@@ -194,6 +196,12 @@ const CiSchema = z.object({
|
|
|
194
196
|
format: z.enum(["json"]).default("json")
|
|
195
197
|
});
|
|
196
198
|
const TelemetrySchema = z.object({ enabled: z.boolean().default(true) });
|
|
199
|
+
const RuleSeverityOverride = z.enum([
|
|
200
|
+
"error",
|
|
201
|
+
"warning",
|
|
202
|
+
"off"
|
|
203
|
+
]);
|
|
204
|
+
const RulesSchema = z.record(z.string(), RuleSeverityOverride).default(() => ({}));
|
|
197
205
|
const AislopConfigSchema = z.object({
|
|
198
206
|
version: z.number().default(1),
|
|
199
207
|
engines: EnginesSchema.default(() => ({
|
|
@@ -228,6 +236,7 @@ const AislopConfigSchema = z.object({
|
|
|
228
236
|
format: "json"
|
|
229
237
|
})),
|
|
230
238
|
telemetry: TelemetrySchema.default(() => ({ enabled: true })),
|
|
239
|
+
rules: RulesSchema,
|
|
231
240
|
exclude: z.array(z.string()).default(() => [
|
|
232
241
|
"node_modules",
|
|
233
242
|
".git",
|
|
@@ -544,7 +553,7 @@ const padStart = (s, target, fill = " ") => {
|
|
|
544
553
|
//#endregion
|
|
545
554
|
//#region src/utils/source-files.ts
|
|
546
555
|
const MAX_BUFFER$1 = 50 * 1024 * 1024;
|
|
547
|
-
const SOURCE_EXTENSIONS = new Set([
|
|
556
|
+
const SOURCE_EXTENSIONS$1 = new Set([
|
|
548
557
|
".ts",
|
|
549
558
|
".tsx",
|
|
550
559
|
".js",
|
|
@@ -652,7 +661,7 @@ const toProjectPath = (rootDirectory, filePath) => {
|
|
|
652
661
|
const isWithinProject = (relativePath) => relativePath.length > 0 && !relativePath.startsWith("..");
|
|
653
662
|
const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
654
663
|
const extension = path.extname(filePath);
|
|
655
|
-
return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
|
|
664
|
+
return SOURCE_EXTENSIONS$1.has(extension) || extraExtensions.has(extension);
|
|
656
665
|
};
|
|
657
666
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
658
667
|
const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
|
|
@@ -1246,7 +1255,7 @@ const doctorCommand = async (directory, options = {}) => {
|
|
|
1246
1255
|
|
|
1247
1256
|
//#endregion
|
|
1248
1257
|
//#region src/engines/ai-slop/abstractions.ts
|
|
1249
|
-
const JS_EXTS$
|
|
1258
|
+
const JS_EXTS$2 = new Set([
|
|
1250
1259
|
".ts",
|
|
1251
1260
|
".tsx",
|
|
1252
1261
|
".js",
|
|
@@ -1257,11 +1266,11 @@ const JS_EXTS$1 = new Set([
|
|
|
1257
1266
|
const THIN_WRAPPER_PATTERNS = [
|
|
1258
1267
|
{
|
|
1259
1268
|
pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
|
|
1260
|
-
extensions: JS_EXTS$
|
|
1269
|
+
extensions: JS_EXTS$2
|
|
1261
1270
|
},
|
|
1262
1271
|
{
|
|
1263
1272
|
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
1264
|
-
extensions: JS_EXTS$
|
|
1273
|
+
extensions: JS_EXTS$2
|
|
1265
1274
|
},
|
|
1266
1275
|
{
|
|
1267
1276
|
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
@@ -1494,6 +1503,12 @@ const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
|
1494
1503
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
1495
1504
|
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
1496
1505
|
const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
|
|
1506
|
+
const isLineComment = (trimmed) => isJsComment(trimmed) || isPythonComment(trimmed);
|
|
1507
|
+
const isInMultiLineCommentRun = (lines, index) => {
|
|
1508
|
+
const prev = index > 0 ? lines[index - 1].trim() : "";
|
|
1509
|
+
const next = index + 1 < lines.length ? lines[index + 1].trim() : "";
|
|
1510
|
+
return isLineComment(prev) || isLineComment(next);
|
|
1511
|
+
};
|
|
1497
1512
|
/**
|
|
1498
1513
|
* Extract just the comment text after the comment marker.
|
|
1499
1514
|
*/
|
|
@@ -1546,6 +1561,7 @@ const scanFileForTrivialComments = (content, relativePath, ext) => {
|
|
|
1546
1561
|
const lines = content.split("\n");
|
|
1547
1562
|
for (let i = 0; i < lines.length; i++) {
|
|
1548
1563
|
if (!isTrivialComment(lines[i].trim(), i + 1 < lines.length ? lines[i + 1] : void 0)) continue;
|
|
1564
|
+
if (isInMultiLineCommentRun(lines, i)) continue;
|
|
1549
1565
|
if (isDocCommentForDeclaration(lines, i, ext)) continue;
|
|
1550
1566
|
diagnostics.push({
|
|
1551
1567
|
filePath: relativePath,
|
|
@@ -1707,6 +1723,177 @@ const detectDeadPatterns = async (context) => {
|
|
|
1707
1723
|
return diagnostics;
|
|
1708
1724
|
};
|
|
1709
1725
|
|
|
1726
|
+
//#endregion
|
|
1727
|
+
//#region src/engines/ai-slop/defensive-patterns.ts
|
|
1728
|
+
const JS_TS_EXTENSIONS = new Set([
|
|
1729
|
+
".ts",
|
|
1730
|
+
".tsx",
|
|
1731
|
+
".js",
|
|
1732
|
+
".jsx",
|
|
1733
|
+
".mjs",
|
|
1734
|
+
".cjs"
|
|
1735
|
+
]);
|
|
1736
|
+
const TS_EXTENSIONS = new Set([".ts", ".tsx"]);
|
|
1737
|
+
const COERCION_CTORS = {
|
|
1738
|
+
string: "String",
|
|
1739
|
+
number: "Number",
|
|
1740
|
+
boolean: "Boolean"
|
|
1741
|
+
};
|
|
1742
|
+
const scriptKindFor = (ext) => {
|
|
1743
|
+
switch (ext) {
|
|
1744
|
+
case ".tsx": return ts.ScriptKind.TSX;
|
|
1745
|
+
case ".jsx": return ts.ScriptKind.JSX;
|
|
1746
|
+
case ".js":
|
|
1747
|
+
case ".mjs":
|
|
1748
|
+
case ".cjs": return ts.ScriptKind.JS;
|
|
1749
|
+
default: return ts.ScriptKind.TS;
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
const lineFor = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
1753
|
+
const makeDiagnostic = (filePath, line, rule, message, help) => ({
|
|
1754
|
+
filePath,
|
|
1755
|
+
engine: "ai-slop",
|
|
1756
|
+
rule,
|
|
1757
|
+
severity: "warning",
|
|
1758
|
+
message,
|
|
1759
|
+
help,
|
|
1760
|
+
line,
|
|
1761
|
+
column: 0,
|
|
1762
|
+
category: "AI Slop",
|
|
1763
|
+
fixable: false
|
|
1764
|
+
});
|
|
1765
|
+
const isFunctionNode = (node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node);
|
|
1766
|
+
const primitiveKindOf = (node) => {
|
|
1767
|
+
if (!node) return null;
|
|
1768
|
+
switch (node.kind) {
|
|
1769
|
+
case ts.SyntaxKind.StringKeyword: return "string";
|
|
1770
|
+
case ts.SyntaxKind.NumberKeyword: return "number";
|
|
1771
|
+
case ts.SyntaxKind.BooleanKeyword: return "boolean";
|
|
1772
|
+
default: return null;
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
const hasExportModifier = (node) => node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
1776
|
+
const primitiveParamsOf = (node) => {
|
|
1777
|
+
const params = /* @__PURE__ */ new Map();
|
|
1778
|
+
for (const param of node.parameters) {
|
|
1779
|
+
if (!ts.isIdentifier(param.name)) continue;
|
|
1780
|
+
const kind = primitiveKindOf(param.type);
|
|
1781
|
+
if (!kind) continue;
|
|
1782
|
+
params.set(param.name.text, kind);
|
|
1783
|
+
}
|
|
1784
|
+
return params;
|
|
1785
|
+
};
|
|
1786
|
+
const isRethrowStatement = (statement, errorName) => ts.isThrowStatement(statement) && statement.expression !== void 0 && ts.isIdentifier(statement.expression) && statement.expression.text === errorName;
|
|
1787
|
+
const isPromiseRejectRethrow = (statement, errorName) => {
|
|
1788
|
+
if (!ts.isReturnStatement(statement) || !statement.expression) return false;
|
|
1789
|
+
const expression = statement.expression;
|
|
1790
|
+
if (!ts.isCallExpression(expression) || expression.arguments.length !== 1) return false;
|
|
1791
|
+
const [arg] = expression.arguments;
|
|
1792
|
+
if (!ts.isIdentifier(arg) || arg.text !== errorName) return false;
|
|
1793
|
+
if (!ts.isPropertyAccessExpression(expression.expression)) return false;
|
|
1794
|
+
const target = expression.expression;
|
|
1795
|
+
return ts.isIdentifier(target.expression) && target.expression.text === "Promise" && target.name.text === "reject";
|
|
1796
|
+
};
|
|
1797
|
+
const detectRedundantTryCatch = (sourceFile, relativePath) => {
|
|
1798
|
+
const diagnostics = [];
|
|
1799
|
+
const visit = (node) => {
|
|
1800
|
+
if (ts.isTryStatement(node) && node.catchClause && !node.finallyBlock) {
|
|
1801
|
+
const catchNameNode = node.catchClause.variableDeclaration?.name;
|
|
1802
|
+
const [onlyStatement] = node.catchClause.block.statements;
|
|
1803
|
+
if (catchNameNode && ts.isIdentifier(catchNameNode) && node.catchClause.block.statements.length === 1 && onlyStatement && (isRethrowStatement(onlyStatement, catchNameNode.text) || isPromiseRejectRethrow(onlyStatement, catchNameNode.text))) diagnostics.push(makeDiagnostic(relativePath, lineFor(sourceFile, node.catchClause), "ai-slop/redundant-try-catch", "Catch block only rethrows the same error", "Remove the try/catch or add useful context, cleanup, or recovery. Rethrowing unchanged errors is usually defensive agent noise."));
|
|
1804
|
+
}
|
|
1805
|
+
ts.forEachChild(node, visit);
|
|
1806
|
+
};
|
|
1807
|
+
visit(sourceFile);
|
|
1808
|
+
return diagnostics;
|
|
1809
|
+
};
|
|
1810
|
+
const detectPrimitiveCoercions = (sourceFile, relativePath) => {
|
|
1811
|
+
const diagnostics = [];
|
|
1812
|
+
const scanFunctionBody = (node, params) => {
|
|
1813
|
+
const body = node.body;
|
|
1814
|
+
if (!body || params.size === 0) return;
|
|
1815
|
+
const visitBody = (child) => {
|
|
1816
|
+
if (child !== body && isFunctionNode(child)) return;
|
|
1817
|
+
if (ts.isCallExpression(child) && ts.isIdentifier(child.expression)) {
|
|
1818
|
+
const [arg] = child.arguments;
|
|
1819
|
+
if (arg && ts.isIdentifier(arg)) {
|
|
1820
|
+
const primitive = params.get(arg.text);
|
|
1821
|
+
if (primitive && child.expression.text === COERCION_CTORS[primitive]) diagnostics.push(makeDiagnostic(relativePath, lineFor(sourceFile, child), "ai-slop/redundant-type-coercion", `Parameter '${arg.text}' is already typed as ${primitive} but is coerced again`, "Trust the typed boundary or validate unknown input before this function. Re-coercing already typed parameters is usually defensive agent noise."));
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
ts.forEachChild(child, visitBody);
|
|
1825
|
+
};
|
|
1826
|
+
visitBody(body);
|
|
1827
|
+
};
|
|
1828
|
+
const visit = (node) => {
|
|
1829
|
+
if (isFunctionNode(node)) scanFunctionBody(node, primitiveParamsOf(node));
|
|
1830
|
+
ts.forEachChild(node, visit);
|
|
1831
|
+
};
|
|
1832
|
+
visit(sourceFile);
|
|
1833
|
+
return diagnostics;
|
|
1834
|
+
};
|
|
1835
|
+
const normalizedTypeDeclaration = (sourceFile, node) => sourceFile.text.slice(node.getStart(sourceFile), node.getEnd()).replace(/\bexport\b/g, "").replace(/\bdeclare\b/g, "").replace(/\s+/g, " ").trim();
|
|
1836
|
+
const exportedTypesOf = (parsed) => {
|
|
1837
|
+
const declarations = [];
|
|
1838
|
+
const visit = (node) => {
|
|
1839
|
+
if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && hasExportModifier(node)) declarations.push({
|
|
1840
|
+
name: node.name.text,
|
|
1841
|
+
signature: normalizedTypeDeclaration(parsed.sourceFile, node),
|
|
1842
|
+
filePath: parsed.relativePath,
|
|
1843
|
+
line: lineFor(parsed.sourceFile, node)
|
|
1844
|
+
});
|
|
1845
|
+
ts.forEachChild(node, visit);
|
|
1846
|
+
};
|
|
1847
|
+
visit(parsed.sourceFile);
|
|
1848
|
+
return declarations;
|
|
1849
|
+
};
|
|
1850
|
+
const duplicateTypeKeyOf = (declaration) => `${declaration.name}\0${declaration.signature}`;
|
|
1851
|
+
const detectDuplicateExportedTypes = (parsedSources) => {
|
|
1852
|
+
const diagnostics = [];
|
|
1853
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1854
|
+
for (const parsed of parsedSources) {
|
|
1855
|
+
if (!TS_EXTENSIONS.has(parsed.ext)) continue;
|
|
1856
|
+
for (const declaration of exportedTypesOf(parsed)) {
|
|
1857
|
+
const key = duplicateTypeKeyOf(declaration);
|
|
1858
|
+
const previous = seen.get(key);
|
|
1859
|
+
if (!previous) {
|
|
1860
|
+
seen.set(key, declaration);
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
if (previous.filePath === declaration.filePath) continue;
|
|
1864
|
+
diagnostics.push(makeDiagnostic(declaration.filePath, declaration.line, "ai-slop/duplicate-type-declaration", `Exported type '${declaration.name}' duplicates an existing declaration`, `Reuse or import the existing type from ${previous.filePath} instead of re-declaring the same shape in another file.`));
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return diagnostics;
|
|
1868
|
+
};
|
|
1869
|
+
const detectDefensivePatterns = async (context) => {
|
|
1870
|
+
const diagnostics = [];
|
|
1871
|
+
const parsedSources = [];
|
|
1872
|
+
for (const filePath of getSourceFiles(context)) {
|
|
1873
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1874
|
+
let content;
|
|
1875
|
+
try {
|
|
1876
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
1877
|
+
} catch {
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1881
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
1882
|
+
const ext = path.extname(filePath);
|
|
1883
|
+
if (!JS_TS_EXTENSIONS.has(ext)) continue;
|
|
1884
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKindFor(ext));
|
|
1885
|
+
parsedSources.push({
|
|
1886
|
+
sourceFile,
|
|
1887
|
+
relativePath,
|
|
1888
|
+
ext
|
|
1889
|
+
});
|
|
1890
|
+
diagnostics.push(...detectRedundantTryCatch(sourceFile, relativePath));
|
|
1891
|
+
if (TS_EXTENSIONS.has(ext)) diagnostics.push(...detectPrimitiveCoercions(sourceFile, relativePath));
|
|
1892
|
+
}
|
|
1893
|
+
diagnostics.push(...detectDuplicateExportedTypes(parsedSources));
|
|
1894
|
+
return diagnostics;
|
|
1895
|
+
};
|
|
1896
|
+
|
|
1710
1897
|
//#endregion
|
|
1711
1898
|
//#region src/engines/ai-slop/duplicate-imports.ts
|
|
1712
1899
|
const JS_EXTENSIONS$3 = new Set([
|
|
@@ -1717,7 +1904,16 @@ const JS_EXTENSIONS$3 = new Set([
|
|
|
1717
1904
|
".mjs",
|
|
1718
1905
|
".cjs"
|
|
1719
1906
|
]);
|
|
1720
|
-
const IMPORT_FROM_RE$1 = /^\s*import\s+[^;]*?from\s+["']([^"']+)["']/;
|
|
1907
|
+
const IMPORT_FROM_RE$1 = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
|
|
1908
|
+
const TYPE_ONLY_RE = /^\s*type\b/;
|
|
1909
|
+
const VALUE_BINDING_RE = /\{([^}]*)\}/;
|
|
1910
|
+
const isTypeOnly = (clause) => {
|
|
1911
|
+
if (TYPE_ONLY_RE.test(clause)) return true;
|
|
1912
|
+
const braces = VALUE_BINDING_RE.exec(clause);
|
|
1913
|
+
if (!braces) return false;
|
|
1914
|
+
const members = braces[1].split(",").map((member) => member.trim()).filter((member) => member.length > 0);
|
|
1915
|
+
return members.length > 0 && members.every((member) => /^type\b/.test(member));
|
|
1916
|
+
};
|
|
1721
1917
|
const extractImportLines = (content) => {
|
|
1722
1918
|
const lines = content.split("\n");
|
|
1723
1919
|
const results = [];
|
|
@@ -1726,8 +1922,9 @@ const extractImportLines = (content) => {
|
|
|
1726
1922
|
const match = IMPORT_FROM_RE$1.exec(line);
|
|
1727
1923
|
if (!match) continue;
|
|
1728
1924
|
results.push({
|
|
1729
|
-
spec: match[
|
|
1730
|
-
line: i + 1
|
|
1925
|
+
spec: match[2],
|
|
1926
|
+
line: i + 1,
|
|
1927
|
+
typeOnly: isTypeOnly(match[1])
|
|
1731
1928
|
});
|
|
1732
1929
|
}
|
|
1733
1930
|
return results;
|
|
@@ -1746,14 +1943,16 @@ const detectDuplicateImports = async (context) => {
|
|
|
1746
1943
|
}
|
|
1747
1944
|
const imports = extractImportLines(content);
|
|
1748
1945
|
if (imports.length < 2) continue;
|
|
1749
|
-
const
|
|
1946
|
+
const byBucket = /* @__PURE__ */ new Map();
|
|
1750
1947
|
for (const imp of imports) {
|
|
1751
|
-
const
|
|
1948
|
+
const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
1949
|
+
const list = byBucket.get(key) ?? [];
|
|
1752
1950
|
list.push(imp);
|
|
1753
|
-
|
|
1951
|
+
byBucket.set(key, list);
|
|
1754
1952
|
}
|
|
1755
1953
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
1756
|
-
for (const
|
|
1954
|
+
for (const occurrences of byBucket.values()) {
|
|
1955
|
+
const { spec } = occurrences[0];
|
|
1757
1956
|
if (occurrences.length < 2) continue;
|
|
1758
1957
|
for (const dup of occurrences.slice(1)) {
|
|
1759
1958
|
const firstLine = occurrences[0].line;
|
|
@@ -1958,6 +2157,130 @@ const detectGoPatterns = async (context) => {
|
|
|
1958
2157
|
return diagnostics;
|
|
1959
2158
|
};
|
|
1960
2159
|
|
|
2160
|
+
//#endregion
|
|
2161
|
+
//#region src/engines/ai-slop/hardcoded-config.ts
|
|
2162
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
2163
|
+
".ts",
|
|
2164
|
+
".tsx",
|
|
2165
|
+
".js",
|
|
2166
|
+
".jsx",
|
|
2167
|
+
".mjs",
|
|
2168
|
+
".cjs",
|
|
2169
|
+
".py",
|
|
2170
|
+
".go",
|
|
2171
|
+
".rs",
|
|
2172
|
+
".rb",
|
|
2173
|
+
".java",
|
|
2174
|
+
".php"
|
|
2175
|
+
]);
|
|
2176
|
+
const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
|
|
2177
|
+
const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
|
|
2178
|
+
const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
|
|
2179
|
+
const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
|
|
2180
|
+
const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
|
|
2181
|
+
const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
|
|
2182
|
+
const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
|
|
2183
|
+
const PLACEHOLDER_HOSTS = new Set([
|
|
2184
|
+
"example.com",
|
|
2185
|
+
"example.org",
|
|
2186
|
+
"example.net"
|
|
2187
|
+
]);
|
|
2188
|
+
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2189
|
+
const HARDCODED_URL_FINDING = {
|
|
2190
|
+
rule: "ai-slop/hardcoded-url",
|
|
2191
|
+
message: "Hardcoded environment URL in production code",
|
|
2192
|
+
help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
|
|
2193
|
+
};
|
|
2194
|
+
const HARDCODED_ID_FINDING = {
|
|
2195
|
+
rule: "ai-slop/hardcoded-id",
|
|
2196
|
+
message: "Hardcoded provider/project ID in production code",
|
|
2197
|
+
help: "Move provider IDs, tenant IDs, price IDs, and similar deployment-specific identifiers to env/config so agents do not bake one environment into source."
|
|
2198
|
+
};
|
|
2199
|
+
const makeFinding = (filePath, line, spec) => ({
|
|
2200
|
+
filePath,
|
|
2201
|
+
engine: "ai-slop",
|
|
2202
|
+
rule: spec.rule,
|
|
2203
|
+
severity: "warning",
|
|
2204
|
+
message: spec.message,
|
|
2205
|
+
help: spec.help,
|
|
2206
|
+
line,
|
|
2207
|
+
column: 0,
|
|
2208
|
+
category: "AI Slop",
|
|
2209
|
+
fixable: false
|
|
2210
|
+
});
|
|
2211
|
+
const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
|
|
2212
|
+
const commentStartsBefore = (line, index, ext) => {
|
|
2213
|
+
const prefix = line.slice(0, index);
|
|
2214
|
+
if (ext === ".py" || ext === ".rb") return prefix.includes("#");
|
|
2215
|
+
if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
|
|
2216
|
+
return prefix.includes("//") || prefix.includes("/*");
|
|
2217
|
+
};
|
|
2218
|
+
const safeUrlHost = (urlText) => {
|
|
2219
|
+
try {
|
|
2220
|
+
return new URL(urlText).hostname.toLowerCase();
|
|
2221
|
+
} catch {
|
|
2222
|
+
return null;
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
|
|
2226
|
+
const shouldFlagUrlLiteral = (line, urlText) => {
|
|
2227
|
+
if (isEnvBackedLine(line)) return false;
|
|
2228
|
+
const host = safeUrlHost(urlText);
|
|
2229
|
+
if (!host) return false;
|
|
2230
|
+
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
2231
|
+
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
2232
|
+
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
2233
|
+
};
|
|
2234
|
+
const hasUsefulIdShape = (value) => {
|
|
2235
|
+
if (PLACEHOLDER_ID_RE.test(value)) return false;
|
|
2236
|
+
if (/^https?:\/\//i.test(value)) return false;
|
|
2237
|
+
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
2238
|
+
return /[0-9_-]/.test(value);
|
|
2239
|
+
};
|
|
2240
|
+
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
2241
|
+
const diagnostics = [];
|
|
2242
|
+
if (isCommentOnlyLine(line.trim())) return diagnostics;
|
|
2243
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
2244
|
+
let urlMatch;
|
|
2245
|
+
while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
|
|
2246
|
+
const urlText = urlMatch[2];
|
|
2247
|
+
if (commentStartsBefore(line, urlMatch.index, ext)) continue;
|
|
2248
|
+
if (!shouldFlagUrlLiteral(line, urlText)) continue;
|
|
2249
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
|
|
2250
|
+
}
|
|
2251
|
+
if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
|
|
2252
|
+
ID_LITERAL_RE.lastIndex = 0;
|
|
2253
|
+
let idMatch;
|
|
2254
|
+
while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
|
|
2255
|
+
const value = idMatch[2];
|
|
2256
|
+
if (commentStartsBefore(line, idMatch.index, ext)) continue;
|
|
2257
|
+
if (!hasUsefulIdShape(value)) continue;
|
|
2258
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
|
|
2259
|
+
}
|
|
2260
|
+
return diagnostics;
|
|
2261
|
+
};
|
|
2262
|
+
const scanFileForConfigLiterals = (content, relativePath, ext) => {
|
|
2263
|
+
if (!SOURCE_EXTENSIONS.has(ext)) return [];
|
|
2264
|
+
if (isNonProductionPath(relativePath)) return [];
|
|
2265
|
+
return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
|
|
2266
|
+
};
|
|
2267
|
+
const detectHardcodedConfigLiterals = async (context) => {
|
|
2268
|
+
const diagnostics = [];
|
|
2269
|
+
for (const filePath of getSourceFiles(context)) {
|
|
2270
|
+
if (isAutoGenerated(filePath)) continue;
|
|
2271
|
+
let content;
|
|
2272
|
+
try {
|
|
2273
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
2274
|
+
} catch {
|
|
2275
|
+
continue;
|
|
2276
|
+
}
|
|
2277
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2278
|
+
const ext = path.extname(filePath);
|
|
2279
|
+
diagnostics.push(...scanFileForConfigLiterals(content, relativePath, ext));
|
|
2280
|
+
}
|
|
2281
|
+
return diagnostics;
|
|
2282
|
+
};
|
|
2283
|
+
|
|
1961
2284
|
//#endregion
|
|
1962
2285
|
//#region src/engines/ai-slop/js-import-aliases.ts
|
|
1963
2286
|
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
@@ -2225,23 +2548,87 @@ const PYTHON_STDLIB = new Set([
|
|
|
2225
2548
|
"zoneinfo"
|
|
2226
2549
|
]);
|
|
2227
2550
|
const PYTHON_IMPORT_TO_PIP = {
|
|
2228
|
-
yaml: "pyyaml",
|
|
2229
|
-
PIL: "pillow",
|
|
2230
|
-
dateutil: "python-dateutil",
|
|
2231
|
-
cv2:
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2551
|
+
yaml: ["pyyaml"],
|
|
2552
|
+
PIL: ["pillow"],
|
|
2553
|
+
dateutil: ["python-dateutil"],
|
|
2554
|
+
cv2: [
|
|
2555
|
+
"opencv-python",
|
|
2556
|
+
"opencv-python-headless",
|
|
2557
|
+
"opencv-contrib-python"
|
|
2558
|
+
],
|
|
2559
|
+
sklearn: ["scikit-learn"],
|
|
2560
|
+
bs4: ["beautifulsoup4"],
|
|
2561
|
+
typing_extensions: ["typing-extensions"],
|
|
2562
|
+
dotenv: ["python-dotenv"],
|
|
2563
|
+
genai: ["google-genai"],
|
|
2564
|
+
google: [
|
|
2565
|
+
"google-genai",
|
|
2566
|
+
"google-generativeai",
|
|
2567
|
+
"google-api-python-client",
|
|
2568
|
+
"google-cloud-storage",
|
|
2569
|
+
"google-cloud-aiplatform",
|
|
2570
|
+
"google-auth",
|
|
2571
|
+
"protobuf"
|
|
2572
|
+
],
|
|
2573
|
+
jose: ["python-jose"],
|
|
2574
|
+
jwt: ["pyjwt"],
|
|
2575
|
+
OpenSSL: ["pyopenssl"],
|
|
2576
|
+
Crypto: ["pycryptodome", "pycryptodomex"],
|
|
2577
|
+
Cryptodome: ["pycryptodomex", "pycryptodome"],
|
|
2578
|
+
magic: ["python-magic"],
|
|
2579
|
+
docx: ["python-docx"],
|
|
2580
|
+
pptx: ["python-pptx"],
|
|
2581
|
+
git: ["gitpython"],
|
|
2582
|
+
socks: ["pysocks"],
|
|
2583
|
+
redis: ["redis"],
|
|
2584
|
+
cairo: ["pycairo"],
|
|
2585
|
+
serial: ["pyserial"],
|
|
2586
|
+
usb: ["pyusb"],
|
|
2587
|
+
gi: ["pygobject"],
|
|
2588
|
+
Xlib: ["python-xlib"],
|
|
2589
|
+
ldap: ["python-ldap"],
|
|
2590
|
+
slugify: ["python-slugify"],
|
|
2591
|
+
memcache: ["python-memcached"],
|
|
2592
|
+
dns: ["dnspython"],
|
|
2593
|
+
attr: ["attrs"],
|
|
2594
|
+
attrs: ["attrs"],
|
|
2595
|
+
zoneinfo_data: ["tzdata"],
|
|
2596
|
+
pkg_resources: ["setuptools"],
|
|
2597
|
+
setuptools: ["setuptools"],
|
|
2598
|
+
wx: ["wxpython"],
|
|
2599
|
+
skimage: ["scikit-image"],
|
|
2600
|
+
OpenGL: ["pyopengl"],
|
|
2601
|
+
win32api: ["pywin32"],
|
|
2602
|
+
win32con: ["pywin32"],
|
|
2603
|
+
win32com: ["pywin32"],
|
|
2604
|
+
pythoncom: ["pywin32"],
|
|
2605
|
+
pywintypes: ["pywin32"],
|
|
2606
|
+
rest_framework: ["djangorestframework"],
|
|
2607
|
+
allauth: ["django-allauth"],
|
|
2608
|
+
corsheaders: ["django-cors-headers"],
|
|
2609
|
+
debug_toolbar: ["django-debug-toolbar"],
|
|
2610
|
+
environ: ["django-environ"],
|
|
2611
|
+
flask_cors: ["flask-cors"],
|
|
2612
|
+
flask_sqlalchemy: ["flask-sqlalchemy"],
|
|
2613
|
+
flask_migrate: ["flask-migrate"],
|
|
2614
|
+
flask_login: ["flask-login"],
|
|
2615
|
+
jwt_extended: ["flask-jwt-extended"],
|
|
2616
|
+
dateparser: ["dateparser"],
|
|
2617
|
+
yaml_include: ["pyyaml-include"],
|
|
2618
|
+
lxml_html_clean: ["lxml-html-clean"],
|
|
2619
|
+
grpc: ["grpcio"],
|
|
2620
|
+
grpc_status: ["grpcio-status"],
|
|
2621
|
+
google_crc32c: ["google-crc32c"],
|
|
2622
|
+
pkg_about: ["pkg-about"],
|
|
2623
|
+
mpl_toolkits: ["matplotlib"],
|
|
2624
|
+
dotmap: ["dotmap"],
|
|
2625
|
+
pydantic_settings: ["pydantic-settings"],
|
|
2626
|
+
telegram: ["python-telegram-bot"],
|
|
2627
|
+
discord: ["discord-py"],
|
|
2628
|
+
nacl: ["pynacl"],
|
|
2629
|
+
jwcrypto: ["jwcrypto"],
|
|
2630
|
+
humanfriendly: ["humanfriendly"],
|
|
2631
|
+
multipart: ["python-multipart"]
|
|
2245
2632
|
};
|
|
2246
2633
|
|
|
2247
2634
|
//#endregion
|
|
@@ -2280,6 +2667,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
|
|
|
2280
2667
|
const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
|
|
2281
2668
|
if (m) addPyDep(pyDeps, m[1]);
|
|
2282
2669
|
}
|
|
2670
|
+
const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
|
|
2671
|
+
if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
2283
2672
|
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
2284
2673
|
let match = poetryRe.exec(content);
|
|
2285
2674
|
while (match !== null) {
|
|
@@ -2478,6 +2867,11 @@ const packageNameFromImport = (spec) => {
|
|
|
2478
2867
|
}
|
|
2479
2868
|
return spec.split("/")[0];
|
|
2480
2869
|
};
|
|
2870
|
+
const typesPackageName = (pkg) => {
|
|
2871
|
+
if (pkg.startsWith("@types/")) return pkg;
|
|
2872
|
+
if (pkg.startsWith("@")) return `@types/${pkg.slice(1).replace("/", "__")}`;
|
|
2873
|
+
return `@types/${pkg}`;
|
|
2874
|
+
};
|
|
2481
2875
|
const STATIC_IMPORT_RE = /^\s*import\s+(?:[\w*{},\s]+\s+from\s+)?["']([^"']+)["']/;
|
|
2482
2876
|
const DYNAMIC_IMPORT_RE = /(?:import|require)\s*\(\s*["']([^"']+)["']/g;
|
|
2483
2877
|
const extractJsImports = (content) => {
|
|
@@ -2542,15 +2936,16 @@ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
|
|
|
2542
2936
|
const realPkg = pkg.slice(7);
|
|
2543
2937
|
if (manifest.jsDeps.has(realPkg)) return null;
|
|
2544
2938
|
}
|
|
2939
|
+
if (manifest.jsDeps.has(typesPackageName(pkg))) return null;
|
|
2545
2940
|
return pkg;
|
|
2546
2941
|
};
|
|
2942
|
+
const normalizePyName = (name) => name.toLowerCase().replace(/_/g, "-");
|
|
2547
2943
|
const checkPyImport = (spec, manifest) => {
|
|
2548
2944
|
const root = spec.split(".")[0];
|
|
2549
2945
|
if (PYTHON_STDLIB.has(root)) return null;
|
|
2550
|
-
const normalized = root
|
|
2946
|
+
const normalized = normalizePyName(root);
|
|
2551
2947
|
if (manifest.pyDeps.has(normalized)) return null;
|
|
2552
|
-
|
|
2553
|
-
if (pipName && manifest.pyDeps.has(pipName)) return null;
|
|
2948
|
+
if ((PYTHON_IMPORT_TO_PIP[root] ?? PYTHON_IMPORT_TO_PIP[normalized])?.some((dist) => manifest.pyDeps.has(normalizePyName(dist)))) return null;
|
|
2554
2949
|
return root;
|
|
2555
2950
|
};
|
|
2556
2951
|
const detectHallucinatedImports = async (context) => {
|
|
@@ -2700,6 +3095,85 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
2700
3095
|
return blocks;
|
|
2701
3096
|
};
|
|
2702
3097
|
|
|
3098
|
+
//#endregion
|
|
3099
|
+
//#region src/engines/ai-slop/meta-comment.ts
|
|
3100
|
+
const PLAN_REFERENCE_RES = [
|
|
3101
|
+
/\b(?:stage|step|phase)\s+\d+\b(?!\s*[:.]?\s*(?:bytes|ms|seconds|of\s+\d))/i,
|
|
3102
|
+
/\bstep\s+\d+\s+of\s+the\s+plan\b/i,
|
|
3103
|
+
/\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
|
|
3104
|
+
/\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
|
|
3105
|
+
/\bfrom\s+the\s+(?:task|todo|plan|spec|ticket|prompt|requirements?)\b/i,
|
|
3106
|
+
/\bimplement(?:ing|s|ed)?\s+use\s*case\s+\d*/i,
|
|
3107
|
+
/\b(?:requirements?\s+doc|requirement\s+\d+)\b/i,
|
|
3108
|
+
/\bas\s+(?:instructed|specified|outlined)\s+(?:above|below|in\s+the)\b/i
|
|
3109
|
+
];
|
|
3110
|
+
const BEFORE_AFTER_RES = [
|
|
3111
|
+
/\bpreviously[,:]?\s+(?:this|we|it|the)\b/i,
|
|
3112
|
+
/\bused\s+to\s+(?:be|use|call|return|do|have|rely)\b/i,
|
|
3113
|
+
/\bchanged\s+(?:\w+\s+){0,3}from\s+.+\bto\b/i,
|
|
3114
|
+
/\bno\s+longer\s+(?:needed|used|required|necessary|calls?|returns?|does)\b/i,
|
|
3115
|
+
/\bthis\s+was\s+.+\bbut\s+now\b/i,
|
|
3116
|
+
/\bwe\s+(?:now|used\s+to)\s+(?:no\s+longer\s+)?(?:use|call|return|do|have)\b/i,
|
|
3117
|
+
/\breplaced\s+the\s+(?:old|previous|former)\b/i,
|
|
3118
|
+
/\b(?:was|were)\s+(?:renamed|moved|removed|refactored|extracted)\s+(?:from|to|out\s+of)\b/i
|
|
3119
|
+
];
|
|
3120
|
+
const WHY_OR_TODO_RE = /\b(?:because|since|otherwise|todo|fixme|hack|note:|reason:|workaround|see\s+(?:issue|#))\b/i;
|
|
3121
|
+
const looksLikeLicenseHeader$1 = (block) => {
|
|
3122
|
+
if (block.startLine !== 1) return false;
|
|
3123
|
+
const text = block.rawLines.join(" ").toLowerCase();
|
|
3124
|
+
return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
|
|
3125
|
+
};
|
|
3126
|
+
const looksLikeSuppressDirective$1 = (block) => block.rawLines.some((line) => /\b(?:biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(line));
|
|
3127
|
+
const matchMetaSignal = (block) => {
|
|
3128
|
+
if (looksLikeLicenseHeader$1(block)) return null;
|
|
3129
|
+
if (looksLikeSuppressDirective$1(block)) return null;
|
|
3130
|
+
if (block.kind === "jsdoc" && block.hasMeaningfulJsdocTag) return null;
|
|
3131
|
+
if (block.isRustDoc) return null;
|
|
3132
|
+
const joined = block.prose.join(" ");
|
|
3133
|
+
if (joined.trim().length === 0) return null;
|
|
3134
|
+
if (WHY_OR_TODO_RE.test(joined)) return null;
|
|
3135
|
+
if (PLAN_REFERENCE_RES.some((re) => re.test(joined))) return "plan/process reference";
|
|
3136
|
+
if (BEFORE_AFTER_RES.some((re) => re.test(joined))) return "before/after state narration";
|
|
3137
|
+
return null;
|
|
3138
|
+
};
|
|
3139
|
+
const detectMetaComments = async (context) => {
|
|
3140
|
+
const files = getSourceFiles(context);
|
|
3141
|
+
const diagnostics = [];
|
|
3142
|
+
for (const filePath of files) {
|
|
3143
|
+
const ext = path.extname(filePath);
|
|
3144
|
+
if (!SUPPORTED_EXTS.has(ext)) continue;
|
|
3145
|
+
if (isAutoGenerated(filePath)) continue;
|
|
3146
|
+
const syntax = getCommentSyntax(ext);
|
|
3147
|
+
if (!syntax) continue;
|
|
3148
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
3149
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
3150
|
+
let content;
|
|
3151
|
+
try {
|
|
3152
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
3153
|
+
} catch {
|
|
3154
|
+
continue;
|
|
3155
|
+
}
|
|
3156
|
+
const blocks = collectBlocks(content.split("\n"), syntax);
|
|
3157
|
+
for (const block of blocks) {
|
|
3158
|
+
const reason = matchMetaSignal(block);
|
|
3159
|
+
if (!reason) continue;
|
|
3160
|
+
diagnostics.push({
|
|
3161
|
+
filePath: relativePath,
|
|
3162
|
+
engine: "ai-slop",
|
|
3163
|
+
rule: "ai-slop/meta-comment",
|
|
3164
|
+
severity: "warning",
|
|
3165
|
+
message: `Meta/plan comment (${reason})`,
|
|
3166
|
+
help: "Remove — references to the build plan or before/after code state belong in PR descriptions and commit messages, not source.",
|
|
3167
|
+
line: block.startLine,
|
|
3168
|
+
column: 0,
|
|
3169
|
+
category: "Comments",
|
|
3170
|
+
fixable: false
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
return diagnostics;
|
|
3175
|
+
};
|
|
3176
|
+
|
|
2703
3177
|
//#endregion
|
|
2704
3178
|
//#region src/engines/ai-slop/narrative-comments.ts
|
|
2705
3179
|
const looksLikeDeclarationPreamble = (nextLine, ext) => {
|
|
@@ -3308,6 +3782,143 @@ const detectRustPatterns = async (context) => {
|
|
|
3308
3782
|
return diagnostics;
|
|
3309
3783
|
};
|
|
3310
3784
|
|
|
3785
|
+
//#endregion
|
|
3786
|
+
//#region src/engines/ai-slop/silent-recovery.ts
|
|
3787
|
+
const JS_EXTS$1 = new Set([
|
|
3788
|
+
".ts",
|
|
3789
|
+
".tsx",
|
|
3790
|
+
".js",
|
|
3791
|
+
".jsx",
|
|
3792
|
+
".mjs",
|
|
3793
|
+
".cjs"
|
|
3794
|
+
]);
|
|
3795
|
+
const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
|
|
3796
|
+
const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
|
|
3797
|
+
const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
|
|
3798
|
+
const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
3799
|
+
const extractCatchBody = (content, openBraceIndex) => {
|
|
3800
|
+
let depth = 0;
|
|
3801
|
+
let inString = null;
|
|
3802
|
+
for (let i = openBraceIndex; i < content.length; i += 1) {
|
|
3803
|
+
const ch = content[i];
|
|
3804
|
+
const prev = content[i - 1];
|
|
3805
|
+
if (inString) {
|
|
3806
|
+
if (ch === inString && prev !== "\\") inString = null;
|
|
3807
|
+
continue;
|
|
3808
|
+
}
|
|
3809
|
+
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
3810
|
+
inString = ch;
|
|
3811
|
+
continue;
|
|
3812
|
+
}
|
|
3813
|
+
if (ch === "{") depth += 1;
|
|
3814
|
+
else if (ch === "}") {
|
|
3815
|
+
depth -= 1;
|
|
3816
|
+
if (depth === 0) return content.slice(openBraceIndex + 1, i);
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
return null;
|
|
3820
|
+
};
|
|
3821
|
+
const isLogOnlyBody = (body) => {
|
|
3822
|
+
const statements = stripBlockComments(body).split("\n").map((line) => line.replace(/\/\/.*$/, "").trim()).filter((line) => line.length > 0 && line !== ";");
|
|
3823
|
+
if (statements.length === 0) return false;
|
|
3824
|
+
if (statements.some((line) => HANDLING_TOKEN_RE.test(line))) return false;
|
|
3825
|
+
let sawLog = false;
|
|
3826
|
+
for (const statement of statements) {
|
|
3827
|
+
const normalized = statement.replace(/;+$/, "");
|
|
3828
|
+
if (LOG_STATEMENT_RE.test(normalized)) {
|
|
3829
|
+
sawLog = true;
|
|
3830
|
+
continue;
|
|
3831
|
+
}
|
|
3832
|
+
if (/^[\w$"'`{[(),.\s+:-]+$/.test(normalized) && !/[=(]\s*(?:async\s+)?\(/.test(normalized)) continue;
|
|
3833
|
+
return false;
|
|
3834
|
+
}
|
|
3835
|
+
return sawLog;
|
|
3836
|
+
};
|
|
3837
|
+
const detectJsSilentRecovery = (content, relPath) => {
|
|
3838
|
+
const out = [];
|
|
3839
|
+
CATCH_HEAD_RE.lastIndex = 0;
|
|
3840
|
+
let match;
|
|
3841
|
+
while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
|
|
3842
|
+
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3843
|
+
if (body === null) continue;
|
|
3844
|
+
if (!isLogOnlyBody(body)) continue;
|
|
3845
|
+
const line = content.slice(0, match.index).split("\n").length;
|
|
3846
|
+
out.push({
|
|
3847
|
+
filePath: relPath,
|
|
3848
|
+
engine: "ai-slop",
|
|
3849
|
+
rule: "ai-slop/silent-recovery",
|
|
3850
|
+
severity: "warning",
|
|
3851
|
+
message: "Catch only logs then continues, leaving execution in a possibly broken state",
|
|
3852
|
+
help: "Handle the error: rethrow, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
|
|
3853
|
+
line,
|
|
3854
|
+
column: 0,
|
|
3855
|
+
category: "AI Slop",
|
|
3856
|
+
fixable: false
|
|
3857
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
return out;
|
|
3860
|
+
};
|
|
3861
|
+
const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
|
|
3862
|
+
const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
|
|
3863
|
+
const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
|
|
3864
|
+
const detectPySilentRecovery = (content, relPath) => {
|
|
3865
|
+
const out = [];
|
|
3866
|
+
const lines = content.split("\n");
|
|
3867
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
3868
|
+
const exceptMatch = PY_EXCEPT_RE.exec(lines[i]);
|
|
3869
|
+
if (!exceptMatch) continue;
|
|
3870
|
+
const indent = exceptMatch[1].length;
|
|
3871
|
+
const bodyLines = [];
|
|
3872
|
+
let j = i + 1;
|
|
3873
|
+
for (; j < lines.length; j += 1) {
|
|
3874
|
+
const raw = lines[j];
|
|
3875
|
+
if (raw.trim() === "") continue;
|
|
3876
|
+
if (raw.length - raw.trimStart().length <= indent) break;
|
|
3877
|
+
bodyLines.push(raw.trim());
|
|
3878
|
+
}
|
|
3879
|
+
if (bodyLines.length === 0) continue;
|
|
3880
|
+
if (bodyLines.some((line) => PY_HANDLING_TOKEN_RE.test(line))) continue;
|
|
3881
|
+
if (bodyLines.some((line) => line === "pass")) continue;
|
|
3882
|
+
const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
|
|
3883
|
+
const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
|
|
3884
|
+
if (!allLogs || !sawLog) continue;
|
|
3885
|
+
out.push({
|
|
3886
|
+
filePath: relPath,
|
|
3887
|
+
engine: "ai-slop",
|
|
3888
|
+
rule: "ai-slop/silent-recovery",
|
|
3889
|
+
severity: "warning",
|
|
3890
|
+
message: "except only logs then continues, leaving execution in a possibly broken state",
|
|
3891
|
+
help: "Handle the error: re-raise, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
|
|
3892
|
+
line: i + 1,
|
|
3893
|
+
column: 0,
|
|
3894
|
+
category: "AI Slop",
|
|
3895
|
+
fixable: false
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
return out;
|
|
3899
|
+
};
|
|
3900
|
+
const detectSilentRecovery = async (context) => {
|
|
3901
|
+
const files = getSourceFiles(context);
|
|
3902
|
+
const diagnostics = [];
|
|
3903
|
+
for (const filePath of files) {
|
|
3904
|
+
if (isAutoGenerated(filePath)) continue;
|
|
3905
|
+
const ext = path.extname(filePath);
|
|
3906
|
+
const isJs = JS_EXTS$1.has(ext);
|
|
3907
|
+
if (!isJs && !(ext === ".py")) continue;
|
|
3908
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
3909
|
+
if (isNonProductionPath(relPath)) continue;
|
|
3910
|
+
let content;
|
|
3911
|
+
try {
|
|
3912
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
3913
|
+
} catch {
|
|
3914
|
+
continue;
|
|
3915
|
+
}
|
|
3916
|
+
if (isJs) diagnostics.push(...detectJsSilentRecovery(content, relPath));
|
|
3917
|
+
else diagnostics.push(...detectPySilentRecovery(content, relPath));
|
|
3918
|
+
}
|
|
3919
|
+
return diagnostics;
|
|
3920
|
+
};
|
|
3921
|
+
|
|
3311
3922
|
//#endregion
|
|
3312
3923
|
//#region src/engines/ai-slop/unused-imports.ts
|
|
3313
3924
|
const JS_EXTENSIONS$1 = new Set([
|
|
@@ -3497,15 +4108,19 @@ const aiSlopEngine = {
|
|
|
3497
4108
|
const results = await Promise.allSettled([
|
|
3498
4109
|
detectTrivialComments(context),
|
|
3499
4110
|
detectSwallowedExceptions(context),
|
|
4111
|
+
detectDefensivePatterns(context),
|
|
3500
4112
|
detectOverAbstraction(context),
|
|
3501
4113
|
detectDeadPatterns(context),
|
|
3502
4114
|
detectUnusedImports(context),
|
|
3503
4115
|
detectNarrativeComments(context),
|
|
3504
4116
|
detectDuplicateImports(context),
|
|
4117
|
+
detectHardcodedConfigLiterals(context),
|
|
3505
4118
|
detectPythonPatterns(context),
|
|
3506
4119
|
detectGoPatterns(context),
|
|
3507
4120
|
detectRustPatterns(context),
|
|
3508
|
-
detectHallucinatedImports(context)
|
|
4121
|
+
detectHallucinatedImports(context),
|
|
4122
|
+
detectSilentRecovery(context),
|
|
4123
|
+
detectMetaComments(context)
|
|
3509
4124
|
]);
|
|
3510
4125
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
3511
4126
|
return {
|
|
@@ -5641,9 +6256,9 @@ const lintEngine = {
|
|
|
5641
6256
|
const promises = [];
|
|
5642
6257
|
if (languages.includes("typescript") || languages.includes("javascript")) {
|
|
5643
6258
|
promises.push(runOxlint(context));
|
|
5644
|
-
if (context.config.lint.typecheck) promises.push(import("./typecheck-
|
|
6259
|
+
if (context.config.lint.typecheck) promises.push(import("./typecheck-DQSzG8fX.js").then((mod) => mod.runTypecheck(context)));
|
|
5645
6260
|
}
|
|
5646
|
-
if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-
|
|
6261
|
+
if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-BcIkOte5.js").then((n) => n.t).then((mod) => mod.runExpoDoctor(context)));
|
|
5647
6262
|
if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
|
|
5648
6263
|
if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
|
|
5649
6264
|
if (languages.includes("rust") && installedTools["cargo"]) promises.push(runGenericLinter(context, "rust"));
|
|
@@ -6194,7 +6809,7 @@ const RISKY_PATTERNS = [
|
|
|
6194
6809
|
help: "Use JSON, MessagePack, or other safe serialization formats for untrusted data"
|
|
6195
6810
|
},
|
|
6196
6811
|
{
|
|
6197
|
-
pattern: new RegExp(
|
|
6812
|
+
pattern: new RegExp(`(?<![\\w.>:\\\\])\\bexec\\s*\\(`, "g"),
|
|
6198
6813
|
extensions: [".py"],
|
|
6199
6814
|
name: "python-exec",
|
|
6200
6815
|
message: "Use of exec() can execute arbitrary code",
|
|
@@ -6762,8 +7377,8 @@ const redactProperties = (props) => {
|
|
|
6762
7377
|
|
|
6763
7378
|
//#endregion
|
|
6764
7379
|
//#region src/telemetry/client.ts
|
|
6765
|
-
const POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
6766
|
-
const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
7380
|
+
const POSTHOG_HOST = process.env.AISLOP_POSTHOG_HOST ?? "https://eu.i.posthog.com";
|
|
7381
|
+
const POSTHOG_KEY = process.env.AISLOP_POSTHOG_KEY ?? "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
6767
7382
|
const SCHEMA_VERSION = "v2";
|
|
6768
7383
|
const REQUEST_TIMEOUT_MS = 3e3;
|
|
6769
7384
|
const isTelemetryDisabled = (config) => {
|
|
@@ -7102,6 +7717,30 @@ const printEngineStatus = (result) => {
|
|
|
7102
7717
|
}
|
|
7103
7718
|
};
|
|
7104
7719
|
|
|
7720
|
+
//#endregion
|
|
7721
|
+
//#region src/scoring/rule-severity.ts
|
|
7722
|
+
/**
|
|
7723
|
+
* Apply per-rule severity overrides from config: "off" drops the diagnostic,
|
|
7724
|
+
* "error"/"warning" rewrite its severity before scoring and rendering.
|
|
7725
|
+
*/
|
|
7726
|
+
const applyRuleSeverities = (diagnostics, overrides) => {
|
|
7727
|
+
if (Object.keys(overrides).length === 0) return diagnostics;
|
|
7728
|
+
const result = [];
|
|
7729
|
+
for (const diagnostic of diagnostics) {
|
|
7730
|
+
const override = overrides[diagnostic.rule];
|
|
7731
|
+
if (!override) {
|
|
7732
|
+
result.push(diagnostic);
|
|
7733
|
+
continue;
|
|
7734
|
+
}
|
|
7735
|
+
if (override === "off") continue;
|
|
7736
|
+
result.push(override === diagnostic.severity ? diagnostic : {
|
|
7737
|
+
...diagnostic,
|
|
7738
|
+
severity: override
|
|
7739
|
+
});
|
|
7740
|
+
}
|
|
7741
|
+
return result;
|
|
7742
|
+
};
|
|
7743
|
+
|
|
7105
7744
|
//#endregion
|
|
7106
7745
|
//#region src/ui/live-grid.ts
|
|
7107
7746
|
const SPINNER = [
|
|
@@ -7238,6 +7877,11 @@ const RULE_LABELS = {
|
|
|
7238
7877
|
"knip/types": "Unused type",
|
|
7239
7878
|
"ai-slop/trivial-comment": "Trivial restating comment",
|
|
7240
7879
|
"ai-slop/swallowed-exception": "Empty catch (swallowed error)",
|
|
7880
|
+
"ai-slop/silent-recovery": "Catch logs then continues",
|
|
7881
|
+
"ai-slop/meta-comment": "Meta/plan comment",
|
|
7882
|
+
"ai-slop/redundant-try-catch": "Redundant try/catch",
|
|
7883
|
+
"ai-slop/redundant-type-coercion": "Redundant type coercion",
|
|
7884
|
+
"ai-slop/duplicate-type-declaration": "Duplicate exported type",
|
|
7241
7885
|
"ai-slop/thin-wrapper": "Thin function wrapper",
|
|
7242
7886
|
"ai-slop/generic-naming": "Generic/vague identifier name",
|
|
7243
7887
|
"ai-slop/unused-import": "Unused import",
|
|
@@ -7251,6 +7895,8 @@ const RULE_LABELS = {
|
|
|
7251
7895
|
"ai-slop/ts-directive": "@ts-ignore / @ts-expect-error",
|
|
7252
7896
|
"ai-slop/narrative-comment": "Narrative comment block",
|
|
7253
7897
|
"ai-slop/duplicate-import": "Duplicate import statement",
|
|
7898
|
+
"ai-slop/hardcoded-url": "Hardcoded URL",
|
|
7899
|
+
"ai-slop/hardcoded-id": "Hardcoded provider ID",
|
|
7254
7900
|
"ai-slop/python-bare-except": "Bare except",
|
|
7255
7901
|
"ai-slop/python-broad-except": "Broad except",
|
|
7256
7902
|
"ai-slop/python-mutable-default": "Mutable default argument",
|
|
@@ -7423,8 +8069,35 @@ const getStagedFiles = (cwd) => {
|
|
|
7423
8069
|
return result.stdout.split("\n").filter((f) => f.length > 0).map((f) => path.resolve(cwd, f));
|
|
7424
8070
|
};
|
|
7425
8071
|
|
|
8072
|
+
//#endregion
|
|
8073
|
+
//#region src/utils/history.ts
|
|
8074
|
+
const HISTORY_FILE = "history.jsonl";
|
|
8075
|
+
const isHistoryDisabled = (env = process.env) => env.AISLOP_NO_HISTORY === "1";
|
|
8076
|
+
const historyPath = (directory) => path.join(path.resolve(directory), CONFIG_DIR, HISTORY_FILE);
|
|
8077
|
+
/**
|
|
8078
|
+
* Append a compact scan record to .aislop/history.jsonl. Best-effort: never
|
|
8079
|
+
* throws, so a read-only checkout or missing config dir can't break a scan.
|
|
8080
|
+
*/
|
|
8081
|
+
const appendHistory = (input) => {
|
|
8082
|
+
if (isHistoryDisabled()) return;
|
|
8083
|
+
const configDir = path.join(path.resolve(input.directory), CONFIG_DIR);
|
|
8084
|
+
if (!fs.existsSync(configDir)) return;
|
|
8085
|
+
const record = {
|
|
8086
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8087
|
+
score: input.score,
|
|
8088
|
+
errors: input.errors,
|
|
8089
|
+
warnings: input.warnings,
|
|
8090
|
+
files: input.files,
|
|
8091
|
+
cliVersion: APP_VERSION
|
|
8092
|
+
};
|
|
8093
|
+
try {
|
|
8094
|
+
fs.appendFileSync(historyPath(input.directory), `${JSON.stringify(record)}\n`);
|
|
8095
|
+
} catch {}
|
|
8096
|
+
};
|
|
8097
|
+
|
|
7426
8098
|
//#endregion
|
|
7427
8099
|
//#region src/commands/scan.ts
|
|
8100
|
+
const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
|
|
7428
8101
|
const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
|
|
7429
8102
|
const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
|
|
7430
8103
|
const BREAKDOWN_TOP_N = 10;
|
|
@@ -7539,17 +8212,18 @@ const scanCommand = async (directory, config, options) => {
|
|
|
7539
8212
|
const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
7540
8213
|
const startTime = performance.now();
|
|
7541
8214
|
const showHeader = options.showHeader !== false;
|
|
7542
|
-
const
|
|
8215
|
+
const machineOutput = isMachineOutput(options);
|
|
8216
|
+
const useLiveProgress = !machineOutput && shouldUseSpinner();
|
|
7543
8217
|
let files;
|
|
7544
8218
|
if (options.staged) {
|
|
7545
8219
|
files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], config.exclude);
|
|
7546
|
-
if (!
|
|
8220
|
+
if (!machineOutput) log.muted(`Scope: ${files.length} staged file(s)`);
|
|
7547
8221
|
} else if (options.changes) {
|
|
7548
8222
|
files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [], config.exclude);
|
|
7549
|
-
if (!
|
|
8223
|
+
if (!machineOutput) log.muted(`Scope: ${files.length} changed file(s)`);
|
|
7550
8224
|
} else {
|
|
7551
8225
|
files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [], config.exclude);
|
|
7552
|
-
if (!
|
|
8226
|
+
if (!machineOutput) log.muted(`Scope: ${files.length} file(s) after exclusions`);
|
|
7553
8227
|
}
|
|
7554
8228
|
const configDir = findConfigDir(resolvedDir);
|
|
7555
8229
|
const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
|
|
@@ -7566,7 +8240,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
7566
8240
|
}));
|
|
7567
8241
|
const progressRenderer = useLiveProgress ? new LiveGrid(gridRows) : null;
|
|
7568
8242
|
progressRenderer?.start();
|
|
7569
|
-
const
|
|
8243
|
+
const rawResults = await runEngines({
|
|
7570
8244
|
rootDirectory: resolvedDir,
|
|
7571
8245
|
languages: projectInfo.languages,
|
|
7572
8246
|
frameworks: projectInfo.frameworks,
|
|
@@ -7599,9 +8273,13 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
7599
8273
|
elapsedMs: result.elapsed
|
|
7600
8274
|
});
|
|
7601
8275
|
}
|
|
7602
|
-
if (!
|
|
8276
|
+
if (!machineOutput && !progressRenderer) printEngineStatus(result);
|
|
7603
8277
|
});
|
|
7604
8278
|
progressRenderer?.stop();
|
|
8279
|
+
const results = rawResults.map((result) => ({
|
|
8280
|
+
...result,
|
|
8281
|
+
diagnostics: applyRuleSeverities(result.diagnostics, config.rules)
|
|
8282
|
+
}));
|
|
7605
8283
|
const allDiagnostics = results.flatMap((r) => r.diagnostics);
|
|
7606
8284
|
const elapsedMs = performance.now() - startTime;
|
|
7607
8285
|
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
@@ -7622,12 +8300,24 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
7622
8300
|
engineIssues,
|
|
7623
8301
|
engineTimings
|
|
7624
8302
|
};
|
|
8303
|
+
if (options.sarif) {
|
|
8304
|
+
const { buildSarifLog } = await import("./sarif-Cneulb6L.js");
|
|
8305
|
+
console.log(JSON.stringify(buildSarifLog(results), null, 2));
|
|
8306
|
+
return completion;
|
|
8307
|
+
}
|
|
7625
8308
|
if (options.json) {
|
|
7626
|
-
const { buildJsonOutput } = await import("./json-
|
|
8309
|
+
const { buildJsonOutput } = await import("./json-CZU3lEfE.js");
|
|
7627
8310
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
7628
8311
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
7629
8312
|
return completion;
|
|
7630
8313
|
}
|
|
8314
|
+
if (!options.staged && !options.changes && options.command !== "ci" && !isCiEnv()) appendHistory({
|
|
8315
|
+
directory: resolvedDir,
|
|
8316
|
+
score: scoreResult.score,
|
|
8317
|
+
errors: completion.errorCount,
|
|
8318
|
+
warnings: completion.warningCount,
|
|
8319
|
+
files: projectInfo.sourceFileCount
|
|
8320
|
+
});
|
|
7631
8321
|
const projectName = projectInfo.projectName ?? "project";
|
|
7632
8322
|
const language = projectInfo.languages[0] ?? "unknown";
|
|
7633
8323
|
process.stdout.write(buildScanRender({
|
|
@@ -7707,6 +8397,16 @@ const AGENT_CONFIGS = {
|
|
|
7707
8397
|
bin: "goose",
|
|
7708
8398
|
args: (p) => ["run", p]
|
|
7709
8399
|
},
|
|
8400
|
+
pi: {
|
|
8401
|
+
type: "cli",
|
|
8402
|
+
bin: "pi",
|
|
8403
|
+
args: (p) => ["-p", p]
|
|
8404
|
+
},
|
|
8405
|
+
crush: {
|
|
8406
|
+
type: "cli",
|
|
8407
|
+
bin: "crush",
|
|
8408
|
+
args: (p) => ["run", p]
|
|
8409
|
+
},
|
|
7710
8410
|
cursor: {
|
|
7711
8411
|
type: "editor",
|
|
7712
8412
|
bin: "cursor"
|
|
@@ -8905,7 +9605,7 @@ const runLintSteps = async (deps) => {
|
|
|
8905
9605
|
if (hasJsOrTs(deps.projectInfo)) await deps.runStep("Lint fixes (js/ts)", () => runOxlint(deps.context), () => fixOxlint(deps.context, { force: deps.force }));
|
|
8906
9606
|
if (deps.projectInfo.languages.includes("python") && deps.projectInfo.installedTools.ruff) await deps.runStep("Lint fixes (python)", () => runRuffLint(deps.context), () => deps.force ? fixRuffLintForce(deps.resolvedDir) : fixRuffLint(deps.resolvedDir));
|
|
8907
9607
|
else if (deps.projectInfo.languages.includes("python")) log.warn("Python detected but ruff is not installed; skipping Python lint fixes.");
|
|
8908
|
-
if (deps.projectInfo.languages.includes("ruby") && deps.projectInfo.installedTools.rubocop) await deps.runStep("Lint fixes (ruby)", () => import("./generic-
|
|
9608
|
+
if (deps.projectInfo.languages.includes("ruby") && deps.projectInfo.installedTools.rubocop) await deps.runStep("Lint fixes (ruby)", () => import("./generic-D_T4cUaC.js").then((n) => n.n).then((mod) => mod.runGenericLinter(deps.context, "ruby")), () => fixRubyLint(deps.resolvedDir));
|
|
8909
9609
|
else if (deps.projectInfo.languages.includes("ruby")) log.warn("Ruby detected but rubocop is not installed; skipping Ruby lint fixes.");
|
|
8910
9610
|
};
|
|
8911
9611
|
const runDependencyStep = async (deps) => {
|
|
@@ -9431,6 +10131,11 @@ const BUILTIN_RULES = [
|
|
|
9431
10131
|
rules: [
|
|
9432
10132
|
"ai-slop/trivial-comment",
|
|
9433
10133
|
"ai-slop/swallowed-exception",
|
|
10134
|
+
"ai-slop/silent-recovery",
|
|
10135
|
+
"ai-slop/meta-comment",
|
|
10136
|
+
"ai-slop/redundant-try-catch",
|
|
10137
|
+
"ai-slop/redundant-type-coercion",
|
|
10138
|
+
"ai-slop/duplicate-type-declaration",
|
|
9434
10139
|
"ai-slop/thin-wrapper",
|
|
9435
10140
|
"ai-slop/generic-naming",
|
|
9436
10141
|
"ai-slop/unused-import",
|
|
@@ -9444,6 +10149,8 @@ const BUILTIN_RULES = [
|
|
|
9444
10149
|
"ai-slop/ts-directive",
|
|
9445
10150
|
"ai-slop/narrative-comment",
|
|
9446
10151
|
"ai-slop/duplicate-import",
|
|
10152
|
+
"ai-slop/hardcoded-url",
|
|
10153
|
+
"ai-slop/hardcoded-id",
|
|
9447
10154
|
"ai-slop/python-bare-except",
|
|
9448
10155
|
"ai-slop/python-broad-except",
|
|
9449
10156
|
"ai-slop/python-mutable-default",
|