aislop 0.9.2 → 0.9.3

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-BNO_Lw7E.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
@@ -2009,68 +2360,23 @@ const JS_EXTENSIONS$2 = new Set([
2009
2360
  ]);
2010
2361
  const PY_EXTENSIONS$2 = new Set([".py"]);
2011
2362
  const readJson = (filePath) => {
2012
- try {
2013
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
2014
- } catch {
2015
- return null;
2016
- }
2017
- };
2018
- const PKG_DEP_SECTIONS = [
2019
- "dependencies",
2020
- "devDependencies",
2021
- "peerDependencies",
2022
- "optionalDependencies"
2023
- ];
2024
- const addDepsFromPkg = (pkg, jsDeps) => {
2025
- for (const section of PKG_DEP_SECTIONS) {
2026
- const deps = pkg[section];
2027
- if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
2028
- }
2029
- };
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;
2363
+ try {
2364
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
2365
+ } catch {
2366
+ return null;
2367
+ }
2368
+ };
2369
+ const PKG_DEP_SECTIONS = [
2370
+ "dependencies",
2371
+ "devDependencies",
2372
+ "peerDependencies",
2373
+ "optionalDependencies"
2374
+ ];
2375
+ const addDepsFromPkg = (pkg, jsDeps) => {
2376
+ for (const section of PKG_DEP_SECTIONS) {
2377
+ const deps = pkg[section];
2378
+ if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
2379
+ }
2074
2380
  };
2075
2381
  const SKIP_DIRS = new Set([
2076
2382
  "node_modules",
@@ -2113,7 +2419,7 @@ const collectJsDeps = (rootDir, jsDeps) => {
2113
2419
  if (!pkg || typeof pkg !== "object") return false;
2114
2420
  addDepsFromPkg(pkg, jsDeps);
2115
2421
  if (typeof pkg.name === "string") jsDeps.add(pkg.name);
2116
- const workspaceDirs = expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, pkg));
2422
+ const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
2117
2423
  for (const wsDir of workspaceDirs) {
2118
2424
  const wsPkg = readJson(path.join(wsDir, "package.json"));
2119
2425
  if (!wsPkg) continue;
@@ -2123,43 +2429,6 @@ const collectJsDeps = (rootDir, jsDeps) => {
2123
2429
  collectNestedManifests(rootDir, jsDeps);
2124
2430
  return true;
2125
2431
  };
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}/`));
2155
- }
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;
2162
- };
2163
2432
  const loadManifest = (rootDir) => {
2164
2433
  const jsDeps = /* @__PURE__ */ new Set();
2165
2434
  const hasJsManifest = collectJsDeps(rootDir, jsDeps);
@@ -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
  };
@@ -2903,7 +3108,23 @@ const isTestFile = (relPath) => {
2903
3108
  if (segments.some((s) => TEST_CRATE_SEGMENT_RE.test(s))) return true;
2904
3109
  const basename = segments[segments.length - 1] ?? "";
2905
3110
  if (TEST_BASENAMES.has(basename)) return true;
2906
- return basename.endsWith("_tests.rs") || basename.endsWith("_testutil.rs");
3111
+ return basename.endsWith("_tests.rs") || basename.endsWith("_test.rs") || basename.endsWith("_testutil.rs");
3112
+ };
3113
+ const buildBlockCommentRanges = (lines) => {
3114
+ const ranges = [];
3115
+ let openLine = -1;
3116
+ for (let i = 0; i < lines.length; i++) {
3117
+ const line = lines[i];
3118
+ if (openLine === -1) {
3119
+ const openIdx = line.indexOf("/*");
3120
+ if (openIdx !== -1 && line.indexOf("*/", openIdx + 2) === -1) openLine = i;
3121
+ } else if (line.indexOf("*/") !== -1) {
3122
+ ranges.push([openLine, i]);
3123
+ openLine = -1;
3124
+ }
3125
+ }
3126
+ if (openLine !== -1) ranges.push([openLine, lines.length - 1]);
3127
+ return ranges;
2907
3128
  };
2908
3129
  const isExampleFile = (relPath) => relPath.split(path.sep).some((seg) => seg === "examples" || seg === "benches");
2909
3130
  const UNWRAP_INTENT_LOOKBACK = 2;
@@ -2934,11 +3155,12 @@ const buildTestRanges = (lines) => {
2934
3155
  return ranges;
2935
3156
  };
2936
3157
  const isInRange = (ranges, lineIdx) => ranges.some(([start, end]) => lineIdx >= start && lineIdx <= end);
2937
- const flagNonTestUnwrap = (lines, relPath, testRanges, out) => {
3158
+ const flagNonTestUnwrap = (lines, relPath, testRanges, blockCommentRanges, out) => {
2938
3159
  for (let i = 0; i < lines.length; i++) {
2939
3160
  const line = lines[i];
2940
3161
  if (COMMENT_LINE_RE.test(line)) continue;
2941
3162
  if (isInRange(testRanges, i)) continue;
3163
+ if (isInRange(blockCommentRanges, i)) continue;
2942
3164
  if (!UNWRAP_CALL_RE.test(line)) continue;
2943
3165
  if (WRITELN_UNWRAP_RE.test(line)) continue;
2944
3166
  if (hasIntentComment(lines, i)) continue;
@@ -2995,7 +3217,7 @@ const detectRustPatterns = async (context) => {
2995
3217
  flagTodoMacro(lines, relPath, diagnostics);
2996
3218
  continue;
2997
3219
  }
2998
- flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), diagnostics);
3220
+ flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), buildBlockCommentRanges(lines), diagnostics);
2999
3221
  flagTodoMacro(lines, relPath, diagnostics);
3000
3222
  }
3001
3223
  return diagnostics;
@@ -3081,7 +3303,9 @@ const extractPyImportedSymbols = (lines) => {
3081
3303
  const cleaned = importPart.replace(/[()]/g, "");
3082
3304
  for (const item of cleaned.split(",")) {
3083
3305
  const parts = item.trim().split(/\s+as\s+/);
3084
- const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
3306
+ const original = parts[0].trim();
3307
+ const localName = parts.length > 1 ? parts[1].trim() : original;
3308
+ if (parts.length > 1 && original === localName) continue;
3085
3309
  if (localName && /^\w+$/.test(localName)) symbols.push({
3086
3310
  name: localName,
3087
3311
  line: i + 1,
@@ -3094,7 +3318,9 @@ const extractPyImportedSymbols = (lines) => {
3094
3318
  const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
3095
3319
  if (importMatch) {
3096
3320
  importLines.add(i);
3097
- const simpleName = (importMatch[2] ?? importMatch[1]).split(".")[0];
3321
+ const alias = importMatch[2];
3322
+ if (alias && alias === importMatch[1]) continue;
3323
+ const simpleName = (alias ?? importMatch[1]).split(".")[0];
3098
3324
  if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
3099
3325
  name: simpleName,
3100
3326
  line: i + 1,
@@ -3738,9 +3964,17 @@ const isTrivialLine = (line) => {
3738
3964
  if (trimmed.startsWith("/*") || trimmed.startsWith("*")) return true;
3739
3965
  return false;
3740
3966
  };
3967
+ 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)=/;
3968
+ const DATA_LITERAL_RE = /^\s*(?:[A-Za-z_$][\w$-]*:\s*(?:["'`[{]|\d|true\b|false\b|null\b)|["'`][^"'`]*["'`],?\s*$|[{}\]],?\s*$|\),?\s*$)/;
3741
3969
  const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
3742
3970
  const FILE_SUPPRESS_RE = /aislop[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
3743
3971
  const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
3972
+ const isLowSignalMarkupWindow = (lines) => {
3973
+ return lines.filter((line) => SVG_MARKUP_RE.test(line)).length >= Math.ceil(WINDOW_SIZE / 2);
3974
+ };
3975
+ const isLowSignalDataWindow = (lines) => {
3976
+ return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= WINDOW_SIZE - 1;
3977
+ };
3744
3978
  const findSuppressedLines = (lines) => {
3745
3979
  const suppressed = /* @__PURE__ */ new Set();
3746
3980
  for (let i = 0; i < lines.length; i++) {
@@ -3767,6 +4001,8 @@ const collectMeaningfulLines = (content) => {
3767
4001
  if (suppressed.has(i + 1)) continue;
3768
4002
  const window = lines.slice(i, i + WINDOW_SIZE);
3769
4003
  if (window.some((l) => !MEANINGFUL_LINE.test(l))) continue;
4004
+ if (isLowSignalMarkupWindow(window)) continue;
4005
+ if (isLowSignalDataWindow(window)) continue;
3770
4006
  if (window.every(isTrivialLine)) continue;
3771
4007
  const normalised = window.map(normaliseLine);
3772
4008
  if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < WINDOW_SIZE - 1) continue;
@@ -4814,7 +5050,20 @@ const createOxlintConfig = (options) => {
4814
5050
  ...buildFrameworkPlugins(options.framework)
4815
5051
  ];
4816
5052
  const globals = buildTestGlobals(options.testFramework ?? null);
4817
- if (options.framework === "expo" || options.framework === "react") globals.__DEV__ = "readonly";
5053
+ for (const name of [
5054
+ "__DEV__",
5055
+ "__TEST__",
5056
+ "__BROWSER__",
5057
+ "__NODE__",
5058
+ "__GLOBAL__",
5059
+ "__SSR__",
5060
+ "__ESM_BROWSER__",
5061
+ "__ESM_BUNDLER__",
5062
+ "__VERSION__",
5063
+ "__COMMIT__",
5064
+ "__BUILD__"
5065
+ ]) globals[name] = "readonly";
5066
+ for (const globalName of options.globals ?? []) globals[globalName] = "readonly";
4818
5067
  if (options.framework === "astro") {
4819
5068
  globals.Astro = "readonly";
4820
5069
  rules["no-undef"] = "off";
@@ -4834,19 +5083,7 @@ const createOxlintConfig = (options) => {
4834
5083
  };
4835
5084
 
4836
5085
  //#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);
5086
+ //#region src/engines/lint/oxlint-context-filters.ts
4850
5087
  const AMBIENT_GLOBAL_DEPS = [
4851
5088
  "unplugin-icons",
4852
5089
  "@types/bun",
@@ -4908,6 +5145,9 @@ const isAmbientFalsePositive = (rule, message, sources) => {
4908
5145
  return false;
4909
5146
  };
4910
5147
  const sstReferencedFiles = /* @__PURE__ */ new Map();
5148
+ const clearSstReferenceCache = () => {
5149
+ sstReferencedFiles.clear();
5150
+ };
4911
5151
  const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
4912
5152
  const cached = sstReferencedFiles.get(relativeFilePath);
4913
5153
  if (cached !== void 0) return cached;
@@ -4928,12 +5168,114 @@ const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
4928
5168
  sstReferencedFiles.set(relativeFilePath, referenced);
4929
5169
  return referenced;
4930
5170
  };
5171
+
5172
+ //#endregion
5173
+ //#region src/engines/lint/oxlint-globals.ts
5174
+ const readTextFile$1 = (filePath) => {
5175
+ try {
5176
+ return fs.readFileSync(filePath, "utf-8");
5177
+ } catch {
5178
+ return null;
5179
+ }
5180
+ };
5181
+ const collectPackageNames = (dir) => {
5182
+ const names = /* @__PURE__ */ new Set();
5183
+ const raw = readTextFile$1(path.join(dir, "package.json"));
5184
+ if (!raw) return names;
5185
+ try {
5186
+ const pkg = JSON.parse(raw);
5187
+ for (const section of [
5188
+ "dependencies",
5189
+ "devDependencies",
5190
+ "peerDependencies",
5191
+ "optionalDependencies"
5192
+ ]) {
5193
+ const deps = pkg[section];
5194
+ if (deps && typeof deps === "object") for (const name of Object.keys(deps)) names.add(name);
5195
+ }
5196
+ } catch {
5197
+ return names;
5198
+ }
5199
+ return names;
5200
+ };
5201
+ const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
5202
+ const collectAmbientGlobals = (rootDir) => {
5203
+ const globals = /* @__PURE__ */ new Set();
5204
+ const projectFiles = listProjectFiles(rootDir);
5205
+ for (const relativePath of projectFiles) {
5206
+ if (!relativePath.endsWith(".d.ts")) continue;
5207
+ const content = readTextFile$1(path.join(rootDir, relativePath));
5208
+ if (!content) continue;
5209
+ AMBIENT_GLOBAL_RE.lastIndex = 0;
5210
+ let match;
5211
+ while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
5212
+ }
5213
+ const deps = collectPackageNames(rootDir);
5214
+ if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
5215
+ if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
5216
+ "$app",
5217
+ "$config",
5218
+ "$dev",
5219
+ "$interpolate",
5220
+ "$resolve",
5221
+ "$jsonParse",
5222
+ "$jsonStringify",
5223
+ "aws",
5224
+ "cloudflare",
5225
+ "docker",
5226
+ "random",
5227
+ "sst",
5228
+ "vercel",
5229
+ "pulumi"
5230
+ ]) globals.add(name);
5231
+ return [...globals];
5232
+ };
5233
+
5234
+ //#endregion
5235
+ //#region src/engines/lint/oxlint.ts
5236
+ const esmRequire = createRequire(import.meta.url);
5237
+ const OXLINT_EXTENSIONS = new Set([
5238
+ ".ts",
5239
+ ".tsx",
5240
+ ".js",
5241
+ ".jsx",
5242
+ ".mjs",
5243
+ ".cjs"
5244
+ ]);
5245
+ const resolveOxlintBinary = () => {
5246
+ try {
5247
+ const oxlintMainPath = esmRequire.resolve("oxlint");
5248
+ const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
5249
+ return path.join(oxlintDir, "bin", "oxlint");
5250
+ } catch {
5251
+ return "oxlint";
5252
+ }
5253
+ };
5254
+ const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
5255
+ const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
4931
5256
  const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
4932
5257
  const isUnderscoreUnusedVar = (rule, message) => {
4933
5258
  if (rule !== "eslint/no-unused-vars") return false;
4934
5259
  const match = UNUSED_VAR_IDENT_RE.exec(message);
4935
5260
  return match ? match[1].startsWith("_") : false;
4936
5261
  };
5262
+ const readTextFile = (filePath) => {
5263
+ try {
5264
+ return fs.readFileSync(filePath, "utf-8");
5265
+ } catch {
5266
+ return null;
5267
+ }
5268
+ };
5269
+ const isSolidRefFalsePositive = (context, diagnostic) => {
5270
+ if (diagnostic.rule !== "eslint/no-unassigned-vars") return false;
5271
+ const name = diagnostic.message.match(/^'([^']+)' is always 'undefined'/)?.[1];
5272
+ if (!name) return false;
5273
+ const content = readTextFile(path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath : path.join(context.rootDirectory, diagnostic.filePath));
5274
+ if (!content) return false;
5275
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5276
+ return new RegExp(`\\bref=\\{\\s*${escaped}\\s*\\}`).test(content);
5277
+ };
5278
+ const isContextualTypeScriptFalsePositive = (diagnostic) => diagnostic.rule === "typescript-eslint/triple-slash-reference" && (diagnostic.filePath.endsWith(".d.ts") || /(?:^|\/)sst\.config\.ts$/.test(diagnostic.filePath));
4937
5279
  const parseRuleCode = (code) => {
4938
5280
  if (!code) return {
4939
5281
  plugin: "eslint",
@@ -4966,6 +5308,7 @@ const detectTestFramework = (rootDir) => {
4966
5308
  } catch {}
4967
5309
  return null;
4968
5310
  };
5311
+ 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
5312
  const extractUnusedVarName = (message) => {
4970
5313
  const variableMatch = message.match(/Variable '([^']+)' is declared but never used/);
4971
5314
  if (variableMatch?.[1]) return {
@@ -5026,12 +5369,17 @@ const removeDuplicateKeyLines = (rootDirectory, diagnostics) => {
5026
5369
  };
5027
5370
  const runOxlint = async (context) => {
5028
5371
  const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
5372
+ const framework = context.frameworks.find((f) => f !== "none");
5373
+ const testFramework = detectTestFramework(context.rootDirectory);
5374
+ const targets = getOxlintTargets(context);
5375
+ if (targets.length === 0) return [];
5029
5376
  const config = createOxlintConfig({
5030
- framework: context.frameworks.find((f) => f !== "none"),
5031
- testFramework: detectTestFramework(context.rootDirectory)
5377
+ framework,
5378
+ testFramework,
5379
+ globals: collectAmbientGlobals(context.rootDirectory)
5032
5380
  });
5033
5381
  const ambientSources = detectAmbientSources(context.rootDirectory);
5034
- sstReferencedFiles.clear();
5382
+ clearSstReferenceCache();
5035
5383
  try {
5036
5384
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
5037
5385
  const args = [
@@ -5042,7 +5390,7 @@ const runOxlint = async (context) => {
5042
5390
  "json"
5043
5391
  ];
5044
5392
  if (context.languages.includes("typescript") && fs.existsSync(path.join(context.rootDirectory, "tsconfig.json"))) args.push("--tsconfig", "./tsconfig.json");
5045
- args.push(".");
5393
+ args.push(...targets);
5046
5394
  const result = await runSubprocess(process.execPath, args, {
5047
5395
  cwd: context.rootDirectory,
5048
5396
  timeout: 12e4
@@ -5074,6 +5422,8 @@ const runOxlint = async (context) => {
5074
5422
  if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
5075
5423
  if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
5076
5424
  if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
5425
+ if (isSolidRefFalsePositive(context, d)) return false;
5426
+ if (isContextualTypeScriptFalsePositive(d)) return false;
5077
5427
  if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
5078
5428
  if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
5079
5429
  const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
@@ -5088,10 +5438,15 @@ const runOxlint = async (context) => {
5088
5438
  const fixOxlint = async (context, options = {}) => {
5089
5439
  const dangerous = options.force ?? false;
5090
5440
  const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-fix-${process.pid}.json`);
5441
+ const framework = context.frameworks.find((f) => f !== "none");
5442
+ const testFramework = detectTestFramework(context.rootDirectory);
5443
+ const targets = getOxlintTargets(context);
5444
+ if (targets.length === 0) return;
5091
5445
  const config = createOxlintConfig({
5092
- framework: context.frameworks.find((f) => f !== "none"),
5093
- testFramework: detectTestFramework(context.rootDirectory),
5094
- mode: "fix"
5446
+ framework,
5447
+ testFramework,
5448
+ mode: "fix",
5449
+ globals: collectAmbientGlobals(context.rootDirectory)
5095
5450
  });
5096
5451
  try {
5097
5452
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -5103,13 +5458,13 @@ const fixOxlint = async (context, options = {}) => {
5103
5458
  "--fix",
5104
5459
  "--fix-suggestions",
5105
5460
  "--fix-dangerously",
5106
- "."
5461
+ ...targets
5107
5462
  ] : [
5108
5463
  binary,
5109
5464
  "-c",
5110
5465
  configPath,
5111
5466
  "--fix",
5112
- "."
5467
+ ...targets
5113
5468
  ];
5114
5469
  const result = await runSubprocess(process.execPath, args, {
5115
5470
  cwd: context.rootDirectory,
@@ -5697,7 +6052,7 @@ const DB_RECEIVER = "(?:db|database|knex|client|connection|conn|pool|sql|prisma|
5697
6052
  const DB_METHOD = "(?:query|execute|exec|raw|\\$queryRaw|\\$queryRawUnsafe|\\$executeRaw|\\$executeRawUnsafe)";
5698
6053
  const RISKY_PATTERNS = [
5699
6054
  {
5700
- pattern: new RegExp(`\\b${ev}\\s*\\(`, "g"),
6055
+ pattern: new RegExp(`(?<![\\w.>:\\\\])\\b${ev}\\s*\\(`, "g"),
5701
6056
  extensions: [
5702
6057
  ".ts",
5703
6058
  ".tsx",
@@ -5804,6 +6159,16 @@ const RISKY_PATTERNS = [
5804
6159
  help: "Use parameterized queries or an ORM instead of string concatenation"
5805
6160
  }
5806
6161
  ];
6162
+ const hasDangerouslySetInnerHtmlIgnore = (lines, lineIndex) => {
6163
+ const start = Math.max(0, lineIndex - 2);
6164
+ return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
6165
+ };
6166
+ const isStructuredDataScript = (content, matchIndex) => {
6167
+ const before = content.slice(Math.max(0, matchIndex - 300), matchIndex);
6168
+ if (/type=["']application\/ld\+json["']/.test(before)) return true;
6169
+ const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
6170
+ return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
6171
+ };
5807
6172
  const detectRiskyConstructs = async (context) => {
5808
6173
  const files = getSourceFiles(context);
5809
6174
  const diagnostics = [];
@@ -5819,6 +6184,7 @@ const detectRiskyConstructs = async (context) => {
5819
6184
  const normalizedPath = relativePath.split(path.sep).join("/");
5820
6185
  const isMigrationOrSeeder = /(?:^|\/)(migrations|seeders|seeds|migrate)\//.test(normalizedPath);
5821
6186
  const masked = maskStringsAndComments(content, ext);
6187
+ const lines = content.split("\n");
5822
6188
  for (const { pattern, extensions, name, message, help } of RISKY_PATTERNS) {
5823
6189
  if (!extensions.includes(ext)) continue;
5824
6190
  if (isMigrationOrSeeder && name === "sql-injection") continue;
@@ -5830,6 +6196,10 @@ const detectRiskyConstructs = async (context) => {
5830
6196
  const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
5831
6197
  if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
5832
6198
  }
6199
+ if (name === "dangerously-set-innerhtml") {
6200
+ if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
6201
+ if (isStructuredDataScript(content, match.index)) continue;
6202
+ }
5833
6203
  diagnostics.push({
5834
6204
  filePath: relativePath,
5835
6205
  engine: "security",
@@ -5853,7 +6223,8 @@ const detectRiskyConstructs = async (context) => {
5853
6223
  const SECRET_PATTERNS = [
5854
6224
  {
5855
6225
  pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
5856
- name: "API key"
6226
+ name: "API key",
6227
+ keywordPrefixed: true
5857
6228
  },
5858
6229
  {
5859
6230
  pattern: /AKIA[0-9A-Z]{16}/g,
@@ -5861,11 +6232,13 @@ const SECRET_PATTERNS = [
5861
6232
  },
5862
6233
  {
5863
6234
  pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
5864
- name: "AWS Secret Key"
6235
+ name: "AWS Secret Key",
6236
+ keywordPrefixed: true
5865
6237
  },
5866
6238
  {
5867
6239
  pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
5868
- name: "Hardcoded password/secret"
6240
+ name: "Hardcoded password/secret",
6241
+ keywordPrefixed: true
5869
6242
  },
5870
6243
  {
5871
6244
  pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
@@ -5877,7 +6250,8 @@ const SECRET_PATTERNS = [
5877
6250
  },
5878
6251
  {
5879
6252
  pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
5880
- name: "Authentication token"
6253
+ name: "Authentication token",
6254
+ keywordPrefixed: true
5881
6255
  },
5882
6256
  {
5883
6257
  pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
@@ -5892,6 +6266,24 @@ const SECRET_PATTERNS = [
5892
6266
  name: "Database connection string with credentials"
5893
6267
  }
5894
6268
  ];
6269
+ const isInsideStringLiteral = (content, matchIndex) => {
6270
+ const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
6271
+ const prefix = content.slice(lineStart, matchIndex);
6272
+ let inDouble = false;
6273
+ let inSingle = false;
6274
+ let inBacktick = false;
6275
+ for (let i = 0; i < prefix.length; i++) {
6276
+ const ch = prefix[i];
6277
+ if (ch === "\\") {
6278
+ i++;
6279
+ continue;
6280
+ }
6281
+ if (ch === "\"" && !inSingle && !inBacktick) inDouble = !inDouble;
6282
+ else if (ch === "'" && !inDouble && !inBacktick) inSingle = !inSingle;
6283
+ else if (ch === "`" && !inDouble && !inSingle) inBacktick = !inBacktick;
6284
+ }
6285
+ return inDouble || inSingle || inBacktick;
6286
+ };
5895
6287
  const PLACEHOLDER_EXACT = new Set([
5896
6288
  "changeme",
5897
6289
  "password",
@@ -5927,11 +6319,12 @@ const scanSecrets = async (context) => {
5927
6319
  continue;
5928
6320
  }
5929
6321
  const relativePath = path.relative(context.rootDirectory, filePath);
5930
- for (const { pattern, name } of SECRET_PATTERNS) {
6322
+ for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
5931
6323
  const regex = new RegExp(pattern.source, pattern.flags);
5932
6324
  let match;
5933
6325
  while ((match = regex.exec(content)) !== null) {
5934
6326
  if (isPlaceholderValue(match[1] ?? match[0])) continue;
6327
+ if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
5935
6328
  const line = content.slice(0, match.index).split("\n").length;
5936
6329
  diagnostics.push({
5937
6330
  filePath: relativePath,
@@ -7137,7 +7530,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
7137
7530
  engineTimings
7138
7531
  };
7139
7532
  if (options.json) {
7140
- const { buildJsonOutput } = await import("./json-DZHn6AE3.js");
7533
+ const { buildJsonOutput } = await import("./json-BhO1Ufj3.js");
7141
7534
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
7142
7535
  console.log(JSON.stringify(jsonOut, null, 2));
7143
7536
  return completion;