aislop 0.9.3 → 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 +58 -5
- package/dist/cli.js +1133 -64
- package/dist/{version-BNO_Lw7E.js → engine-info-DCvIfZ0f.js} +1 -5
- package/dist/index.d.ts +6 -0
- package/dist/index.js +857 -53
- package/dist/{json-BhO1Ufj3.js → json-CZU3lEfE.js} +2 -1
- package/dist/mcp.js +738 -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 +93 -91
- 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/mcp.js
CHANGED
|
@@ -11,9 +11,9 @@ import fs from "node:fs";
|
|
|
11
11
|
import YAML from "yaml";
|
|
12
12
|
import { z as z$1 } from "zod/v4";
|
|
13
13
|
import micromatch from "micromatch";
|
|
14
|
+
import ts from "typescript";
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
15
16
|
import os from "node:os";
|
|
16
|
-
import "typescript";
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
18
|
|
|
19
19
|
//#region src/config/defaults.ts
|
|
@@ -65,7 +65,8 @@ const DEFAULT_CONFIG = {
|
|
|
65
65
|
failBelow: 70,
|
|
66
66
|
format: "json"
|
|
67
67
|
},
|
|
68
|
-
telemetry: { enabled: true }
|
|
68
|
+
telemetry: { enabled: true },
|
|
69
|
+
rules: {}
|
|
69
70
|
};
|
|
70
71
|
|
|
71
72
|
//#endregion
|
|
@@ -156,6 +157,12 @@ const CiSchema = z$1.object({
|
|
|
156
157
|
format: z$1.enum(["json"]).default("json")
|
|
157
158
|
});
|
|
158
159
|
const TelemetrySchema = z$1.object({ enabled: z$1.boolean().default(true) });
|
|
160
|
+
const RuleSeverityOverride = z$1.enum([
|
|
161
|
+
"error",
|
|
162
|
+
"warning",
|
|
163
|
+
"off"
|
|
164
|
+
]);
|
|
165
|
+
const RulesSchema = z$1.record(z$1.string(), RuleSeverityOverride).default(() => ({}));
|
|
159
166
|
const AislopConfigSchema = z$1.object({
|
|
160
167
|
version: z$1.number().default(1),
|
|
161
168
|
engines: EnginesSchema.default(() => ({
|
|
@@ -190,6 +197,7 @@ const AislopConfigSchema = z$1.object({
|
|
|
190
197
|
format: "json"
|
|
191
198
|
})),
|
|
192
199
|
telemetry: TelemetrySchema.default(() => ({ enabled: true })),
|
|
200
|
+
rules: RulesSchema,
|
|
193
201
|
exclude: z$1.array(z$1.string()).default(() => [
|
|
194
202
|
"node_modules",
|
|
195
203
|
".git",
|
|
@@ -258,7 +266,7 @@ const loadConfig = (directory) => {
|
|
|
258
266
|
//#endregion
|
|
259
267
|
//#region src/utils/source-files.ts
|
|
260
268
|
const MAX_BUFFER = 50 * 1024 * 1024;
|
|
261
|
-
const SOURCE_EXTENSIONS = new Set([
|
|
269
|
+
const SOURCE_EXTENSIONS$1 = new Set([
|
|
262
270
|
".ts",
|
|
263
271
|
".tsx",
|
|
264
272
|
".js",
|
|
@@ -366,7 +374,7 @@ const toProjectPath = (rootDirectory, filePath) => {
|
|
|
366
374
|
const isWithinProject = (relativePath) => relativePath.length > 0 && !relativePath.startsWith("..");
|
|
367
375
|
const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
368
376
|
const extension = path.extname(filePath);
|
|
369
|
-
return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
|
|
377
|
+
return SOURCE_EXTENSIONS$1.has(extension) || extraExtensions.has(extension);
|
|
370
378
|
};
|
|
371
379
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
372
380
|
const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
|
|
@@ -505,7 +513,7 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
|
|
|
505
513
|
|
|
506
514
|
//#endregion
|
|
507
515
|
//#region src/engines/ai-slop/abstractions.ts
|
|
508
|
-
const JS_EXTS$
|
|
516
|
+
const JS_EXTS$2 = new Set([
|
|
509
517
|
".ts",
|
|
510
518
|
".tsx",
|
|
511
519
|
".js",
|
|
@@ -516,11 +524,11 @@ const JS_EXTS$1 = new Set([
|
|
|
516
524
|
const THIN_WRAPPER_PATTERNS = [
|
|
517
525
|
{
|
|
518
526
|
pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
|
|
519
|
-
extensions: JS_EXTS$
|
|
527
|
+
extensions: JS_EXTS$2
|
|
520
528
|
},
|
|
521
529
|
{
|
|
522
530
|
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
523
|
-
extensions: JS_EXTS$
|
|
531
|
+
extensions: JS_EXTS$2
|
|
524
532
|
},
|
|
525
533
|
{
|
|
526
534
|
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
@@ -753,6 +761,12 @@ const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
|
753
761
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
754
762
|
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
755
763
|
const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
|
|
764
|
+
const isLineComment = (trimmed) => isJsComment(trimmed) || isPythonComment(trimmed);
|
|
765
|
+
const isInMultiLineCommentRun = (lines, index) => {
|
|
766
|
+
const prev = index > 0 ? lines[index - 1].trim() : "";
|
|
767
|
+
const next = index + 1 < lines.length ? lines[index + 1].trim() : "";
|
|
768
|
+
return isLineComment(prev) || isLineComment(next);
|
|
769
|
+
};
|
|
756
770
|
/**
|
|
757
771
|
* Extract just the comment text after the comment marker.
|
|
758
772
|
*/
|
|
@@ -805,6 +819,7 @@ const scanFileForTrivialComments = (content, relativePath, ext) => {
|
|
|
805
819
|
const lines = content.split("\n");
|
|
806
820
|
for (let i = 0; i < lines.length; i++) {
|
|
807
821
|
if (!isTrivialComment(lines[i].trim(), i + 1 < lines.length ? lines[i + 1] : void 0)) continue;
|
|
822
|
+
if (isInMultiLineCommentRun(lines, i)) continue;
|
|
808
823
|
if (isDocCommentForDeclaration(lines, i, ext)) continue;
|
|
809
824
|
diagnostics.push({
|
|
810
825
|
filePath: relativePath,
|
|
@@ -966,6 +981,177 @@ const detectDeadPatterns = async (context) => {
|
|
|
966
981
|
return diagnostics;
|
|
967
982
|
};
|
|
968
983
|
|
|
984
|
+
//#endregion
|
|
985
|
+
//#region src/engines/ai-slop/defensive-patterns.ts
|
|
986
|
+
const JS_TS_EXTENSIONS = new Set([
|
|
987
|
+
".ts",
|
|
988
|
+
".tsx",
|
|
989
|
+
".js",
|
|
990
|
+
".jsx",
|
|
991
|
+
".mjs",
|
|
992
|
+
".cjs"
|
|
993
|
+
]);
|
|
994
|
+
const TS_EXTENSIONS = new Set([".ts", ".tsx"]);
|
|
995
|
+
const COERCION_CTORS = {
|
|
996
|
+
string: "String",
|
|
997
|
+
number: "Number",
|
|
998
|
+
boolean: "Boolean"
|
|
999
|
+
};
|
|
1000
|
+
const scriptKindFor = (ext) => {
|
|
1001
|
+
switch (ext) {
|
|
1002
|
+
case ".tsx": return ts.ScriptKind.TSX;
|
|
1003
|
+
case ".jsx": return ts.ScriptKind.JSX;
|
|
1004
|
+
case ".js":
|
|
1005
|
+
case ".mjs":
|
|
1006
|
+
case ".cjs": return ts.ScriptKind.JS;
|
|
1007
|
+
default: return ts.ScriptKind.TS;
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
const lineFor = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
1011
|
+
const makeDiagnostic = (filePath, line, rule, message, help) => ({
|
|
1012
|
+
filePath,
|
|
1013
|
+
engine: "ai-slop",
|
|
1014
|
+
rule,
|
|
1015
|
+
severity: "warning",
|
|
1016
|
+
message,
|
|
1017
|
+
help,
|
|
1018
|
+
line,
|
|
1019
|
+
column: 0,
|
|
1020
|
+
category: "AI Slop",
|
|
1021
|
+
fixable: false
|
|
1022
|
+
});
|
|
1023
|
+
const isFunctionNode = (node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node);
|
|
1024
|
+
const primitiveKindOf = (node) => {
|
|
1025
|
+
if (!node) return null;
|
|
1026
|
+
switch (node.kind) {
|
|
1027
|
+
case ts.SyntaxKind.StringKeyword: return "string";
|
|
1028
|
+
case ts.SyntaxKind.NumberKeyword: return "number";
|
|
1029
|
+
case ts.SyntaxKind.BooleanKeyword: return "boolean";
|
|
1030
|
+
default: return null;
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
const hasExportModifier = (node) => node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
1034
|
+
const primitiveParamsOf = (node) => {
|
|
1035
|
+
const params = /* @__PURE__ */ new Map();
|
|
1036
|
+
for (const param of node.parameters) {
|
|
1037
|
+
if (!ts.isIdentifier(param.name)) continue;
|
|
1038
|
+
const kind = primitiveKindOf(param.type);
|
|
1039
|
+
if (!kind) continue;
|
|
1040
|
+
params.set(param.name.text, kind);
|
|
1041
|
+
}
|
|
1042
|
+
return params;
|
|
1043
|
+
};
|
|
1044
|
+
const isRethrowStatement = (statement, errorName) => ts.isThrowStatement(statement) && statement.expression !== void 0 && ts.isIdentifier(statement.expression) && statement.expression.text === errorName;
|
|
1045
|
+
const isPromiseRejectRethrow = (statement, errorName) => {
|
|
1046
|
+
if (!ts.isReturnStatement(statement) || !statement.expression) return false;
|
|
1047
|
+
const expression = statement.expression;
|
|
1048
|
+
if (!ts.isCallExpression(expression) || expression.arguments.length !== 1) return false;
|
|
1049
|
+
const [arg] = expression.arguments;
|
|
1050
|
+
if (!ts.isIdentifier(arg) || arg.text !== errorName) return false;
|
|
1051
|
+
if (!ts.isPropertyAccessExpression(expression.expression)) return false;
|
|
1052
|
+
const target = expression.expression;
|
|
1053
|
+
return ts.isIdentifier(target.expression) && target.expression.text === "Promise" && target.name.text === "reject";
|
|
1054
|
+
};
|
|
1055
|
+
const detectRedundantTryCatch = (sourceFile, relativePath) => {
|
|
1056
|
+
const diagnostics = [];
|
|
1057
|
+
const visit = (node) => {
|
|
1058
|
+
if (ts.isTryStatement(node) && node.catchClause && !node.finallyBlock) {
|
|
1059
|
+
const catchNameNode = node.catchClause.variableDeclaration?.name;
|
|
1060
|
+
const [onlyStatement] = node.catchClause.block.statements;
|
|
1061
|
+
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."));
|
|
1062
|
+
}
|
|
1063
|
+
ts.forEachChild(node, visit);
|
|
1064
|
+
};
|
|
1065
|
+
visit(sourceFile);
|
|
1066
|
+
return diagnostics;
|
|
1067
|
+
};
|
|
1068
|
+
const detectPrimitiveCoercions = (sourceFile, relativePath) => {
|
|
1069
|
+
const diagnostics = [];
|
|
1070
|
+
const scanFunctionBody = (node, params) => {
|
|
1071
|
+
const body = node.body;
|
|
1072
|
+
if (!body || params.size === 0) return;
|
|
1073
|
+
const visitBody = (child) => {
|
|
1074
|
+
if (child !== body && isFunctionNode(child)) return;
|
|
1075
|
+
if (ts.isCallExpression(child) && ts.isIdentifier(child.expression)) {
|
|
1076
|
+
const [arg] = child.arguments;
|
|
1077
|
+
if (arg && ts.isIdentifier(arg)) {
|
|
1078
|
+
const primitive = params.get(arg.text);
|
|
1079
|
+
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."));
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
ts.forEachChild(child, visitBody);
|
|
1083
|
+
};
|
|
1084
|
+
visitBody(body);
|
|
1085
|
+
};
|
|
1086
|
+
const visit = (node) => {
|
|
1087
|
+
if (isFunctionNode(node)) scanFunctionBody(node, primitiveParamsOf(node));
|
|
1088
|
+
ts.forEachChild(node, visit);
|
|
1089
|
+
};
|
|
1090
|
+
visit(sourceFile);
|
|
1091
|
+
return diagnostics;
|
|
1092
|
+
};
|
|
1093
|
+
const normalizedTypeDeclaration = (sourceFile, node) => sourceFile.text.slice(node.getStart(sourceFile), node.getEnd()).replace(/\bexport\b/g, "").replace(/\bdeclare\b/g, "").replace(/\s+/g, " ").trim();
|
|
1094
|
+
const exportedTypesOf = (parsed) => {
|
|
1095
|
+
const declarations = [];
|
|
1096
|
+
const visit = (node) => {
|
|
1097
|
+
if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && hasExportModifier(node)) declarations.push({
|
|
1098
|
+
name: node.name.text,
|
|
1099
|
+
signature: normalizedTypeDeclaration(parsed.sourceFile, node),
|
|
1100
|
+
filePath: parsed.relativePath,
|
|
1101
|
+
line: lineFor(parsed.sourceFile, node)
|
|
1102
|
+
});
|
|
1103
|
+
ts.forEachChild(node, visit);
|
|
1104
|
+
};
|
|
1105
|
+
visit(parsed.sourceFile);
|
|
1106
|
+
return declarations;
|
|
1107
|
+
};
|
|
1108
|
+
const duplicateTypeKeyOf = (declaration) => `${declaration.name}\0${declaration.signature}`;
|
|
1109
|
+
const detectDuplicateExportedTypes = (parsedSources) => {
|
|
1110
|
+
const diagnostics = [];
|
|
1111
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1112
|
+
for (const parsed of parsedSources) {
|
|
1113
|
+
if (!TS_EXTENSIONS.has(parsed.ext)) continue;
|
|
1114
|
+
for (const declaration of exportedTypesOf(parsed)) {
|
|
1115
|
+
const key = duplicateTypeKeyOf(declaration);
|
|
1116
|
+
const previous = seen.get(key);
|
|
1117
|
+
if (!previous) {
|
|
1118
|
+
seen.set(key, declaration);
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
if (previous.filePath === declaration.filePath) continue;
|
|
1122
|
+
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.`));
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return diagnostics;
|
|
1126
|
+
};
|
|
1127
|
+
const detectDefensivePatterns = async (context) => {
|
|
1128
|
+
const diagnostics = [];
|
|
1129
|
+
const parsedSources = [];
|
|
1130
|
+
for (const filePath of getSourceFiles(context)) {
|
|
1131
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1132
|
+
let content;
|
|
1133
|
+
try {
|
|
1134
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
1135
|
+
} catch {
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1139
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
1140
|
+
const ext = path.extname(filePath);
|
|
1141
|
+
if (!JS_TS_EXTENSIONS.has(ext)) continue;
|
|
1142
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKindFor(ext));
|
|
1143
|
+
parsedSources.push({
|
|
1144
|
+
sourceFile,
|
|
1145
|
+
relativePath,
|
|
1146
|
+
ext
|
|
1147
|
+
});
|
|
1148
|
+
diagnostics.push(...detectRedundantTryCatch(sourceFile, relativePath));
|
|
1149
|
+
if (TS_EXTENSIONS.has(ext)) diagnostics.push(...detectPrimitiveCoercions(sourceFile, relativePath));
|
|
1150
|
+
}
|
|
1151
|
+
diagnostics.push(...detectDuplicateExportedTypes(parsedSources));
|
|
1152
|
+
return diagnostics;
|
|
1153
|
+
};
|
|
1154
|
+
|
|
969
1155
|
//#endregion
|
|
970
1156
|
//#region src/engines/ai-slop/duplicate-imports.ts
|
|
971
1157
|
const JS_EXTENSIONS$2 = new Set([
|
|
@@ -976,7 +1162,16 @@ const JS_EXTENSIONS$2 = new Set([
|
|
|
976
1162
|
".mjs",
|
|
977
1163
|
".cjs"
|
|
978
1164
|
]);
|
|
979
|
-
const IMPORT_FROM_RE = /^\s*import\s+[^;]*?from\s+["']([^"']+)["']/;
|
|
1165
|
+
const IMPORT_FROM_RE = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
|
|
1166
|
+
const TYPE_ONLY_RE = /^\s*type\b/;
|
|
1167
|
+
const VALUE_BINDING_RE = /\{([^}]*)\}/;
|
|
1168
|
+
const isTypeOnly = (clause) => {
|
|
1169
|
+
if (TYPE_ONLY_RE.test(clause)) return true;
|
|
1170
|
+
const braces = VALUE_BINDING_RE.exec(clause);
|
|
1171
|
+
if (!braces) return false;
|
|
1172
|
+
const members = braces[1].split(",").map((member) => member.trim()).filter((member) => member.length > 0);
|
|
1173
|
+
return members.length > 0 && members.every((member) => /^type\b/.test(member));
|
|
1174
|
+
};
|
|
980
1175
|
const extractImportLines = (content) => {
|
|
981
1176
|
const lines = content.split("\n");
|
|
982
1177
|
const results = [];
|
|
@@ -985,8 +1180,9 @@ const extractImportLines = (content) => {
|
|
|
985
1180
|
const match = IMPORT_FROM_RE.exec(line);
|
|
986
1181
|
if (!match) continue;
|
|
987
1182
|
results.push({
|
|
988
|
-
spec: match[
|
|
989
|
-
line: i + 1
|
|
1183
|
+
spec: match[2],
|
|
1184
|
+
line: i + 1,
|
|
1185
|
+
typeOnly: isTypeOnly(match[1])
|
|
990
1186
|
});
|
|
991
1187
|
}
|
|
992
1188
|
return results;
|
|
@@ -1005,14 +1201,16 @@ const detectDuplicateImports = async (context) => {
|
|
|
1005
1201
|
}
|
|
1006
1202
|
const imports = extractImportLines(content);
|
|
1007
1203
|
if (imports.length < 2) continue;
|
|
1008
|
-
const
|
|
1204
|
+
const byBucket = /* @__PURE__ */ new Map();
|
|
1009
1205
|
for (const imp of imports) {
|
|
1010
|
-
const
|
|
1206
|
+
const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
1207
|
+
const list = byBucket.get(key) ?? [];
|
|
1011
1208
|
list.push(imp);
|
|
1012
|
-
|
|
1209
|
+
byBucket.set(key, list);
|
|
1013
1210
|
}
|
|
1014
1211
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
1015
|
-
for (const
|
|
1212
|
+
for (const occurrences of byBucket.values()) {
|
|
1213
|
+
const { spec } = occurrences[0];
|
|
1016
1214
|
if (occurrences.length < 2) continue;
|
|
1017
1215
|
for (const dup of occurrences.slice(1)) {
|
|
1018
1216
|
const firstLine = occurrences[0].line;
|
|
@@ -1217,6 +1415,130 @@ const detectGoPatterns = async (context) => {
|
|
|
1217
1415
|
return diagnostics;
|
|
1218
1416
|
};
|
|
1219
1417
|
|
|
1418
|
+
//#endregion
|
|
1419
|
+
//#region src/engines/ai-slop/hardcoded-config.ts
|
|
1420
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
1421
|
+
".ts",
|
|
1422
|
+
".tsx",
|
|
1423
|
+
".js",
|
|
1424
|
+
".jsx",
|
|
1425
|
+
".mjs",
|
|
1426
|
+
".cjs",
|
|
1427
|
+
".py",
|
|
1428
|
+
".go",
|
|
1429
|
+
".rs",
|
|
1430
|
+
".rb",
|
|
1431
|
+
".java",
|
|
1432
|
+
".php"
|
|
1433
|
+
]);
|
|
1434
|
+
const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
|
|
1435
|
+
const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
|
|
1436
|
+
const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
|
|
1437
|
+
const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
|
|
1438
|
+
const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
|
|
1439
|
+
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;
|
|
1440
|
+
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;
|
|
1441
|
+
const PLACEHOLDER_HOSTS = new Set([
|
|
1442
|
+
"example.com",
|
|
1443
|
+
"example.org",
|
|
1444
|
+
"example.net"
|
|
1445
|
+
]);
|
|
1446
|
+
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
1447
|
+
const HARDCODED_URL_FINDING = {
|
|
1448
|
+
rule: "ai-slop/hardcoded-url",
|
|
1449
|
+
message: "Hardcoded environment URL in production code",
|
|
1450
|
+
help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
|
|
1451
|
+
};
|
|
1452
|
+
const HARDCODED_ID_FINDING = {
|
|
1453
|
+
rule: "ai-slop/hardcoded-id",
|
|
1454
|
+
message: "Hardcoded provider/project ID in production code",
|
|
1455
|
+
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."
|
|
1456
|
+
};
|
|
1457
|
+
const makeFinding = (filePath, line, spec) => ({
|
|
1458
|
+
filePath,
|
|
1459
|
+
engine: "ai-slop",
|
|
1460
|
+
rule: spec.rule,
|
|
1461
|
+
severity: "warning",
|
|
1462
|
+
message: spec.message,
|
|
1463
|
+
help: spec.help,
|
|
1464
|
+
line,
|
|
1465
|
+
column: 0,
|
|
1466
|
+
category: "AI Slop",
|
|
1467
|
+
fixable: false
|
|
1468
|
+
});
|
|
1469
|
+
const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
|
|
1470
|
+
const commentStartsBefore = (line, index, ext) => {
|
|
1471
|
+
const prefix = line.slice(0, index);
|
|
1472
|
+
if (ext === ".py" || ext === ".rb") return prefix.includes("#");
|
|
1473
|
+
if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
|
|
1474
|
+
return prefix.includes("//") || prefix.includes("/*");
|
|
1475
|
+
};
|
|
1476
|
+
const safeUrlHost = (urlText) => {
|
|
1477
|
+
try {
|
|
1478
|
+
return new URL(urlText).hostname.toLowerCase();
|
|
1479
|
+
} catch {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
|
|
1484
|
+
const shouldFlagUrlLiteral = (line, urlText) => {
|
|
1485
|
+
if (isEnvBackedLine(line)) return false;
|
|
1486
|
+
const host = safeUrlHost(urlText);
|
|
1487
|
+
if (!host) return false;
|
|
1488
|
+
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
1489
|
+
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
1490
|
+
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
1491
|
+
};
|
|
1492
|
+
const hasUsefulIdShape = (value) => {
|
|
1493
|
+
if (PLACEHOLDER_ID_RE.test(value)) return false;
|
|
1494
|
+
if (/^https?:\/\//i.test(value)) return false;
|
|
1495
|
+
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
1496
|
+
return /[0-9_-]/.test(value);
|
|
1497
|
+
};
|
|
1498
|
+
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
1499
|
+
const diagnostics = [];
|
|
1500
|
+
if (isCommentOnlyLine(line.trim())) return diagnostics;
|
|
1501
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
1502
|
+
let urlMatch;
|
|
1503
|
+
while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
|
|
1504
|
+
const urlText = urlMatch[2];
|
|
1505
|
+
if (commentStartsBefore(line, urlMatch.index, ext)) continue;
|
|
1506
|
+
if (!shouldFlagUrlLiteral(line, urlText)) continue;
|
|
1507
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
|
|
1508
|
+
}
|
|
1509
|
+
if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
|
|
1510
|
+
ID_LITERAL_RE.lastIndex = 0;
|
|
1511
|
+
let idMatch;
|
|
1512
|
+
while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
|
|
1513
|
+
const value = idMatch[2];
|
|
1514
|
+
if (commentStartsBefore(line, idMatch.index, ext)) continue;
|
|
1515
|
+
if (!hasUsefulIdShape(value)) continue;
|
|
1516
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
|
|
1517
|
+
}
|
|
1518
|
+
return diagnostics;
|
|
1519
|
+
};
|
|
1520
|
+
const scanFileForConfigLiterals = (content, relativePath, ext) => {
|
|
1521
|
+
if (!SOURCE_EXTENSIONS.has(ext)) return [];
|
|
1522
|
+
if (isNonProductionPath(relativePath)) return [];
|
|
1523
|
+
return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
|
|
1524
|
+
};
|
|
1525
|
+
const detectHardcodedConfigLiterals = async (context) => {
|
|
1526
|
+
const diagnostics = [];
|
|
1527
|
+
for (const filePath of getSourceFiles(context)) {
|
|
1528
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1529
|
+
let content;
|
|
1530
|
+
try {
|
|
1531
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
1532
|
+
} catch {
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1536
|
+
const ext = path.extname(filePath);
|
|
1537
|
+
diagnostics.push(...scanFileForConfigLiterals(content, relativePath, ext));
|
|
1538
|
+
}
|
|
1539
|
+
return diagnostics;
|
|
1540
|
+
};
|
|
1541
|
+
|
|
1220
1542
|
//#endregion
|
|
1221
1543
|
//#region src/engines/ai-slop/js-import-aliases.ts
|
|
1222
1544
|
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
@@ -1484,23 +1806,87 @@ const PYTHON_STDLIB = new Set([
|
|
|
1484
1806
|
"zoneinfo"
|
|
1485
1807
|
]);
|
|
1486
1808
|
const PYTHON_IMPORT_TO_PIP = {
|
|
1487
|
-
yaml: "pyyaml",
|
|
1488
|
-
PIL: "pillow",
|
|
1489
|
-
dateutil: "python-dateutil",
|
|
1490
|
-
cv2:
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1809
|
+
yaml: ["pyyaml"],
|
|
1810
|
+
PIL: ["pillow"],
|
|
1811
|
+
dateutil: ["python-dateutil"],
|
|
1812
|
+
cv2: [
|
|
1813
|
+
"opencv-python",
|
|
1814
|
+
"opencv-python-headless",
|
|
1815
|
+
"opencv-contrib-python"
|
|
1816
|
+
],
|
|
1817
|
+
sklearn: ["scikit-learn"],
|
|
1818
|
+
bs4: ["beautifulsoup4"],
|
|
1819
|
+
typing_extensions: ["typing-extensions"],
|
|
1820
|
+
dotenv: ["python-dotenv"],
|
|
1821
|
+
genai: ["google-genai"],
|
|
1822
|
+
google: [
|
|
1823
|
+
"google-genai",
|
|
1824
|
+
"google-generativeai",
|
|
1825
|
+
"google-api-python-client",
|
|
1826
|
+
"google-cloud-storage",
|
|
1827
|
+
"google-cloud-aiplatform",
|
|
1828
|
+
"google-auth",
|
|
1829
|
+
"protobuf"
|
|
1830
|
+
],
|
|
1831
|
+
jose: ["python-jose"],
|
|
1832
|
+
jwt: ["pyjwt"],
|
|
1833
|
+
OpenSSL: ["pyopenssl"],
|
|
1834
|
+
Crypto: ["pycryptodome", "pycryptodomex"],
|
|
1835
|
+
Cryptodome: ["pycryptodomex", "pycryptodome"],
|
|
1836
|
+
magic: ["python-magic"],
|
|
1837
|
+
docx: ["python-docx"],
|
|
1838
|
+
pptx: ["python-pptx"],
|
|
1839
|
+
git: ["gitpython"],
|
|
1840
|
+
socks: ["pysocks"],
|
|
1841
|
+
redis: ["redis"],
|
|
1842
|
+
cairo: ["pycairo"],
|
|
1843
|
+
serial: ["pyserial"],
|
|
1844
|
+
usb: ["pyusb"],
|
|
1845
|
+
gi: ["pygobject"],
|
|
1846
|
+
Xlib: ["python-xlib"],
|
|
1847
|
+
ldap: ["python-ldap"],
|
|
1848
|
+
slugify: ["python-slugify"],
|
|
1849
|
+
memcache: ["python-memcached"],
|
|
1850
|
+
dns: ["dnspython"],
|
|
1851
|
+
attr: ["attrs"],
|
|
1852
|
+
attrs: ["attrs"],
|
|
1853
|
+
zoneinfo_data: ["tzdata"],
|
|
1854
|
+
pkg_resources: ["setuptools"],
|
|
1855
|
+
setuptools: ["setuptools"],
|
|
1856
|
+
wx: ["wxpython"],
|
|
1857
|
+
skimage: ["scikit-image"],
|
|
1858
|
+
OpenGL: ["pyopengl"],
|
|
1859
|
+
win32api: ["pywin32"],
|
|
1860
|
+
win32con: ["pywin32"],
|
|
1861
|
+
win32com: ["pywin32"],
|
|
1862
|
+
pythoncom: ["pywin32"],
|
|
1863
|
+
pywintypes: ["pywin32"],
|
|
1864
|
+
rest_framework: ["djangorestframework"],
|
|
1865
|
+
allauth: ["django-allauth"],
|
|
1866
|
+
corsheaders: ["django-cors-headers"],
|
|
1867
|
+
debug_toolbar: ["django-debug-toolbar"],
|
|
1868
|
+
environ: ["django-environ"],
|
|
1869
|
+
flask_cors: ["flask-cors"],
|
|
1870
|
+
flask_sqlalchemy: ["flask-sqlalchemy"],
|
|
1871
|
+
flask_migrate: ["flask-migrate"],
|
|
1872
|
+
flask_login: ["flask-login"],
|
|
1873
|
+
jwt_extended: ["flask-jwt-extended"],
|
|
1874
|
+
dateparser: ["dateparser"],
|
|
1875
|
+
yaml_include: ["pyyaml-include"],
|
|
1876
|
+
lxml_html_clean: ["lxml-html-clean"],
|
|
1877
|
+
grpc: ["grpcio"],
|
|
1878
|
+
grpc_status: ["grpcio-status"],
|
|
1879
|
+
google_crc32c: ["google-crc32c"],
|
|
1880
|
+
pkg_about: ["pkg-about"],
|
|
1881
|
+
mpl_toolkits: ["matplotlib"],
|
|
1882
|
+
dotmap: ["dotmap"],
|
|
1883
|
+
pydantic_settings: ["pydantic-settings"],
|
|
1884
|
+
telegram: ["python-telegram-bot"],
|
|
1885
|
+
discord: ["discord-py"],
|
|
1886
|
+
nacl: ["pynacl"],
|
|
1887
|
+
jwcrypto: ["jwcrypto"],
|
|
1888
|
+
humanfriendly: ["humanfriendly"],
|
|
1889
|
+
multipart: ["python-multipart"]
|
|
1504
1890
|
};
|
|
1505
1891
|
|
|
1506
1892
|
//#endregion
|
|
@@ -1539,6 +1925,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
|
|
|
1539
1925
|
const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
|
|
1540
1926
|
if (m) addPyDep(pyDeps, m[1]);
|
|
1541
1927
|
}
|
|
1928
|
+
const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
|
|
1929
|
+
if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
1542
1930
|
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1543
1931
|
let match = poetryRe.exec(content);
|
|
1544
1932
|
while (match !== null) {
|
|
@@ -1737,6 +2125,11 @@ const packageNameFromImport = (spec) => {
|
|
|
1737
2125
|
}
|
|
1738
2126
|
return spec.split("/")[0];
|
|
1739
2127
|
};
|
|
2128
|
+
const typesPackageName = (pkg) => {
|
|
2129
|
+
if (pkg.startsWith("@types/")) return pkg;
|
|
2130
|
+
if (pkg.startsWith("@")) return `@types/${pkg.slice(1).replace("/", "__")}`;
|
|
2131
|
+
return `@types/${pkg}`;
|
|
2132
|
+
};
|
|
1740
2133
|
const STATIC_IMPORT_RE = /^\s*import\s+(?:[\w*{},\s]+\s+from\s+)?["']([^"']+)["']/;
|
|
1741
2134
|
const DYNAMIC_IMPORT_RE = /(?:import|require)\s*\(\s*["']([^"']+)["']/g;
|
|
1742
2135
|
const extractJsImports = (content) => {
|
|
@@ -1801,15 +2194,16 @@ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
|
|
|
1801
2194
|
const realPkg = pkg.slice(7);
|
|
1802
2195
|
if (manifest.jsDeps.has(realPkg)) return null;
|
|
1803
2196
|
}
|
|
2197
|
+
if (manifest.jsDeps.has(typesPackageName(pkg))) return null;
|
|
1804
2198
|
return pkg;
|
|
1805
2199
|
};
|
|
2200
|
+
const normalizePyName = (name) => name.toLowerCase().replace(/_/g, "-");
|
|
1806
2201
|
const checkPyImport = (spec, manifest) => {
|
|
1807
2202
|
const root = spec.split(".")[0];
|
|
1808
2203
|
if (PYTHON_STDLIB.has(root)) return null;
|
|
1809
|
-
const normalized = root
|
|
2204
|
+
const normalized = normalizePyName(root);
|
|
1810
2205
|
if (manifest.pyDeps.has(normalized)) return null;
|
|
1811
|
-
|
|
1812
|
-
if (pipName && manifest.pyDeps.has(pipName)) return null;
|
|
2206
|
+
if ((PYTHON_IMPORT_TO_PIP[root] ?? PYTHON_IMPORT_TO_PIP[normalized])?.some((dist) => manifest.pyDeps.has(normalizePyName(dist)))) return null;
|
|
1813
2207
|
return root;
|
|
1814
2208
|
};
|
|
1815
2209
|
const detectHallucinatedImports = async (context) => {
|
|
@@ -1959,6 +2353,85 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1959
2353
|
return blocks;
|
|
1960
2354
|
};
|
|
1961
2355
|
|
|
2356
|
+
//#endregion
|
|
2357
|
+
//#region src/engines/ai-slop/meta-comment.ts
|
|
2358
|
+
const PLAN_REFERENCE_RES = [
|
|
2359
|
+
/\b(?:stage|step|phase)\s+\d+\b(?!\s*[:.]?\s*(?:bytes|ms|seconds|of\s+\d))/i,
|
|
2360
|
+
/\bstep\s+\d+\s+of\s+the\s+plan\b/i,
|
|
2361
|
+
/\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
|
|
2362
|
+
/\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
|
|
2363
|
+
/\bfrom\s+the\s+(?:task|todo|plan|spec|ticket|prompt|requirements?)\b/i,
|
|
2364
|
+
/\bimplement(?:ing|s|ed)?\s+use\s*case\s+\d*/i,
|
|
2365
|
+
/\b(?:requirements?\s+doc|requirement\s+\d+)\b/i,
|
|
2366
|
+
/\bas\s+(?:instructed|specified|outlined)\s+(?:above|below|in\s+the)\b/i
|
|
2367
|
+
];
|
|
2368
|
+
const BEFORE_AFTER_RES = [
|
|
2369
|
+
/\bpreviously[,:]?\s+(?:this|we|it|the)\b/i,
|
|
2370
|
+
/\bused\s+to\s+(?:be|use|call|return|do|have|rely)\b/i,
|
|
2371
|
+
/\bchanged\s+(?:\w+\s+){0,3}from\s+.+\bto\b/i,
|
|
2372
|
+
/\bno\s+longer\s+(?:needed|used|required|necessary|calls?|returns?|does)\b/i,
|
|
2373
|
+
/\bthis\s+was\s+.+\bbut\s+now\b/i,
|
|
2374
|
+
/\bwe\s+(?:now|used\s+to)\s+(?:no\s+longer\s+)?(?:use|call|return|do|have)\b/i,
|
|
2375
|
+
/\breplaced\s+the\s+(?:old|previous|former)\b/i,
|
|
2376
|
+
/\b(?:was|were)\s+(?:renamed|moved|removed|refactored|extracted)\s+(?:from|to|out\s+of)\b/i
|
|
2377
|
+
];
|
|
2378
|
+
const WHY_OR_TODO_RE = /\b(?:because|since|otherwise|todo|fixme|hack|note:|reason:|workaround|see\s+(?:issue|#))\b/i;
|
|
2379
|
+
const looksLikeLicenseHeader$1 = (block) => {
|
|
2380
|
+
if (block.startLine !== 1) return false;
|
|
2381
|
+
const text = block.rawLines.join(" ").toLowerCase();
|
|
2382
|
+
return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
|
|
2383
|
+
};
|
|
2384
|
+
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));
|
|
2385
|
+
const matchMetaSignal = (block) => {
|
|
2386
|
+
if (looksLikeLicenseHeader$1(block)) return null;
|
|
2387
|
+
if (looksLikeSuppressDirective$1(block)) return null;
|
|
2388
|
+
if (block.kind === "jsdoc" && block.hasMeaningfulJsdocTag) return null;
|
|
2389
|
+
if (block.isRustDoc) return null;
|
|
2390
|
+
const joined = block.prose.join(" ");
|
|
2391
|
+
if (joined.trim().length === 0) return null;
|
|
2392
|
+
if (WHY_OR_TODO_RE.test(joined)) return null;
|
|
2393
|
+
if (PLAN_REFERENCE_RES.some((re) => re.test(joined))) return "plan/process reference";
|
|
2394
|
+
if (BEFORE_AFTER_RES.some((re) => re.test(joined))) return "before/after state narration";
|
|
2395
|
+
return null;
|
|
2396
|
+
};
|
|
2397
|
+
const detectMetaComments = async (context) => {
|
|
2398
|
+
const files = getSourceFiles(context);
|
|
2399
|
+
const diagnostics = [];
|
|
2400
|
+
for (const filePath of files) {
|
|
2401
|
+
const ext = path.extname(filePath);
|
|
2402
|
+
if (!SUPPORTED_EXTS.has(ext)) continue;
|
|
2403
|
+
if (isAutoGenerated(filePath)) continue;
|
|
2404
|
+
const syntax = getCommentSyntax(ext);
|
|
2405
|
+
if (!syntax) continue;
|
|
2406
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2407
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
2408
|
+
let content;
|
|
2409
|
+
try {
|
|
2410
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
2411
|
+
} catch {
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
const blocks = collectBlocks(content.split("\n"), syntax);
|
|
2415
|
+
for (const block of blocks) {
|
|
2416
|
+
const reason = matchMetaSignal(block);
|
|
2417
|
+
if (!reason) continue;
|
|
2418
|
+
diagnostics.push({
|
|
2419
|
+
filePath: relativePath,
|
|
2420
|
+
engine: "ai-slop",
|
|
2421
|
+
rule: "ai-slop/meta-comment",
|
|
2422
|
+
severity: "warning",
|
|
2423
|
+
message: `Meta/plan comment (${reason})`,
|
|
2424
|
+
help: "Remove — references to the build plan or before/after code state belong in PR descriptions and commit messages, not source.",
|
|
2425
|
+
line: block.startLine,
|
|
2426
|
+
column: 0,
|
|
2427
|
+
category: "Comments",
|
|
2428
|
+
fixable: false
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
return diagnostics;
|
|
2433
|
+
};
|
|
2434
|
+
|
|
1962
2435
|
//#endregion
|
|
1963
2436
|
//#region src/engines/ai-slop/narrative-comments.ts
|
|
1964
2437
|
const looksLikeDeclarationPreamble = (nextLine, ext) => {
|
|
@@ -2190,6 +2663,11 @@ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\
|
|
|
2190
2663
|
const PRINT_RE = /^\s*print\s*\(/;
|
|
2191
2664
|
const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
|
|
2192
2665
|
const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
|
|
2666
|
+
const RANGE_LEN_LOOP_RE = /^\s*for\s+([A-Za-z_]\w*)\s+in\s+range\s*\(\s*len\s*\(\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\)\s*\)\s*:\s*(?:#.*)?$/;
|
|
2667
|
+
const CHAINED_DICT_GET_RE = /\.get\s*\([^)]*,\s*\{\s*\}\s*\)\s*\.get\s*\(/;
|
|
2668
|
+
const SAME_VALUE_BRANCH_RE = /^(\s*)(?:if|elif)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*==\s*["'][^"']+["']\s*:/;
|
|
2669
|
+
const INSTANCE_BRANCH_RE = /^(\s*)(?:if|elif)\s+isinstance\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^)]+\)\s*:/;
|
|
2670
|
+
const BRANCH_LADDER_THRESHOLD = 4;
|
|
2193
2671
|
const isTestFile$1 = (relPath, basename) => basename.startsWith("test_") || basename.endsWith("_test.py") || basename === "conftest.py" || relPath.split(path.sep).some((seg) => seg === "tests" || seg === "test");
|
|
2194
2672
|
const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
|
|
2195
2673
|
const SCRIPT_DIR_NAMES = new Set([
|
|
@@ -2242,6 +2720,13 @@ const pushFinding = (out, a) => {
|
|
|
2242
2720
|
fixable: false
|
|
2243
2721
|
});
|
|
2244
2722
|
};
|
|
2723
|
+
const pushLineFinding = (out, relPath, line, finding) => {
|
|
2724
|
+
pushFinding(out, {
|
|
2725
|
+
relPath,
|
|
2726
|
+
line,
|
|
2727
|
+
...finding
|
|
2728
|
+
});
|
|
2729
|
+
};
|
|
2245
2730
|
const flagBareExcept = (lines, relPath, out) => {
|
|
2246
2731
|
for (let i = 0; i < lines.length; i++) {
|
|
2247
2732
|
if (!BARE_EXCEPT_RE.test(lines[i])) continue;
|
|
@@ -2323,6 +2808,76 @@ const flagPrintInProduction = (lines, relPath, basename, out) => {
|
|
|
2323
2808
|
});
|
|
2324
2809
|
}
|
|
2325
2810
|
};
|
|
2811
|
+
const flagRangeLenLoops = (lines, relPath, out) => {
|
|
2812
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2813
|
+
const match = RANGE_LEN_LOOP_RE.exec(lines[i]);
|
|
2814
|
+
if (!match) continue;
|
|
2815
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2816
|
+
rule: "ai-slop/python-range-len-loop",
|
|
2817
|
+
severity: "info",
|
|
2818
|
+
message: `\`range(len(${match[2]}))\` loop — usually a hand-rolled iteration pattern.`,
|
|
2819
|
+
help: "Prefer direct iteration (`for item in items`) or `enumerate(items)` when the index is needed. Keeping index plumbing out of the loop reduces checkpoint-to-checkpoint bloat."
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
};
|
|
2823
|
+
const flagChainedDictGets = (lines, relPath, out) => {
|
|
2824
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2825
|
+
if (!CHAINED_DICT_GET_RE.test(lines[i])) continue;
|
|
2826
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2827
|
+
rule: "ai-slop/python-chained-dict-get",
|
|
2828
|
+
severity: "warning",
|
|
2829
|
+
message: "Chained `.get(..., {})` defaults hide missing-data cases.",
|
|
2830
|
+
help: "Normalize the input at the boundary, use a typed object, or split the lookup into explicit steps. Empty-dict fallback chains are a common agent shortcut that becomes brittle as schemas evolve."
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
};
|
|
2834
|
+
const countBranchLadder = (lines, start, pattern, selector, indent) => {
|
|
2835
|
+
let count = 1;
|
|
2836
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
2837
|
+
const line = lines[i];
|
|
2838
|
+
const trimmed = line.trim();
|
|
2839
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
2840
|
+
const match = pattern.exec(line);
|
|
2841
|
+
if (match?.[1] === indent && match[2] === selector) {
|
|
2842
|
+
count++;
|
|
2843
|
+
continue;
|
|
2844
|
+
}
|
|
2845
|
+
if (line.startsWith(`${indent}elif `)) break;
|
|
2846
|
+
if (line.length - line.trimStart().length <= indent.length && !line.startsWith(`${indent}else`)) break;
|
|
2847
|
+
}
|
|
2848
|
+
return count;
|
|
2849
|
+
};
|
|
2850
|
+
const flagBranchLadders = (lines, relPath, out) => {
|
|
2851
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2852
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2853
|
+
if (reported.has(i)) continue;
|
|
2854
|
+
const valueMatch = SAME_VALUE_BRANCH_RE.exec(lines[i]);
|
|
2855
|
+
if (valueMatch) {
|
|
2856
|
+
const count = countBranchLadder(lines, i, SAME_VALUE_BRANCH_RE, valueMatch[2], valueMatch[1]);
|
|
2857
|
+
if (count >= BRANCH_LADDER_THRESHOLD) {
|
|
2858
|
+
reported.add(i);
|
|
2859
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2860
|
+
rule: "ai-slop/python-repetitive-dispatch",
|
|
2861
|
+
severity: "warning",
|
|
2862
|
+
message: `${count} repeated branches dispatch on \`${valueMatch[2]}\`.`,
|
|
2863
|
+
help: "Use a table, set membership, or handler map when branches share the same shape. SlopCodeBench highlights these selector ladders as code that keeps growing instead of absorbing new cases cleanly."
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
continue;
|
|
2867
|
+
}
|
|
2868
|
+
const instanceMatch = INSTANCE_BRANCH_RE.exec(lines[i]);
|
|
2869
|
+
if (!instanceMatch) continue;
|
|
2870
|
+
const count = countBranchLadder(lines, i, INSTANCE_BRANCH_RE, instanceMatch[2], instanceMatch[1]);
|
|
2871
|
+
if (count < BRANCH_LADDER_THRESHOLD) continue;
|
|
2872
|
+
reported.add(i);
|
|
2873
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2874
|
+
rule: "ai-slop/python-isinstance-ladder",
|
|
2875
|
+
severity: "warning",
|
|
2876
|
+
message: `${count} repeated \`isinstance(${instanceMatch[2]}, ...)\` branches.`,
|
|
2877
|
+
help: "Prefer a handler map, protocol, or normalized intermediate representation when each type branch has the same role. Repeated type ladders are one of the maintainability smells SCBench-style checks look for."
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
};
|
|
2326
2881
|
const detectPythonPatterns = async (context) => {
|
|
2327
2882
|
const diagnostics = [];
|
|
2328
2883
|
const files = getSourceFiles(context);
|
|
@@ -2342,6 +2897,9 @@ const detectPythonPatterns = async (context) => {
|
|
|
2342
2897
|
flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
|
|
2343
2898
|
flagMutableDefaults(lines, relPath, diagnostics);
|
|
2344
2899
|
flagPrintInProduction(lines, relPath, basename, diagnostics);
|
|
2900
|
+
flagRangeLenLoops(lines, relPath, diagnostics);
|
|
2901
|
+
flagChainedDictGets(lines, relPath, diagnostics);
|
|
2902
|
+
flagBranchLadders(lines, relPath, diagnostics);
|
|
2345
2903
|
}
|
|
2346
2904
|
return diagnostics;
|
|
2347
2905
|
};
|
|
@@ -2482,6 +3040,143 @@ const detectRustPatterns = async (context) => {
|
|
|
2482
3040
|
return diagnostics;
|
|
2483
3041
|
};
|
|
2484
3042
|
|
|
3043
|
+
//#endregion
|
|
3044
|
+
//#region src/engines/ai-slop/silent-recovery.ts
|
|
3045
|
+
const JS_EXTS$1 = new Set([
|
|
3046
|
+
".ts",
|
|
3047
|
+
".tsx",
|
|
3048
|
+
".js",
|
|
3049
|
+
".jsx",
|
|
3050
|
+
".mjs",
|
|
3051
|
+
".cjs"
|
|
3052
|
+
]);
|
|
3053
|
+
const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
|
|
3054
|
+
const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
|
|
3055
|
+
const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
|
|
3056
|
+
const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
3057
|
+
const extractCatchBody = (content, openBraceIndex) => {
|
|
3058
|
+
let depth = 0;
|
|
3059
|
+
let inString = null;
|
|
3060
|
+
for (let i = openBraceIndex; i < content.length; i += 1) {
|
|
3061
|
+
const ch = content[i];
|
|
3062
|
+
const prev = content[i - 1];
|
|
3063
|
+
if (inString) {
|
|
3064
|
+
if (ch === inString && prev !== "\\") inString = null;
|
|
3065
|
+
continue;
|
|
3066
|
+
}
|
|
3067
|
+
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
3068
|
+
inString = ch;
|
|
3069
|
+
continue;
|
|
3070
|
+
}
|
|
3071
|
+
if (ch === "{") depth += 1;
|
|
3072
|
+
else if (ch === "}") {
|
|
3073
|
+
depth -= 1;
|
|
3074
|
+
if (depth === 0) return content.slice(openBraceIndex + 1, i);
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
return null;
|
|
3078
|
+
};
|
|
3079
|
+
const isLogOnlyBody = (body) => {
|
|
3080
|
+
const statements = stripBlockComments(body).split("\n").map((line) => line.replace(/\/\/.*$/, "").trim()).filter((line) => line.length > 0 && line !== ";");
|
|
3081
|
+
if (statements.length === 0) return false;
|
|
3082
|
+
if (statements.some((line) => HANDLING_TOKEN_RE.test(line))) return false;
|
|
3083
|
+
let sawLog = false;
|
|
3084
|
+
for (const statement of statements) {
|
|
3085
|
+
const normalized = statement.replace(/;+$/, "");
|
|
3086
|
+
if (LOG_STATEMENT_RE.test(normalized)) {
|
|
3087
|
+
sawLog = true;
|
|
3088
|
+
continue;
|
|
3089
|
+
}
|
|
3090
|
+
if (/^[\w$"'`{[(),.\s+:-]+$/.test(normalized) && !/[=(]\s*(?:async\s+)?\(/.test(normalized)) continue;
|
|
3091
|
+
return false;
|
|
3092
|
+
}
|
|
3093
|
+
return sawLog;
|
|
3094
|
+
};
|
|
3095
|
+
const detectJsSilentRecovery = (content, relPath) => {
|
|
3096
|
+
const out = [];
|
|
3097
|
+
CATCH_HEAD_RE.lastIndex = 0;
|
|
3098
|
+
let match;
|
|
3099
|
+
while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
|
|
3100
|
+
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3101
|
+
if (body === null) continue;
|
|
3102
|
+
if (!isLogOnlyBody(body)) continue;
|
|
3103
|
+
const line = content.slice(0, match.index).split("\n").length;
|
|
3104
|
+
out.push({
|
|
3105
|
+
filePath: relPath,
|
|
3106
|
+
engine: "ai-slop",
|
|
3107
|
+
rule: "ai-slop/silent-recovery",
|
|
3108
|
+
severity: "warning",
|
|
3109
|
+
message: "Catch only logs then continues, leaving execution in a possibly broken state",
|
|
3110
|
+
help: "Handle the error: rethrow, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
|
|
3111
|
+
line,
|
|
3112
|
+
column: 0,
|
|
3113
|
+
category: "AI Slop",
|
|
3114
|
+
fixable: false
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
return out;
|
|
3118
|
+
};
|
|
3119
|
+
const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
|
|
3120
|
+
const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
|
|
3121
|
+
const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
|
|
3122
|
+
const detectPySilentRecovery = (content, relPath) => {
|
|
3123
|
+
const out = [];
|
|
3124
|
+
const lines = content.split("\n");
|
|
3125
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
3126
|
+
const exceptMatch = PY_EXCEPT_RE.exec(lines[i]);
|
|
3127
|
+
if (!exceptMatch) continue;
|
|
3128
|
+
const indent = exceptMatch[1].length;
|
|
3129
|
+
const bodyLines = [];
|
|
3130
|
+
let j = i + 1;
|
|
3131
|
+
for (; j < lines.length; j += 1) {
|
|
3132
|
+
const raw = lines[j];
|
|
3133
|
+
if (raw.trim() === "") continue;
|
|
3134
|
+
if (raw.length - raw.trimStart().length <= indent) break;
|
|
3135
|
+
bodyLines.push(raw.trim());
|
|
3136
|
+
}
|
|
3137
|
+
if (bodyLines.length === 0) continue;
|
|
3138
|
+
if (bodyLines.some((line) => PY_HANDLING_TOKEN_RE.test(line))) continue;
|
|
3139
|
+
if (bodyLines.some((line) => line === "pass")) continue;
|
|
3140
|
+
const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
|
|
3141
|
+
const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
|
|
3142
|
+
if (!allLogs || !sawLog) continue;
|
|
3143
|
+
out.push({
|
|
3144
|
+
filePath: relPath,
|
|
3145
|
+
engine: "ai-slop",
|
|
3146
|
+
rule: "ai-slop/silent-recovery",
|
|
3147
|
+
severity: "warning",
|
|
3148
|
+
message: "except only logs then continues, leaving execution in a possibly broken state",
|
|
3149
|
+
help: "Handle the error: re-raise, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
|
|
3150
|
+
line: i + 1,
|
|
3151
|
+
column: 0,
|
|
3152
|
+
category: "AI Slop",
|
|
3153
|
+
fixable: false
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
return out;
|
|
3157
|
+
};
|
|
3158
|
+
const detectSilentRecovery = async (context) => {
|
|
3159
|
+
const files = getSourceFiles(context);
|
|
3160
|
+
const diagnostics = [];
|
|
3161
|
+
for (const filePath of files) {
|
|
3162
|
+
if (isAutoGenerated(filePath)) continue;
|
|
3163
|
+
const ext = path.extname(filePath);
|
|
3164
|
+
const isJs = JS_EXTS$1.has(ext);
|
|
3165
|
+
if (!isJs && !(ext === ".py")) continue;
|
|
3166
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
3167
|
+
if (isNonProductionPath(relPath)) continue;
|
|
3168
|
+
let content;
|
|
3169
|
+
try {
|
|
3170
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
3171
|
+
} catch {
|
|
3172
|
+
continue;
|
|
3173
|
+
}
|
|
3174
|
+
if (isJs) diagnostics.push(...detectJsSilentRecovery(content, relPath));
|
|
3175
|
+
else diagnostics.push(...detectPySilentRecovery(content, relPath));
|
|
3176
|
+
}
|
|
3177
|
+
return diagnostics;
|
|
3178
|
+
};
|
|
3179
|
+
|
|
2485
3180
|
//#endregion
|
|
2486
3181
|
//#region src/engines/ai-slop/unused-imports.ts
|
|
2487
3182
|
const JS_EXTENSIONS = new Set([
|
|
@@ -2670,15 +3365,19 @@ const aiSlopEngine = {
|
|
|
2670
3365
|
const results = await Promise.allSettled([
|
|
2671
3366
|
detectTrivialComments(context),
|
|
2672
3367
|
detectSwallowedExceptions(context),
|
|
3368
|
+
detectDefensivePatterns(context),
|
|
2673
3369
|
detectOverAbstraction(context),
|
|
2674
3370
|
detectDeadPatterns(context),
|
|
2675
3371
|
detectUnusedImports(context),
|
|
2676
3372
|
detectNarrativeComments(context),
|
|
2677
3373
|
detectDuplicateImports(context),
|
|
3374
|
+
detectHardcodedConfigLiterals(context),
|
|
2678
3375
|
detectPythonPatterns(context),
|
|
2679
3376
|
detectGoPatterns(context),
|
|
2680
3377
|
detectRustPatterns(context),
|
|
2681
|
-
detectHallucinatedImports(context)
|
|
3378
|
+
detectHallucinatedImports(context),
|
|
3379
|
+
detectSilentRecovery(context),
|
|
3380
|
+
detectMetaComments(context)
|
|
2682
3381
|
]);
|
|
2683
3382
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
2684
3383
|
return {
|
|
@@ -5079,7 +5778,7 @@ const RISKY_PATTERNS = [
|
|
|
5079
5778
|
help: "Use JSON, MessagePack, or other safe serialization formats for untrusted data"
|
|
5080
5779
|
},
|
|
5081
5780
|
{
|
|
5082
|
-
pattern: new RegExp(
|
|
5781
|
+
pattern: new RegExp(`(?<![\\w.>:\\\\])\\bexec\\s*\\(`, "g"),
|
|
5083
5782
|
extensions: [".py"],
|
|
5084
5783
|
name: "python-exec",
|
|
5085
5784
|
message: "Use of exec() can execute arbitrary code",
|
|
@@ -5757,7 +6456,7 @@ const handleAislopBaseline = (input) => {
|
|
|
5757
6456
|
|
|
5758
6457
|
//#endregion
|
|
5759
6458
|
//#region src/version.ts
|
|
5760
|
-
const APP_VERSION = "0.9.
|
|
6459
|
+
const APP_VERSION = "0.9.5";
|
|
5761
6460
|
|
|
5762
6461
|
//#endregion
|
|
5763
6462
|
//#region src/telemetry/env.ts
|
|
@@ -5889,8 +6588,8 @@ const redactProperties = (props) => {
|
|
|
5889
6588
|
|
|
5890
6589
|
//#endregion
|
|
5891
6590
|
//#region src/telemetry/client.ts
|
|
5892
|
-
const POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
5893
|
-
const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
6591
|
+
const POSTHOG_HOST = process.env.AISLOP_POSTHOG_HOST ?? "https://eu.i.posthog.com";
|
|
6592
|
+
const POSTHOG_KEY = process.env.AISLOP_POSTHOG_KEY ?? "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
5894
6593
|
const SCHEMA_VERSION = "v2";
|
|
5895
6594
|
const REQUEST_TIMEOUT_MS = 3e3;
|
|
5896
6595
|
const isTelemetryDisabled = (config) => {
|