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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-C3JZkQGA.js";
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 = [/\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i];
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 normalizedExcludePatterns = exclude.length ? normalizeExcludePatterns(exclude) : [];
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
- /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
1233
- /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
1234
- /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm
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
- diagnostics.push(...detectThinWrappers(content, relativePath));
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 scanFileForTrivialComments = (content, relativePath) => {
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 && !nextLine.startsWith("}") && !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));
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 = expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, pkg));
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
- const collectTsPathAliases = (rootDir) => {
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
- "~icons/"
2453
+ "file:"
2185
2454
  ];
2186
- const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
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/narrative-comments-patterns.ts
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 declMatch = GO_DECL_NAME_RE.exec(next.trim());
2590
- if (!declMatch) return false;
2591
- return ((block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "") === declMatch[1];
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 ((hasWhyMarker || hasDocIndicator(block)) && block.kind === "jsdoc") return {
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
- if (nonEmptyProseCount >= 5) return {
2671
- matched: true,
2672
- reason: "long narrative block"
2673
- };
2674
- if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line") return {
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 localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
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 simpleName = (importMatch[2] ?? importMatch[1]).split(".")[0];
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
- if (options.framework === "expo" || options.framework === "react") globals.__DEV__ = "readonly";
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: context.frameworks.find((f) => f !== "none"),
5031
- testFramework: detectTestFramework(context.rootDirectory)
5462
+ framework,
5463
+ testFramework,
5464
+ globals: collectAmbientGlobals(context.rootDirectory)
5032
5465
  });
5033
5466
  const ambientSources = detectAmbientSources(context.rootDirectory);
5034
- sstReferencedFiles.clear();
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: context.frameworks.find((f) => f !== "none"),
5093
- testFramework: detectTestFramework(context.rootDirectory),
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(`\\b${ev}\\s*\\(`, "g"),
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-DZHn6AE3.js");
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",