aislop 0.9.2 → 0.9.4
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 +8 -2
- package/dist/cli.js +764 -274
- package/dist/index.js +765 -275
- package/dist/{json-DZHn6AE3.js → json-CXiEvR_M.js} +1 -1
- package/dist/mcp.js +748 -275
- package/dist/{version-C3JZkQGA.js → version-C45P3Q1N.js} +1 -1
- package/package.json +92 -91
package/dist/cli.js
CHANGED
|
@@ -34,7 +34,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region src/version.ts
|
|
37
|
-
const APP_VERSION = "0.9.
|
|
37
|
+
const APP_VERSION = "0.9.4";
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/telemetry/env.ts
|
|
@@ -851,6 +851,7 @@ const EXCLUDED_DIRS = [
|
|
|
851
851
|
"build",
|
|
852
852
|
".git",
|
|
853
853
|
".agents",
|
|
854
|
+
".pnpm-store",
|
|
854
855
|
"vendor",
|
|
855
856
|
"examples",
|
|
856
857
|
"example",
|
|
@@ -887,6 +888,7 @@ const FIND_PRUNE_DIRS = [
|
|
|
887
888
|
"build",
|
|
888
889
|
".git",
|
|
889
890
|
".agents",
|
|
891
|
+
".pnpm-store",
|
|
890
892
|
"vendor",
|
|
891
893
|
"examples",
|
|
892
894
|
"example",
|
|
@@ -910,7 +912,11 @@ const FIND_PRUNE_DIRS = [
|
|
|
910
912
|
".turbo",
|
|
911
913
|
"public"
|
|
912
914
|
];
|
|
913
|
-
const BUILD_CACHE_FILE_PATTERNS = [
|
|
915
|
+
const BUILD_CACHE_FILE_PATTERNS = [
|
|
916
|
+
/\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i,
|
|
917
|
+
/\.min\.(?:js|css|mjs|cjs)$/i,
|
|
918
|
+
/\.bundle\.(?:js|css|mjs|cjs)$/i
|
|
919
|
+
];
|
|
914
920
|
const isBuildCacheFile = (filePath) => BUILD_CACHE_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
915
921
|
const TEST_FILE_PATTERNS = [
|
|
916
922
|
/(?:^|\/).*\.test\.[^/]+$/i,
|
|
@@ -938,6 +944,17 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
|
938
944
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
939
945
|
const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
|
|
940
946
|
const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
947
|
+
const readBiomeExcludePatterns = (rootDirectory) => {
|
|
948
|
+
const biomePath = path.join(rootDirectory, "biome.json");
|
|
949
|
+
if (!fs.existsSync(biomePath)) return [];
|
|
950
|
+
try {
|
|
951
|
+
const includes = JSON.parse(fs.readFileSync(biomePath, "utf-8")).files?.includes;
|
|
952
|
+
if (!Array.isArray(includes)) return [];
|
|
953
|
+
return includes.filter((entry) => typeof entry === "string").filter((entry) => entry.startsWith("!") && entry.length > 1).map((entry) => entry.slice(1));
|
|
954
|
+
} catch {
|
|
955
|
+
return [];
|
|
956
|
+
}
|
|
957
|
+
};
|
|
941
958
|
const getIgnoredPaths = (rootDirectory, files) => {
|
|
942
959
|
if (files.length === 0) return /* @__PURE__ */ new Set();
|
|
943
960
|
const result = spawnSync("git", [
|
|
@@ -1005,7 +1022,8 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
|
|
|
1005
1022
|
};
|
|
1006
1023
|
}).filter(({ relativePath }) => isWithinProject(relativePath));
|
|
1007
1024
|
const ignoredPaths = getIgnoredPaths(rootDirectory, normalizedFiles.map(({ relativePath }) => relativePath));
|
|
1008
|
-
const
|
|
1025
|
+
const excludePatterns = [...readBiomeExcludePatterns(rootDirectory), ...exclude];
|
|
1026
|
+
const normalizedExcludePatterns = excludePatterns.length ? normalizeExcludePatterns(excludePatterns) : [];
|
|
1009
1027
|
const isUserExcluded = (relativePath) => {
|
|
1010
1028
|
if (!normalizedExcludePatterns.length) return false;
|
|
1011
1029
|
return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
|
|
@@ -1060,10 +1078,27 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
|
|
|
1060
1078
|
|
|
1061
1079
|
//#endregion
|
|
1062
1080
|
//#region src/engines/ai-slop/abstractions.ts
|
|
1081
|
+
const JS_EXTS$1 = new Set([
|
|
1082
|
+
".ts",
|
|
1083
|
+
".tsx",
|
|
1084
|
+
".js",
|
|
1085
|
+
".jsx",
|
|
1086
|
+
".mjs",
|
|
1087
|
+
".cjs"
|
|
1088
|
+
]);
|
|
1063
1089
|
const THIN_WRAPPER_PATTERNS = [
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1090
|
+
{
|
|
1091
|
+
pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
|
|
1092
|
+
extensions: JS_EXTS$1
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
1096
|
+
extensions: JS_EXTS$1
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
1100
|
+
extensions: new Set([".py"])
|
|
1101
|
+
}
|
|
1067
1102
|
];
|
|
1068
1103
|
const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
|
|
1069
1104
|
const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
|
|
@@ -1078,10 +1113,11 @@ const hasHardcodedArgs = (matchText) => {
|
|
|
1078
1113
|
return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(innerCallMatch[1]);
|
|
1079
1114
|
};
|
|
1080
1115
|
const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
|
|
1081
|
-
const detectThinWrappers = (content, relativePath) => {
|
|
1116
|
+
const detectThinWrappers = (content, relativePath, ext) => {
|
|
1082
1117
|
const diagnostics = [];
|
|
1083
1118
|
const lines = content.split("\n");
|
|
1084
|
-
for (const pattern of THIN_WRAPPER_PATTERNS) {
|
|
1119
|
+
for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
|
|
1120
|
+
if (!extensions.has(ext)) continue;
|
|
1085
1121
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
1086
1122
|
let match;
|
|
1087
1123
|
while ((match = regex.exec(content)) !== null) {
|
|
@@ -1147,12 +1183,133 @@ const detectOverAbstraction = async (context) => {
|
|
|
1147
1183
|
continue;
|
|
1148
1184
|
}
|
|
1149
1185
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1150
|
-
|
|
1186
|
+
const ext = path.extname(filePath);
|
|
1187
|
+
diagnostics.push(...detectThinWrappers(content, relativePath, ext));
|
|
1151
1188
|
diagnostics.push(...detectAiNaming(content, relativePath));
|
|
1152
1189
|
}
|
|
1153
1190
|
return diagnostics;
|
|
1154
1191
|
};
|
|
1155
1192
|
|
|
1193
|
+
//#endregion
|
|
1194
|
+
//#region src/engines/ai-slop/narrative-comments-patterns.ts
|
|
1195
|
+
const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
|
|
1196
|
+
const DECORATIVE_SECTION_HEADER = /^[-=─━~_*#]{3,}[\s\S]+?[-=─━~_*#]{3,}$/;
|
|
1197
|
+
const SECTION_HEADER = /^(Phase|Step|Section|Part)\s+\d+[:.-]/i;
|
|
1198
|
+
const CROSS_REFERENCE_PHRASES = [
|
|
1199
|
+
/\bwill then be\b/i,
|
|
1200
|
+
/\bused by\b/i,
|
|
1201
|
+
/\bcalled from\b/i,
|
|
1202
|
+
/\bcalled later\b/i,
|
|
1203
|
+
/\bsee (?:above|below|later|earlier)\b/i,
|
|
1204
|
+
/\breplaces the\b/i,
|
|
1205
|
+
/\bmatches the one\b/i,
|
|
1206
|
+
/\bwe moved\b/i,
|
|
1207
|
+
/\bwe used to\b/i,
|
|
1208
|
+
/\brefactor(?:ed)? from\b/i,
|
|
1209
|
+
/\bcombined with\b.*\bthis\b/i
|
|
1210
|
+
];
|
|
1211
|
+
const JUSTIFICATION_OPENERS = [
|
|
1212
|
+
/^(The idea here|The trick is|This was needed|Originally,?)/i,
|
|
1213
|
+
/^This\s+(?:function|method|class|module|component|hook|util|helper|handler|service)\b/i,
|
|
1214
|
+
/^It\s+(?:does|handles|takes|returns|processes|reads|writes|sends|fetches|loads|creates|deletes|updates|parses|validates)\b/i,
|
|
1215
|
+
/^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
|
|
1216
|
+
];
|
|
1217
|
+
const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
|
|
1218
|
+
const STEP_COMMENT_VERB_RE = /^(?:Render|Enable|Disable|Initialize|Init|Setup|Set|Get|Fetch|Load|Save|Build|Create|Delete|Remove|Add|Update|Process|Execute|Run|Start|Stop|Clean|Cleanup|Configure|Validate|Check|Verify|Parse|Extract|Apply|Wait|Sleep|Skip|Allow|Deny|Lock|Unlock|Refresh|Reload|Reset|Clear|Send|Receive|Read|Write|Print|Log|Emit|Dispatch|Fire|Open|Close|Bind|Connect|Disconnect|Register|Unregister|Push|Pop|Insert|Append|Prepend|Sort|Filter|Find|Search|Replace|Encode|Decode|Convert|Transform|Map|Reduce|Iterate|Loop|Walk|Visit|Mark|Unmark|Toggle|Switch|Restart|Resume|Pause|Abort|Cancel|Compute|Calculate|Resolve|Reject|Ignore|Handle|Track|Trace|Increment|Decrement|Round|Truncate|Resize|Move|Copy|Clone|Merge|Split|Join|Wrap|Unwrap|Bump|Drain|Flush|Sync|Persist|Commit|Rollback|Yield|Return|Discard|Defer|Pin|Unpin|Mount|Unmount|Spawn|Kill|Restore)(?:\s|$)/;
|
|
1219
|
+
const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design)\b/i;
|
|
1220
|
+
const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
1221
|
+
"deprecated",
|
|
1222
|
+
"see",
|
|
1223
|
+
"example",
|
|
1224
|
+
"type",
|
|
1225
|
+
"returns",
|
|
1226
|
+
"return",
|
|
1227
|
+
"param",
|
|
1228
|
+
"throws",
|
|
1229
|
+
"typedef",
|
|
1230
|
+
"callback",
|
|
1231
|
+
"override",
|
|
1232
|
+
"template",
|
|
1233
|
+
"internal",
|
|
1234
|
+
"public",
|
|
1235
|
+
"private",
|
|
1236
|
+
"protected",
|
|
1237
|
+
"experimental",
|
|
1238
|
+
"alpha",
|
|
1239
|
+
"beta",
|
|
1240
|
+
"since",
|
|
1241
|
+
"todo",
|
|
1242
|
+
"link",
|
|
1243
|
+
"license",
|
|
1244
|
+
"preserve",
|
|
1245
|
+
"swagger",
|
|
1246
|
+
"openapi",
|
|
1247
|
+
"route",
|
|
1248
|
+
"group",
|
|
1249
|
+
"summary",
|
|
1250
|
+
"description",
|
|
1251
|
+
"operationid",
|
|
1252
|
+
"response",
|
|
1253
|
+
"responses",
|
|
1254
|
+
"request",
|
|
1255
|
+
"requestbody",
|
|
1256
|
+
"security",
|
|
1257
|
+
"tag",
|
|
1258
|
+
"tags",
|
|
1259
|
+
"path",
|
|
1260
|
+
"body",
|
|
1261
|
+
"query",
|
|
1262
|
+
"queryparam",
|
|
1263
|
+
"header",
|
|
1264
|
+
"headers",
|
|
1265
|
+
"produces",
|
|
1266
|
+
"accept",
|
|
1267
|
+
"middleware",
|
|
1268
|
+
"api",
|
|
1269
|
+
"apiname",
|
|
1270
|
+
"apidefine",
|
|
1271
|
+
"apigroup",
|
|
1272
|
+
"apiparam",
|
|
1273
|
+
"apiquery",
|
|
1274
|
+
"apibody",
|
|
1275
|
+
"apiheader",
|
|
1276
|
+
"apisuccess",
|
|
1277
|
+
"apierror",
|
|
1278
|
+
"apiexample",
|
|
1279
|
+
"apiversion",
|
|
1280
|
+
"apidescription",
|
|
1281
|
+
"apipermission",
|
|
1282
|
+
"apiuse",
|
|
1283
|
+
"apiignore",
|
|
1284
|
+
"apiprivate",
|
|
1285
|
+
"namespace",
|
|
1286
|
+
"category"
|
|
1287
|
+
]);
|
|
1288
|
+
const SUPPORTED_EXTS = new Set([
|
|
1289
|
+
".ts",
|
|
1290
|
+
".tsx",
|
|
1291
|
+
".js",
|
|
1292
|
+
".jsx",
|
|
1293
|
+
".mjs",
|
|
1294
|
+
".cjs",
|
|
1295
|
+
".py",
|
|
1296
|
+
".go",
|
|
1297
|
+
".rs",
|
|
1298
|
+
".rb",
|
|
1299
|
+
".java",
|
|
1300
|
+
".php"
|
|
1301
|
+
]);
|
|
1302
|
+
const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
|
|
1303
|
+
const EXPORT_DEFAULT = /^\s*export\s+default\b/;
|
|
1304
|
+
const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
|
|
1305
|
+
const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
|
|
1306
|
+
const GO_DECL_START = /^\s*(func|type|var|const|import)\b/;
|
|
1307
|
+
const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
|
|
1308
|
+
const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
|
|
1309
|
+
const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
|
|
1310
|
+
const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
|
|
1311
|
+
const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract|readonly)\s+)*(function|class|interface|trait|enum|const)\s+/;
|
|
1312
|
+
|
|
1156
1313
|
//#endregion
|
|
1157
1314
|
//#region src/engines/ai-slop/non-production-paths.ts
|
|
1158
1315
|
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
@@ -1191,11 +1348,37 @@ const isTrivialComment = (trimmed, nextLine) => {
|
|
|
1191
1348
|
if (/^-{3,}|─{3,}/.test(commentBody)) return false;
|
|
1192
1349
|
return (isJs ? TRIVIAL_JS_COMMENT_PATTERNS : TRIVIAL_PYTHON_COMMENT_PATTERNS).some((pattern) => pattern.test(trimmed));
|
|
1193
1350
|
};
|
|
1194
|
-
const
|
|
1351
|
+
const declStartForExt = (ext) => {
|
|
1352
|
+
switch (ext) {
|
|
1353
|
+
case ".rb": return [RUBY_DECL_START];
|
|
1354
|
+
case ".java": return [JAVA_DECL_START, JAVA_DECL_START_FALLBACK];
|
|
1355
|
+
case ".php": return [PHP_DECL_START];
|
|
1356
|
+
default: return [];
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
const isCommentLineForExt = (line, ext) => {
|
|
1360
|
+
const trimmed = line.trim();
|
|
1361
|
+
if (ext === ".rb") return trimmed.startsWith("#") && !trimmed.startsWith("#!");
|
|
1362
|
+
if (ext === ".java" || ext === ".php") return trimmed.startsWith("//") || trimmed.startsWith("#");
|
|
1363
|
+
return false;
|
|
1364
|
+
};
|
|
1365
|
+
const isDocCommentForDeclaration = (lines, lineIdx, ext) => {
|
|
1366
|
+
const patterns = declStartForExt(ext);
|
|
1367
|
+
if (patterns.length === 0) return false;
|
|
1368
|
+
for (let j = lineIdx + 1; j < lines.length; j++) {
|
|
1369
|
+
const candidate = lines[j];
|
|
1370
|
+
if (candidate.trim() === "") continue;
|
|
1371
|
+
if (isCommentLineForExt(candidate, ext)) continue;
|
|
1372
|
+
return patterns.some((re) => re.test(candidate));
|
|
1373
|
+
}
|
|
1374
|
+
return false;
|
|
1375
|
+
};
|
|
1376
|
+
const scanFileForTrivialComments = (content, relativePath, ext) => {
|
|
1195
1377
|
const diagnostics = [];
|
|
1196
1378
|
const lines = content.split("\n");
|
|
1197
1379
|
for (let i = 0; i < lines.length; i++) {
|
|
1198
1380
|
if (!isTrivialComment(lines[i].trim(), i + 1 < lines.length ? lines[i + 1] : void 0)) continue;
|
|
1381
|
+
if (isDocCommentForDeclaration(lines, i, ext)) continue;
|
|
1199
1382
|
diagnostics.push({
|
|
1200
1383
|
filePath: relativePath,
|
|
1201
1384
|
engine: "ai-slop",
|
|
@@ -1224,7 +1407,7 @@ const detectTrivialComments = async (context) => {
|
|
|
1224
1407
|
} catch {
|
|
1225
1408
|
continue;
|
|
1226
1409
|
}
|
|
1227
|
-
diagnostics.push(...scanFileForTrivialComments(content, relativePath));
|
|
1410
|
+
diagnostics.push(...scanFileForTrivialComments(content, relativePath, path.extname(filePath)));
|
|
1228
1411
|
}
|
|
1229
1412
|
return diagnostics;
|
|
1230
1413
|
};
|
|
@@ -1280,6 +1463,19 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
|
|
|
1280
1463
|
"PLACEHOLDER",
|
|
1281
1464
|
"STUB"
|
|
1282
1465
|
].join("|")})[:\\s]`);
|
|
1466
|
+
const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
|
|
1467
|
+
const isGuardedSingleLineExit = (lines, lineIndex) => {
|
|
1468
|
+
const contextLines = [];
|
|
1469
|
+
for (let i = lineIndex - 1; i >= 0 && contextLines.length < 16; i--) {
|
|
1470
|
+
const trimmed = lines[i].trim();
|
|
1471
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
1472
|
+
contextLines.unshift(trimmed);
|
|
1473
|
+
if (/^(?:if|else\s+if|for|while)\b/.test(trimmed) || /^}\s*else\s+if\b/.test(trimmed)) break;
|
|
1474
|
+
if (/;\s*$/.test(trimmed)) break;
|
|
1475
|
+
}
|
|
1476
|
+
const control = contextLines.join(" ");
|
|
1477
|
+
return /(?:^|[}\s])(?:if|else\s+if|for|while)\s*\(/.test(control) && !/{\s*$/.test(control);
|
|
1478
|
+
};
|
|
1283
1479
|
const detectTodoStubs = (content, relativePath) => {
|
|
1284
1480
|
const diagnostics = [];
|
|
1285
1481
|
const lines = content.split("\n");
|
|
@@ -1296,7 +1492,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1296
1492
|
for (let i = 0; i < lines.length; i++) {
|
|
1297
1493
|
const trimmed = lines[i].trim();
|
|
1298
1494
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
1299
|
-
if (JS_EXTENSIONS$4.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !
|
|
1495
|
+
if (JS_EXTENSIONS$4.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !isGuardedSingleLineExit(lines, i) && !isBlockCloserAfterReturn(nextLine) && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push(slop(relativePath, i + 2, "ai-slop/unreachable-code", "warning", "Code after return/throw statement is unreachable", "Remove the unreachable code or restructure the control flow", false));
|
|
1300
1496
|
if (/\bif\s*\(\s*(?:false|true|0|1)\s*\)/.test(trimmed) && !trimmed.startsWith("//") && !trimmed.startsWith("*") && !/["'`].*\bif\s*\(/.test(trimmed) && !/\/.*\bif\s*\(/.test(trimmed.replace(/\/\/.*$/, ""))) diagnostics.push(slop(relativePath, i + 1, "ai-slop/constant-condition", "warning", "Conditional with a constant value — likely debugging leftover", "Remove the constant condition or replace with proper logic", false));
|
|
1301
1497
|
if (JS_EXTENSIONS$4.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
|
|
1302
1498
|
}
|
|
@@ -1464,6 +1660,25 @@ const SWALLOWED_EXCEPTION_PATTERNS = [
|
|
|
1464
1660
|
message: "Empty catch block swallows errors silently"
|
|
1465
1661
|
}
|
|
1466
1662
|
];
|
|
1663
|
+
const INTENTIONAL_IGNORE_NAMES = new Set([
|
|
1664
|
+
"ignored",
|
|
1665
|
+
"ignore",
|
|
1666
|
+
"tolerated",
|
|
1667
|
+
"expected",
|
|
1668
|
+
"unused",
|
|
1669
|
+
"_",
|
|
1670
|
+
"_e",
|
|
1671
|
+
"_err",
|
|
1672
|
+
"_ex",
|
|
1673
|
+
"_t"
|
|
1674
|
+
]);
|
|
1675
|
+
const CATCH_PARAM_RE = /catch\s*\(\s*(?:\w+\s+)?([\w$]+)/;
|
|
1676
|
+
const RESCUE_PARAM_RE = /rescue(?:\s+[\w:]+)?\s*=>\s*([\w$]+)/;
|
|
1677
|
+
const isIntentionalIgnore = (matchText, ext) => {
|
|
1678
|
+
const m = (ext === ".rb" ? RESCUE_PARAM_RE : CATCH_PARAM_RE).exec(matchText);
|
|
1679
|
+
if (!m) return false;
|
|
1680
|
+
return INTENTIONAL_IGNORE_NAMES.has(m[1].toLowerCase());
|
|
1681
|
+
};
|
|
1467
1682
|
const detectSwallowedExceptions = async (context) => {
|
|
1468
1683
|
const files = getSourceFiles(context);
|
|
1469
1684
|
const diagnostics = [];
|
|
@@ -1482,6 +1697,7 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
1482
1697
|
let match;
|
|
1483
1698
|
const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
|
|
1484
1699
|
while ((match = regex.exec(content)) !== null) {
|
|
1700
|
+
if (isIntentionalIgnore(match[0], ext)) continue;
|
|
1485
1701
|
const line = content.slice(0, match.index).split("\n").length;
|
|
1486
1702
|
diagnostics.push({
|
|
1487
1703
|
filePath: relativePath,
|
|
@@ -1574,6 +1790,117 @@ const detectGoPatterns = async (context) => {
|
|
|
1574
1790
|
return diagnostics;
|
|
1575
1791
|
};
|
|
1576
1792
|
|
|
1793
|
+
//#endregion
|
|
1794
|
+
//#region src/engines/ai-slop/js-import-aliases.ts
|
|
1795
|
+
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
1796
|
+
const JS_RESOLUTION_EXTENSIONS = [
|
|
1797
|
+
"",
|
|
1798
|
+
".ts",
|
|
1799
|
+
".tsx",
|
|
1800
|
+
".js",
|
|
1801
|
+
".jsx",
|
|
1802
|
+
".mjs",
|
|
1803
|
+
".cjs",
|
|
1804
|
+
".json",
|
|
1805
|
+
"/index.ts",
|
|
1806
|
+
"/index.tsx",
|
|
1807
|
+
"/index.js",
|
|
1808
|
+
"/index.jsx"
|
|
1809
|
+
];
|
|
1810
|
+
const readJson$2 = (filePath) => {
|
|
1811
|
+
try {
|
|
1812
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1813
|
+
} catch {
|
|
1814
|
+
return null;
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
const buildAliasMatcher = (key) => {
|
|
1818
|
+
const starIdx = key.indexOf("*");
|
|
1819
|
+
if (starIdx === -1) return (spec) => spec === key;
|
|
1820
|
+
const before = key.slice(0, starIdx);
|
|
1821
|
+
const after = key.slice(starIdx + 1);
|
|
1822
|
+
return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
|
|
1823
|
+
};
|
|
1824
|
+
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
1825
|
+
const opts = readJson$2(configPath)?.compilerOptions;
|
|
1826
|
+
if (!opts || typeof opts !== "object") return;
|
|
1827
|
+
const configDir = path.dirname(configPath);
|
|
1828
|
+
const paths = opts.paths;
|
|
1829
|
+
if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
|
|
1830
|
+
const baseUrl = opts.baseUrl;
|
|
1831
|
+
if (typeof baseUrl === "string") {
|
|
1832
|
+
const baseDir = path.resolve(configDir, baseUrl);
|
|
1833
|
+
matchers.push((spec) => {
|
|
1834
|
+
if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("@")) return false;
|
|
1835
|
+
return JS_RESOLUTION_EXTENSIONS.some((suffix) => fs.existsSync(path.join(baseDir, `${spec}${suffix}`)));
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
const collectTsPathAliases = (rootDir, workspaceDirs) => {
|
|
1840
|
+
const matchers = [];
|
|
1841
|
+
const dirs = [rootDir, ...workspaceDirs];
|
|
1842
|
+
for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
|
|
1843
|
+
return matchers;
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1846
|
+
//#endregion
|
|
1847
|
+
//#region src/engines/ai-slop/js-workspaces.ts
|
|
1848
|
+
const readJson$1 = (filePath) => {
|
|
1849
|
+
try {
|
|
1850
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1851
|
+
} catch {
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
1856
|
+
const globs = [];
|
|
1857
|
+
if (rootPkg && typeof rootPkg === "object") {
|
|
1858
|
+
const ws = rootPkg.workspaces;
|
|
1859
|
+
if (Array.isArray(ws)) {
|
|
1860
|
+
for (const g of ws) if (typeof g === "string") globs.push(g);
|
|
1861
|
+
} else if (ws && typeof ws === "object") {
|
|
1862
|
+
const pkgs = ws.packages;
|
|
1863
|
+
if (Array.isArray(pkgs)) {
|
|
1864
|
+
for (const g of pkgs) if (typeof g === "string") globs.push(g);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
const lerna = readJson$1(path.join(rootDir, "lerna.json"));
|
|
1869
|
+
if (lerna && Array.isArray(lerna.packages)) {
|
|
1870
|
+
for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
|
|
1871
|
+
}
|
|
1872
|
+
try {
|
|
1873
|
+
const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
|
|
1874
|
+
let inPackages = false;
|
|
1875
|
+
for (const rawLine of pnpmWs.split("\n")) {
|
|
1876
|
+
if (/^packages\s*:\s*$/.test(rawLine)) {
|
|
1877
|
+
inPackages = true;
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (!inPackages) continue;
|
|
1881
|
+
if (/^\S/.test(rawLine)) break;
|
|
1882
|
+
const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
|
|
1883
|
+
if (m) globs.push(m[1].trim());
|
|
1884
|
+
}
|
|
1885
|
+
} catch {
|
|
1886
|
+
return globs;
|
|
1887
|
+
}
|
|
1888
|
+
return globs;
|
|
1889
|
+
};
|
|
1890
|
+
const expandWorkspaceDirs = (rootDir, globs) => {
|
|
1891
|
+
const dirs = [];
|
|
1892
|
+
for (const glob of globs) if (glob.endsWith("/*")) {
|
|
1893
|
+
const parent = path.join(rootDir, glob.slice(0, -2));
|
|
1894
|
+
try {
|
|
1895
|
+
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
1896
|
+
} catch {
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
} else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
|
|
1900
|
+
return dirs;
|
|
1901
|
+
};
|
|
1902
|
+
const collectWorkspaceDirs = (rootDir, rootPkg) => expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, rootPkg));
|
|
1903
|
+
|
|
1577
1904
|
//#endregion
|
|
1578
1905
|
//#region src/engines/ai-slop/python-data.ts
|
|
1579
1906
|
const PYTHON_STDLIB = new Set([
|
|
@@ -1818,11 +2145,35 @@ const collectFromPipfile = (rootDir, pyDeps) => {
|
|
|
1818
2145
|
return false;
|
|
1819
2146
|
}
|
|
1820
2147
|
};
|
|
2148
|
+
const LOCAL_PACKAGE_ROOTS = [
|
|
2149
|
+
"",
|
|
2150
|
+
"src",
|
|
2151
|
+
"lib"
|
|
2152
|
+
];
|
|
2153
|
+
const collectLocalPythonPackages = (rootDir, pyDeps) => {
|
|
2154
|
+
for (const sub of LOCAL_PACKAGE_ROOTS) {
|
|
2155
|
+
const dir = sub ? path.join(rootDir, sub) : rootDir;
|
|
2156
|
+
let entries;
|
|
2157
|
+
try {
|
|
2158
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2159
|
+
} catch {
|
|
2160
|
+
continue;
|
|
2161
|
+
}
|
|
2162
|
+
for (const entry of entries) {
|
|
2163
|
+
if (!entry.isDirectory()) continue;
|
|
2164
|
+
if (entry.name.startsWith(".")) continue;
|
|
2165
|
+
if (entry.name === "node_modules" || entry.name === "__pycache__") continue;
|
|
2166
|
+
const initPath = path.join(dir, entry.name, "__init__.py");
|
|
2167
|
+
if (fs.existsSync(initPath)) addPyDep(pyDeps, entry.name);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
1821
2171
|
const collectPythonDeps = (rootDir) => {
|
|
1822
2172
|
const pyDeps = /* @__PURE__ */ new Set();
|
|
1823
2173
|
const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
|
|
1824
2174
|
const hasPyproject = collectFromPyproject(rootDir, pyDeps);
|
|
1825
2175
|
const hasPipfile = collectFromPipfile(rootDir, pyDeps);
|
|
2176
|
+
collectLocalPythonPackages(rootDir, pyDeps);
|
|
1826
2177
|
return {
|
|
1827
2178
|
pyDeps,
|
|
1828
2179
|
hasPyManifest: hasReq || hasPyproject || hasPipfile
|
|
@@ -1859,51 +2210,6 @@ const addDepsFromPkg = (pkg, jsDeps) => {
|
|
|
1859
2210
|
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
|
|
1860
2211
|
}
|
|
1861
2212
|
};
|
|
1862
|
-
const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
1863
|
-
const globs = [];
|
|
1864
|
-
if (rootPkg && typeof rootPkg === "object") {
|
|
1865
|
-
const ws = rootPkg.workspaces;
|
|
1866
|
-
if (Array.isArray(ws)) {
|
|
1867
|
-
for (const g of ws) if (typeof g === "string") globs.push(g);
|
|
1868
|
-
} else if (ws && typeof ws === "object") {
|
|
1869
|
-
const pkgs = ws.packages;
|
|
1870
|
-
if (Array.isArray(pkgs)) {
|
|
1871
|
-
for (const g of pkgs) if (typeof g === "string") globs.push(g);
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
const lerna = readJson(path.join(rootDir, "lerna.json"));
|
|
1876
|
-
if (lerna && Array.isArray(lerna.packages)) {
|
|
1877
|
-
for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
|
|
1878
|
-
}
|
|
1879
|
-
try {
|
|
1880
|
-
const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
|
|
1881
|
-
let inPackages = false;
|
|
1882
|
-
for (const rawLine of pnpmWs.split("\n")) {
|
|
1883
|
-
if (/^packages\s*:\s*$/.test(rawLine)) {
|
|
1884
|
-
inPackages = true;
|
|
1885
|
-
continue;
|
|
1886
|
-
}
|
|
1887
|
-
if (!inPackages) continue;
|
|
1888
|
-
if (/^\S/.test(rawLine)) break;
|
|
1889
|
-
const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
|
|
1890
|
-
if (m) globs.push(m[1].trim());
|
|
1891
|
-
}
|
|
1892
|
-
} catch {}
|
|
1893
|
-
return globs;
|
|
1894
|
-
};
|
|
1895
|
-
const expandWorkspaceDirs = (rootDir, globs) => {
|
|
1896
|
-
const dirs = [];
|
|
1897
|
-
for (const glob of globs) if (glob.endsWith("/*")) {
|
|
1898
|
-
const parent = path.join(rootDir, glob.slice(0, -2));
|
|
1899
|
-
try {
|
|
1900
|
-
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
1901
|
-
} catch {
|
|
1902
|
-
continue;
|
|
1903
|
-
}
|
|
1904
|
-
} else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
|
|
1905
|
-
return dirs;
|
|
1906
|
-
};
|
|
1907
2213
|
const SKIP_DIRS = new Set([
|
|
1908
2214
|
"node_modules",
|
|
1909
2215
|
".git",
|
|
@@ -1943,54 +2249,17 @@ const collectJsDeps = (rootDir, jsDeps) => {
|
|
|
1943
2249
|
if (!fs.existsSync(pkgPath)) return false;
|
|
1944
2250
|
const pkg = readJson(pkgPath);
|
|
1945
2251
|
if (!pkg || typeof pkg !== "object") return false;
|
|
1946
|
-
addDepsFromPkg(pkg, jsDeps);
|
|
1947
|
-
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
1948
|
-
const workspaceDirs =
|
|
1949
|
-
for (const wsDir of workspaceDirs) {
|
|
1950
|
-
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
1951
|
-
if (!wsPkg) continue;
|
|
1952
|
-
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
1953
|
-
addDepsFromPkg(wsPkg, jsDeps);
|
|
1954
|
-
}
|
|
1955
|
-
collectNestedManifests(rootDir, jsDeps);
|
|
1956
|
-
return true;
|
|
1957
|
-
};
|
|
1958
|
-
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
1959
|
-
const buildAliasMatcher = (key) => {
|
|
1960
|
-
const starIdx = key.indexOf("*");
|
|
1961
|
-
if (starIdx === -1) return (spec) => spec === key;
|
|
1962
|
-
const before = key.slice(0, starIdx);
|
|
1963
|
-
const after = key.slice(starIdx + 1);
|
|
1964
|
-
return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
|
|
1965
|
-
};
|
|
1966
|
-
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
1967
|
-
const opts = readJson(configPath)?.compilerOptions;
|
|
1968
|
-
if (!opts) return;
|
|
1969
|
-
const paths = opts.paths;
|
|
1970
|
-
if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
|
|
1971
|
-
const baseUrl = opts.baseUrl;
|
|
1972
|
-
if (typeof baseUrl === "string") {
|
|
1973
|
-
const baseUrlDir = path.resolve(path.dirname(configPath), baseUrl);
|
|
1974
|
-
let entries;
|
|
1975
|
-
try {
|
|
1976
|
-
entries = fs.readdirSync(baseUrlDir);
|
|
1977
|
-
} catch {
|
|
1978
|
-
return;
|
|
1979
|
-
}
|
|
1980
|
-
const baseSpecifiers = /* @__PURE__ */ new Set();
|
|
1981
|
-
for (const entry of entries) {
|
|
1982
|
-
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
1983
|
-
const base = entry.replace(/\.(?:[jt]sx?|mjs|cjs|d\.ts)$/i, "");
|
|
1984
|
-
if (base.length > 0) baseSpecifiers.add(base);
|
|
1985
|
-
}
|
|
1986
|
-
for (const name of baseSpecifiers) matchers.push((spec) => spec === name || spec.startsWith(`${name}/`));
|
|
2252
|
+
addDepsFromPkg(pkg, jsDeps);
|
|
2253
|
+
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
2254
|
+
const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
|
|
2255
|
+
for (const wsDir of workspaceDirs) {
|
|
2256
|
+
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
2257
|
+
if (!wsPkg) continue;
|
|
2258
|
+
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
2259
|
+
addDepsFromPkg(wsPkg, jsDeps);
|
|
1987
2260
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
const matchers = [];
|
|
1991
|
-
const dirs = [rootDir, ...expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, readJson(path.join(rootDir, "package.json"))))];
|
|
1992
|
-
for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
|
|
1993
|
-
return matchers;
|
|
2261
|
+
collectNestedManifests(rootDir, jsDeps);
|
|
2262
|
+
return true;
|
|
1994
2263
|
};
|
|
1995
2264
|
const loadManifest = (rootDir) => {
|
|
1996
2265
|
const jsDeps = /* @__PURE__ */ new Set();
|
|
@@ -2013,14 +2282,19 @@ const VIRTUAL_MODULE_PREFIXES = [
|
|
|
2013
2282
|
"astro:",
|
|
2014
2283
|
"virtual:",
|
|
2015
2284
|
"bun:",
|
|
2016
|
-
"
|
|
2285
|
+
"file:"
|
|
2017
2286
|
];
|
|
2018
|
-
const isJsVirtualModule = (spec
|
|
2287
|
+
const isJsVirtualModule = (spec, manifest) => {
|
|
2288
|
+
if (VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p))) return true;
|
|
2289
|
+
if (spec === "bun") return true;
|
|
2290
|
+
if (spec === "unfonts.css" && manifest.jsDeps.has("unplugin-fonts")) return true;
|
|
2291
|
+
if (spec.startsWith("~icons/") && manifest.jsDeps.has("unplugin-icons")) return true;
|
|
2292
|
+
return false;
|
|
2293
|
+
};
|
|
2019
2294
|
const stripImportQuery = (spec) => {
|
|
2020
2295
|
const idx = spec.indexOf("?");
|
|
2021
2296
|
return idx === -1 ? spec : spec.slice(0, idx);
|
|
2022
2297
|
};
|
|
2023
|
-
const VIRTUAL_ASSET_FILES = { "unfonts.css": "unplugin-fonts" };
|
|
2024
2298
|
const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
|
|
2025
2299
|
const isLikelyRealImportSpec = (spec) => {
|
|
2026
2300
|
if (spec.length === 0) return false;
|
|
@@ -2092,9 +2366,7 @@ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
|
|
|
2092
2366
|
if (spec.length === 0) return null;
|
|
2093
2367
|
if (isJsRelativeOrAbsolute(spec)) return null;
|
|
2094
2368
|
if (isJsBuiltin(spec)) return null;
|
|
2095
|
-
if (isJsVirtualModule(spec)) return null;
|
|
2096
|
-
const virtualOwner = VIRTUAL_ASSET_FILES[spec];
|
|
2097
|
-
if (virtualOwner && manifest.jsDeps.has(virtualOwner)) return null;
|
|
2369
|
+
if (isJsVirtualModule(spec, manifest)) return null;
|
|
2098
2370
|
if (tsAliasMatchers.some((m) => m(spec))) return null;
|
|
2099
2371
|
const pkg = packageNameFromImport(spec);
|
|
2100
2372
|
if (manifest.jsDeps.has(pkg)) return null;
|
|
@@ -2114,9 +2386,11 @@ const checkPyImport = (spec, manifest) => {
|
|
|
2114
2386
|
return root;
|
|
2115
2387
|
};
|
|
2116
2388
|
const detectHallucinatedImports = async (context) => {
|
|
2389
|
+
const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
|
|
2390
|
+
const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
|
|
2117
2391
|
const manifest = loadManifest(context.rootDirectory);
|
|
2118
2392
|
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
2119
|
-
const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory) : [];
|
|
2393
|
+
const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory, workspaceDirs) : [];
|
|
2120
2394
|
const diagnostics = [];
|
|
2121
2395
|
const files = getSourceFiles(context);
|
|
2122
2396
|
for (const filePath of files) {
|
|
@@ -2157,121 +2431,7 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2157
2431
|
};
|
|
2158
2432
|
|
|
2159
2433
|
//#endregion
|
|
2160
|
-
//#region src/engines/ai-slop/
|
|
2161
|
-
const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
|
|
2162
|
-
const DECORATIVE_SECTION_HEADER = /^[-=─━~_*#]{3,}[\s\S]+?[-=─━~_*#]{3,}$/;
|
|
2163
|
-
const SECTION_HEADER = /^(Phase|Step|Section|Part)\s+\d+[:.-]/i;
|
|
2164
|
-
const CROSS_REFERENCE_PHRASES = [
|
|
2165
|
-
/\bwill then be\b/i,
|
|
2166
|
-
/\bused by\b/i,
|
|
2167
|
-
/\bcalled from\b/i,
|
|
2168
|
-
/\bcalled later\b/i,
|
|
2169
|
-
/\bsee (?:above|below|later|earlier)\b/i,
|
|
2170
|
-
/\breplaces the\b/i,
|
|
2171
|
-
/\bmatches the one\b/i,
|
|
2172
|
-
/\bwe moved\b/i,
|
|
2173
|
-
/\bwe used to\b/i,
|
|
2174
|
-
/\brefactor(?:ed)? from\b/i,
|
|
2175
|
-
/\bcombined with\b.*\bthis\b/i
|
|
2176
|
-
];
|
|
2177
|
-
const JUSTIFICATION_OPENERS = [/^(The idea here|The trick is|This was needed|Originally,?)/i];
|
|
2178
|
-
const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
|
|
2179
|
-
const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:)\b/i;
|
|
2180
|
-
const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
2181
|
-
"deprecated",
|
|
2182
|
-
"see",
|
|
2183
|
-
"example",
|
|
2184
|
-
"type",
|
|
2185
|
-
"returns",
|
|
2186
|
-
"return",
|
|
2187
|
-
"param",
|
|
2188
|
-
"throws",
|
|
2189
|
-
"typedef",
|
|
2190
|
-
"callback",
|
|
2191
|
-
"override",
|
|
2192
|
-
"template",
|
|
2193
|
-
"internal",
|
|
2194
|
-
"public",
|
|
2195
|
-
"private",
|
|
2196
|
-
"protected",
|
|
2197
|
-
"experimental",
|
|
2198
|
-
"alpha",
|
|
2199
|
-
"beta",
|
|
2200
|
-
"since",
|
|
2201
|
-
"todo",
|
|
2202
|
-
"link",
|
|
2203
|
-
"license",
|
|
2204
|
-
"preserve",
|
|
2205
|
-
"swagger",
|
|
2206
|
-
"openapi",
|
|
2207
|
-
"route",
|
|
2208
|
-
"group",
|
|
2209
|
-
"summary",
|
|
2210
|
-
"description",
|
|
2211
|
-
"operationid",
|
|
2212
|
-
"response",
|
|
2213
|
-
"responses",
|
|
2214
|
-
"request",
|
|
2215
|
-
"requestbody",
|
|
2216
|
-
"security",
|
|
2217
|
-
"tag",
|
|
2218
|
-
"tags",
|
|
2219
|
-
"path",
|
|
2220
|
-
"body",
|
|
2221
|
-
"query",
|
|
2222
|
-
"queryparam",
|
|
2223
|
-
"header",
|
|
2224
|
-
"headers",
|
|
2225
|
-
"produces",
|
|
2226
|
-
"accept",
|
|
2227
|
-
"middleware",
|
|
2228
|
-
"api",
|
|
2229
|
-
"apiname",
|
|
2230
|
-
"apidefine",
|
|
2231
|
-
"apigroup",
|
|
2232
|
-
"apiparam",
|
|
2233
|
-
"apiquery",
|
|
2234
|
-
"apibody",
|
|
2235
|
-
"apiheader",
|
|
2236
|
-
"apisuccess",
|
|
2237
|
-
"apierror",
|
|
2238
|
-
"apiexample",
|
|
2239
|
-
"apiversion",
|
|
2240
|
-
"apidescription",
|
|
2241
|
-
"apipermission",
|
|
2242
|
-
"apiuse",
|
|
2243
|
-
"apiignore",
|
|
2244
|
-
"apiprivate",
|
|
2245
|
-
"namespace",
|
|
2246
|
-
"category"
|
|
2247
|
-
]);
|
|
2248
|
-
const SUPPORTED_EXTS = new Set([
|
|
2249
|
-
".ts",
|
|
2250
|
-
".tsx",
|
|
2251
|
-
".js",
|
|
2252
|
-
".jsx",
|
|
2253
|
-
".mjs",
|
|
2254
|
-
".cjs",
|
|
2255
|
-
".py",
|
|
2256
|
-
".go",
|
|
2257
|
-
".rs",
|
|
2258
|
-
".rb",
|
|
2259
|
-
".java",
|
|
2260
|
-
".php"
|
|
2261
|
-
]);
|
|
2262
|
-
const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
|
|
2263
|
-
const EXPORT_DEFAULT = /^\s*export\s+default\b/;
|
|
2264
|
-
const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
|
|
2265
|
-
const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
|
|
2266
|
-
const GO_DECL_START = /^\s*(func|type|var|const)\s+/;
|
|
2267
|
-
const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
|
|
2268
|
-
const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
|
|
2269
|
-
const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
|
|
2270
|
-
const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
|
|
2271
|
-
const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|readonly\s+)*(function|class|interface|trait|enum|const)\s+/;
|
|
2272
|
-
|
|
2273
|
-
//#endregion
|
|
2274
|
-
//#region src/engines/ai-slop/narrative-comments.ts
|
|
2434
|
+
//#region src/engines/ai-slop/comment-blocks.ts
|
|
2275
2435
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
2276
2436
|
const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
|
|
2277
2437
|
const getCommentSyntax = (ext) => {
|
|
@@ -2371,6 +2531,9 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
2371
2531
|
}
|
|
2372
2532
|
return blocks;
|
|
2373
2533
|
};
|
|
2534
|
+
|
|
2535
|
+
//#endregion
|
|
2536
|
+
//#region src/engines/ai-slop/narrative-comments.ts
|
|
2374
2537
|
const looksLikeDeclarationPreamble = (nextLine, ext) => {
|
|
2375
2538
|
if (nextLine === null) return false;
|
|
2376
2539
|
if (DECL_START.test(nextLine) || EXPORT_DEFAULT.test(nextLine)) return true;
|
|
@@ -2400,6 +2563,7 @@ const isBareSectionLabel = (prose) => {
|
|
|
2400
2563
|
if (!BARE_LABEL_RE.test(prose)) return false;
|
|
2401
2564
|
if (prose.endsWith(".")) return false;
|
|
2402
2565
|
if (prose.split(/\s+/).length > 3) return false;
|
|
2566
|
+
if (STEP_COMMENT_VERB_RE.test(prose)) return false;
|
|
2403
2567
|
return true;
|
|
2404
2568
|
};
|
|
2405
2569
|
const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
|
|
@@ -2414,15 +2578,45 @@ const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
|
2414
2578
|
};
|
|
2415
2579
|
const looksLikeSuppressDirective = (block) => block.rawLines.some((l) => /\b(biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(l));
|
|
2416
2580
|
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
2581
|
+
const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
|
|
2582
|
+
const GO_KEYWORDS = new Set([
|
|
2583
|
+
"return",
|
|
2584
|
+
"if",
|
|
2585
|
+
"for",
|
|
2586
|
+
"switch",
|
|
2587
|
+
"case",
|
|
2588
|
+
"default",
|
|
2589
|
+
"go",
|
|
2590
|
+
"select",
|
|
2591
|
+
"defer",
|
|
2592
|
+
"else",
|
|
2593
|
+
"break",
|
|
2594
|
+
"continue",
|
|
2595
|
+
"goto",
|
|
2596
|
+
"package",
|
|
2597
|
+
"import",
|
|
2598
|
+
"map",
|
|
2599
|
+
"chan",
|
|
2600
|
+
"range"
|
|
2601
|
+
]);
|
|
2417
2602
|
const looksLikeGoDocComment = (block, ext) => {
|
|
2418
2603
|
if (ext !== ".go" || block.kind !== "line") return false;
|
|
2419
2604
|
const next = block.nextNonBlankLine;
|
|
2420
2605
|
if (!next) return false;
|
|
2421
|
-
const
|
|
2422
|
-
|
|
2423
|
-
|
|
2606
|
+
const trimmedNext = next.trim();
|
|
2607
|
+
const firstWord = (block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "";
|
|
2608
|
+
const declMatch = GO_DECL_NAME_RE.exec(trimmedNext);
|
|
2609
|
+
if (declMatch && firstWord === declMatch[1]) return true;
|
|
2610
|
+
const fieldMatch = GO_FIELD_LEAD_RE.exec(trimmedNext);
|
|
2611
|
+
if (fieldMatch && !GO_KEYWORDS.has(fieldMatch[1]) && firstWord === fieldMatch[1]) return true;
|
|
2612
|
+
return false;
|
|
2613
|
+
};
|
|
2614
|
+
const RUBY_DOC_INDICATORS = /^\s*#\s*(?:#|@\w+|:[\w-]+:|=begin|=end)/;
|
|
2615
|
+
const looksLikeRubyDocBlock = (block, ext) => {
|
|
2616
|
+
if (ext !== ".rb" || block.kind !== "line") return false;
|
|
2617
|
+
return block.rawLines.some((line) => RUBY_DOC_INDICATORS.test(line));
|
|
2424
2618
|
};
|
|
2425
|
-
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):|\(e\.g\.[^)]+\)|\(i\.e\.[^)]+\)/i;
|
|
2619
|
+
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see|todo|fixme|hack|reason|deprecated|deprecation|migration|legacy|historical|context):|\(e\.g\.[^)]+\)|\(i\.e\.[^)]+\)|\b\w+\.\w+(?:\.\w+)+\b|\[[\w/.-]+\]/i;
|
|
2426
2620
|
const hasDocIndicator = (block) => {
|
|
2427
2621
|
const joined = block.prose.join(" ");
|
|
2428
2622
|
if (DOC_INDICATOR_RE.test(joined)) return true;
|
|
@@ -2458,6 +2652,10 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
2458
2652
|
matched: false,
|
|
2459
2653
|
reason: ""
|
|
2460
2654
|
};
|
|
2655
|
+
if (looksLikeRubyDocBlock(block, ext)) return {
|
|
2656
|
+
matched: false,
|
|
2657
|
+
reason: ""
|
|
2658
|
+
};
|
|
2461
2659
|
if (block.kind === "line" && block.prose.some((l) => DECORATIVE_SEPARATOR.test(l) || DECORATIVE_SECTION_HEADER.test(l))) return {
|
|
2462
2660
|
matched: true,
|
|
2463
2661
|
reason: "decorative separator"
|
|
@@ -2466,17 +2664,17 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
2466
2664
|
matched: true,
|
|
2467
2665
|
reason: "phase/section header"
|
|
2468
2666
|
};
|
|
2469
|
-
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine)) return {
|
|
2667
|
+
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
2470
2668
|
matched: true,
|
|
2471
2669
|
reason: "bare section label"
|
|
2472
2670
|
};
|
|
2473
2671
|
const joined = block.prose.join(" ");
|
|
2474
2672
|
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
2475
|
-
if (
|
|
2673
|
+
if (hasWhyMarker || hasDocIndicator(block)) return {
|
|
2476
2674
|
matched: false,
|
|
2477
2675
|
reason: ""
|
|
2478
2676
|
};
|
|
2479
|
-
if (block.kind === "line" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
2677
|
+
if (block.kind === "line" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext) && hasPreambleSlopSignal(block)) return {
|
|
2480
2678
|
matched: true,
|
|
2481
2679
|
reason: "multi-line preamble before declaration"
|
|
2482
2680
|
};
|
|
@@ -2499,11 +2697,18 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
2499
2697
|
reason: "explanatory preamble"
|
|
2500
2698
|
};
|
|
2501
2699
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2700
|
+
const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
|
|
2701
|
+
if (nonEmptyProseCount >= 5) {
|
|
2702
|
+
if (isAboveDeclaration) return {
|
|
2703
|
+
matched: false,
|
|
2704
|
+
reason: ""
|
|
2705
|
+
};
|
|
2706
|
+
return {
|
|
2707
|
+
matched: true,
|
|
2708
|
+
reason: "long narrative block"
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
|
|
2507
2712
|
matched: true,
|
|
2508
2713
|
reason: "multi-line narrative prose"
|
|
2509
2714
|
};
|
|
@@ -2558,6 +2763,11 @@ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\
|
|
|
2558
2763
|
const PRINT_RE = /^\s*print\s*\(/;
|
|
2559
2764
|
const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
|
|
2560
2765
|
const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
|
|
2766
|
+
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*(?:#.*)?$/;
|
|
2767
|
+
const CHAINED_DICT_GET_RE = /\.get\s*\([^)]*,\s*\{\s*\}\s*\)\s*\.get\s*\(/;
|
|
2768
|
+
const SAME_VALUE_BRANCH_RE = /^(\s*)(?:if|elif)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*==\s*["'][^"']+["']\s*:/;
|
|
2769
|
+
const INSTANCE_BRANCH_RE = /^(\s*)(?:if|elif)\s+isinstance\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^)]+\)\s*:/;
|
|
2770
|
+
const BRANCH_LADDER_THRESHOLD = 4;
|
|
2561
2771
|
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");
|
|
2562
2772
|
const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
|
|
2563
2773
|
const SCRIPT_DIR_NAMES = new Set([
|
|
@@ -2610,6 +2820,13 @@ const pushFinding = (out, a) => {
|
|
|
2610
2820
|
fixable: false
|
|
2611
2821
|
});
|
|
2612
2822
|
};
|
|
2823
|
+
const pushLineFinding = (out, relPath, line, finding) => {
|
|
2824
|
+
pushFinding(out, {
|
|
2825
|
+
relPath,
|
|
2826
|
+
line,
|
|
2827
|
+
...finding
|
|
2828
|
+
});
|
|
2829
|
+
};
|
|
2613
2830
|
const flagBareExcept = (lines, relPath, out) => {
|
|
2614
2831
|
for (let i = 0; i < lines.length; i++) {
|
|
2615
2832
|
if (!BARE_EXCEPT_RE.test(lines[i])) continue;
|
|
@@ -2691,6 +2908,76 @@ const flagPrintInProduction = (lines, relPath, basename, out) => {
|
|
|
2691
2908
|
});
|
|
2692
2909
|
}
|
|
2693
2910
|
};
|
|
2911
|
+
const flagRangeLenLoops = (lines, relPath, out) => {
|
|
2912
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2913
|
+
const match = RANGE_LEN_LOOP_RE.exec(lines[i]);
|
|
2914
|
+
if (!match) continue;
|
|
2915
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2916
|
+
rule: "ai-slop/python-range-len-loop",
|
|
2917
|
+
severity: "info",
|
|
2918
|
+
message: `\`range(len(${match[2]}))\` loop — usually a hand-rolled iteration pattern.`,
|
|
2919
|
+
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."
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
};
|
|
2923
|
+
const flagChainedDictGets = (lines, relPath, out) => {
|
|
2924
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2925
|
+
if (!CHAINED_DICT_GET_RE.test(lines[i])) continue;
|
|
2926
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2927
|
+
rule: "ai-slop/python-chained-dict-get",
|
|
2928
|
+
severity: "warning",
|
|
2929
|
+
message: "Chained `.get(..., {})` defaults hide missing-data cases.",
|
|
2930
|
+
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."
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
};
|
|
2934
|
+
const countBranchLadder = (lines, start, pattern, selector, indent) => {
|
|
2935
|
+
let count = 1;
|
|
2936
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
2937
|
+
const line = lines[i];
|
|
2938
|
+
const trimmed = line.trim();
|
|
2939
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
2940
|
+
const match = pattern.exec(line);
|
|
2941
|
+
if (match?.[1] === indent && match[2] === selector) {
|
|
2942
|
+
count++;
|
|
2943
|
+
continue;
|
|
2944
|
+
}
|
|
2945
|
+
if (line.startsWith(`${indent}elif `)) break;
|
|
2946
|
+
if (line.length - line.trimStart().length <= indent.length && !line.startsWith(`${indent}else`)) break;
|
|
2947
|
+
}
|
|
2948
|
+
return count;
|
|
2949
|
+
};
|
|
2950
|
+
const flagBranchLadders = (lines, relPath, out) => {
|
|
2951
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2952
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2953
|
+
if (reported.has(i)) continue;
|
|
2954
|
+
const valueMatch = SAME_VALUE_BRANCH_RE.exec(lines[i]);
|
|
2955
|
+
if (valueMatch) {
|
|
2956
|
+
const count = countBranchLadder(lines, i, SAME_VALUE_BRANCH_RE, valueMatch[2], valueMatch[1]);
|
|
2957
|
+
if (count >= BRANCH_LADDER_THRESHOLD) {
|
|
2958
|
+
reported.add(i);
|
|
2959
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2960
|
+
rule: "ai-slop/python-repetitive-dispatch",
|
|
2961
|
+
severity: "warning",
|
|
2962
|
+
message: `${count} repeated branches dispatch on \`${valueMatch[2]}\`.`,
|
|
2963
|
+
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."
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
continue;
|
|
2967
|
+
}
|
|
2968
|
+
const instanceMatch = INSTANCE_BRANCH_RE.exec(lines[i]);
|
|
2969
|
+
if (!instanceMatch) continue;
|
|
2970
|
+
const count = countBranchLadder(lines, i, INSTANCE_BRANCH_RE, instanceMatch[2], instanceMatch[1]);
|
|
2971
|
+
if (count < BRANCH_LADDER_THRESHOLD) continue;
|
|
2972
|
+
reported.add(i);
|
|
2973
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2974
|
+
rule: "ai-slop/python-isinstance-ladder",
|
|
2975
|
+
severity: "warning",
|
|
2976
|
+
message: `${count} repeated \`isinstance(${instanceMatch[2]}, ...)\` branches.`,
|
|
2977
|
+
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."
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
};
|
|
2694
2981
|
const detectPythonPatterns = async (context) => {
|
|
2695
2982
|
const diagnostics = [];
|
|
2696
2983
|
const files = getSourceFiles(context);
|
|
@@ -2710,6 +2997,9 @@ const detectPythonPatterns = async (context) => {
|
|
|
2710
2997
|
flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
|
|
2711
2998
|
flagMutableDefaults(lines, relPath, diagnostics);
|
|
2712
2999
|
flagPrintInProduction(lines, relPath, basename, diagnostics);
|
|
3000
|
+
flagRangeLenLoops(lines, relPath, diagnostics);
|
|
3001
|
+
flagChainedDictGets(lines, relPath, diagnostics);
|
|
3002
|
+
flagBranchLadders(lines, relPath, diagnostics);
|
|
2713
3003
|
}
|
|
2714
3004
|
return diagnostics;
|
|
2715
3005
|
};
|
|
@@ -2735,7 +3025,23 @@ const isTestFile = (relPath) => {
|
|
|
2735
3025
|
if (segments.some((s) => TEST_CRATE_SEGMENT_RE.test(s))) return true;
|
|
2736
3026
|
const basename = segments[segments.length - 1] ?? "";
|
|
2737
3027
|
if (TEST_BASENAMES.has(basename)) return true;
|
|
2738
|
-
return basename.endsWith("_tests.rs") || basename.endsWith("_testutil.rs");
|
|
3028
|
+
return basename.endsWith("_tests.rs") || basename.endsWith("_test.rs") || basename.endsWith("_testutil.rs");
|
|
3029
|
+
};
|
|
3030
|
+
const buildBlockCommentRanges = (lines) => {
|
|
3031
|
+
const ranges = [];
|
|
3032
|
+
let openLine = -1;
|
|
3033
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3034
|
+
const line = lines[i];
|
|
3035
|
+
if (openLine === -1) {
|
|
3036
|
+
const openIdx = line.indexOf("/*");
|
|
3037
|
+
if (openIdx !== -1 && line.indexOf("*/", openIdx + 2) === -1) openLine = i;
|
|
3038
|
+
} else if (line.indexOf("*/") !== -1) {
|
|
3039
|
+
ranges.push([openLine, i]);
|
|
3040
|
+
openLine = -1;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
if (openLine !== -1) ranges.push([openLine, lines.length - 1]);
|
|
3044
|
+
return ranges;
|
|
2739
3045
|
};
|
|
2740
3046
|
const isExampleFile = (relPath) => relPath.split(path.sep).some((seg) => seg === "examples" || seg === "benches");
|
|
2741
3047
|
const UNWRAP_INTENT_LOOKBACK = 2;
|
|
@@ -2766,11 +3072,12 @@ const buildTestRanges = (lines) => {
|
|
|
2766
3072
|
return ranges;
|
|
2767
3073
|
};
|
|
2768
3074
|
const isInRange = (ranges, lineIdx) => ranges.some(([start, end]) => lineIdx >= start && lineIdx <= end);
|
|
2769
|
-
const flagNonTestUnwrap = (lines, relPath, testRanges, out) => {
|
|
3075
|
+
const flagNonTestUnwrap = (lines, relPath, testRanges, blockCommentRanges, out) => {
|
|
2770
3076
|
for (let i = 0; i < lines.length; i++) {
|
|
2771
3077
|
const line = lines[i];
|
|
2772
3078
|
if (COMMENT_LINE_RE.test(line)) continue;
|
|
2773
3079
|
if (isInRange(testRanges, i)) continue;
|
|
3080
|
+
if (isInRange(blockCommentRanges, i)) continue;
|
|
2774
3081
|
if (!UNWRAP_CALL_RE.test(line)) continue;
|
|
2775
3082
|
if (WRITELN_UNWRAP_RE.test(line)) continue;
|
|
2776
3083
|
if (hasIntentComment(lines, i)) continue;
|
|
@@ -2827,7 +3134,7 @@ const detectRustPatterns = async (context) => {
|
|
|
2827
3134
|
flagTodoMacro(lines, relPath, diagnostics);
|
|
2828
3135
|
continue;
|
|
2829
3136
|
}
|
|
2830
|
-
flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), diagnostics);
|
|
3137
|
+
flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), buildBlockCommentRanges(lines), diagnostics);
|
|
2831
3138
|
flagTodoMacro(lines, relPath, diagnostics);
|
|
2832
3139
|
}
|
|
2833
3140
|
return diagnostics;
|
|
@@ -2913,7 +3220,9 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
2913
3220
|
const cleaned = importPart.replace(/[()]/g, "");
|
|
2914
3221
|
for (const item of cleaned.split(",")) {
|
|
2915
3222
|
const parts = item.trim().split(/\s+as\s+/);
|
|
2916
|
-
const
|
|
3223
|
+
const original = parts[0].trim();
|
|
3224
|
+
const localName = parts.length > 1 ? parts[1].trim() : original;
|
|
3225
|
+
if (parts.length > 1 && original === localName) continue;
|
|
2917
3226
|
if (localName && /^\w+$/.test(localName)) symbols.push({
|
|
2918
3227
|
name: localName,
|
|
2919
3228
|
line: i + 1,
|
|
@@ -2926,7 +3235,9 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
2926
3235
|
const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
|
|
2927
3236
|
if (importMatch) {
|
|
2928
3237
|
importLines.add(i);
|
|
2929
|
-
const
|
|
3238
|
+
const alias = importMatch[2];
|
|
3239
|
+
if (alias && alias === importMatch[1]) continue;
|
|
3240
|
+
const simpleName = (alias ?? importMatch[1]).split(".")[0];
|
|
2930
3241
|
if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
|
|
2931
3242
|
name: simpleName,
|
|
2932
3243
|
line: i + 1,
|
|
@@ -3582,9 +3893,17 @@ const isTrivialLine = (line) => {
|
|
|
3582
3893
|
if (trimmed.startsWith("/*") || trimmed.startsWith("*")) return true;
|
|
3583
3894
|
return false;
|
|
3584
3895
|
};
|
|
3896
|
+
const SVG_MARKUP_RE = /<\/?(?:svg|path|polyline|line|circle|rect|g)\b|(?:xmlns|viewBox|stroke(?:-width|-linecap|-linejoin)?|fill|fill-opacity|d|points|x1|x2|y1|y2)=/;
|
|
3897
|
+
const DATA_LITERAL_RE = /^\s*(?:[A-Za-z_$][\w$-]*:\s*(?:["'`[{]|\d|true\b|false\b|null\b)|["'`][^"'`]*["'`],?\s*$|[{}\]],?\s*$|\),?\s*$)/;
|
|
3585
3898
|
const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
|
|
3586
3899
|
const FILE_SUPPRESS_RE = /aislop[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
|
|
3587
3900
|
const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
|
|
3901
|
+
const isLowSignalMarkupWindow = (lines) => {
|
|
3902
|
+
return lines.filter((line) => SVG_MARKUP_RE.test(line)).length >= Math.ceil(WINDOW_SIZE / 2);
|
|
3903
|
+
};
|
|
3904
|
+
const isLowSignalDataWindow = (lines) => {
|
|
3905
|
+
return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= WINDOW_SIZE - 1;
|
|
3906
|
+
};
|
|
3588
3907
|
const findSuppressedLines = (lines) => {
|
|
3589
3908
|
const suppressed = /* @__PURE__ */ new Set();
|
|
3590
3909
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -3611,6 +3930,8 @@ const collectMeaningfulLines = (content) => {
|
|
|
3611
3930
|
if (suppressed.has(i + 1)) continue;
|
|
3612
3931
|
const window = lines.slice(i, i + WINDOW_SIZE);
|
|
3613
3932
|
if (window.some((l) => !MEANINGFUL_LINE.test(l))) continue;
|
|
3933
|
+
if (isLowSignalMarkupWindow(window)) continue;
|
|
3934
|
+
if (isLowSignalDataWindow(window)) continue;
|
|
3614
3935
|
if (window.every(isTrivialLine)) continue;
|
|
3615
3936
|
const normalised = window.map(normaliseLine);
|
|
3616
3937
|
if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < WINDOW_SIZE - 1) continue;
|
|
@@ -4852,7 +5173,20 @@ const createOxlintConfig = (options) => {
|
|
|
4852
5173
|
...buildFrameworkPlugins(options.framework)
|
|
4853
5174
|
];
|
|
4854
5175
|
const globals = buildTestGlobals(options.testFramework ?? null);
|
|
4855
|
-
|
|
5176
|
+
for (const name of [
|
|
5177
|
+
"__DEV__",
|
|
5178
|
+
"__TEST__",
|
|
5179
|
+
"__BROWSER__",
|
|
5180
|
+
"__NODE__",
|
|
5181
|
+
"__GLOBAL__",
|
|
5182
|
+
"__SSR__",
|
|
5183
|
+
"__ESM_BROWSER__",
|
|
5184
|
+
"__ESM_BUNDLER__",
|
|
5185
|
+
"__VERSION__",
|
|
5186
|
+
"__COMMIT__",
|
|
5187
|
+
"__BUILD__"
|
|
5188
|
+
]) globals[name] = "readonly";
|
|
5189
|
+
for (const globalName of options.globals ?? []) globals[globalName] = "readonly";
|
|
4856
5190
|
if (options.framework === "astro") {
|
|
4857
5191
|
globals.Astro = "readonly";
|
|
4858
5192
|
rules["no-undef"] = "off";
|
|
@@ -4872,19 +5206,7 @@ const createOxlintConfig = (options) => {
|
|
|
4872
5206
|
};
|
|
4873
5207
|
|
|
4874
5208
|
//#endregion
|
|
4875
|
-
//#region src/engines/lint/oxlint.ts
|
|
4876
|
-
const esmRequire$1 = createRequire(import.meta.url);
|
|
4877
|
-
const resolveOxlintBinary = () => {
|
|
4878
|
-
try {
|
|
4879
|
-
const oxlintMainPath = esmRequire$1.resolve("oxlint");
|
|
4880
|
-
const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
|
|
4881
|
-
return path.join(oxlintDir, "bin", "oxlint");
|
|
4882
|
-
} catch {
|
|
4883
|
-
return "oxlint";
|
|
4884
|
-
}
|
|
4885
|
-
};
|
|
4886
|
-
const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
|
|
4887
|
-
const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
|
|
5209
|
+
//#region src/engines/lint/oxlint-context-filters.ts
|
|
4888
5210
|
const AMBIENT_GLOBAL_DEPS = [
|
|
4889
5211
|
"unplugin-icons",
|
|
4890
5212
|
"@types/bun",
|
|
@@ -4946,6 +5268,9 @@ const isAmbientFalsePositive = (rule, message, sources) => {
|
|
|
4946
5268
|
return false;
|
|
4947
5269
|
};
|
|
4948
5270
|
const sstReferencedFiles = /* @__PURE__ */ new Map();
|
|
5271
|
+
const clearSstReferenceCache = () => {
|
|
5272
|
+
sstReferencedFiles.clear();
|
|
5273
|
+
};
|
|
4949
5274
|
const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
4950
5275
|
const cached = sstReferencedFiles.get(relativeFilePath);
|
|
4951
5276
|
if (cached !== void 0) return cached;
|
|
@@ -4966,12 +5291,114 @@ const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
|
4966
5291
|
sstReferencedFiles.set(relativeFilePath, referenced);
|
|
4967
5292
|
return referenced;
|
|
4968
5293
|
};
|
|
5294
|
+
|
|
5295
|
+
//#endregion
|
|
5296
|
+
//#region src/engines/lint/oxlint-globals.ts
|
|
5297
|
+
const readTextFile$1 = (filePath) => {
|
|
5298
|
+
try {
|
|
5299
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
5300
|
+
} catch {
|
|
5301
|
+
return null;
|
|
5302
|
+
}
|
|
5303
|
+
};
|
|
5304
|
+
const collectPackageNames = (dir) => {
|
|
5305
|
+
const names = /* @__PURE__ */ new Set();
|
|
5306
|
+
const raw = readTextFile$1(path.join(dir, "package.json"));
|
|
5307
|
+
if (!raw) return names;
|
|
5308
|
+
try {
|
|
5309
|
+
const pkg = JSON.parse(raw);
|
|
5310
|
+
for (const section of [
|
|
5311
|
+
"dependencies",
|
|
5312
|
+
"devDependencies",
|
|
5313
|
+
"peerDependencies",
|
|
5314
|
+
"optionalDependencies"
|
|
5315
|
+
]) {
|
|
5316
|
+
const deps = pkg[section];
|
|
5317
|
+
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) names.add(name);
|
|
5318
|
+
}
|
|
5319
|
+
} catch {
|
|
5320
|
+
return names;
|
|
5321
|
+
}
|
|
5322
|
+
return names;
|
|
5323
|
+
};
|
|
5324
|
+
const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
|
|
5325
|
+
const collectAmbientGlobals = (rootDir) => {
|
|
5326
|
+
const globals = /* @__PURE__ */ new Set();
|
|
5327
|
+
const projectFiles = listProjectFiles(rootDir);
|
|
5328
|
+
for (const relativePath of projectFiles) {
|
|
5329
|
+
if (!relativePath.endsWith(".d.ts")) continue;
|
|
5330
|
+
const content = readTextFile$1(path.join(rootDir, relativePath));
|
|
5331
|
+
if (!content) continue;
|
|
5332
|
+
AMBIENT_GLOBAL_RE.lastIndex = 0;
|
|
5333
|
+
let match;
|
|
5334
|
+
while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
|
|
5335
|
+
}
|
|
5336
|
+
const deps = collectPackageNames(rootDir);
|
|
5337
|
+
if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
|
|
5338
|
+
if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
|
|
5339
|
+
"$app",
|
|
5340
|
+
"$config",
|
|
5341
|
+
"$dev",
|
|
5342
|
+
"$interpolate",
|
|
5343
|
+
"$resolve",
|
|
5344
|
+
"$jsonParse",
|
|
5345
|
+
"$jsonStringify",
|
|
5346
|
+
"aws",
|
|
5347
|
+
"cloudflare",
|
|
5348
|
+
"docker",
|
|
5349
|
+
"random",
|
|
5350
|
+
"sst",
|
|
5351
|
+
"vercel",
|
|
5352
|
+
"pulumi"
|
|
5353
|
+
]) globals.add(name);
|
|
5354
|
+
return [...globals];
|
|
5355
|
+
};
|
|
5356
|
+
|
|
5357
|
+
//#endregion
|
|
5358
|
+
//#region src/engines/lint/oxlint.ts
|
|
5359
|
+
const esmRequire$1 = createRequire(import.meta.url);
|
|
5360
|
+
const OXLINT_EXTENSIONS = new Set([
|
|
5361
|
+
".ts",
|
|
5362
|
+
".tsx",
|
|
5363
|
+
".js",
|
|
5364
|
+
".jsx",
|
|
5365
|
+
".mjs",
|
|
5366
|
+
".cjs"
|
|
5367
|
+
]);
|
|
5368
|
+
const resolveOxlintBinary = () => {
|
|
5369
|
+
try {
|
|
5370
|
+
const oxlintMainPath = esmRequire$1.resolve("oxlint");
|
|
5371
|
+
const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
|
|
5372
|
+
return path.join(oxlintDir, "bin", "oxlint");
|
|
5373
|
+
} catch {
|
|
5374
|
+
return "oxlint";
|
|
5375
|
+
}
|
|
5376
|
+
};
|
|
5377
|
+
const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
|
|
5378
|
+
const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
|
|
4969
5379
|
const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
|
|
4970
5380
|
const isUnderscoreUnusedVar = (rule, message) => {
|
|
4971
5381
|
if (rule !== "eslint/no-unused-vars") return false;
|
|
4972
5382
|
const match = UNUSED_VAR_IDENT_RE.exec(message);
|
|
4973
5383
|
return match ? match[1].startsWith("_") : false;
|
|
4974
5384
|
};
|
|
5385
|
+
const readTextFile = (filePath) => {
|
|
5386
|
+
try {
|
|
5387
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
5388
|
+
} catch {
|
|
5389
|
+
return null;
|
|
5390
|
+
}
|
|
5391
|
+
};
|
|
5392
|
+
const isSolidRefFalsePositive = (context, diagnostic) => {
|
|
5393
|
+
if (diagnostic.rule !== "eslint/no-unassigned-vars") return false;
|
|
5394
|
+
const name = diagnostic.message.match(/^'([^']+)' is always 'undefined'/)?.[1];
|
|
5395
|
+
if (!name) return false;
|
|
5396
|
+
const content = readTextFile(path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath : path.join(context.rootDirectory, diagnostic.filePath));
|
|
5397
|
+
if (!content) return false;
|
|
5398
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5399
|
+
return new RegExp(`\\bref=\\{\\s*${escaped}\\s*\\}`).test(content);
|
|
5400
|
+
};
|
|
5401
|
+
const isContextualTypeScriptFalsePositive = (diagnostic) => diagnostic.rule === "typescript-eslint/triple-slash-reference" && (diagnostic.filePath.endsWith(".d.ts") || /(?:^|\/)sst\.config\.ts$/.test(diagnostic.filePath));
|
|
4975
5402
|
const parseRuleCode = (code) => {
|
|
4976
5403
|
if (!code) return {
|
|
4977
5404
|
plugin: "eslint",
|
|
@@ -5004,6 +5431,7 @@ const detectTestFramework = (rootDir) => {
|
|
|
5004
5431
|
} catch {}
|
|
5005
5432
|
return null;
|
|
5006
5433
|
};
|
|
5434
|
+
const getOxlintTargets = (context) => getSourceFiles(context).filter((filePath) => OXLINT_EXTENSIONS.has(path.extname(filePath).toLowerCase())).filter((filePath) => !isAutoGenerated(filePath)).map((filePath) => path.relative(context.rootDirectory, filePath).split(path.sep).join("/"));
|
|
5007
5435
|
const extractUnusedVarName = (message) => {
|
|
5008
5436
|
const variableMatch = message.match(/Variable '([^']+)' is declared but never used/);
|
|
5009
5437
|
if (variableMatch?.[1]) return {
|
|
@@ -5064,12 +5492,17 @@ const removeDuplicateKeyLines = (rootDirectory, diagnostics) => {
|
|
|
5064
5492
|
};
|
|
5065
5493
|
const runOxlint = async (context) => {
|
|
5066
5494
|
const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
|
|
5495
|
+
const framework = context.frameworks.find((f) => f !== "none");
|
|
5496
|
+
const testFramework = detectTestFramework(context.rootDirectory);
|
|
5497
|
+
const targets = getOxlintTargets(context);
|
|
5498
|
+
if (targets.length === 0) return [];
|
|
5067
5499
|
const config = createOxlintConfig({
|
|
5068
|
-
framework
|
|
5069
|
-
testFramework
|
|
5500
|
+
framework,
|
|
5501
|
+
testFramework,
|
|
5502
|
+
globals: collectAmbientGlobals(context.rootDirectory)
|
|
5070
5503
|
});
|
|
5071
5504
|
const ambientSources = detectAmbientSources(context.rootDirectory);
|
|
5072
|
-
|
|
5505
|
+
clearSstReferenceCache();
|
|
5073
5506
|
try {
|
|
5074
5507
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
5075
5508
|
const args = [
|
|
@@ -5080,7 +5513,7 @@ const runOxlint = async (context) => {
|
|
|
5080
5513
|
"json"
|
|
5081
5514
|
];
|
|
5082
5515
|
if (context.languages.includes("typescript") && fs.existsSync(path.join(context.rootDirectory, "tsconfig.json"))) args.push("--tsconfig", "./tsconfig.json");
|
|
5083
|
-
args.push(
|
|
5516
|
+
args.push(...targets);
|
|
5084
5517
|
const result = await runSubprocess(process.execPath, args, {
|
|
5085
5518
|
cwd: context.rootDirectory,
|
|
5086
5519
|
timeout: 12e4
|
|
@@ -5112,6 +5545,8 @@ const runOxlint = async (context) => {
|
|
|
5112
5545
|
if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
|
|
5113
5546
|
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
5114
5547
|
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
5548
|
+
if (isSolidRefFalsePositive(context, d)) return false;
|
|
5549
|
+
if (isContextualTypeScriptFalsePositive(d)) return false;
|
|
5115
5550
|
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
5116
5551
|
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
5117
5552
|
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
@@ -5126,10 +5561,15 @@ const runOxlint = async (context) => {
|
|
|
5126
5561
|
const fixOxlint = async (context, options = {}) => {
|
|
5127
5562
|
const dangerous = options.force ?? false;
|
|
5128
5563
|
const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-fix-${process.pid}.json`);
|
|
5564
|
+
const framework = context.frameworks.find((f) => f !== "none");
|
|
5565
|
+
const testFramework = detectTestFramework(context.rootDirectory);
|
|
5566
|
+
const targets = getOxlintTargets(context);
|
|
5567
|
+
if (targets.length === 0) return;
|
|
5129
5568
|
const config = createOxlintConfig({
|
|
5130
|
-
framework
|
|
5131
|
-
testFramework
|
|
5132
|
-
mode: "fix"
|
|
5569
|
+
framework,
|
|
5570
|
+
testFramework,
|
|
5571
|
+
mode: "fix",
|
|
5572
|
+
globals: collectAmbientGlobals(context.rootDirectory)
|
|
5133
5573
|
});
|
|
5134
5574
|
try {
|
|
5135
5575
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
@@ -5141,13 +5581,13 @@ const fixOxlint = async (context, options = {}) => {
|
|
|
5141
5581
|
"--fix",
|
|
5142
5582
|
"--fix-suggestions",
|
|
5143
5583
|
"--fix-dangerously",
|
|
5144
|
-
|
|
5584
|
+
...targets
|
|
5145
5585
|
] : [
|
|
5146
5586
|
binary,
|
|
5147
5587
|
"-c",
|
|
5148
5588
|
configPath,
|
|
5149
5589
|
"--fix",
|
|
5150
|
-
|
|
5590
|
+
...targets
|
|
5151
5591
|
];
|
|
5152
5592
|
const result = await runSubprocess(process.execPath, args, {
|
|
5153
5593
|
cwd: context.rootDirectory,
|
|
@@ -5739,7 +6179,7 @@ const DB_RECEIVER = "(?:db|database|knex|client|connection|conn|pool|sql|prisma|
|
|
|
5739
6179
|
const DB_METHOD = "(?:query|execute|exec|raw|\\$queryRaw|\\$queryRawUnsafe|\\$executeRaw|\\$executeRawUnsafe)";
|
|
5740
6180
|
const RISKY_PATTERNS = [
|
|
5741
6181
|
{
|
|
5742
|
-
pattern: new RegExp(
|
|
6182
|
+
pattern: new RegExp(`(?<![\\w.>:\\\\])\\b${ev}\\s*\\(`, "g"),
|
|
5743
6183
|
extensions: [
|
|
5744
6184
|
".ts",
|
|
5745
6185
|
".tsx",
|
|
@@ -5846,6 +6286,16 @@ const RISKY_PATTERNS = [
|
|
|
5846
6286
|
help: "Use parameterized queries or an ORM instead of string concatenation"
|
|
5847
6287
|
}
|
|
5848
6288
|
];
|
|
6289
|
+
const hasDangerouslySetInnerHtmlIgnore = (lines, lineIndex) => {
|
|
6290
|
+
const start = Math.max(0, lineIndex - 2);
|
|
6291
|
+
return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
|
|
6292
|
+
};
|
|
6293
|
+
const isStructuredDataScript = (content, matchIndex) => {
|
|
6294
|
+
const before = content.slice(Math.max(0, matchIndex - 300), matchIndex);
|
|
6295
|
+
if (/type=["']application\/ld\+json["']/.test(before)) return true;
|
|
6296
|
+
const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
|
|
6297
|
+
return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
|
|
6298
|
+
};
|
|
5849
6299
|
const detectRiskyConstructs = async (context) => {
|
|
5850
6300
|
const files = getSourceFiles(context);
|
|
5851
6301
|
const diagnostics = [];
|
|
@@ -5861,6 +6311,7 @@ const detectRiskyConstructs = async (context) => {
|
|
|
5861
6311
|
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
5862
6312
|
const isMigrationOrSeeder = /(?:^|\/)(migrations|seeders|seeds|migrate)\//.test(normalizedPath);
|
|
5863
6313
|
const masked = maskStringsAndComments(content, ext);
|
|
6314
|
+
const lines = content.split("\n");
|
|
5864
6315
|
for (const { pattern, extensions, name, message, help } of RISKY_PATTERNS) {
|
|
5865
6316
|
if (!extensions.includes(ext)) continue;
|
|
5866
6317
|
if (isMigrationOrSeeder && name === "sql-injection") continue;
|
|
@@ -5872,6 +6323,10 @@ const detectRiskyConstructs = async (context) => {
|
|
|
5872
6323
|
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
5873
6324
|
if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
|
|
5874
6325
|
}
|
|
6326
|
+
if (name === "dangerously-set-innerhtml") {
|
|
6327
|
+
if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
|
|
6328
|
+
if (isStructuredDataScript(content, match.index)) continue;
|
|
6329
|
+
}
|
|
5875
6330
|
diagnostics.push({
|
|
5876
6331
|
filePath: relativePath,
|
|
5877
6332
|
engine: "security",
|
|
@@ -5895,7 +6350,8 @@ const detectRiskyConstructs = async (context) => {
|
|
|
5895
6350
|
const SECRET_PATTERNS = [
|
|
5896
6351
|
{
|
|
5897
6352
|
pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
|
|
5898
|
-
name: "API key"
|
|
6353
|
+
name: "API key",
|
|
6354
|
+
keywordPrefixed: true
|
|
5899
6355
|
},
|
|
5900
6356
|
{
|
|
5901
6357
|
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
@@ -5903,11 +6359,13 @@ const SECRET_PATTERNS = [
|
|
|
5903
6359
|
},
|
|
5904
6360
|
{
|
|
5905
6361
|
pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
|
|
5906
|
-
name: "AWS Secret Key"
|
|
6362
|
+
name: "AWS Secret Key",
|
|
6363
|
+
keywordPrefixed: true
|
|
5907
6364
|
},
|
|
5908
6365
|
{
|
|
5909
6366
|
pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
|
|
5910
|
-
name: "Hardcoded password/secret"
|
|
6367
|
+
name: "Hardcoded password/secret",
|
|
6368
|
+
keywordPrefixed: true
|
|
5911
6369
|
},
|
|
5912
6370
|
{
|
|
5913
6371
|
pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
|
|
@@ -5919,7 +6377,8 @@ const SECRET_PATTERNS = [
|
|
|
5919
6377
|
},
|
|
5920
6378
|
{
|
|
5921
6379
|
pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
|
|
5922
|
-
name: "Authentication token"
|
|
6380
|
+
name: "Authentication token",
|
|
6381
|
+
keywordPrefixed: true
|
|
5923
6382
|
},
|
|
5924
6383
|
{
|
|
5925
6384
|
pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
|
|
@@ -5934,6 +6393,24 @@ const SECRET_PATTERNS = [
|
|
|
5934
6393
|
name: "Database connection string with credentials"
|
|
5935
6394
|
}
|
|
5936
6395
|
];
|
|
6396
|
+
const isInsideStringLiteral = (content, matchIndex) => {
|
|
6397
|
+
const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
|
|
6398
|
+
const prefix = content.slice(lineStart, matchIndex);
|
|
6399
|
+
let inDouble = false;
|
|
6400
|
+
let inSingle = false;
|
|
6401
|
+
let inBacktick = false;
|
|
6402
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
6403
|
+
const ch = prefix[i];
|
|
6404
|
+
if (ch === "\\") {
|
|
6405
|
+
i++;
|
|
6406
|
+
continue;
|
|
6407
|
+
}
|
|
6408
|
+
if (ch === "\"" && !inSingle && !inBacktick) inDouble = !inDouble;
|
|
6409
|
+
else if (ch === "'" && !inDouble && !inBacktick) inSingle = !inSingle;
|
|
6410
|
+
else if (ch === "`" && !inDouble && !inSingle) inBacktick = !inBacktick;
|
|
6411
|
+
}
|
|
6412
|
+
return inDouble || inSingle || inBacktick;
|
|
6413
|
+
};
|
|
5937
6414
|
const PLACEHOLDER_EXACT = new Set([
|
|
5938
6415
|
"changeme",
|
|
5939
6416
|
"password",
|
|
@@ -5969,11 +6446,12 @@ const scanSecrets = async (context) => {
|
|
|
5969
6446
|
continue;
|
|
5970
6447
|
}
|
|
5971
6448
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
5972
|
-
for (const { pattern, name } of SECRET_PATTERNS) {
|
|
6449
|
+
for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
|
|
5973
6450
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
5974
6451
|
let match;
|
|
5975
6452
|
while ((match = regex.exec(content)) !== null) {
|
|
5976
6453
|
if (isPlaceholderValue(match[1] ?? match[0])) continue;
|
|
6454
|
+
if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
|
|
5977
6455
|
const line = content.slice(0, match.index).split("\n").length;
|
|
5978
6456
|
diagnostics.push({
|
|
5979
6457
|
filePath: relativePath,
|
|
@@ -8324,6 +8802,10 @@ const RULE_LABELS = {
|
|
|
8324
8802
|
"ai-slop/python-broad-except": "Broad except",
|
|
8325
8803
|
"ai-slop/python-mutable-default": "Mutable default argument",
|
|
8326
8804
|
"ai-slop/python-print-debug": "print() left in code",
|
|
8805
|
+
"ai-slop/python-range-len-loop": "range(len(...)) loop",
|
|
8806
|
+
"ai-slop/python-chained-dict-get": "Chained dict get",
|
|
8807
|
+
"ai-slop/python-repetitive-dispatch": "Repetitive dispatch ladder",
|
|
8808
|
+
"ai-slop/python-isinstance-ladder": "isinstance ladder",
|
|
8327
8809
|
"ai-slop/go-library-panic": "panic() in Go library code",
|
|
8328
8810
|
"ai-slop/rust-non-test-unwrap": "Rust .unwrap() in production code",
|
|
8329
8811
|
"ai-slop/rust-todo-stub": "Rust todo!() stub",
|
|
@@ -8427,6 +8909,9 @@ const renderSummary = (input, deps = {}) => {
|
|
|
8427
8909
|
}
|
|
8428
8910
|
return lines.join("\n");
|
|
8429
8911
|
};
|
|
8912
|
+
const renderStarCta = (deps = {}) => {
|
|
8913
|
+
return `\n ${style(deps.theme ?? theme, "muted", "★ Found this useful? Star us at github.com/scanaislop/aislop")}\n`;
|
|
8914
|
+
};
|
|
8430
8915
|
const renderCleanRun = (input, deps = {}) => {
|
|
8431
8916
|
const t = deps.theme ?? theme;
|
|
8432
8917
|
const s = deps.symbols ?? symbols;
|
|
@@ -8496,11 +8981,12 @@ const buildScanRender = (input) => {
|
|
|
8496
8981
|
const warnings = input.diagnostics.filter((d) => d.severity === "warning").length;
|
|
8497
8982
|
const fixable = input.diagnostics.filter((d) => d.fixable).length;
|
|
8498
8983
|
const hasVulnerableDeps = input.diagnostics.some((d) => d.rule === "security/vulnerable-dependency");
|
|
8984
|
+
const starCta = input.printBrand !== false ? renderStarCta(deps) : "";
|
|
8499
8985
|
if (input.diagnostics.length === 0 && input.score.score === 100) return `${header}${renderCleanRun({
|
|
8500
8986
|
score: input.score.score,
|
|
8501
8987
|
label: input.score.label,
|
|
8502
8988
|
elapsedMs: input.elapsedMs
|
|
8503
|
-
}, deps)}`;
|
|
8989
|
+
}, deps)}${starCta}`;
|
|
8504
8990
|
const diagBlock = input.diagnostics.length === 0 ? "" : renderDiagnostics(input.diagnostics, input.verbose);
|
|
8505
8991
|
const nextSteps = [];
|
|
8506
8992
|
if (fixable > 0) nextSteps.push({
|
|
@@ -8527,7 +9013,7 @@ const buildScanRender = (input) => {
|
|
|
8527
9013
|
nextSteps,
|
|
8528
9014
|
breakdown: computeBreakdown(input.diagnostics),
|
|
8529
9015
|
thresholds: input.thresholds
|
|
8530
|
-
}, deps)}`;
|
|
9016
|
+
}, deps)}${starCta}`;
|
|
8531
9017
|
};
|
|
8532
9018
|
const scanCommand = async (directory, config, options) => {
|
|
8533
9019
|
const resolvedDir = path.resolve(directory);
|
|
@@ -11066,6 +11552,10 @@ const BUILTIN_RULES = [
|
|
|
11066
11552
|
"ai-slop/python-broad-except",
|
|
11067
11553
|
"ai-slop/python-mutable-default",
|
|
11068
11554
|
"ai-slop/python-print-debug",
|
|
11555
|
+
"ai-slop/python-range-len-loop",
|
|
11556
|
+
"ai-slop/python-chained-dict-get",
|
|
11557
|
+
"ai-slop/python-repetitive-dispatch",
|
|
11558
|
+
"ai-slop/python-isinstance-ladder",
|
|
11069
11559
|
"ai-slop/go-library-panic",
|
|
11070
11560
|
"ai-slop/rust-non-test-unwrap",
|
|
11071
11561
|
"ai-slop/rust-todo-stub",
|