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/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-
|
|
1
|
+
import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-C45P3Q1N.js";
|
|
2
2
|
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
|
|
3
3
|
import { r as runGenericLinter, t as fixRubyLint } from "./generic-BrcWMW7E.js";
|
|
4
4
|
import { n as runExpoDoctor } from "./expo-doctor-Bz0LZhQ6.js";
|
|
@@ -564,6 +564,7 @@ const EXCLUDED_DIRS = [
|
|
|
564
564
|
"build",
|
|
565
565
|
".git",
|
|
566
566
|
".agents",
|
|
567
|
+
".pnpm-store",
|
|
567
568
|
"vendor",
|
|
568
569
|
"examples",
|
|
569
570
|
"example",
|
|
@@ -600,6 +601,7 @@ const FIND_PRUNE_DIRS = [
|
|
|
600
601
|
"build",
|
|
601
602
|
".git",
|
|
602
603
|
".agents",
|
|
604
|
+
".pnpm-store",
|
|
603
605
|
"vendor",
|
|
604
606
|
"examples",
|
|
605
607
|
"example",
|
|
@@ -623,7 +625,11 @@ const FIND_PRUNE_DIRS = [
|
|
|
623
625
|
".turbo",
|
|
624
626
|
"public"
|
|
625
627
|
];
|
|
626
|
-
const BUILD_CACHE_FILE_PATTERNS = [
|
|
628
|
+
const BUILD_CACHE_FILE_PATTERNS = [
|
|
629
|
+
/\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i,
|
|
630
|
+
/\.min\.(?:js|css|mjs|cjs)$/i,
|
|
631
|
+
/\.bundle\.(?:js|css|mjs|cjs)$/i
|
|
632
|
+
];
|
|
627
633
|
const isBuildCacheFile = (filePath) => BUILD_CACHE_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
628
634
|
const TEST_FILE_PATTERNS = [
|
|
629
635
|
/(?:^|\/).*\.test\.[^/]+$/i,
|
|
@@ -651,6 +657,17 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
|
651
657
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
652
658
|
const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
|
|
653
659
|
const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
660
|
+
const readBiomeExcludePatterns = (rootDirectory) => {
|
|
661
|
+
const biomePath = path.join(rootDirectory, "biome.json");
|
|
662
|
+
if (!fs.existsSync(biomePath)) return [];
|
|
663
|
+
try {
|
|
664
|
+
const includes = JSON.parse(fs.readFileSync(biomePath, "utf-8")).files?.includes;
|
|
665
|
+
if (!Array.isArray(includes)) return [];
|
|
666
|
+
return includes.filter((entry) => typeof entry === "string").filter((entry) => entry.startsWith("!") && entry.length > 1).map((entry) => entry.slice(1));
|
|
667
|
+
} catch {
|
|
668
|
+
return [];
|
|
669
|
+
}
|
|
670
|
+
};
|
|
654
671
|
const getIgnoredPaths = (rootDirectory, files) => {
|
|
655
672
|
if (files.length === 0) return /* @__PURE__ */ new Set();
|
|
656
673
|
const result = spawnSync("git", [
|
|
@@ -718,7 +735,8 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
|
|
|
718
735
|
};
|
|
719
736
|
}).filter(({ relativePath }) => isWithinProject(relativePath));
|
|
720
737
|
const ignoredPaths = getIgnoredPaths(rootDirectory, normalizedFiles.map(({ relativePath }) => relativePath));
|
|
721
|
-
const
|
|
738
|
+
const excludePatterns = [...readBiomeExcludePatterns(rootDirectory), ...exclude];
|
|
739
|
+
const normalizedExcludePatterns = excludePatterns.length ? normalizeExcludePatterns(excludePatterns) : [];
|
|
722
740
|
const isUserExcluded = (relativePath) => {
|
|
723
741
|
if (!normalizedExcludePatterns.length) return false;
|
|
724
742
|
return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
|
|
@@ -1228,10 +1246,27 @@ const doctorCommand = async (directory, options = {}) => {
|
|
|
1228
1246
|
|
|
1229
1247
|
//#endregion
|
|
1230
1248
|
//#region src/engines/ai-slop/abstractions.ts
|
|
1249
|
+
const JS_EXTS$1 = new Set([
|
|
1250
|
+
".ts",
|
|
1251
|
+
".tsx",
|
|
1252
|
+
".js",
|
|
1253
|
+
".jsx",
|
|
1254
|
+
".mjs",
|
|
1255
|
+
".cjs"
|
|
1256
|
+
]);
|
|
1231
1257
|
const THIN_WRAPPER_PATTERNS = [
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1258
|
+
{
|
|
1259
|
+
pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
|
|
1260
|
+
extensions: JS_EXTS$1
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
1264
|
+
extensions: JS_EXTS$1
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
1268
|
+
extensions: new Set([".py"])
|
|
1269
|
+
}
|
|
1235
1270
|
];
|
|
1236
1271
|
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+/];
|
|
1237
1272
|
const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
|
|
@@ -1246,10 +1281,11 @@ const hasHardcodedArgs = (matchText) => {
|
|
|
1246
1281
|
return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(innerCallMatch[1]);
|
|
1247
1282
|
};
|
|
1248
1283
|
const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
|
|
1249
|
-
const detectThinWrappers = (content, relativePath) => {
|
|
1284
|
+
const detectThinWrappers = (content, relativePath, ext) => {
|
|
1250
1285
|
const diagnostics = [];
|
|
1251
1286
|
const lines = content.split("\n");
|
|
1252
|
-
for (const pattern of THIN_WRAPPER_PATTERNS) {
|
|
1287
|
+
for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
|
|
1288
|
+
if (!extensions.has(ext)) continue;
|
|
1253
1289
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
1254
1290
|
let match;
|
|
1255
1291
|
while ((match = regex.exec(content)) !== null) {
|
|
@@ -1315,12 +1351,133 @@ const detectOverAbstraction = async (context) => {
|
|
|
1315
1351
|
continue;
|
|
1316
1352
|
}
|
|
1317
1353
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1318
|
-
|
|
1354
|
+
const ext = path.extname(filePath);
|
|
1355
|
+
diagnostics.push(...detectThinWrappers(content, relativePath, ext));
|
|
1319
1356
|
diagnostics.push(...detectAiNaming(content, relativePath));
|
|
1320
1357
|
}
|
|
1321
1358
|
return diagnostics;
|
|
1322
1359
|
};
|
|
1323
1360
|
|
|
1361
|
+
//#endregion
|
|
1362
|
+
//#region src/engines/ai-slop/narrative-comments-patterns.ts
|
|
1363
|
+
const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
|
|
1364
|
+
const DECORATIVE_SECTION_HEADER = /^[-=─━~_*#]{3,}[\s\S]+?[-=─━~_*#]{3,}$/;
|
|
1365
|
+
const SECTION_HEADER = /^(Phase|Step|Section|Part)\s+\d+[:.-]/i;
|
|
1366
|
+
const CROSS_REFERENCE_PHRASES = [
|
|
1367
|
+
/\bwill then be\b/i,
|
|
1368
|
+
/\bused by\b/i,
|
|
1369
|
+
/\bcalled from\b/i,
|
|
1370
|
+
/\bcalled later\b/i,
|
|
1371
|
+
/\bsee (?:above|below|later|earlier)\b/i,
|
|
1372
|
+
/\breplaces the\b/i,
|
|
1373
|
+
/\bmatches the one\b/i,
|
|
1374
|
+
/\bwe moved\b/i,
|
|
1375
|
+
/\bwe used to\b/i,
|
|
1376
|
+
/\brefactor(?:ed)? from\b/i,
|
|
1377
|
+
/\bcombined with\b.*\bthis\b/i
|
|
1378
|
+
];
|
|
1379
|
+
const JUSTIFICATION_OPENERS = [
|
|
1380
|
+
/^(The idea here|The trick is|This was needed|Originally,?)/i,
|
|
1381
|
+
/^This\s+(?:function|method|class|module|component|hook|util|helper|handler|service)\b/i,
|
|
1382
|
+
/^It\s+(?:does|handles|takes|returns|processes|reads|writes|sends|fetches|loads|creates|deletes|updates|parses|validates)\b/i,
|
|
1383
|
+
/^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
|
|
1384
|
+
];
|
|
1385
|
+
const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
|
|
1386
|
+
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|$)/;
|
|
1387
|
+
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;
|
|
1388
|
+
const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
1389
|
+
"deprecated",
|
|
1390
|
+
"see",
|
|
1391
|
+
"example",
|
|
1392
|
+
"type",
|
|
1393
|
+
"returns",
|
|
1394
|
+
"return",
|
|
1395
|
+
"param",
|
|
1396
|
+
"throws",
|
|
1397
|
+
"typedef",
|
|
1398
|
+
"callback",
|
|
1399
|
+
"override",
|
|
1400
|
+
"template",
|
|
1401
|
+
"internal",
|
|
1402
|
+
"public",
|
|
1403
|
+
"private",
|
|
1404
|
+
"protected",
|
|
1405
|
+
"experimental",
|
|
1406
|
+
"alpha",
|
|
1407
|
+
"beta",
|
|
1408
|
+
"since",
|
|
1409
|
+
"todo",
|
|
1410
|
+
"link",
|
|
1411
|
+
"license",
|
|
1412
|
+
"preserve",
|
|
1413
|
+
"swagger",
|
|
1414
|
+
"openapi",
|
|
1415
|
+
"route",
|
|
1416
|
+
"group",
|
|
1417
|
+
"summary",
|
|
1418
|
+
"description",
|
|
1419
|
+
"operationid",
|
|
1420
|
+
"response",
|
|
1421
|
+
"responses",
|
|
1422
|
+
"request",
|
|
1423
|
+
"requestbody",
|
|
1424
|
+
"security",
|
|
1425
|
+
"tag",
|
|
1426
|
+
"tags",
|
|
1427
|
+
"path",
|
|
1428
|
+
"body",
|
|
1429
|
+
"query",
|
|
1430
|
+
"queryparam",
|
|
1431
|
+
"header",
|
|
1432
|
+
"headers",
|
|
1433
|
+
"produces",
|
|
1434
|
+
"accept",
|
|
1435
|
+
"middleware",
|
|
1436
|
+
"api",
|
|
1437
|
+
"apiname",
|
|
1438
|
+
"apidefine",
|
|
1439
|
+
"apigroup",
|
|
1440
|
+
"apiparam",
|
|
1441
|
+
"apiquery",
|
|
1442
|
+
"apibody",
|
|
1443
|
+
"apiheader",
|
|
1444
|
+
"apisuccess",
|
|
1445
|
+
"apierror",
|
|
1446
|
+
"apiexample",
|
|
1447
|
+
"apiversion",
|
|
1448
|
+
"apidescription",
|
|
1449
|
+
"apipermission",
|
|
1450
|
+
"apiuse",
|
|
1451
|
+
"apiignore",
|
|
1452
|
+
"apiprivate",
|
|
1453
|
+
"namespace",
|
|
1454
|
+
"category"
|
|
1455
|
+
]);
|
|
1456
|
+
const SUPPORTED_EXTS = new Set([
|
|
1457
|
+
".ts",
|
|
1458
|
+
".tsx",
|
|
1459
|
+
".js",
|
|
1460
|
+
".jsx",
|
|
1461
|
+
".mjs",
|
|
1462
|
+
".cjs",
|
|
1463
|
+
".py",
|
|
1464
|
+
".go",
|
|
1465
|
+
".rs",
|
|
1466
|
+
".rb",
|
|
1467
|
+
".java",
|
|
1468
|
+
".php"
|
|
1469
|
+
]);
|
|
1470
|
+
const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
|
|
1471
|
+
const EXPORT_DEFAULT = /^\s*export\s+default\b/;
|
|
1472
|
+
const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
|
|
1473
|
+
const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
|
|
1474
|
+
const GO_DECL_START = /^\s*(func|type|var|const|import)\b/;
|
|
1475
|
+
const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
|
|
1476
|
+
const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
|
|
1477
|
+
const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
|
|
1478
|
+
const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
|
|
1479
|
+
const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract|readonly)\s+)*(function|class|interface|trait|enum|const)\s+/;
|
|
1480
|
+
|
|
1324
1481
|
//#endregion
|
|
1325
1482
|
//#region src/engines/ai-slop/non-production-paths.ts
|
|
1326
1483
|
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;
|
|
@@ -1359,11 +1516,37 @@ const isTrivialComment = (trimmed, nextLine) => {
|
|
|
1359
1516
|
if (/^-{3,}|─{3,}/.test(commentBody)) return false;
|
|
1360
1517
|
return (isJs ? TRIVIAL_JS_COMMENT_PATTERNS : TRIVIAL_PYTHON_COMMENT_PATTERNS).some((pattern) => pattern.test(trimmed));
|
|
1361
1518
|
};
|
|
1362
|
-
const
|
|
1519
|
+
const declStartForExt = (ext) => {
|
|
1520
|
+
switch (ext) {
|
|
1521
|
+
case ".rb": return [RUBY_DECL_START];
|
|
1522
|
+
case ".java": return [JAVA_DECL_START, JAVA_DECL_START_FALLBACK];
|
|
1523
|
+
case ".php": return [PHP_DECL_START];
|
|
1524
|
+
default: return [];
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
const isCommentLineForExt = (line, ext) => {
|
|
1528
|
+
const trimmed = line.trim();
|
|
1529
|
+
if (ext === ".rb") return trimmed.startsWith("#") && !trimmed.startsWith("#!");
|
|
1530
|
+
if (ext === ".java" || ext === ".php") return trimmed.startsWith("//") || trimmed.startsWith("#");
|
|
1531
|
+
return false;
|
|
1532
|
+
};
|
|
1533
|
+
const isDocCommentForDeclaration = (lines, lineIdx, ext) => {
|
|
1534
|
+
const patterns = declStartForExt(ext);
|
|
1535
|
+
if (patterns.length === 0) return false;
|
|
1536
|
+
for (let j = lineIdx + 1; j < lines.length; j++) {
|
|
1537
|
+
const candidate = lines[j];
|
|
1538
|
+
if (candidate.trim() === "") continue;
|
|
1539
|
+
if (isCommentLineForExt(candidate, ext)) continue;
|
|
1540
|
+
return patterns.some((re) => re.test(candidate));
|
|
1541
|
+
}
|
|
1542
|
+
return false;
|
|
1543
|
+
};
|
|
1544
|
+
const scanFileForTrivialComments = (content, relativePath, ext) => {
|
|
1363
1545
|
const diagnostics = [];
|
|
1364
1546
|
const lines = content.split("\n");
|
|
1365
1547
|
for (let i = 0; i < lines.length; i++) {
|
|
1366
1548
|
if (!isTrivialComment(lines[i].trim(), i + 1 < lines.length ? lines[i + 1] : void 0)) continue;
|
|
1549
|
+
if (isDocCommentForDeclaration(lines, i, ext)) continue;
|
|
1367
1550
|
diagnostics.push({
|
|
1368
1551
|
filePath: relativePath,
|
|
1369
1552
|
engine: "ai-slop",
|
|
@@ -1392,7 +1575,7 @@ const detectTrivialComments = async (context) => {
|
|
|
1392
1575
|
} catch {
|
|
1393
1576
|
continue;
|
|
1394
1577
|
}
|
|
1395
|
-
diagnostics.push(...scanFileForTrivialComments(content, relativePath));
|
|
1578
|
+
diagnostics.push(...scanFileForTrivialComments(content, relativePath, path.extname(filePath)));
|
|
1396
1579
|
}
|
|
1397
1580
|
return diagnostics;
|
|
1398
1581
|
};
|
|
@@ -1448,6 +1631,19 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
|
|
|
1448
1631
|
"PLACEHOLDER",
|
|
1449
1632
|
"STUB"
|
|
1450
1633
|
].join("|")})[:\\s]`);
|
|
1634
|
+
const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
|
|
1635
|
+
const isGuardedSingleLineExit = (lines, lineIndex) => {
|
|
1636
|
+
const contextLines = [];
|
|
1637
|
+
for (let i = lineIndex - 1; i >= 0 && contextLines.length < 16; i--) {
|
|
1638
|
+
const trimmed = lines[i].trim();
|
|
1639
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
1640
|
+
contextLines.unshift(trimmed);
|
|
1641
|
+
if (/^(?:if|else\s+if|for|while)\b/.test(trimmed) || /^}\s*else\s+if\b/.test(trimmed)) break;
|
|
1642
|
+
if (/;\s*$/.test(trimmed)) break;
|
|
1643
|
+
}
|
|
1644
|
+
const control = contextLines.join(" ");
|
|
1645
|
+
return /(?:^|[}\s])(?:if|else\s+if|for|while)\s*\(/.test(control) && !/{\s*$/.test(control);
|
|
1646
|
+
};
|
|
1451
1647
|
const detectTodoStubs = (content, relativePath) => {
|
|
1452
1648
|
const diagnostics = [];
|
|
1453
1649
|
const lines = content.split("\n");
|
|
@@ -1464,7 +1660,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1464
1660
|
for (let i = 0; i < lines.length; i++) {
|
|
1465
1661
|
const trimmed = lines[i].trim();
|
|
1466
1662
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
1467
|
-
if (JS_EXTENSIONS$4.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !
|
|
1663
|
+
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));
|
|
1468
1664
|
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));
|
|
1469
1665
|
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));
|
|
1470
1666
|
}
|
|
@@ -1632,6 +1828,25 @@ const SWALLOWED_EXCEPTION_PATTERNS = [
|
|
|
1632
1828
|
message: "Empty catch block swallows errors silently"
|
|
1633
1829
|
}
|
|
1634
1830
|
];
|
|
1831
|
+
const INTENTIONAL_IGNORE_NAMES = new Set([
|
|
1832
|
+
"ignored",
|
|
1833
|
+
"ignore",
|
|
1834
|
+
"tolerated",
|
|
1835
|
+
"expected",
|
|
1836
|
+
"unused",
|
|
1837
|
+
"_",
|
|
1838
|
+
"_e",
|
|
1839
|
+
"_err",
|
|
1840
|
+
"_ex",
|
|
1841
|
+
"_t"
|
|
1842
|
+
]);
|
|
1843
|
+
const CATCH_PARAM_RE = /catch\s*\(\s*(?:\w+\s+)?([\w$]+)/;
|
|
1844
|
+
const RESCUE_PARAM_RE = /rescue(?:\s+[\w:]+)?\s*=>\s*([\w$]+)/;
|
|
1845
|
+
const isIntentionalIgnore = (matchText, ext) => {
|
|
1846
|
+
const m = (ext === ".rb" ? RESCUE_PARAM_RE : CATCH_PARAM_RE).exec(matchText);
|
|
1847
|
+
if (!m) return false;
|
|
1848
|
+
return INTENTIONAL_IGNORE_NAMES.has(m[1].toLowerCase());
|
|
1849
|
+
};
|
|
1635
1850
|
const detectSwallowedExceptions = async (context) => {
|
|
1636
1851
|
const files = getSourceFiles(context);
|
|
1637
1852
|
const diagnostics = [];
|
|
@@ -1650,6 +1865,7 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
1650
1865
|
let match;
|
|
1651
1866
|
const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
|
|
1652
1867
|
while ((match = regex.exec(content)) !== null) {
|
|
1868
|
+
if (isIntentionalIgnore(match[0], ext)) continue;
|
|
1653
1869
|
const line = content.slice(0, match.index).split("\n").length;
|
|
1654
1870
|
diagnostics.push({
|
|
1655
1871
|
filePath: relativePath,
|
|
@@ -1742,6 +1958,117 @@ const detectGoPatterns = async (context) => {
|
|
|
1742
1958
|
return diagnostics;
|
|
1743
1959
|
};
|
|
1744
1960
|
|
|
1961
|
+
//#endregion
|
|
1962
|
+
//#region src/engines/ai-slop/js-import-aliases.ts
|
|
1963
|
+
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
1964
|
+
const JS_RESOLUTION_EXTENSIONS = [
|
|
1965
|
+
"",
|
|
1966
|
+
".ts",
|
|
1967
|
+
".tsx",
|
|
1968
|
+
".js",
|
|
1969
|
+
".jsx",
|
|
1970
|
+
".mjs",
|
|
1971
|
+
".cjs",
|
|
1972
|
+
".json",
|
|
1973
|
+
"/index.ts",
|
|
1974
|
+
"/index.tsx",
|
|
1975
|
+
"/index.js",
|
|
1976
|
+
"/index.jsx"
|
|
1977
|
+
];
|
|
1978
|
+
const readJson$2 = (filePath) => {
|
|
1979
|
+
try {
|
|
1980
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1981
|
+
} catch {
|
|
1982
|
+
return null;
|
|
1983
|
+
}
|
|
1984
|
+
};
|
|
1985
|
+
const buildAliasMatcher = (key) => {
|
|
1986
|
+
const starIdx = key.indexOf("*");
|
|
1987
|
+
if (starIdx === -1) return (spec) => spec === key;
|
|
1988
|
+
const before = key.slice(0, starIdx);
|
|
1989
|
+
const after = key.slice(starIdx + 1);
|
|
1990
|
+
return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
|
|
1991
|
+
};
|
|
1992
|
+
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
1993
|
+
const opts = readJson$2(configPath)?.compilerOptions;
|
|
1994
|
+
if (!opts || typeof opts !== "object") return;
|
|
1995
|
+
const configDir = path.dirname(configPath);
|
|
1996
|
+
const paths = opts.paths;
|
|
1997
|
+
if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
|
|
1998
|
+
const baseUrl = opts.baseUrl;
|
|
1999
|
+
if (typeof baseUrl === "string") {
|
|
2000
|
+
const baseDir = path.resolve(configDir, baseUrl);
|
|
2001
|
+
matchers.push((spec) => {
|
|
2002
|
+
if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("@")) return false;
|
|
2003
|
+
return JS_RESOLUTION_EXTENSIONS.some((suffix) => fs.existsSync(path.join(baseDir, `${spec}${suffix}`)));
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
const collectTsPathAliases = (rootDir, workspaceDirs) => {
|
|
2008
|
+
const matchers = [];
|
|
2009
|
+
const dirs = [rootDir, ...workspaceDirs];
|
|
2010
|
+
for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
|
|
2011
|
+
return matchers;
|
|
2012
|
+
};
|
|
2013
|
+
|
|
2014
|
+
//#endregion
|
|
2015
|
+
//#region src/engines/ai-slop/js-workspaces.ts
|
|
2016
|
+
const readJson$1 = (filePath) => {
|
|
2017
|
+
try {
|
|
2018
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
2019
|
+
} catch {
|
|
2020
|
+
return null;
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
2024
|
+
const globs = [];
|
|
2025
|
+
if (rootPkg && typeof rootPkg === "object") {
|
|
2026
|
+
const ws = rootPkg.workspaces;
|
|
2027
|
+
if (Array.isArray(ws)) {
|
|
2028
|
+
for (const g of ws) if (typeof g === "string") globs.push(g);
|
|
2029
|
+
} else if (ws && typeof ws === "object") {
|
|
2030
|
+
const pkgs = ws.packages;
|
|
2031
|
+
if (Array.isArray(pkgs)) {
|
|
2032
|
+
for (const g of pkgs) if (typeof g === "string") globs.push(g);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
const lerna = readJson$1(path.join(rootDir, "lerna.json"));
|
|
2037
|
+
if (lerna && Array.isArray(lerna.packages)) {
|
|
2038
|
+
for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
|
|
2039
|
+
}
|
|
2040
|
+
try {
|
|
2041
|
+
const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
|
|
2042
|
+
let inPackages = false;
|
|
2043
|
+
for (const rawLine of pnpmWs.split("\n")) {
|
|
2044
|
+
if (/^packages\s*:\s*$/.test(rawLine)) {
|
|
2045
|
+
inPackages = true;
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
if (!inPackages) continue;
|
|
2049
|
+
if (/^\S/.test(rawLine)) break;
|
|
2050
|
+
const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
|
|
2051
|
+
if (m) globs.push(m[1].trim());
|
|
2052
|
+
}
|
|
2053
|
+
} catch {
|
|
2054
|
+
return globs;
|
|
2055
|
+
}
|
|
2056
|
+
return globs;
|
|
2057
|
+
};
|
|
2058
|
+
const expandWorkspaceDirs = (rootDir, globs) => {
|
|
2059
|
+
const dirs = [];
|
|
2060
|
+
for (const glob of globs) if (glob.endsWith("/*")) {
|
|
2061
|
+
const parent = path.join(rootDir, glob.slice(0, -2));
|
|
2062
|
+
try {
|
|
2063
|
+
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
2064
|
+
} catch {
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
} else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
|
|
2068
|
+
return dirs;
|
|
2069
|
+
};
|
|
2070
|
+
const collectWorkspaceDirs = (rootDir, rootPkg) => expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, rootPkg));
|
|
2071
|
+
|
|
1745
2072
|
//#endregion
|
|
1746
2073
|
//#region src/engines/ai-slop/python-data.ts
|
|
1747
2074
|
const PYTHON_STDLIB = new Set([
|
|
@@ -1986,11 +2313,35 @@ const collectFromPipfile = (rootDir, pyDeps) => {
|
|
|
1986
2313
|
return false;
|
|
1987
2314
|
}
|
|
1988
2315
|
};
|
|
2316
|
+
const LOCAL_PACKAGE_ROOTS = [
|
|
2317
|
+
"",
|
|
2318
|
+
"src",
|
|
2319
|
+
"lib"
|
|
2320
|
+
];
|
|
2321
|
+
const collectLocalPythonPackages = (rootDir, pyDeps) => {
|
|
2322
|
+
for (const sub of LOCAL_PACKAGE_ROOTS) {
|
|
2323
|
+
const dir = sub ? path.join(rootDir, sub) : rootDir;
|
|
2324
|
+
let entries;
|
|
2325
|
+
try {
|
|
2326
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2327
|
+
} catch {
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
for (const entry of entries) {
|
|
2331
|
+
if (!entry.isDirectory()) continue;
|
|
2332
|
+
if (entry.name.startsWith(".")) continue;
|
|
2333
|
+
if (entry.name === "node_modules" || entry.name === "__pycache__") continue;
|
|
2334
|
+
const initPath = path.join(dir, entry.name, "__init__.py");
|
|
2335
|
+
if (fs.existsSync(initPath)) addPyDep(pyDeps, entry.name);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
};
|
|
1989
2339
|
const collectPythonDeps = (rootDir) => {
|
|
1990
2340
|
const pyDeps = /* @__PURE__ */ new Set();
|
|
1991
2341
|
const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
|
|
1992
2342
|
const hasPyproject = collectFromPyproject(rootDir, pyDeps);
|
|
1993
2343
|
const hasPipfile = collectFromPipfile(rootDir, pyDeps);
|
|
2344
|
+
collectLocalPythonPackages(rootDir, pyDeps);
|
|
1994
2345
|
return {
|
|
1995
2346
|
pyDeps,
|
|
1996
2347
|
hasPyManifest: hasReq || hasPyproject || hasPipfile
|
|
@@ -2027,51 +2378,6 @@ const addDepsFromPkg = (pkg, jsDeps) => {
|
|
|
2027
2378
|
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
|
|
2028
2379
|
}
|
|
2029
2380
|
};
|
|
2030
|
-
const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
2031
|
-
const globs = [];
|
|
2032
|
-
if (rootPkg && typeof rootPkg === "object") {
|
|
2033
|
-
const ws = rootPkg.workspaces;
|
|
2034
|
-
if (Array.isArray(ws)) {
|
|
2035
|
-
for (const g of ws) if (typeof g === "string") globs.push(g);
|
|
2036
|
-
} else if (ws && typeof ws === "object") {
|
|
2037
|
-
const pkgs = ws.packages;
|
|
2038
|
-
if (Array.isArray(pkgs)) {
|
|
2039
|
-
for (const g of pkgs) if (typeof g === "string") globs.push(g);
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
const lerna = readJson(path.join(rootDir, "lerna.json"));
|
|
2044
|
-
if (lerna && Array.isArray(lerna.packages)) {
|
|
2045
|
-
for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
|
|
2046
|
-
}
|
|
2047
|
-
try {
|
|
2048
|
-
const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
|
|
2049
|
-
let inPackages = false;
|
|
2050
|
-
for (const rawLine of pnpmWs.split("\n")) {
|
|
2051
|
-
if (/^packages\s*:\s*$/.test(rawLine)) {
|
|
2052
|
-
inPackages = true;
|
|
2053
|
-
continue;
|
|
2054
|
-
}
|
|
2055
|
-
if (!inPackages) continue;
|
|
2056
|
-
if (/^\S/.test(rawLine)) break;
|
|
2057
|
-
const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
|
|
2058
|
-
if (m) globs.push(m[1].trim());
|
|
2059
|
-
}
|
|
2060
|
-
} catch {}
|
|
2061
|
-
return globs;
|
|
2062
|
-
};
|
|
2063
|
-
const expandWorkspaceDirs = (rootDir, globs) => {
|
|
2064
|
-
const dirs = [];
|
|
2065
|
-
for (const glob of globs) if (glob.endsWith("/*")) {
|
|
2066
|
-
const parent = path.join(rootDir, glob.slice(0, -2));
|
|
2067
|
-
try {
|
|
2068
|
-
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
2069
|
-
} catch {
|
|
2070
|
-
continue;
|
|
2071
|
-
}
|
|
2072
|
-
} else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
|
|
2073
|
-
return dirs;
|
|
2074
|
-
};
|
|
2075
2381
|
const SKIP_DIRS = new Set([
|
|
2076
2382
|
"node_modules",
|
|
2077
2383
|
".git",
|
|
@@ -2111,54 +2417,17 @@ const collectJsDeps = (rootDir, jsDeps) => {
|
|
|
2111
2417
|
if (!fs.existsSync(pkgPath)) return false;
|
|
2112
2418
|
const pkg = readJson(pkgPath);
|
|
2113
2419
|
if (!pkg || typeof pkg !== "object") return false;
|
|
2114
|
-
addDepsFromPkg(pkg, jsDeps);
|
|
2115
|
-
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
2116
|
-
const workspaceDirs =
|
|
2117
|
-
for (const wsDir of workspaceDirs) {
|
|
2118
|
-
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
2119
|
-
if (!wsPkg) continue;
|
|
2120
|
-
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
2121
|
-
addDepsFromPkg(wsPkg, jsDeps);
|
|
2122
|
-
}
|
|
2123
|
-
collectNestedManifests(rootDir, jsDeps);
|
|
2124
|
-
return true;
|
|
2125
|
-
};
|
|
2126
|
-
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
2127
|
-
const buildAliasMatcher = (key) => {
|
|
2128
|
-
const starIdx = key.indexOf("*");
|
|
2129
|
-
if (starIdx === -1) return (spec) => spec === key;
|
|
2130
|
-
const before = key.slice(0, starIdx);
|
|
2131
|
-
const after = key.slice(starIdx + 1);
|
|
2132
|
-
return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
|
|
2133
|
-
};
|
|
2134
|
-
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
2135
|
-
const opts = readJson(configPath)?.compilerOptions;
|
|
2136
|
-
if (!opts) return;
|
|
2137
|
-
const paths = opts.paths;
|
|
2138
|
-
if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
|
|
2139
|
-
const baseUrl = opts.baseUrl;
|
|
2140
|
-
if (typeof baseUrl === "string") {
|
|
2141
|
-
const baseUrlDir = path.resolve(path.dirname(configPath), baseUrl);
|
|
2142
|
-
let entries;
|
|
2143
|
-
try {
|
|
2144
|
-
entries = fs.readdirSync(baseUrlDir);
|
|
2145
|
-
} catch {
|
|
2146
|
-
return;
|
|
2147
|
-
}
|
|
2148
|
-
const baseSpecifiers = /* @__PURE__ */ new Set();
|
|
2149
|
-
for (const entry of entries) {
|
|
2150
|
-
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
2151
|
-
const base = entry.replace(/\.(?:[jt]sx?|mjs|cjs|d\.ts)$/i, "");
|
|
2152
|
-
if (base.length > 0) baseSpecifiers.add(base);
|
|
2153
|
-
}
|
|
2154
|
-
for (const name of baseSpecifiers) matchers.push((spec) => spec === name || spec.startsWith(`${name}/`));
|
|
2420
|
+
addDepsFromPkg(pkg, jsDeps);
|
|
2421
|
+
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
2422
|
+
const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
|
|
2423
|
+
for (const wsDir of workspaceDirs) {
|
|
2424
|
+
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
2425
|
+
if (!wsPkg) continue;
|
|
2426
|
+
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
2427
|
+
addDepsFromPkg(wsPkg, jsDeps);
|
|
2155
2428
|
}
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
const matchers = [];
|
|
2159
|
-
const dirs = [rootDir, ...expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, readJson(path.join(rootDir, "package.json"))))];
|
|
2160
|
-
for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
|
|
2161
|
-
return matchers;
|
|
2429
|
+
collectNestedManifests(rootDir, jsDeps);
|
|
2430
|
+
return true;
|
|
2162
2431
|
};
|
|
2163
2432
|
const loadManifest = (rootDir) => {
|
|
2164
2433
|
const jsDeps = /* @__PURE__ */ new Set();
|
|
@@ -2181,14 +2450,19 @@ const VIRTUAL_MODULE_PREFIXES = [
|
|
|
2181
2450
|
"astro:",
|
|
2182
2451
|
"virtual:",
|
|
2183
2452
|
"bun:",
|
|
2184
|
-
"
|
|
2453
|
+
"file:"
|
|
2185
2454
|
];
|
|
2186
|
-
const isJsVirtualModule = (spec
|
|
2455
|
+
const isJsVirtualModule = (spec, manifest) => {
|
|
2456
|
+
if (VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p))) return true;
|
|
2457
|
+
if (spec === "bun") return true;
|
|
2458
|
+
if (spec === "unfonts.css" && manifest.jsDeps.has("unplugin-fonts")) return true;
|
|
2459
|
+
if (spec.startsWith("~icons/") && manifest.jsDeps.has("unplugin-icons")) return true;
|
|
2460
|
+
return false;
|
|
2461
|
+
};
|
|
2187
2462
|
const stripImportQuery = (spec) => {
|
|
2188
2463
|
const idx = spec.indexOf("?");
|
|
2189
2464
|
return idx === -1 ? spec : spec.slice(0, idx);
|
|
2190
2465
|
};
|
|
2191
|
-
const VIRTUAL_ASSET_FILES = { "unfonts.css": "unplugin-fonts" };
|
|
2192
2466
|
const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
|
|
2193
2467
|
const isLikelyRealImportSpec = (spec) => {
|
|
2194
2468
|
if (spec.length === 0) return false;
|
|
@@ -2260,9 +2534,7 @@ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
|
|
|
2260
2534
|
if (spec.length === 0) return null;
|
|
2261
2535
|
if (isJsRelativeOrAbsolute(spec)) return null;
|
|
2262
2536
|
if (isJsBuiltin(spec)) return null;
|
|
2263
|
-
if (isJsVirtualModule(spec)) return null;
|
|
2264
|
-
const virtualOwner = VIRTUAL_ASSET_FILES[spec];
|
|
2265
|
-
if (virtualOwner && manifest.jsDeps.has(virtualOwner)) return null;
|
|
2537
|
+
if (isJsVirtualModule(spec, manifest)) return null;
|
|
2266
2538
|
if (tsAliasMatchers.some((m) => m(spec))) return null;
|
|
2267
2539
|
const pkg = packageNameFromImport(spec);
|
|
2268
2540
|
if (manifest.jsDeps.has(pkg)) return null;
|
|
@@ -2282,9 +2554,11 @@ const checkPyImport = (spec, manifest) => {
|
|
|
2282
2554
|
return root;
|
|
2283
2555
|
};
|
|
2284
2556
|
const detectHallucinatedImports = async (context) => {
|
|
2557
|
+
const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
|
|
2558
|
+
const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
|
|
2285
2559
|
const manifest = loadManifest(context.rootDirectory);
|
|
2286
2560
|
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
2287
|
-
const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory) : [];
|
|
2561
|
+
const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory, workspaceDirs) : [];
|
|
2288
2562
|
const diagnostics = [];
|
|
2289
2563
|
const files = getSourceFiles(context);
|
|
2290
2564
|
for (const filePath of files) {
|
|
@@ -2325,121 +2599,7 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2325
2599
|
};
|
|
2326
2600
|
|
|
2327
2601
|
//#endregion
|
|
2328
|
-
//#region src/engines/ai-slop/
|
|
2329
|
-
const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
|
|
2330
|
-
const DECORATIVE_SECTION_HEADER = /^[-=─━~_*#]{3,}[\s\S]+?[-=─━~_*#]{3,}$/;
|
|
2331
|
-
const SECTION_HEADER = /^(Phase|Step|Section|Part)\s+\d+[:.-]/i;
|
|
2332
|
-
const CROSS_REFERENCE_PHRASES = [
|
|
2333
|
-
/\bwill then be\b/i,
|
|
2334
|
-
/\bused by\b/i,
|
|
2335
|
-
/\bcalled from\b/i,
|
|
2336
|
-
/\bcalled later\b/i,
|
|
2337
|
-
/\bsee (?:above|below|later|earlier)\b/i,
|
|
2338
|
-
/\breplaces the\b/i,
|
|
2339
|
-
/\bmatches the one\b/i,
|
|
2340
|
-
/\bwe moved\b/i,
|
|
2341
|
-
/\bwe used to\b/i,
|
|
2342
|
-
/\brefactor(?:ed)? from\b/i,
|
|
2343
|
-
/\bcombined with\b.*\bthis\b/i
|
|
2344
|
-
];
|
|
2345
|
-
const JUSTIFICATION_OPENERS = [/^(The idea here|The trick is|This was needed|Originally,?)/i];
|
|
2346
|
-
const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
|
|
2347
|
-
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;
|
|
2348
|
-
const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
2349
|
-
"deprecated",
|
|
2350
|
-
"see",
|
|
2351
|
-
"example",
|
|
2352
|
-
"type",
|
|
2353
|
-
"returns",
|
|
2354
|
-
"return",
|
|
2355
|
-
"param",
|
|
2356
|
-
"throws",
|
|
2357
|
-
"typedef",
|
|
2358
|
-
"callback",
|
|
2359
|
-
"override",
|
|
2360
|
-
"template",
|
|
2361
|
-
"internal",
|
|
2362
|
-
"public",
|
|
2363
|
-
"private",
|
|
2364
|
-
"protected",
|
|
2365
|
-
"experimental",
|
|
2366
|
-
"alpha",
|
|
2367
|
-
"beta",
|
|
2368
|
-
"since",
|
|
2369
|
-
"todo",
|
|
2370
|
-
"link",
|
|
2371
|
-
"license",
|
|
2372
|
-
"preserve",
|
|
2373
|
-
"swagger",
|
|
2374
|
-
"openapi",
|
|
2375
|
-
"route",
|
|
2376
|
-
"group",
|
|
2377
|
-
"summary",
|
|
2378
|
-
"description",
|
|
2379
|
-
"operationid",
|
|
2380
|
-
"response",
|
|
2381
|
-
"responses",
|
|
2382
|
-
"request",
|
|
2383
|
-
"requestbody",
|
|
2384
|
-
"security",
|
|
2385
|
-
"tag",
|
|
2386
|
-
"tags",
|
|
2387
|
-
"path",
|
|
2388
|
-
"body",
|
|
2389
|
-
"query",
|
|
2390
|
-
"queryparam",
|
|
2391
|
-
"header",
|
|
2392
|
-
"headers",
|
|
2393
|
-
"produces",
|
|
2394
|
-
"accept",
|
|
2395
|
-
"middleware",
|
|
2396
|
-
"api",
|
|
2397
|
-
"apiname",
|
|
2398
|
-
"apidefine",
|
|
2399
|
-
"apigroup",
|
|
2400
|
-
"apiparam",
|
|
2401
|
-
"apiquery",
|
|
2402
|
-
"apibody",
|
|
2403
|
-
"apiheader",
|
|
2404
|
-
"apisuccess",
|
|
2405
|
-
"apierror",
|
|
2406
|
-
"apiexample",
|
|
2407
|
-
"apiversion",
|
|
2408
|
-
"apidescription",
|
|
2409
|
-
"apipermission",
|
|
2410
|
-
"apiuse",
|
|
2411
|
-
"apiignore",
|
|
2412
|
-
"apiprivate",
|
|
2413
|
-
"namespace",
|
|
2414
|
-
"category"
|
|
2415
|
-
]);
|
|
2416
|
-
const SUPPORTED_EXTS = new Set([
|
|
2417
|
-
".ts",
|
|
2418
|
-
".tsx",
|
|
2419
|
-
".js",
|
|
2420
|
-
".jsx",
|
|
2421
|
-
".mjs",
|
|
2422
|
-
".cjs",
|
|
2423
|
-
".py",
|
|
2424
|
-
".go",
|
|
2425
|
-
".rs",
|
|
2426
|
-
".rb",
|
|
2427
|
-
".java",
|
|
2428
|
-
".php"
|
|
2429
|
-
]);
|
|
2430
|
-
const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
|
|
2431
|
-
const EXPORT_DEFAULT = /^\s*export\s+default\b/;
|
|
2432
|
-
const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
|
|
2433
|
-
const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
|
|
2434
|
-
const GO_DECL_START = /^\s*(func|type|var|const)\s+/;
|
|
2435
|
-
const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
|
|
2436
|
-
const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
|
|
2437
|
-
const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
|
|
2438
|
-
const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
|
|
2439
|
-
const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|readonly\s+)*(function|class|interface|trait|enum|const)\s+/;
|
|
2440
|
-
|
|
2441
|
-
//#endregion
|
|
2442
|
-
//#region src/engines/ai-slop/narrative-comments.ts
|
|
2602
|
+
//#region src/engines/ai-slop/comment-blocks.ts
|
|
2443
2603
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
2444
2604
|
const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
|
|
2445
2605
|
const getCommentSyntax = (ext) => {
|
|
@@ -2539,6 +2699,9 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
2539
2699
|
}
|
|
2540
2700
|
return blocks;
|
|
2541
2701
|
};
|
|
2702
|
+
|
|
2703
|
+
//#endregion
|
|
2704
|
+
//#region src/engines/ai-slop/narrative-comments.ts
|
|
2542
2705
|
const looksLikeDeclarationPreamble = (nextLine, ext) => {
|
|
2543
2706
|
if (nextLine === null) return false;
|
|
2544
2707
|
if (DECL_START.test(nextLine) || EXPORT_DEFAULT.test(nextLine)) return true;
|
|
@@ -2568,6 +2731,7 @@ const isBareSectionLabel = (prose) => {
|
|
|
2568
2731
|
if (!BARE_LABEL_RE.test(prose)) return false;
|
|
2569
2732
|
if (prose.endsWith(".")) return false;
|
|
2570
2733
|
if (prose.split(/\s+/).length > 3) return false;
|
|
2734
|
+
if (STEP_COMMENT_VERB_RE.test(prose)) return false;
|
|
2571
2735
|
return true;
|
|
2572
2736
|
};
|
|
2573
2737
|
const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
|
|
@@ -2582,15 +2746,45 @@ const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
|
2582
2746
|
};
|
|
2583
2747
|
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));
|
|
2584
2748
|
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
2749
|
+
const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
|
|
2750
|
+
const GO_KEYWORDS = new Set([
|
|
2751
|
+
"return",
|
|
2752
|
+
"if",
|
|
2753
|
+
"for",
|
|
2754
|
+
"switch",
|
|
2755
|
+
"case",
|
|
2756
|
+
"default",
|
|
2757
|
+
"go",
|
|
2758
|
+
"select",
|
|
2759
|
+
"defer",
|
|
2760
|
+
"else",
|
|
2761
|
+
"break",
|
|
2762
|
+
"continue",
|
|
2763
|
+
"goto",
|
|
2764
|
+
"package",
|
|
2765
|
+
"import",
|
|
2766
|
+
"map",
|
|
2767
|
+
"chan",
|
|
2768
|
+
"range"
|
|
2769
|
+
]);
|
|
2585
2770
|
const looksLikeGoDocComment = (block, ext) => {
|
|
2586
2771
|
if (ext !== ".go" || block.kind !== "line") return false;
|
|
2587
2772
|
const next = block.nextNonBlankLine;
|
|
2588
2773
|
if (!next) return false;
|
|
2589
|
-
const
|
|
2590
|
-
|
|
2591
|
-
|
|
2774
|
+
const trimmedNext = next.trim();
|
|
2775
|
+
const firstWord = (block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "";
|
|
2776
|
+
const declMatch = GO_DECL_NAME_RE.exec(trimmedNext);
|
|
2777
|
+
if (declMatch && firstWord === declMatch[1]) return true;
|
|
2778
|
+
const fieldMatch = GO_FIELD_LEAD_RE.exec(trimmedNext);
|
|
2779
|
+
if (fieldMatch && !GO_KEYWORDS.has(fieldMatch[1]) && firstWord === fieldMatch[1]) return true;
|
|
2780
|
+
return false;
|
|
2781
|
+
};
|
|
2782
|
+
const RUBY_DOC_INDICATORS = /^\s*#\s*(?:#|@\w+|:[\w-]+:|=begin|=end)/;
|
|
2783
|
+
const looksLikeRubyDocBlock = (block, ext) => {
|
|
2784
|
+
if (ext !== ".rb" || block.kind !== "line") return false;
|
|
2785
|
+
return block.rawLines.some((line) => RUBY_DOC_INDICATORS.test(line));
|
|
2592
2786
|
};
|
|
2593
|
-
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):|\(e\.g\.[^)]+\)|\(i\.e\.[^)]+\)/i;
|
|
2787
|
+
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;
|
|
2594
2788
|
const hasDocIndicator = (block) => {
|
|
2595
2789
|
const joined = block.prose.join(" ");
|
|
2596
2790
|
if (DOC_INDICATOR_RE.test(joined)) return true;
|
|
@@ -2626,6 +2820,10 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
2626
2820
|
matched: false,
|
|
2627
2821
|
reason: ""
|
|
2628
2822
|
};
|
|
2823
|
+
if (looksLikeRubyDocBlock(block, ext)) return {
|
|
2824
|
+
matched: false,
|
|
2825
|
+
reason: ""
|
|
2826
|
+
};
|
|
2629
2827
|
if (block.kind === "line" && block.prose.some((l) => DECORATIVE_SEPARATOR.test(l) || DECORATIVE_SECTION_HEADER.test(l))) return {
|
|
2630
2828
|
matched: true,
|
|
2631
2829
|
reason: "decorative separator"
|
|
@@ -2634,17 +2832,17 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
2634
2832
|
matched: true,
|
|
2635
2833
|
reason: "phase/section header"
|
|
2636
2834
|
};
|
|
2637
|
-
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine)) return {
|
|
2835
|
+
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
2638
2836
|
matched: true,
|
|
2639
2837
|
reason: "bare section label"
|
|
2640
2838
|
};
|
|
2641
2839
|
const joined = block.prose.join(" ");
|
|
2642
2840
|
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
2643
|
-
if (
|
|
2841
|
+
if (hasWhyMarker || hasDocIndicator(block)) return {
|
|
2644
2842
|
matched: false,
|
|
2645
2843
|
reason: ""
|
|
2646
2844
|
};
|
|
2647
|
-
if (block.kind === "line" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
2845
|
+
if (block.kind === "line" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext) && hasPreambleSlopSignal(block)) return {
|
|
2648
2846
|
matched: true,
|
|
2649
2847
|
reason: "multi-line preamble before declaration"
|
|
2650
2848
|
};
|
|
@@ -2667,11 +2865,18 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
2667
2865
|
reason: "explanatory preamble"
|
|
2668
2866
|
};
|
|
2669
2867
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2868
|
+
const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
|
|
2869
|
+
if (nonEmptyProseCount >= 5) {
|
|
2870
|
+
if (isAboveDeclaration) return {
|
|
2871
|
+
matched: false,
|
|
2872
|
+
reason: ""
|
|
2873
|
+
};
|
|
2874
|
+
return {
|
|
2875
|
+
matched: true,
|
|
2876
|
+
reason: "long narrative block"
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
|
|
2675
2880
|
matched: true,
|
|
2676
2881
|
reason: "multi-line narrative prose"
|
|
2677
2882
|
};
|
|
@@ -2726,6 +2931,11 @@ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\
|
|
|
2726
2931
|
const PRINT_RE = /^\s*print\s*\(/;
|
|
2727
2932
|
const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
|
|
2728
2933
|
const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
|
|
2934
|
+
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*(?:#.*)?$/;
|
|
2935
|
+
const CHAINED_DICT_GET_RE = /\.get\s*\([^)]*,\s*\{\s*\}\s*\)\s*\.get\s*\(/;
|
|
2936
|
+
const SAME_VALUE_BRANCH_RE = /^(\s*)(?:if|elif)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*==\s*["'][^"']+["']\s*:/;
|
|
2937
|
+
const INSTANCE_BRANCH_RE = /^(\s*)(?:if|elif)\s+isinstance\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^)]+\)\s*:/;
|
|
2938
|
+
const BRANCH_LADDER_THRESHOLD = 4;
|
|
2729
2939
|
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");
|
|
2730
2940
|
const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
|
|
2731
2941
|
const SCRIPT_DIR_NAMES = new Set([
|
|
@@ -2778,6 +2988,13 @@ const pushFinding = (out, a) => {
|
|
|
2778
2988
|
fixable: false
|
|
2779
2989
|
});
|
|
2780
2990
|
};
|
|
2991
|
+
const pushLineFinding = (out, relPath, line, finding) => {
|
|
2992
|
+
pushFinding(out, {
|
|
2993
|
+
relPath,
|
|
2994
|
+
line,
|
|
2995
|
+
...finding
|
|
2996
|
+
});
|
|
2997
|
+
};
|
|
2781
2998
|
const flagBareExcept = (lines, relPath, out) => {
|
|
2782
2999
|
for (let i = 0; i < lines.length; i++) {
|
|
2783
3000
|
if (!BARE_EXCEPT_RE.test(lines[i])) continue;
|
|
@@ -2859,6 +3076,76 @@ const flagPrintInProduction = (lines, relPath, basename, out) => {
|
|
|
2859
3076
|
});
|
|
2860
3077
|
}
|
|
2861
3078
|
};
|
|
3079
|
+
const flagRangeLenLoops = (lines, relPath, out) => {
|
|
3080
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3081
|
+
const match = RANGE_LEN_LOOP_RE.exec(lines[i]);
|
|
3082
|
+
if (!match) continue;
|
|
3083
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
3084
|
+
rule: "ai-slop/python-range-len-loop",
|
|
3085
|
+
severity: "info",
|
|
3086
|
+
message: `\`range(len(${match[2]}))\` loop — usually a hand-rolled iteration pattern.`,
|
|
3087
|
+
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."
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
};
|
|
3091
|
+
const flagChainedDictGets = (lines, relPath, out) => {
|
|
3092
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3093
|
+
if (!CHAINED_DICT_GET_RE.test(lines[i])) continue;
|
|
3094
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
3095
|
+
rule: "ai-slop/python-chained-dict-get",
|
|
3096
|
+
severity: "warning",
|
|
3097
|
+
message: "Chained `.get(..., {})` defaults hide missing-data cases.",
|
|
3098
|
+
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."
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
const countBranchLadder = (lines, start, pattern, selector, indent) => {
|
|
3103
|
+
let count = 1;
|
|
3104
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
3105
|
+
const line = lines[i];
|
|
3106
|
+
const trimmed = line.trim();
|
|
3107
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
3108
|
+
const match = pattern.exec(line);
|
|
3109
|
+
if (match?.[1] === indent && match[2] === selector) {
|
|
3110
|
+
count++;
|
|
3111
|
+
continue;
|
|
3112
|
+
}
|
|
3113
|
+
if (line.startsWith(`${indent}elif `)) break;
|
|
3114
|
+
if (line.length - line.trimStart().length <= indent.length && !line.startsWith(`${indent}else`)) break;
|
|
3115
|
+
}
|
|
3116
|
+
return count;
|
|
3117
|
+
};
|
|
3118
|
+
const flagBranchLadders = (lines, relPath, out) => {
|
|
3119
|
+
const reported = /* @__PURE__ */ new Set();
|
|
3120
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3121
|
+
if (reported.has(i)) continue;
|
|
3122
|
+
const valueMatch = SAME_VALUE_BRANCH_RE.exec(lines[i]);
|
|
3123
|
+
if (valueMatch) {
|
|
3124
|
+
const count = countBranchLadder(lines, i, SAME_VALUE_BRANCH_RE, valueMatch[2], valueMatch[1]);
|
|
3125
|
+
if (count >= BRANCH_LADDER_THRESHOLD) {
|
|
3126
|
+
reported.add(i);
|
|
3127
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
3128
|
+
rule: "ai-slop/python-repetitive-dispatch",
|
|
3129
|
+
severity: "warning",
|
|
3130
|
+
message: `${count} repeated branches dispatch on \`${valueMatch[2]}\`.`,
|
|
3131
|
+
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."
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
const instanceMatch = INSTANCE_BRANCH_RE.exec(lines[i]);
|
|
3137
|
+
if (!instanceMatch) continue;
|
|
3138
|
+
const count = countBranchLadder(lines, i, INSTANCE_BRANCH_RE, instanceMatch[2], instanceMatch[1]);
|
|
3139
|
+
if (count < BRANCH_LADDER_THRESHOLD) continue;
|
|
3140
|
+
reported.add(i);
|
|
3141
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
3142
|
+
rule: "ai-slop/python-isinstance-ladder",
|
|
3143
|
+
severity: "warning",
|
|
3144
|
+
message: `${count} repeated \`isinstance(${instanceMatch[2]}, ...)\` branches.`,
|
|
3145
|
+
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."
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
};
|
|
2862
3149
|
const detectPythonPatterns = async (context) => {
|
|
2863
3150
|
const diagnostics = [];
|
|
2864
3151
|
const files = getSourceFiles(context);
|
|
@@ -2878,6 +3165,9 @@ const detectPythonPatterns = async (context) => {
|
|
|
2878
3165
|
flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
|
|
2879
3166
|
flagMutableDefaults(lines, relPath, diagnostics);
|
|
2880
3167
|
flagPrintInProduction(lines, relPath, basename, diagnostics);
|
|
3168
|
+
flagRangeLenLoops(lines, relPath, diagnostics);
|
|
3169
|
+
flagChainedDictGets(lines, relPath, diagnostics);
|
|
3170
|
+
flagBranchLadders(lines, relPath, diagnostics);
|
|
2881
3171
|
}
|
|
2882
3172
|
return diagnostics;
|
|
2883
3173
|
};
|
|
@@ -2903,7 +3193,23 @@ const isTestFile = (relPath) => {
|
|
|
2903
3193
|
if (segments.some((s) => TEST_CRATE_SEGMENT_RE.test(s))) return true;
|
|
2904
3194
|
const basename = segments[segments.length - 1] ?? "";
|
|
2905
3195
|
if (TEST_BASENAMES.has(basename)) return true;
|
|
2906
|
-
return basename.endsWith("_tests.rs") || basename.endsWith("_testutil.rs");
|
|
3196
|
+
return basename.endsWith("_tests.rs") || basename.endsWith("_test.rs") || basename.endsWith("_testutil.rs");
|
|
3197
|
+
};
|
|
3198
|
+
const buildBlockCommentRanges = (lines) => {
|
|
3199
|
+
const ranges = [];
|
|
3200
|
+
let openLine = -1;
|
|
3201
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3202
|
+
const line = lines[i];
|
|
3203
|
+
if (openLine === -1) {
|
|
3204
|
+
const openIdx = line.indexOf("/*");
|
|
3205
|
+
if (openIdx !== -1 && line.indexOf("*/", openIdx + 2) === -1) openLine = i;
|
|
3206
|
+
} else if (line.indexOf("*/") !== -1) {
|
|
3207
|
+
ranges.push([openLine, i]);
|
|
3208
|
+
openLine = -1;
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
if (openLine !== -1) ranges.push([openLine, lines.length - 1]);
|
|
3212
|
+
return ranges;
|
|
2907
3213
|
};
|
|
2908
3214
|
const isExampleFile = (relPath) => relPath.split(path.sep).some((seg) => seg === "examples" || seg === "benches");
|
|
2909
3215
|
const UNWRAP_INTENT_LOOKBACK = 2;
|
|
@@ -2934,11 +3240,12 @@ const buildTestRanges = (lines) => {
|
|
|
2934
3240
|
return ranges;
|
|
2935
3241
|
};
|
|
2936
3242
|
const isInRange = (ranges, lineIdx) => ranges.some(([start, end]) => lineIdx >= start && lineIdx <= end);
|
|
2937
|
-
const flagNonTestUnwrap = (lines, relPath, testRanges, out) => {
|
|
3243
|
+
const flagNonTestUnwrap = (lines, relPath, testRanges, blockCommentRanges, out) => {
|
|
2938
3244
|
for (let i = 0; i < lines.length; i++) {
|
|
2939
3245
|
const line = lines[i];
|
|
2940
3246
|
if (COMMENT_LINE_RE.test(line)) continue;
|
|
2941
3247
|
if (isInRange(testRanges, i)) continue;
|
|
3248
|
+
if (isInRange(blockCommentRanges, i)) continue;
|
|
2942
3249
|
if (!UNWRAP_CALL_RE.test(line)) continue;
|
|
2943
3250
|
if (WRITELN_UNWRAP_RE.test(line)) continue;
|
|
2944
3251
|
if (hasIntentComment(lines, i)) continue;
|
|
@@ -2995,7 +3302,7 @@ const detectRustPatterns = async (context) => {
|
|
|
2995
3302
|
flagTodoMacro(lines, relPath, diagnostics);
|
|
2996
3303
|
continue;
|
|
2997
3304
|
}
|
|
2998
|
-
flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), diagnostics);
|
|
3305
|
+
flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), buildBlockCommentRanges(lines), diagnostics);
|
|
2999
3306
|
flagTodoMacro(lines, relPath, diagnostics);
|
|
3000
3307
|
}
|
|
3001
3308
|
return diagnostics;
|
|
@@ -3081,7 +3388,9 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3081
3388
|
const cleaned = importPart.replace(/[()]/g, "");
|
|
3082
3389
|
for (const item of cleaned.split(",")) {
|
|
3083
3390
|
const parts = item.trim().split(/\s+as\s+/);
|
|
3084
|
-
const
|
|
3391
|
+
const original = parts[0].trim();
|
|
3392
|
+
const localName = parts.length > 1 ? parts[1].trim() : original;
|
|
3393
|
+
if (parts.length > 1 && original === localName) continue;
|
|
3085
3394
|
if (localName && /^\w+$/.test(localName)) symbols.push({
|
|
3086
3395
|
name: localName,
|
|
3087
3396
|
line: i + 1,
|
|
@@ -3094,7 +3403,9 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3094
3403
|
const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
|
|
3095
3404
|
if (importMatch) {
|
|
3096
3405
|
importLines.add(i);
|
|
3097
|
-
const
|
|
3406
|
+
const alias = importMatch[2];
|
|
3407
|
+
if (alias && alias === importMatch[1]) continue;
|
|
3408
|
+
const simpleName = (alias ?? importMatch[1]).split(".")[0];
|
|
3098
3409
|
if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
|
|
3099
3410
|
name: simpleName,
|
|
3100
3411
|
line: i + 1,
|
|
@@ -3738,9 +4049,17 @@ const isTrivialLine = (line) => {
|
|
|
3738
4049
|
if (trimmed.startsWith("/*") || trimmed.startsWith("*")) return true;
|
|
3739
4050
|
return false;
|
|
3740
4051
|
};
|
|
4052
|
+
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)=/;
|
|
4053
|
+
const DATA_LITERAL_RE = /^\s*(?:[A-Za-z_$][\w$-]*:\s*(?:["'`[{]|\d|true\b|false\b|null\b)|["'`][^"'`]*["'`],?\s*$|[{}\]],?\s*$|\),?\s*$)/;
|
|
3741
4054
|
const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
|
|
3742
4055
|
const FILE_SUPPRESS_RE = /aislop[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
|
|
3743
4056
|
const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
|
|
4057
|
+
const isLowSignalMarkupWindow = (lines) => {
|
|
4058
|
+
return lines.filter((line) => SVG_MARKUP_RE.test(line)).length >= Math.ceil(WINDOW_SIZE / 2);
|
|
4059
|
+
};
|
|
4060
|
+
const isLowSignalDataWindow = (lines) => {
|
|
4061
|
+
return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= WINDOW_SIZE - 1;
|
|
4062
|
+
};
|
|
3744
4063
|
const findSuppressedLines = (lines) => {
|
|
3745
4064
|
const suppressed = /* @__PURE__ */ new Set();
|
|
3746
4065
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -3767,6 +4086,8 @@ const collectMeaningfulLines = (content) => {
|
|
|
3767
4086
|
if (suppressed.has(i + 1)) continue;
|
|
3768
4087
|
const window = lines.slice(i, i + WINDOW_SIZE);
|
|
3769
4088
|
if (window.some((l) => !MEANINGFUL_LINE.test(l))) continue;
|
|
4089
|
+
if (isLowSignalMarkupWindow(window)) continue;
|
|
4090
|
+
if (isLowSignalDataWindow(window)) continue;
|
|
3770
4091
|
if (window.every(isTrivialLine)) continue;
|
|
3771
4092
|
const normalised = window.map(normaliseLine);
|
|
3772
4093
|
if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < WINDOW_SIZE - 1) continue;
|
|
@@ -4814,7 +5135,20 @@ const createOxlintConfig = (options) => {
|
|
|
4814
5135
|
...buildFrameworkPlugins(options.framework)
|
|
4815
5136
|
];
|
|
4816
5137
|
const globals = buildTestGlobals(options.testFramework ?? null);
|
|
4817
|
-
|
|
5138
|
+
for (const name of [
|
|
5139
|
+
"__DEV__",
|
|
5140
|
+
"__TEST__",
|
|
5141
|
+
"__BROWSER__",
|
|
5142
|
+
"__NODE__",
|
|
5143
|
+
"__GLOBAL__",
|
|
5144
|
+
"__SSR__",
|
|
5145
|
+
"__ESM_BROWSER__",
|
|
5146
|
+
"__ESM_BUNDLER__",
|
|
5147
|
+
"__VERSION__",
|
|
5148
|
+
"__COMMIT__",
|
|
5149
|
+
"__BUILD__"
|
|
5150
|
+
]) globals[name] = "readonly";
|
|
5151
|
+
for (const globalName of options.globals ?? []) globals[globalName] = "readonly";
|
|
4818
5152
|
if (options.framework === "astro") {
|
|
4819
5153
|
globals.Astro = "readonly";
|
|
4820
5154
|
rules["no-undef"] = "off";
|
|
@@ -4834,19 +5168,7 @@ const createOxlintConfig = (options) => {
|
|
|
4834
5168
|
};
|
|
4835
5169
|
|
|
4836
5170
|
//#endregion
|
|
4837
|
-
//#region src/engines/lint/oxlint.ts
|
|
4838
|
-
const esmRequire = createRequire(import.meta.url);
|
|
4839
|
-
const resolveOxlintBinary = () => {
|
|
4840
|
-
try {
|
|
4841
|
-
const oxlintMainPath = esmRequire.resolve("oxlint");
|
|
4842
|
-
const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
|
|
4843
|
-
return path.join(oxlintDir, "bin", "oxlint");
|
|
4844
|
-
} catch {
|
|
4845
|
-
return "oxlint";
|
|
4846
|
-
}
|
|
4847
|
-
};
|
|
4848
|
-
const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
|
|
4849
|
-
const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
|
|
5171
|
+
//#region src/engines/lint/oxlint-context-filters.ts
|
|
4850
5172
|
const AMBIENT_GLOBAL_DEPS = [
|
|
4851
5173
|
"unplugin-icons",
|
|
4852
5174
|
"@types/bun",
|
|
@@ -4908,6 +5230,9 @@ const isAmbientFalsePositive = (rule, message, sources) => {
|
|
|
4908
5230
|
return false;
|
|
4909
5231
|
};
|
|
4910
5232
|
const sstReferencedFiles = /* @__PURE__ */ new Map();
|
|
5233
|
+
const clearSstReferenceCache = () => {
|
|
5234
|
+
sstReferencedFiles.clear();
|
|
5235
|
+
};
|
|
4911
5236
|
const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
4912
5237
|
const cached = sstReferencedFiles.get(relativeFilePath);
|
|
4913
5238
|
if (cached !== void 0) return cached;
|
|
@@ -4928,12 +5253,114 @@ const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
|
4928
5253
|
sstReferencedFiles.set(relativeFilePath, referenced);
|
|
4929
5254
|
return referenced;
|
|
4930
5255
|
};
|
|
5256
|
+
|
|
5257
|
+
//#endregion
|
|
5258
|
+
//#region src/engines/lint/oxlint-globals.ts
|
|
5259
|
+
const readTextFile$1 = (filePath) => {
|
|
5260
|
+
try {
|
|
5261
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
5262
|
+
} catch {
|
|
5263
|
+
return null;
|
|
5264
|
+
}
|
|
5265
|
+
};
|
|
5266
|
+
const collectPackageNames = (dir) => {
|
|
5267
|
+
const names = /* @__PURE__ */ new Set();
|
|
5268
|
+
const raw = readTextFile$1(path.join(dir, "package.json"));
|
|
5269
|
+
if (!raw) return names;
|
|
5270
|
+
try {
|
|
5271
|
+
const pkg = JSON.parse(raw);
|
|
5272
|
+
for (const section of [
|
|
5273
|
+
"dependencies",
|
|
5274
|
+
"devDependencies",
|
|
5275
|
+
"peerDependencies",
|
|
5276
|
+
"optionalDependencies"
|
|
5277
|
+
]) {
|
|
5278
|
+
const deps = pkg[section];
|
|
5279
|
+
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) names.add(name);
|
|
5280
|
+
}
|
|
5281
|
+
} catch {
|
|
5282
|
+
return names;
|
|
5283
|
+
}
|
|
5284
|
+
return names;
|
|
5285
|
+
};
|
|
5286
|
+
const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
|
|
5287
|
+
const collectAmbientGlobals = (rootDir) => {
|
|
5288
|
+
const globals = /* @__PURE__ */ new Set();
|
|
5289
|
+
const projectFiles = listProjectFiles(rootDir);
|
|
5290
|
+
for (const relativePath of projectFiles) {
|
|
5291
|
+
if (!relativePath.endsWith(".d.ts")) continue;
|
|
5292
|
+
const content = readTextFile$1(path.join(rootDir, relativePath));
|
|
5293
|
+
if (!content) continue;
|
|
5294
|
+
AMBIENT_GLOBAL_RE.lastIndex = 0;
|
|
5295
|
+
let match;
|
|
5296
|
+
while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
|
|
5297
|
+
}
|
|
5298
|
+
const deps = collectPackageNames(rootDir);
|
|
5299
|
+
if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
|
|
5300
|
+
if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
|
|
5301
|
+
"$app",
|
|
5302
|
+
"$config",
|
|
5303
|
+
"$dev",
|
|
5304
|
+
"$interpolate",
|
|
5305
|
+
"$resolve",
|
|
5306
|
+
"$jsonParse",
|
|
5307
|
+
"$jsonStringify",
|
|
5308
|
+
"aws",
|
|
5309
|
+
"cloudflare",
|
|
5310
|
+
"docker",
|
|
5311
|
+
"random",
|
|
5312
|
+
"sst",
|
|
5313
|
+
"vercel",
|
|
5314
|
+
"pulumi"
|
|
5315
|
+
]) globals.add(name);
|
|
5316
|
+
return [...globals];
|
|
5317
|
+
};
|
|
5318
|
+
|
|
5319
|
+
//#endregion
|
|
5320
|
+
//#region src/engines/lint/oxlint.ts
|
|
5321
|
+
const esmRequire = createRequire(import.meta.url);
|
|
5322
|
+
const OXLINT_EXTENSIONS = new Set([
|
|
5323
|
+
".ts",
|
|
5324
|
+
".tsx",
|
|
5325
|
+
".js",
|
|
5326
|
+
".jsx",
|
|
5327
|
+
".mjs",
|
|
5328
|
+
".cjs"
|
|
5329
|
+
]);
|
|
5330
|
+
const resolveOxlintBinary = () => {
|
|
5331
|
+
try {
|
|
5332
|
+
const oxlintMainPath = esmRequire.resolve("oxlint");
|
|
5333
|
+
const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
|
|
5334
|
+
return path.join(oxlintDir, "bin", "oxlint");
|
|
5335
|
+
} catch {
|
|
5336
|
+
return "oxlint";
|
|
5337
|
+
}
|
|
5338
|
+
};
|
|
5339
|
+
const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
|
|
5340
|
+
const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
|
|
4931
5341
|
const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
|
|
4932
5342
|
const isUnderscoreUnusedVar = (rule, message) => {
|
|
4933
5343
|
if (rule !== "eslint/no-unused-vars") return false;
|
|
4934
5344
|
const match = UNUSED_VAR_IDENT_RE.exec(message);
|
|
4935
5345
|
return match ? match[1].startsWith("_") : false;
|
|
4936
5346
|
};
|
|
5347
|
+
const readTextFile = (filePath) => {
|
|
5348
|
+
try {
|
|
5349
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
5350
|
+
} catch {
|
|
5351
|
+
return null;
|
|
5352
|
+
}
|
|
5353
|
+
};
|
|
5354
|
+
const isSolidRefFalsePositive = (context, diagnostic) => {
|
|
5355
|
+
if (diagnostic.rule !== "eslint/no-unassigned-vars") return false;
|
|
5356
|
+
const name = diagnostic.message.match(/^'([^']+)' is always 'undefined'/)?.[1];
|
|
5357
|
+
if (!name) return false;
|
|
5358
|
+
const content = readTextFile(path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath : path.join(context.rootDirectory, diagnostic.filePath));
|
|
5359
|
+
if (!content) return false;
|
|
5360
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5361
|
+
return new RegExp(`\\bref=\\{\\s*${escaped}\\s*\\}`).test(content);
|
|
5362
|
+
};
|
|
5363
|
+
const isContextualTypeScriptFalsePositive = (diagnostic) => diagnostic.rule === "typescript-eslint/triple-slash-reference" && (diagnostic.filePath.endsWith(".d.ts") || /(?:^|\/)sst\.config\.ts$/.test(diagnostic.filePath));
|
|
4937
5364
|
const parseRuleCode = (code) => {
|
|
4938
5365
|
if (!code) return {
|
|
4939
5366
|
plugin: "eslint",
|
|
@@ -4966,6 +5393,7 @@ const detectTestFramework = (rootDir) => {
|
|
|
4966
5393
|
} catch {}
|
|
4967
5394
|
return null;
|
|
4968
5395
|
};
|
|
5396
|
+
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("/"));
|
|
4969
5397
|
const extractUnusedVarName = (message) => {
|
|
4970
5398
|
const variableMatch = message.match(/Variable '([^']+)' is declared but never used/);
|
|
4971
5399
|
if (variableMatch?.[1]) return {
|
|
@@ -5026,12 +5454,17 @@ const removeDuplicateKeyLines = (rootDirectory, diagnostics) => {
|
|
|
5026
5454
|
};
|
|
5027
5455
|
const runOxlint = async (context) => {
|
|
5028
5456
|
const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
|
|
5457
|
+
const framework = context.frameworks.find((f) => f !== "none");
|
|
5458
|
+
const testFramework = detectTestFramework(context.rootDirectory);
|
|
5459
|
+
const targets = getOxlintTargets(context);
|
|
5460
|
+
if (targets.length === 0) return [];
|
|
5029
5461
|
const config = createOxlintConfig({
|
|
5030
|
-
framework
|
|
5031
|
-
testFramework
|
|
5462
|
+
framework,
|
|
5463
|
+
testFramework,
|
|
5464
|
+
globals: collectAmbientGlobals(context.rootDirectory)
|
|
5032
5465
|
});
|
|
5033
5466
|
const ambientSources = detectAmbientSources(context.rootDirectory);
|
|
5034
|
-
|
|
5467
|
+
clearSstReferenceCache();
|
|
5035
5468
|
try {
|
|
5036
5469
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
5037
5470
|
const args = [
|
|
@@ -5042,7 +5475,7 @@ const runOxlint = async (context) => {
|
|
|
5042
5475
|
"json"
|
|
5043
5476
|
];
|
|
5044
5477
|
if (context.languages.includes("typescript") && fs.existsSync(path.join(context.rootDirectory, "tsconfig.json"))) args.push("--tsconfig", "./tsconfig.json");
|
|
5045
|
-
args.push(
|
|
5478
|
+
args.push(...targets);
|
|
5046
5479
|
const result = await runSubprocess(process.execPath, args, {
|
|
5047
5480
|
cwd: context.rootDirectory,
|
|
5048
5481
|
timeout: 12e4
|
|
@@ -5074,6 +5507,8 @@ const runOxlint = async (context) => {
|
|
|
5074
5507
|
if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
|
|
5075
5508
|
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
5076
5509
|
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
5510
|
+
if (isSolidRefFalsePositive(context, d)) return false;
|
|
5511
|
+
if (isContextualTypeScriptFalsePositive(d)) return false;
|
|
5077
5512
|
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
5078
5513
|
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
5079
5514
|
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
@@ -5088,10 +5523,15 @@ const runOxlint = async (context) => {
|
|
|
5088
5523
|
const fixOxlint = async (context, options = {}) => {
|
|
5089
5524
|
const dangerous = options.force ?? false;
|
|
5090
5525
|
const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-fix-${process.pid}.json`);
|
|
5526
|
+
const framework = context.frameworks.find((f) => f !== "none");
|
|
5527
|
+
const testFramework = detectTestFramework(context.rootDirectory);
|
|
5528
|
+
const targets = getOxlintTargets(context);
|
|
5529
|
+
if (targets.length === 0) return;
|
|
5091
5530
|
const config = createOxlintConfig({
|
|
5092
|
-
framework
|
|
5093
|
-
testFramework
|
|
5094
|
-
mode: "fix"
|
|
5531
|
+
framework,
|
|
5532
|
+
testFramework,
|
|
5533
|
+
mode: "fix",
|
|
5534
|
+
globals: collectAmbientGlobals(context.rootDirectory)
|
|
5095
5535
|
});
|
|
5096
5536
|
try {
|
|
5097
5537
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
@@ -5103,13 +5543,13 @@ const fixOxlint = async (context, options = {}) => {
|
|
|
5103
5543
|
"--fix",
|
|
5104
5544
|
"--fix-suggestions",
|
|
5105
5545
|
"--fix-dangerously",
|
|
5106
|
-
|
|
5546
|
+
...targets
|
|
5107
5547
|
] : [
|
|
5108
5548
|
binary,
|
|
5109
5549
|
"-c",
|
|
5110
5550
|
configPath,
|
|
5111
5551
|
"--fix",
|
|
5112
|
-
|
|
5552
|
+
...targets
|
|
5113
5553
|
];
|
|
5114
5554
|
const result = await runSubprocess(process.execPath, args, {
|
|
5115
5555
|
cwd: context.rootDirectory,
|
|
@@ -5697,7 +6137,7 @@ const DB_RECEIVER = "(?:db|database|knex|client|connection|conn|pool|sql|prisma|
|
|
|
5697
6137
|
const DB_METHOD = "(?:query|execute|exec|raw|\\$queryRaw|\\$queryRawUnsafe|\\$executeRaw|\\$executeRawUnsafe)";
|
|
5698
6138
|
const RISKY_PATTERNS = [
|
|
5699
6139
|
{
|
|
5700
|
-
pattern: new RegExp(
|
|
6140
|
+
pattern: new RegExp(`(?<![\\w.>:\\\\])\\b${ev}\\s*\\(`, "g"),
|
|
5701
6141
|
extensions: [
|
|
5702
6142
|
".ts",
|
|
5703
6143
|
".tsx",
|
|
@@ -5804,6 +6244,16 @@ const RISKY_PATTERNS = [
|
|
|
5804
6244
|
help: "Use parameterized queries or an ORM instead of string concatenation"
|
|
5805
6245
|
}
|
|
5806
6246
|
];
|
|
6247
|
+
const hasDangerouslySetInnerHtmlIgnore = (lines, lineIndex) => {
|
|
6248
|
+
const start = Math.max(0, lineIndex - 2);
|
|
6249
|
+
return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
|
|
6250
|
+
};
|
|
6251
|
+
const isStructuredDataScript = (content, matchIndex) => {
|
|
6252
|
+
const before = content.slice(Math.max(0, matchIndex - 300), matchIndex);
|
|
6253
|
+
if (/type=["']application\/ld\+json["']/.test(before)) return true;
|
|
6254
|
+
const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
|
|
6255
|
+
return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
|
|
6256
|
+
};
|
|
5807
6257
|
const detectRiskyConstructs = async (context) => {
|
|
5808
6258
|
const files = getSourceFiles(context);
|
|
5809
6259
|
const diagnostics = [];
|
|
@@ -5819,6 +6269,7 @@ const detectRiskyConstructs = async (context) => {
|
|
|
5819
6269
|
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
5820
6270
|
const isMigrationOrSeeder = /(?:^|\/)(migrations|seeders|seeds|migrate)\//.test(normalizedPath);
|
|
5821
6271
|
const masked = maskStringsAndComments(content, ext);
|
|
6272
|
+
const lines = content.split("\n");
|
|
5822
6273
|
for (const { pattern, extensions, name, message, help } of RISKY_PATTERNS) {
|
|
5823
6274
|
if (!extensions.includes(ext)) continue;
|
|
5824
6275
|
if (isMigrationOrSeeder && name === "sql-injection") continue;
|
|
@@ -5830,6 +6281,10 @@ const detectRiskyConstructs = async (context) => {
|
|
|
5830
6281
|
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
5831
6282
|
if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
|
|
5832
6283
|
}
|
|
6284
|
+
if (name === "dangerously-set-innerhtml") {
|
|
6285
|
+
if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
|
|
6286
|
+
if (isStructuredDataScript(content, match.index)) continue;
|
|
6287
|
+
}
|
|
5833
6288
|
diagnostics.push({
|
|
5834
6289
|
filePath: relativePath,
|
|
5835
6290
|
engine: "security",
|
|
@@ -5853,7 +6308,8 @@ const detectRiskyConstructs = async (context) => {
|
|
|
5853
6308
|
const SECRET_PATTERNS = [
|
|
5854
6309
|
{
|
|
5855
6310
|
pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
|
|
5856
|
-
name: "API key"
|
|
6311
|
+
name: "API key",
|
|
6312
|
+
keywordPrefixed: true
|
|
5857
6313
|
},
|
|
5858
6314
|
{
|
|
5859
6315
|
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
@@ -5861,11 +6317,13 @@ const SECRET_PATTERNS = [
|
|
|
5861
6317
|
},
|
|
5862
6318
|
{
|
|
5863
6319
|
pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
|
|
5864
|
-
name: "AWS Secret Key"
|
|
6320
|
+
name: "AWS Secret Key",
|
|
6321
|
+
keywordPrefixed: true
|
|
5865
6322
|
},
|
|
5866
6323
|
{
|
|
5867
6324
|
pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
|
|
5868
|
-
name: "Hardcoded password/secret"
|
|
6325
|
+
name: "Hardcoded password/secret",
|
|
6326
|
+
keywordPrefixed: true
|
|
5869
6327
|
},
|
|
5870
6328
|
{
|
|
5871
6329
|
pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
|
|
@@ -5877,7 +6335,8 @@ const SECRET_PATTERNS = [
|
|
|
5877
6335
|
},
|
|
5878
6336
|
{
|
|
5879
6337
|
pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
|
|
5880
|
-
name: "Authentication token"
|
|
6338
|
+
name: "Authentication token",
|
|
6339
|
+
keywordPrefixed: true
|
|
5881
6340
|
},
|
|
5882
6341
|
{
|
|
5883
6342
|
pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
|
|
@@ -5892,6 +6351,24 @@ const SECRET_PATTERNS = [
|
|
|
5892
6351
|
name: "Database connection string with credentials"
|
|
5893
6352
|
}
|
|
5894
6353
|
];
|
|
6354
|
+
const isInsideStringLiteral = (content, matchIndex) => {
|
|
6355
|
+
const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
|
|
6356
|
+
const prefix = content.slice(lineStart, matchIndex);
|
|
6357
|
+
let inDouble = false;
|
|
6358
|
+
let inSingle = false;
|
|
6359
|
+
let inBacktick = false;
|
|
6360
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
6361
|
+
const ch = prefix[i];
|
|
6362
|
+
if (ch === "\\") {
|
|
6363
|
+
i++;
|
|
6364
|
+
continue;
|
|
6365
|
+
}
|
|
6366
|
+
if (ch === "\"" && !inSingle && !inBacktick) inDouble = !inDouble;
|
|
6367
|
+
else if (ch === "'" && !inDouble && !inBacktick) inSingle = !inSingle;
|
|
6368
|
+
else if (ch === "`" && !inDouble && !inSingle) inBacktick = !inBacktick;
|
|
6369
|
+
}
|
|
6370
|
+
return inDouble || inSingle || inBacktick;
|
|
6371
|
+
};
|
|
5895
6372
|
const PLACEHOLDER_EXACT = new Set([
|
|
5896
6373
|
"changeme",
|
|
5897
6374
|
"password",
|
|
@@ -5927,11 +6404,12 @@ const scanSecrets = async (context) => {
|
|
|
5927
6404
|
continue;
|
|
5928
6405
|
}
|
|
5929
6406
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
5930
|
-
for (const { pattern, name } of SECRET_PATTERNS) {
|
|
6407
|
+
for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
|
|
5931
6408
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
5932
6409
|
let match;
|
|
5933
6410
|
while ((match = regex.exec(content)) !== null) {
|
|
5934
6411
|
if (isPlaceholderValue(match[1] ?? match[0])) continue;
|
|
6412
|
+
if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
|
|
5935
6413
|
const line = content.slice(0, match.index).split("\n").length;
|
|
5936
6414
|
diagnostics.push({
|
|
5937
6415
|
filePath: relativePath,
|
|
@@ -6777,6 +7255,10 @@ const RULE_LABELS = {
|
|
|
6777
7255
|
"ai-slop/python-broad-except": "Broad except",
|
|
6778
7256
|
"ai-slop/python-mutable-default": "Mutable default argument",
|
|
6779
7257
|
"ai-slop/python-print-debug": "print() left in code",
|
|
7258
|
+
"ai-slop/python-range-len-loop": "range(len(...)) loop",
|
|
7259
|
+
"ai-slop/python-chained-dict-get": "Chained dict get",
|
|
7260
|
+
"ai-slop/python-repetitive-dispatch": "Repetitive dispatch ladder",
|
|
7261
|
+
"ai-slop/python-isinstance-ladder": "isinstance ladder",
|
|
6780
7262
|
"ai-slop/go-library-panic": "panic() in Go library code",
|
|
6781
7263
|
"ai-slop/rust-non-test-unwrap": "Rust .unwrap() in production code",
|
|
6782
7264
|
"ai-slop/rust-todo-stub": "Rust todo!() stub",
|
|
@@ -6880,6 +7362,9 @@ const renderSummary = (input, deps = {}) => {
|
|
|
6880
7362
|
}
|
|
6881
7363
|
return lines.join("\n");
|
|
6882
7364
|
};
|
|
7365
|
+
const renderStarCta = (deps = {}) => {
|
|
7366
|
+
return `\n ${style(deps.theme ?? theme, "muted", "★ Found this useful? Star us at github.com/scanaislop/aislop")}\n`;
|
|
7367
|
+
};
|
|
6883
7368
|
const renderCleanRun = (input, deps = {}) => {
|
|
6884
7369
|
const t = deps.theme ?? theme;
|
|
6885
7370
|
const s = deps.symbols ?? symbols;
|
|
@@ -6995,11 +7480,12 @@ const buildScanRender = (input) => {
|
|
|
6995
7480
|
const warnings = input.diagnostics.filter((d) => d.severity === "warning").length;
|
|
6996
7481
|
const fixable = input.diagnostics.filter((d) => d.fixable).length;
|
|
6997
7482
|
const hasVulnerableDeps = input.diagnostics.some((d) => d.rule === "security/vulnerable-dependency");
|
|
7483
|
+
const starCta = input.printBrand !== false ? renderStarCta(deps) : "";
|
|
6998
7484
|
if (input.diagnostics.length === 0 && input.score.score === 100) return `${header}${renderCleanRun({
|
|
6999
7485
|
score: input.score.score,
|
|
7000
7486
|
label: input.score.label,
|
|
7001
7487
|
elapsedMs: input.elapsedMs
|
|
7002
|
-
}, deps)}`;
|
|
7488
|
+
}, deps)}${starCta}`;
|
|
7003
7489
|
const diagBlock = input.diagnostics.length === 0 ? "" : renderDiagnostics(input.diagnostics, input.verbose);
|
|
7004
7490
|
const nextSteps = [];
|
|
7005
7491
|
if (fixable > 0) nextSteps.push({
|
|
@@ -7026,7 +7512,7 @@ const buildScanRender = (input) => {
|
|
|
7026
7512
|
nextSteps,
|
|
7027
7513
|
breakdown: computeBreakdown(input.diagnostics),
|
|
7028
7514
|
thresholds: input.thresholds
|
|
7029
|
-
}, deps)}`;
|
|
7515
|
+
}, deps)}${starCta}`;
|
|
7030
7516
|
};
|
|
7031
7517
|
const scanCommand = async (directory, config, options) => {
|
|
7032
7518
|
const resolvedDir = path.resolve(directory);
|
|
@@ -7137,7 +7623,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
7137
7623
|
engineTimings
|
|
7138
7624
|
};
|
|
7139
7625
|
if (options.json) {
|
|
7140
|
-
const { buildJsonOutput } = await import("./json-
|
|
7626
|
+
const { buildJsonOutput } = await import("./json-CXiEvR_M.js");
|
|
7141
7627
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
7142
7628
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
7143
7629
|
return completion;
|
|
@@ -8962,6 +9448,10 @@ const BUILTIN_RULES = [
|
|
|
8962
9448
|
"ai-slop/python-broad-except",
|
|
8963
9449
|
"ai-slop/python-mutable-default",
|
|
8964
9450
|
"ai-slop/python-print-debug",
|
|
9451
|
+
"ai-slop/python-range-len-loop",
|
|
9452
|
+
"ai-slop/python-chained-dict-get",
|
|
9453
|
+
"ai-slop/python-repetitive-dispatch",
|
|
9454
|
+
"ai-slop/python-isinstance-ladder",
|
|
8965
9455
|
"ai-slop/go-library-panic",
|
|
8966
9456
|
"ai-slop/rust-non-test-unwrap",
|
|
8967
9457
|
"ai-slop/rust-todo-stub",
|