aislop 0.9.1 → 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/cli.js CHANGED
@@ -34,7 +34,7 @@ var __exportAll = (all, no_symbols) => {
34
34
 
35
35
  //#endregion
36
36
  //#region src/version.ts
37
- const APP_VERSION = "0.9.1";
37
+ const APP_VERSION = "0.9.3";
38
38
 
39
39
  //#endregion
40
40
  //#region src/telemetry/env.ts
@@ -851,6 +851,7 @@ const EXCLUDED_DIRS = [
851
851
  "build",
852
852
  ".git",
853
853
  ".agents",
854
+ ".pnpm-store",
854
855
  "vendor",
855
856
  "examples",
856
857
  "example",
@@ -887,6 +888,7 @@ const FIND_PRUNE_DIRS = [
887
888
  "build",
888
889
  ".git",
889
890
  ".agents",
891
+ ".pnpm-store",
890
892
  "vendor",
891
893
  "examples",
892
894
  "example",
@@ -910,7 +912,11 @@ const FIND_PRUNE_DIRS = [
910
912
  ".turbo",
911
913
  "public"
912
914
  ];
913
- const BUILD_CACHE_FILE_PATTERNS = [/\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i];
915
+ const BUILD_CACHE_FILE_PATTERNS = [
916
+ /\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i,
917
+ /\.min\.(?:js|css|mjs|cjs)$/i,
918
+ /\.bundle\.(?:js|css|mjs|cjs)$/i
919
+ ];
914
920
  const isBuildCacheFile = (filePath) => BUILD_CACHE_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
915
921
  const TEST_FILE_PATTERNS = [
916
922
  /(?:^|\/).*\.test\.[^/]+$/i,
@@ -938,6 +944,17 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
938
944
  const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
939
945
  const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
940
946
  const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
947
+ const readBiomeExcludePatterns = (rootDirectory) => {
948
+ const biomePath = path.join(rootDirectory, "biome.json");
949
+ if (!fs.existsSync(biomePath)) return [];
950
+ try {
951
+ const includes = JSON.parse(fs.readFileSync(biomePath, "utf-8")).files?.includes;
952
+ if (!Array.isArray(includes)) return [];
953
+ return includes.filter((entry) => typeof entry === "string").filter((entry) => entry.startsWith("!") && entry.length > 1).map((entry) => entry.slice(1));
954
+ } catch {
955
+ return [];
956
+ }
957
+ };
941
958
  const getIgnoredPaths = (rootDirectory, files) => {
942
959
  if (files.length === 0) return /* @__PURE__ */ new Set();
943
960
  const result = spawnSync("git", [
@@ -1005,7 +1022,8 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
1005
1022
  };
1006
1023
  }).filter(({ relativePath }) => isWithinProject(relativePath));
1007
1024
  const ignoredPaths = getIgnoredPaths(rootDirectory, normalizedFiles.map(({ relativePath }) => relativePath));
1008
- const normalizedExcludePatterns = exclude.length ? normalizeExcludePatterns(exclude) : [];
1025
+ const excludePatterns = [...readBiomeExcludePatterns(rootDirectory), ...exclude];
1026
+ const normalizedExcludePatterns = excludePatterns.length ? normalizeExcludePatterns(excludePatterns) : [];
1009
1027
  const isUserExcluded = (relativePath) => {
1010
1028
  if (!normalizedExcludePatterns.length) return false;
1011
1029
  return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
@@ -1016,7 +1034,7 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
1016
1034
  return micromatch.isMatch(relativePath, include, { dot: true });
1017
1035
  };
1018
1036
  return normalizedFiles.filter(({ absolutePath, relativePath }) => {
1019
- if (!fs.existsSync(absolutePath) || !isWithinProject(relativePath) || isExcludedPath(relativePath) || isTestFile$2(relativePath) || ignoredPaths.has(relativePath)) return false;
1037
+ if (!fs.existsSync(absolutePath) || !isWithinProject(relativePath) || isExcludedPath(relativePath) || isTestFile$2(relativePath) || isBuildCacheFile(relativePath) || ignoredPaths.has(relativePath)) return false;
1020
1038
  if (!isUserIncluded(relativePath)) return false;
1021
1039
  if (isUserExcluded(relativePath)) return false;
1022
1040
  return hasAllowedExtension(relativePath, extraSet);
@@ -1060,10 +1078,27 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
1060
1078
 
1061
1079
  //#endregion
1062
1080
  //#region src/engines/ai-slop/abstractions.ts
1081
+ const JS_EXTS$1 = new Set([
1082
+ ".ts",
1083
+ ".tsx",
1084
+ ".js",
1085
+ ".jsx",
1086
+ ".mjs",
1087
+ ".cjs"
1088
+ ]);
1063
1089
  const THIN_WRAPPER_PATTERNS = [
1064
- /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
1065
- /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
1066
- /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm
1090
+ {
1091
+ pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
1092
+ extensions: JS_EXTS$1
1093
+ },
1094
+ {
1095
+ pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
1096
+ extensions: JS_EXTS$1
1097
+ },
1098
+ {
1099
+ pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
1100
+ extensions: new Set([".py"])
1101
+ }
1067
1102
  ];
1068
1103
  const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
1069
1104
  const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
@@ -1078,10 +1113,11 @@ const hasHardcodedArgs = (matchText) => {
1078
1113
  return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(innerCallMatch[1]);
1079
1114
  };
1080
1115
  const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
1081
- const detectThinWrappers = (content, relativePath) => {
1116
+ const detectThinWrappers = (content, relativePath, ext) => {
1082
1117
  const diagnostics = [];
1083
1118
  const lines = content.split("\n");
1084
- for (const pattern of THIN_WRAPPER_PATTERNS) {
1119
+ for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
1120
+ if (!extensions.has(ext)) continue;
1085
1121
  const regex = new RegExp(pattern.source, pattern.flags);
1086
1122
  let match;
1087
1123
  while ((match = regex.exec(content)) !== null) {
@@ -1147,12 +1183,133 @@ const detectOverAbstraction = async (context) => {
1147
1183
  continue;
1148
1184
  }
1149
1185
  const relativePath = path.relative(context.rootDirectory, filePath);
1150
- diagnostics.push(...detectThinWrappers(content, relativePath));
1186
+ const ext = path.extname(filePath);
1187
+ diagnostics.push(...detectThinWrappers(content, relativePath, ext));
1151
1188
  diagnostics.push(...detectAiNaming(content, relativePath));
1152
1189
  }
1153
1190
  return diagnostics;
1154
1191
  };
1155
1192
 
1193
+ //#endregion
1194
+ //#region src/engines/ai-slop/narrative-comments-patterns.ts
1195
+ const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
1196
+ const DECORATIVE_SECTION_HEADER = /^[-=─━~_*#]{3,}[\s\S]+?[-=─━~_*#]{3,}$/;
1197
+ const SECTION_HEADER = /^(Phase|Step|Section|Part)\s+\d+[:.-]/i;
1198
+ const CROSS_REFERENCE_PHRASES = [
1199
+ /\bwill then be\b/i,
1200
+ /\bused by\b/i,
1201
+ /\bcalled from\b/i,
1202
+ /\bcalled later\b/i,
1203
+ /\bsee (?:above|below|later|earlier)\b/i,
1204
+ /\breplaces the\b/i,
1205
+ /\bmatches the one\b/i,
1206
+ /\bwe moved\b/i,
1207
+ /\bwe used to\b/i,
1208
+ /\brefactor(?:ed)? from\b/i,
1209
+ /\bcombined with\b.*\bthis\b/i
1210
+ ];
1211
+ const JUSTIFICATION_OPENERS = [
1212
+ /^(The idea here|The trick is|This was needed|Originally,?)/i,
1213
+ /^This\s+(?:function|method|class|module|component|hook|util|helper|handler|service)\b/i,
1214
+ /^It\s+(?:does|handles|takes|returns|processes|reads|writes|sends|fetches|loads|creates|deletes|updates|parses|validates)\b/i,
1215
+ /^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
1216
+ ];
1217
+ const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
1218
+ const STEP_COMMENT_VERB_RE = /^(?:Render|Enable|Disable|Initialize|Init|Setup|Set|Get|Fetch|Load|Save|Build|Create|Delete|Remove|Add|Update|Process|Execute|Run|Start|Stop|Clean|Cleanup|Configure|Validate|Check|Verify|Parse|Extract|Apply|Wait|Sleep|Skip|Allow|Deny|Lock|Unlock|Refresh|Reload|Reset|Clear|Send|Receive|Read|Write|Print|Log|Emit|Dispatch|Fire|Open|Close|Bind|Connect|Disconnect|Register|Unregister|Push|Pop|Insert|Append|Prepend|Sort|Filter|Find|Search|Replace|Encode|Decode|Convert|Transform|Map|Reduce|Iterate|Loop|Walk|Visit|Mark|Unmark|Toggle|Switch|Restart|Resume|Pause|Abort|Cancel|Compute|Calculate|Resolve|Reject|Ignore|Handle|Track|Trace|Increment|Decrement|Round|Truncate|Resize|Move|Copy|Clone|Merge|Split|Join|Wrap|Unwrap|Bump|Drain|Flush|Sync|Persist|Commit|Rollback|Yield|Return|Discard|Defer|Pin|Unpin|Mount|Unmount|Spawn|Kill|Restore)(?:\s|$)/;
1219
+ const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design)\b/i;
1220
+ const MEANINGFUL_JSDOC_TAGS = new Set([
1221
+ "deprecated",
1222
+ "see",
1223
+ "example",
1224
+ "type",
1225
+ "returns",
1226
+ "return",
1227
+ "param",
1228
+ "throws",
1229
+ "typedef",
1230
+ "callback",
1231
+ "override",
1232
+ "template",
1233
+ "internal",
1234
+ "public",
1235
+ "private",
1236
+ "protected",
1237
+ "experimental",
1238
+ "alpha",
1239
+ "beta",
1240
+ "since",
1241
+ "todo",
1242
+ "link",
1243
+ "license",
1244
+ "preserve",
1245
+ "swagger",
1246
+ "openapi",
1247
+ "route",
1248
+ "group",
1249
+ "summary",
1250
+ "description",
1251
+ "operationid",
1252
+ "response",
1253
+ "responses",
1254
+ "request",
1255
+ "requestbody",
1256
+ "security",
1257
+ "tag",
1258
+ "tags",
1259
+ "path",
1260
+ "body",
1261
+ "query",
1262
+ "queryparam",
1263
+ "header",
1264
+ "headers",
1265
+ "produces",
1266
+ "accept",
1267
+ "middleware",
1268
+ "api",
1269
+ "apiname",
1270
+ "apidefine",
1271
+ "apigroup",
1272
+ "apiparam",
1273
+ "apiquery",
1274
+ "apibody",
1275
+ "apiheader",
1276
+ "apisuccess",
1277
+ "apierror",
1278
+ "apiexample",
1279
+ "apiversion",
1280
+ "apidescription",
1281
+ "apipermission",
1282
+ "apiuse",
1283
+ "apiignore",
1284
+ "apiprivate",
1285
+ "namespace",
1286
+ "category"
1287
+ ]);
1288
+ const SUPPORTED_EXTS = new Set([
1289
+ ".ts",
1290
+ ".tsx",
1291
+ ".js",
1292
+ ".jsx",
1293
+ ".mjs",
1294
+ ".cjs",
1295
+ ".py",
1296
+ ".go",
1297
+ ".rs",
1298
+ ".rb",
1299
+ ".java",
1300
+ ".php"
1301
+ ]);
1302
+ const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
1303
+ const EXPORT_DEFAULT = /^\s*export\s+default\b/;
1304
+ const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
1305
+ const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
1306
+ const GO_DECL_START = /^\s*(func|type|var|const|import)\b/;
1307
+ const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
1308
+ const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
1309
+ const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
1310
+ const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
1311
+ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract|readonly)\s+)*(function|class|interface|trait|enum|const)\s+/;
1312
+
1156
1313
  //#endregion
1157
1314
  //#region src/engines/ai-slop/non-production-paths.ts
1158
1315
  const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
@@ -1191,11 +1348,37 @@ const isTrivialComment = (trimmed, nextLine) => {
1191
1348
  if (/^-{3,}|─{3,}/.test(commentBody)) return false;
1192
1349
  return (isJs ? TRIVIAL_JS_COMMENT_PATTERNS : TRIVIAL_PYTHON_COMMENT_PATTERNS).some((pattern) => pattern.test(trimmed));
1193
1350
  };
1194
- const scanFileForTrivialComments = (content, relativePath) => {
1351
+ const declStartForExt = (ext) => {
1352
+ switch (ext) {
1353
+ case ".rb": return [RUBY_DECL_START];
1354
+ case ".java": return [JAVA_DECL_START, JAVA_DECL_START_FALLBACK];
1355
+ case ".php": return [PHP_DECL_START];
1356
+ default: return [];
1357
+ }
1358
+ };
1359
+ const isCommentLineForExt = (line, ext) => {
1360
+ const trimmed = line.trim();
1361
+ if (ext === ".rb") return trimmed.startsWith("#") && !trimmed.startsWith("#!");
1362
+ if (ext === ".java" || ext === ".php") return trimmed.startsWith("//") || trimmed.startsWith("#");
1363
+ return false;
1364
+ };
1365
+ const isDocCommentForDeclaration = (lines, lineIdx, ext) => {
1366
+ const patterns = declStartForExt(ext);
1367
+ if (patterns.length === 0) return false;
1368
+ for (let j = lineIdx + 1; j < lines.length; j++) {
1369
+ const candidate = lines[j];
1370
+ if (candidate.trim() === "") continue;
1371
+ if (isCommentLineForExt(candidate, ext)) continue;
1372
+ return patterns.some((re) => re.test(candidate));
1373
+ }
1374
+ return false;
1375
+ };
1376
+ const scanFileForTrivialComments = (content, relativePath, ext) => {
1195
1377
  const diagnostics = [];
1196
1378
  const lines = content.split("\n");
1197
1379
  for (let i = 0; i < lines.length; i++) {
1198
1380
  if (!isTrivialComment(lines[i].trim(), i + 1 < lines.length ? lines[i + 1] : void 0)) continue;
1381
+ if (isDocCommentForDeclaration(lines, i, ext)) continue;
1199
1382
  diagnostics.push({
1200
1383
  filePath: relativePath,
1201
1384
  engine: "ai-slop",
@@ -1224,7 +1407,7 @@ const detectTrivialComments = async (context) => {
1224
1407
  } catch {
1225
1408
  continue;
1226
1409
  }
1227
- diagnostics.push(...scanFileForTrivialComments(content, relativePath));
1410
+ diagnostics.push(...scanFileForTrivialComments(content, relativePath, path.extname(filePath)));
1228
1411
  }
1229
1412
  return diagnostics;
1230
1413
  };
@@ -1280,6 +1463,19 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
1280
1463
  "PLACEHOLDER",
1281
1464
  "STUB"
1282
1465
  ].join("|")})[:\\s]`);
1466
+ const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
1467
+ const isGuardedSingleLineExit = (lines, lineIndex) => {
1468
+ const contextLines = [];
1469
+ for (let i = lineIndex - 1; i >= 0 && contextLines.length < 16; i--) {
1470
+ const trimmed = lines[i].trim();
1471
+ if (!trimmed || trimmed.startsWith("//")) continue;
1472
+ contextLines.unshift(trimmed);
1473
+ if (/^(?:if|else\s+if|for|while)\b/.test(trimmed) || /^}\s*else\s+if\b/.test(trimmed)) break;
1474
+ if (/;\s*$/.test(trimmed)) break;
1475
+ }
1476
+ const control = contextLines.join(" ");
1477
+ return /(?:^|[}\s])(?:if|else\s+if|for|while)\s*\(/.test(control) && !/{\s*$/.test(control);
1478
+ };
1283
1479
  const detectTodoStubs = (content, relativePath) => {
1284
1480
  const diagnostics = [];
1285
1481
  const lines = content.split("\n");
@@ -1296,7 +1492,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
1296
1492
  for (let i = 0; i < lines.length; i++) {
1297
1493
  const trimmed = lines[i].trim();
1298
1494
  const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
1299
- if (JS_EXTENSIONS$4.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !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));
1495
+ if (JS_EXTENSIONS$4.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !isGuardedSingleLineExit(lines, i) && !isBlockCloserAfterReturn(nextLine) && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push(slop(relativePath, i + 2, "ai-slop/unreachable-code", "warning", "Code after return/throw statement is unreachable", "Remove the unreachable code or restructure the control flow", false));
1300
1496
  if (/\bif\s*\(\s*(?:false|true|0|1)\s*\)/.test(trimmed) && !trimmed.startsWith("//") && !trimmed.startsWith("*") && !/["'`].*\bif\s*\(/.test(trimmed) && !/\/.*\bif\s*\(/.test(trimmed.replace(/\/\/.*$/, ""))) diagnostics.push(slop(relativePath, i + 1, "ai-slop/constant-condition", "warning", "Conditional with a constant value — likely debugging leftover", "Remove the constant condition or replace with proper logic", false));
1301
1497
  if (JS_EXTENSIONS$4.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
1302
1498
  }
@@ -1464,6 +1660,25 @@ const SWALLOWED_EXCEPTION_PATTERNS = [
1464
1660
  message: "Empty catch block swallows errors silently"
1465
1661
  }
1466
1662
  ];
1663
+ const INTENTIONAL_IGNORE_NAMES = new Set([
1664
+ "ignored",
1665
+ "ignore",
1666
+ "tolerated",
1667
+ "expected",
1668
+ "unused",
1669
+ "_",
1670
+ "_e",
1671
+ "_err",
1672
+ "_ex",
1673
+ "_t"
1674
+ ]);
1675
+ const CATCH_PARAM_RE = /catch\s*\(\s*(?:\w+\s+)?([\w$]+)/;
1676
+ const RESCUE_PARAM_RE = /rescue(?:\s+[\w:]+)?\s*=>\s*([\w$]+)/;
1677
+ const isIntentionalIgnore = (matchText, ext) => {
1678
+ const m = (ext === ".rb" ? RESCUE_PARAM_RE : CATCH_PARAM_RE).exec(matchText);
1679
+ if (!m) return false;
1680
+ return INTENTIONAL_IGNORE_NAMES.has(m[1].toLowerCase());
1681
+ };
1467
1682
  const detectSwallowedExceptions = async (context) => {
1468
1683
  const files = getSourceFiles(context);
1469
1684
  const diagnostics = [];
@@ -1482,6 +1697,7 @@ const detectSwallowedExceptions = async (context) => {
1482
1697
  let match;
1483
1698
  const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
1484
1699
  while ((match = regex.exec(content)) !== null) {
1700
+ if (isIntentionalIgnore(match[0], ext)) continue;
1485
1701
  const line = content.slice(0, match.index).split("\n").length;
1486
1702
  diagnostics.push({
1487
1703
  filePath: relativePath,
@@ -1574,6 +1790,117 @@ const detectGoPatterns = async (context) => {
1574
1790
  return diagnostics;
1575
1791
  };
1576
1792
 
1793
+ //#endregion
1794
+ //#region src/engines/ai-slop/js-import-aliases.ts
1795
+ const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
1796
+ const JS_RESOLUTION_EXTENSIONS = [
1797
+ "",
1798
+ ".ts",
1799
+ ".tsx",
1800
+ ".js",
1801
+ ".jsx",
1802
+ ".mjs",
1803
+ ".cjs",
1804
+ ".json",
1805
+ "/index.ts",
1806
+ "/index.tsx",
1807
+ "/index.js",
1808
+ "/index.jsx"
1809
+ ];
1810
+ const readJson$2 = (filePath) => {
1811
+ try {
1812
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1813
+ } catch {
1814
+ return null;
1815
+ }
1816
+ };
1817
+ const buildAliasMatcher = (key) => {
1818
+ const starIdx = key.indexOf("*");
1819
+ if (starIdx === -1) return (spec) => spec === key;
1820
+ const before = key.slice(0, starIdx);
1821
+ const after = key.slice(starIdx + 1);
1822
+ return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
1823
+ };
1824
+ const collectAliasMatchersFromConfig = (configPath, matchers) => {
1825
+ const opts = readJson$2(configPath)?.compilerOptions;
1826
+ if (!opts || typeof opts !== "object") return;
1827
+ const configDir = path.dirname(configPath);
1828
+ const paths = opts.paths;
1829
+ if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
1830
+ const baseUrl = opts.baseUrl;
1831
+ if (typeof baseUrl === "string") {
1832
+ const baseDir = path.resolve(configDir, baseUrl);
1833
+ matchers.push((spec) => {
1834
+ if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("@")) return false;
1835
+ return JS_RESOLUTION_EXTENSIONS.some((suffix) => fs.existsSync(path.join(baseDir, `${spec}${suffix}`)));
1836
+ });
1837
+ }
1838
+ };
1839
+ const collectTsPathAliases = (rootDir, workspaceDirs) => {
1840
+ const matchers = [];
1841
+ const dirs = [rootDir, ...workspaceDirs];
1842
+ for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
1843
+ return matchers;
1844
+ };
1845
+
1846
+ //#endregion
1847
+ //#region src/engines/ai-slop/js-workspaces.ts
1848
+ const readJson$1 = (filePath) => {
1849
+ try {
1850
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1851
+ } catch {
1852
+ return null;
1853
+ }
1854
+ };
1855
+ const readWorkspaceGlobs = (rootDir, rootPkg) => {
1856
+ const globs = [];
1857
+ if (rootPkg && typeof rootPkg === "object") {
1858
+ const ws = rootPkg.workspaces;
1859
+ if (Array.isArray(ws)) {
1860
+ for (const g of ws) if (typeof g === "string") globs.push(g);
1861
+ } else if (ws && typeof ws === "object") {
1862
+ const pkgs = ws.packages;
1863
+ if (Array.isArray(pkgs)) {
1864
+ for (const g of pkgs) if (typeof g === "string") globs.push(g);
1865
+ }
1866
+ }
1867
+ }
1868
+ const lerna = readJson$1(path.join(rootDir, "lerna.json"));
1869
+ if (lerna && Array.isArray(lerna.packages)) {
1870
+ for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
1871
+ }
1872
+ try {
1873
+ const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
1874
+ let inPackages = false;
1875
+ for (const rawLine of pnpmWs.split("\n")) {
1876
+ if (/^packages\s*:\s*$/.test(rawLine)) {
1877
+ inPackages = true;
1878
+ continue;
1879
+ }
1880
+ if (!inPackages) continue;
1881
+ if (/^\S/.test(rawLine)) break;
1882
+ const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
1883
+ if (m) globs.push(m[1].trim());
1884
+ }
1885
+ } catch {
1886
+ return globs;
1887
+ }
1888
+ return globs;
1889
+ };
1890
+ const expandWorkspaceDirs = (rootDir, globs) => {
1891
+ const dirs = [];
1892
+ for (const glob of globs) if (glob.endsWith("/*")) {
1893
+ const parent = path.join(rootDir, glob.slice(0, -2));
1894
+ try {
1895
+ for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
1896
+ } catch {
1897
+ continue;
1898
+ }
1899
+ } else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
1900
+ return dirs;
1901
+ };
1902
+ const collectWorkspaceDirs = (rootDir, rootPkg) => expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, rootPkg));
1903
+
1577
1904
  //#endregion
1578
1905
  //#region src/engines/ai-slop/python-data.ts
1579
1906
  const PYTHON_STDLIB = new Set([
@@ -1818,11 +2145,35 @@ const collectFromPipfile = (rootDir, pyDeps) => {
1818
2145
  return false;
1819
2146
  }
1820
2147
  };
2148
+ const LOCAL_PACKAGE_ROOTS = [
2149
+ "",
2150
+ "src",
2151
+ "lib"
2152
+ ];
2153
+ const collectLocalPythonPackages = (rootDir, pyDeps) => {
2154
+ for (const sub of LOCAL_PACKAGE_ROOTS) {
2155
+ const dir = sub ? path.join(rootDir, sub) : rootDir;
2156
+ let entries;
2157
+ try {
2158
+ entries = fs.readdirSync(dir, { withFileTypes: true });
2159
+ } catch {
2160
+ continue;
2161
+ }
2162
+ for (const entry of entries) {
2163
+ if (!entry.isDirectory()) continue;
2164
+ if (entry.name.startsWith(".")) continue;
2165
+ if (entry.name === "node_modules" || entry.name === "__pycache__") continue;
2166
+ const initPath = path.join(dir, entry.name, "__init__.py");
2167
+ if (fs.existsSync(initPath)) addPyDep(pyDeps, entry.name);
2168
+ }
2169
+ }
2170
+ };
1821
2171
  const collectPythonDeps = (rootDir) => {
1822
2172
  const pyDeps = /* @__PURE__ */ new Set();
1823
2173
  const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
1824
2174
  const hasPyproject = collectFromPyproject(rootDir, pyDeps);
1825
2175
  const hasPipfile = collectFromPipfile(rootDir, pyDeps);
2176
+ collectLocalPythonPackages(rootDir, pyDeps);
1826
2177
  return {
1827
2178
  pyDeps,
1828
2179
  hasPyManifest: hasReq || hasPyproject || hasPipfile
@@ -1841,68 +2192,23 @@ const JS_EXTENSIONS$2 = new Set([
1841
2192
  ]);
1842
2193
  const PY_EXTENSIONS$2 = new Set([".py"]);
1843
2194
  const readJson = (filePath) => {
1844
- try {
1845
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1846
- } catch {
1847
- return null;
1848
- }
1849
- };
1850
- const PKG_DEP_SECTIONS = [
1851
- "dependencies",
1852
- "devDependencies",
1853
- "peerDependencies",
1854
- "optionalDependencies"
1855
- ];
1856
- const addDepsFromPkg = (pkg, jsDeps) => {
1857
- for (const section of PKG_DEP_SECTIONS) {
1858
- const deps = pkg[section];
1859
- if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
1860
- }
1861
- };
1862
- const readWorkspaceGlobs = (rootDir, rootPkg) => {
1863
- const globs = [];
1864
- if (rootPkg && typeof rootPkg === "object") {
1865
- const ws = rootPkg.workspaces;
1866
- if (Array.isArray(ws)) {
1867
- for (const g of ws) if (typeof g === "string") globs.push(g);
1868
- } else if (ws && typeof ws === "object") {
1869
- const pkgs = ws.packages;
1870
- if (Array.isArray(pkgs)) {
1871
- for (const g of pkgs) if (typeof g === "string") globs.push(g);
1872
- }
1873
- }
1874
- }
1875
- const lerna = readJson(path.join(rootDir, "lerna.json"));
1876
- if (lerna && Array.isArray(lerna.packages)) {
1877
- for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
1878
- }
1879
- try {
1880
- const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
1881
- let inPackages = false;
1882
- for (const rawLine of pnpmWs.split("\n")) {
1883
- if (/^packages\s*:\s*$/.test(rawLine)) {
1884
- inPackages = true;
1885
- continue;
1886
- }
1887
- if (!inPackages) continue;
1888
- if (/^\S/.test(rawLine)) break;
1889
- const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
1890
- if (m) globs.push(m[1].trim());
1891
- }
1892
- } catch {}
1893
- return globs;
1894
- };
1895
- const expandWorkspaceDirs = (rootDir, globs) => {
1896
- const dirs = [];
1897
- for (const glob of globs) if (glob.endsWith("/*")) {
1898
- const parent = path.join(rootDir, glob.slice(0, -2));
1899
- try {
1900
- for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
1901
- } catch {
1902
- continue;
1903
- }
1904
- } else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
1905
- return dirs;
2195
+ try {
2196
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
2197
+ } catch {
2198
+ return null;
2199
+ }
2200
+ };
2201
+ const PKG_DEP_SECTIONS = [
2202
+ "dependencies",
2203
+ "devDependencies",
2204
+ "peerDependencies",
2205
+ "optionalDependencies"
2206
+ ];
2207
+ const addDepsFromPkg = (pkg, jsDeps) => {
2208
+ for (const section of PKG_DEP_SECTIONS) {
2209
+ const deps = pkg[section];
2210
+ if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
2211
+ }
1906
2212
  };
1907
2213
  const SKIP_DIRS = new Set([
1908
2214
  "node_modules",
@@ -1945,7 +2251,7 @@ const collectJsDeps = (rootDir, jsDeps) => {
1945
2251
  if (!pkg || typeof pkg !== "object") return false;
1946
2252
  addDepsFromPkg(pkg, jsDeps);
1947
2253
  if (typeof pkg.name === "string") jsDeps.add(pkg.name);
1948
- const workspaceDirs = expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, pkg));
2254
+ const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
1949
2255
  for (const wsDir of workspaceDirs) {
1950
2256
  const wsPkg = readJson(path.join(wsDir, "package.json"));
1951
2257
  if (!wsPkg) continue;
@@ -1955,43 +2261,6 @@ const collectJsDeps = (rootDir, jsDeps) => {
1955
2261
  collectNestedManifests(rootDir, jsDeps);
1956
2262
  return true;
1957
2263
  };
1958
- const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
1959
- const buildAliasMatcher = (key) => {
1960
- const starIdx = key.indexOf("*");
1961
- if (starIdx === -1) return (spec) => spec === key;
1962
- const before = key.slice(0, starIdx);
1963
- const after = key.slice(starIdx + 1);
1964
- return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
1965
- };
1966
- const collectAliasMatchersFromConfig = (configPath, matchers) => {
1967
- const opts = readJson(configPath)?.compilerOptions;
1968
- if (!opts) return;
1969
- const paths = opts.paths;
1970
- if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
1971
- const baseUrl = opts.baseUrl;
1972
- if (typeof baseUrl === "string") {
1973
- const baseUrlDir = path.resolve(path.dirname(configPath), baseUrl);
1974
- let entries;
1975
- try {
1976
- entries = fs.readdirSync(baseUrlDir);
1977
- } catch {
1978
- return;
1979
- }
1980
- const baseSpecifiers = /* @__PURE__ */ new Set();
1981
- for (const entry of entries) {
1982
- if (entry.startsWith(".") || entry === "node_modules") continue;
1983
- const base = entry.replace(/\.(?:[jt]sx?|mjs|cjs|d\.ts)$/i, "");
1984
- if (base.length > 0) baseSpecifiers.add(base);
1985
- }
1986
- for (const name of baseSpecifiers) matchers.push((spec) => spec === name || spec.startsWith(`${name}/`));
1987
- }
1988
- };
1989
- const collectTsPathAliases = (rootDir) => {
1990
- const matchers = [];
1991
- const dirs = [rootDir, ...expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, readJson(path.join(rootDir, "package.json"))))];
1992
- for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
1993
- return matchers;
1994
- };
1995
2264
  const loadManifest = (rootDir) => {
1996
2265
  const jsDeps = /* @__PURE__ */ new Set();
1997
2266
  const hasJsManifest = collectJsDeps(rootDir, jsDeps);
@@ -2013,14 +2282,19 @@ const VIRTUAL_MODULE_PREFIXES = [
2013
2282
  "astro:",
2014
2283
  "virtual:",
2015
2284
  "bun:",
2016
- "~icons/"
2285
+ "file:"
2017
2286
  ];
2018
- const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
2287
+ const isJsVirtualModule = (spec, manifest) => {
2288
+ if (VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p))) return true;
2289
+ if (spec === "bun") return true;
2290
+ if (spec === "unfonts.css" && manifest.jsDeps.has("unplugin-fonts")) return true;
2291
+ if (spec.startsWith("~icons/") && manifest.jsDeps.has("unplugin-icons")) return true;
2292
+ return false;
2293
+ };
2019
2294
  const stripImportQuery = (spec) => {
2020
2295
  const idx = spec.indexOf("?");
2021
2296
  return idx === -1 ? spec : spec.slice(0, idx);
2022
2297
  };
2023
- const VIRTUAL_ASSET_FILES = { "unfonts.css": "unplugin-fonts" };
2024
2298
  const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
2025
2299
  const isLikelyRealImportSpec = (spec) => {
2026
2300
  if (spec.length === 0) return false;
@@ -2092,9 +2366,7 @@ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
2092
2366
  if (spec.length === 0) return null;
2093
2367
  if (isJsRelativeOrAbsolute(spec)) return null;
2094
2368
  if (isJsBuiltin(spec)) return null;
2095
- if (isJsVirtualModule(spec)) return null;
2096
- const virtualOwner = VIRTUAL_ASSET_FILES[spec];
2097
- if (virtualOwner && manifest.jsDeps.has(virtualOwner)) return null;
2369
+ if (isJsVirtualModule(spec, manifest)) return null;
2098
2370
  if (tsAliasMatchers.some((m) => m(spec))) return null;
2099
2371
  const pkg = packageNameFromImport(spec);
2100
2372
  if (manifest.jsDeps.has(pkg)) return null;
@@ -2114,9 +2386,11 @@ const checkPyImport = (spec, manifest) => {
2114
2386
  return root;
2115
2387
  };
2116
2388
  const detectHallucinatedImports = async (context) => {
2389
+ const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
2390
+ const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
2117
2391
  const manifest = loadManifest(context.rootDirectory);
2118
2392
  if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
2119
- const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory) : [];
2393
+ const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory, workspaceDirs) : [];
2120
2394
  const diagnostics = [];
2121
2395
  const files = getSourceFiles(context);
2122
2396
  for (const filePath of files) {
@@ -2157,121 +2431,7 @@ const detectHallucinatedImports = async (context) => {
2157
2431
  };
2158
2432
 
2159
2433
  //#endregion
2160
- //#region src/engines/ai-slop/narrative-comments-patterns.ts
2161
- const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
2162
- const DECORATIVE_SECTION_HEADER = /^[-=─━~_*#]{3,}[\s\S]+?[-=─━~_*#]{3,}$/;
2163
- const SECTION_HEADER = /^(Phase|Step|Section|Part)\s+\d+[:.-]/i;
2164
- const CROSS_REFERENCE_PHRASES = [
2165
- /\bwill then be\b/i,
2166
- /\bused by\b/i,
2167
- /\bcalled from\b/i,
2168
- /\bcalled later\b/i,
2169
- /\bsee (?:above|below|later|earlier)\b/i,
2170
- /\breplaces the\b/i,
2171
- /\bmatches the one\b/i,
2172
- /\bwe moved\b/i,
2173
- /\bwe used to\b/i,
2174
- /\brefactor(?:ed)? from\b/i,
2175
- /\bcombined with\b.*\bthis\b/i
2176
- ];
2177
- const JUSTIFICATION_OPENERS = [/^(The idea here|The trick is|This was needed|Originally,?)/i];
2178
- const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
2179
- const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:)\b/i;
2180
- const MEANINGFUL_JSDOC_TAGS = new Set([
2181
- "deprecated",
2182
- "see",
2183
- "example",
2184
- "type",
2185
- "returns",
2186
- "return",
2187
- "param",
2188
- "throws",
2189
- "typedef",
2190
- "callback",
2191
- "override",
2192
- "template",
2193
- "internal",
2194
- "public",
2195
- "private",
2196
- "protected",
2197
- "experimental",
2198
- "alpha",
2199
- "beta",
2200
- "since",
2201
- "todo",
2202
- "link",
2203
- "license",
2204
- "preserve",
2205
- "swagger",
2206
- "openapi",
2207
- "route",
2208
- "group",
2209
- "summary",
2210
- "description",
2211
- "operationid",
2212
- "response",
2213
- "responses",
2214
- "request",
2215
- "requestbody",
2216
- "security",
2217
- "tag",
2218
- "tags",
2219
- "path",
2220
- "body",
2221
- "query",
2222
- "queryparam",
2223
- "header",
2224
- "headers",
2225
- "produces",
2226
- "accept",
2227
- "middleware",
2228
- "api",
2229
- "apiname",
2230
- "apidefine",
2231
- "apigroup",
2232
- "apiparam",
2233
- "apiquery",
2234
- "apibody",
2235
- "apiheader",
2236
- "apisuccess",
2237
- "apierror",
2238
- "apiexample",
2239
- "apiversion",
2240
- "apidescription",
2241
- "apipermission",
2242
- "apiuse",
2243
- "apiignore",
2244
- "apiprivate",
2245
- "namespace",
2246
- "category"
2247
- ]);
2248
- const SUPPORTED_EXTS = new Set([
2249
- ".ts",
2250
- ".tsx",
2251
- ".js",
2252
- ".jsx",
2253
- ".mjs",
2254
- ".cjs",
2255
- ".py",
2256
- ".go",
2257
- ".rs",
2258
- ".rb",
2259
- ".java",
2260
- ".php"
2261
- ]);
2262
- const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
2263
- const EXPORT_DEFAULT = /^\s*export\s+default\b/;
2264
- const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
2265
- const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
2266
- const GO_DECL_START = /^\s*(func|type|var|const)\s+/;
2267
- const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
2268
- const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
2269
- const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
2270
- const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
2271
- const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|readonly\s+)*(function|class|interface|trait|enum|const)\s+/;
2272
-
2273
- //#endregion
2274
- //#region src/engines/ai-slop/narrative-comments.ts
2434
+ //#region src/engines/ai-slop/comment-blocks.ts
2275
2435
  const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
2276
2436
  const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
2277
2437
  const getCommentSyntax = (ext) => {
@@ -2371,6 +2531,9 @@ const collectBlocks = (sourceLines, syntax) => {
2371
2531
  }
2372
2532
  return blocks;
2373
2533
  };
2534
+
2535
+ //#endregion
2536
+ //#region src/engines/ai-slop/narrative-comments.ts
2374
2537
  const looksLikeDeclarationPreamble = (nextLine, ext) => {
2375
2538
  if (nextLine === null) return false;
2376
2539
  if (DECL_START.test(nextLine) || EXPORT_DEFAULT.test(nextLine)) return true;
@@ -2400,6 +2563,7 @@ const isBareSectionLabel = (prose) => {
2400
2563
  if (!BARE_LABEL_RE.test(prose)) return false;
2401
2564
  if (prose.endsWith(".")) return false;
2402
2565
  if (prose.split(/\s+/).length > 3) return false;
2566
+ if (STEP_COMMENT_VERB_RE.test(prose)) return false;
2403
2567
  return true;
2404
2568
  };
2405
2569
  const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
@@ -2414,15 +2578,45 @@ const nextLineLooksLikeDataEntry = (nextLine) => {
2414
2578
  };
2415
2579
  const looksLikeSuppressDirective = (block) => block.rawLines.some((l) => /\b(biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(l));
2416
2580
  const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
2581
+ const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
2582
+ const GO_KEYWORDS = new Set([
2583
+ "return",
2584
+ "if",
2585
+ "for",
2586
+ "switch",
2587
+ "case",
2588
+ "default",
2589
+ "go",
2590
+ "select",
2591
+ "defer",
2592
+ "else",
2593
+ "break",
2594
+ "continue",
2595
+ "goto",
2596
+ "package",
2597
+ "import",
2598
+ "map",
2599
+ "chan",
2600
+ "range"
2601
+ ]);
2417
2602
  const looksLikeGoDocComment = (block, ext) => {
2418
2603
  if (ext !== ".go" || block.kind !== "line") return false;
2419
2604
  const next = block.nextNonBlankLine;
2420
2605
  if (!next) return false;
2421
- const declMatch = GO_DECL_NAME_RE.exec(next.trim());
2422
- if (!declMatch) return false;
2423
- return ((block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "") === declMatch[1];
2606
+ const trimmedNext = next.trim();
2607
+ const firstWord = (block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "";
2608
+ const declMatch = GO_DECL_NAME_RE.exec(trimmedNext);
2609
+ if (declMatch && firstWord === declMatch[1]) return true;
2610
+ const fieldMatch = GO_FIELD_LEAD_RE.exec(trimmedNext);
2611
+ if (fieldMatch && !GO_KEYWORDS.has(fieldMatch[1]) && firstWord === fieldMatch[1]) return true;
2612
+ return false;
2613
+ };
2614
+ const RUBY_DOC_INDICATORS = /^\s*#\s*(?:#|@\w+|:[\w-]+:|=begin|=end)/;
2615
+ const looksLikeRubyDocBlock = (block, ext) => {
2616
+ if (ext !== ".rb" || block.kind !== "line") return false;
2617
+ return block.rawLines.some((line) => RUBY_DOC_INDICATORS.test(line));
2424
2618
  };
2425
- const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):|\(e\.g\.[^)]+\)|\(i\.e\.[^)]+\)/i;
2619
+ const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see|todo|fixme|hack|reason|deprecated|deprecation|migration|legacy|historical|context):|\(e\.g\.[^)]+\)|\(i\.e\.[^)]+\)|\b\w+\.\w+(?:\.\w+)+\b|\[[\w/.-]+\]/i;
2426
2620
  const hasDocIndicator = (block) => {
2427
2621
  const joined = block.prose.join(" ");
2428
2622
  if (DOC_INDICATOR_RE.test(joined)) return true;
@@ -2458,6 +2652,10 @@ const detectNarrativeInBlock = (block, ext) => {
2458
2652
  matched: false,
2459
2653
  reason: ""
2460
2654
  };
2655
+ if (looksLikeRubyDocBlock(block, ext)) return {
2656
+ matched: false,
2657
+ reason: ""
2658
+ };
2461
2659
  if (block.kind === "line" && block.prose.some((l) => DECORATIVE_SEPARATOR.test(l) || DECORATIVE_SECTION_HEADER.test(l))) return {
2462
2660
  matched: true,
2463
2661
  reason: "decorative separator"
@@ -2466,17 +2664,17 @@ const detectNarrativeInBlock = (block, ext) => {
2466
2664
  matched: true,
2467
2665
  reason: "phase/section header"
2468
2666
  };
2469
- if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine)) return {
2667
+ if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
2470
2668
  matched: true,
2471
2669
  reason: "bare section label"
2472
2670
  };
2473
2671
  const joined = block.prose.join(" ");
2474
2672
  const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
2475
- if ((hasWhyMarker || hasDocIndicator(block)) && block.kind === "jsdoc") return {
2673
+ if (hasWhyMarker || hasDocIndicator(block)) return {
2476
2674
  matched: false,
2477
2675
  reason: ""
2478
2676
  };
2479
- if (block.kind === "line" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
2677
+ if (block.kind === "line" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext) && hasPreambleSlopSignal(block)) return {
2480
2678
  matched: true,
2481
2679
  reason: "multi-line preamble before declaration"
2482
2680
  };
@@ -2499,11 +2697,18 @@ const detectNarrativeInBlock = (block, ext) => {
2499
2697
  reason: "explanatory preamble"
2500
2698
  };
2501
2699
  const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
2502
- if (nonEmptyProseCount >= 5) return {
2503
- matched: true,
2504
- reason: "long narrative block"
2505
- };
2506
- if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line") return {
2700
+ const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
2701
+ if (nonEmptyProseCount >= 5) {
2702
+ if (isAboveDeclaration) return {
2703
+ matched: false,
2704
+ reason: ""
2705
+ };
2706
+ return {
2707
+ matched: true,
2708
+ reason: "long narrative block"
2709
+ };
2710
+ }
2711
+ if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
2507
2712
  matched: true,
2508
2713
  reason: "multi-line narrative prose"
2509
2714
  };
@@ -2735,7 +2940,23 @@ const isTestFile = (relPath) => {
2735
2940
  if (segments.some((s) => TEST_CRATE_SEGMENT_RE.test(s))) return true;
2736
2941
  const basename = segments[segments.length - 1] ?? "";
2737
2942
  if (TEST_BASENAMES.has(basename)) return true;
2738
- return basename.endsWith("_tests.rs") || basename.endsWith("_testutil.rs");
2943
+ return basename.endsWith("_tests.rs") || basename.endsWith("_test.rs") || basename.endsWith("_testutil.rs");
2944
+ };
2945
+ const buildBlockCommentRanges = (lines) => {
2946
+ const ranges = [];
2947
+ let openLine = -1;
2948
+ for (let i = 0; i < lines.length; i++) {
2949
+ const line = lines[i];
2950
+ if (openLine === -1) {
2951
+ const openIdx = line.indexOf("/*");
2952
+ if (openIdx !== -1 && line.indexOf("*/", openIdx + 2) === -1) openLine = i;
2953
+ } else if (line.indexOf("*/") !== -1) {
2954
+ ranges.push([openLine, i]);
2955
+ openLine = -1;
2956
+ }
2957
+ }
2958
+ if (openLine !== -1) ranges.push([openLine, lines.length - 1]);
2959
+ return ranges;
2739
2960
  };
2740
2961
  const isExampleFile = (relPath) => relPath.split(path.sep).some((seg) => seg === "examples" || seg === "benches");
2741
2962
  const UNWRAP_INTENT_LOOKBACK = 2;
@@ -2766,11 +2987,12 @@ const buildTestRanges = (lines) => {
2766
2987
  return ranges;
2767
2988
  };
2768
2989
  const isInRange = (ranges, lineIdx) => ranges.some(([start, end]) => lineIdx >= start && lineIdx <= end);
2769
- const flagNonTestUnwrap = (lines, relPath, testRanges, out) => {
2990
+ const flagNonTestUnwrap = (lines, relPath, testRanges, blockCommentRanges, out) => {
2770
2991
  for (let i = 0; i < lines.length; i++) {
2771
2992
  const line = lines[i];
2772
2993
  if (COMMENT_LINE_RE.test(line)) continue;
2773
2994
  if (isInRange(testRanges, i)) continue;
2995
+ if (isInRange(blockCommentRanges, i)) continue;
2774
2996
  if (!UNWRAP_CALL_RE.test(line)) continue;
2775
2997
  if (WRITELN_UNWRAP_RE.test(line)) continue;
2776
2998
  if (hasIntentComment(lines, i)) continue;
@@ -2827,7 +3049,7 @@ const detectRustPatterns = async (context) => {
2827
3049
  flagTodoMacro(lines, relPath, diagnostics);
2828
3050
  continue;
2829
3051
  }
2830
- flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), diagnostics);
3052
+ flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), buildBlockCommentRanges(lines), diagnostics);
2831
3053
  flagTodoMacro(lines, relPath, diagnostics);
2832
3054
  }
2833
3055
  return diagnostics;
@@ -2913,7 +3135,9 @@ const extractPyImportedSymbols = (lines) => {
2913
3135
  const cleaned = importPart.replace(/[()]/g, "");
2914
3136
  for (const item of cleaned.split(",")) {
2915
3137
  const parts = item.trim().split(/\s+as\s+/);
2916
- const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
3138
+ const original = parts[0].trim();
3139
+ const localName = parts.length > 1 ? parts[1].trim() : original;
3140
+ if (parts.length > 1 && original === localName) continue;
2917
3141
  if (localName && /^\w+$/.test(localName)) symbols.push({
2918
3142
  name: localName,
2919
3143
  line: i + 1,
@@ -2926,7 +3150,9 @@ const extractPyImportedSymbols = (lines) => {
2926
3150
  const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
2927
3151
  if (importMatch) {
2928
3152
  importLines.add(i);
2929
- const simpleName = (importMatch[2] ?? importMatch[1]).split(".")[0];
3153
+ const alias = importMatch[2];
3154
+ if (alias && alias === importMatch[1]) continue;
3155
+ const simpleName = (alias ?? importMatch[1]).split(".")[0];
2930
3156
  if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
2931
3157
  name: simpleName,
2932
3158
  line: i + 1,
@@ -3582,9 +3808,17 @@ const isTrivialLine = (line) => {
3582
3808
  if (trimmed.startsWith("/*") || trimmed.startsWith("*")) return true;
3583
3809
  return false;
3584
3810
  };
3811
+ 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)=/;
3812
+ const DATA_LITERAL_RE = /^\s*(?:[A-Za-z_$][\w$-]*:\s*(?:["'`[{]|\d|true\b|false\b|null\b)|["'`][^"'`]*["'`],?\s*$|[{}\]],?\s*$|\),?\s*$)/;
3585
3813
  const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
3586
3814
  const FILE_SUPPRESS_RE = /aislop[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
3587
3815
  const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
3816
+ const isLowSignalMarkupWindow = (lines) => {
3817
+ return lines.filter((line) => SVG_MARKUP_RE.test(line)).length >= Math.ceil(WINDOW_SIZE / 2);
3818
+ };
3819
+ const isLowSignalDataWindow = (lines) => {
3820
+ return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= WINDOW_SIZE - 1;
3821
+ };
3588
3822
  const findSuppressedLines = (lines) => {
3589
3823
  const suppressed = /* @__PURE__ */ new Set();
3590
3824
  for (let i = 0; i < lines.length; i++) {
@@ -3611,6 +3845,8 @@ const collectMeaningfulLines = (content) => {
3611
3845
  if (suppressed.has(i + 1)) continue;
3612
3846
  const window = lines.slice(i, i + WINDOW_SIZE);
3613
3847
  if (window.some((l) => !MEANINGFUL_LINE.test(l))) continue;
3848
+ if (isLowSignalMarkupWindow(window)) continue;
3849
+ if (isLowSignalDataWindow(window)) continue;
3614
3850
  if (window.every(isTrivialLine)) continue;
3615
3851
  const normalised = window.map(normaliseLine);
3616
3852
  if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < WINDOW_SIZE - 1) continue;
@@ -4852,7 +5088,20 @@ const createOxlintConfig = (options) => {
4852
5088
  ...buildFrameworkPlugins(options.framework)
4853
5089
  ];
4854
5090
  const globals = buildTestGlobals(options.testFramework ?? null);
4855
- if (options.framework === "expo" || options.framework === "react") globals.__DEV__ = "readonly";
5091
+ for (const name of [
5092
+ "__DEV__",
5093
+ "__TEST__",
5094
+ "__BROWSER__",
5095
+ "__NODE__",
5096
+ "__GLOBAL__",
5097
+ "__SSR__",
5098
+ "__ESM_BROWSER__",
5099
+ "__ESM_BUNDLER__",
5100
+ "__VERSION__",
5101
+ "__COMMIT__",
5102
+ "__BUILD__"
5103
+ ]) globals[name] = "readonly";
5104
+ for (const globalName of options.globals ?? []) globals[globalName] = "readonly";
4856
5105
  if (options.framework === "astro") {
4857
5106
  globals.Astro = "readonly";
4858
5107
  rules["no-undef"] = "off";
@@ -4872,19 +5121,7 @@ const createOxlintConfig = (options) => {
4872
5121
  };
4873
5122
 
4874
5123
  //#endregion
4875
- //#region src/engines/lint/oxlint.ts
4876
- const esmRequire$1 = createRequire(import.meta.url);
4877
- const resolveOxlintBinary = () => {
4878
- try {
4879
- const oxlintMainPath = esmRequire$1.resolve("oxlint");
4880
- const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
4881
- return path.join(oxlintDir, "bin", "oxlint");
4882
- } catch {
4883
- return "oxlint";
4884
- }
4885
- };
4886
- const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
4887
- const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
5124
+ //#region src/engines/lint/oxlint-context-filters.ts
4888
5125
  const AMBIENT_GLOBAL_DEPS = [
4889
5126
  "unplugin-icons",
4890
5127
  "@types/bun",
@@ -4946,6 +5183,9 @@ const isAmbientFalsePositive = (rule, message, sources) => {
4946
5183
  return false;
4947
5184
  };
4948
5185
  const sstReferencedFiles = /* @__PURE__ */ new Map();
5186
+ const clearSstReferenceCache = () => {
5187
+ sstReferencedFiles.clear();
5188
+ };
4949
5189
  const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
4950
5190
  const cached = sstReferencedFiles.get(relativeFilePath);
4951
5191
  if (cached !== void 0) return cached;
@@ -4966,12 +5206,114 @@ const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
4966
5206
  sstReferencedFiles.set(relativeFilePath, referenced);
4967
5207
  return referenced;
4968
5208
  };
5209
+
5210
+ //#endregion
5211
+ //#region src/engines/lint/oxlint-globals.ts
5212
+ const readTextFile$1 = (filePath) => {
5213
+ try {
5214
+ return fs.readFileSync(filePath, "utf-8");
5215
+ } catch {
5216
+ return null;
5217
+ }
5218
+ };
5219
+ const collectPackageNames = (dir) => {
5220
+ const names = /* @__PURE__ */ new Set();
5221
+ const raw = readTextFile$1(path.join(dir, "package.json"));
5222
+ if (!raw) return names;
5223
+ try {
5224
+ const pkg = JSON.parse(raw);
5225
+ for (const section of [
5226
+ "dependencies",
5227
+ "devDependencies",
5228
+ "peerDependencies",
5229
+ "optionalDependencies"
5230
+ ]) {
5231
+ const deps = pkg[section];
5232
+ if (deps && typeof deps === "object") for (const name of Object.keys(deps)) names.add(name);
5233
+ }
5234
+ } catch {
5235
+ return names;
5236
+ }
5237
+ return names;
5238
+ };
5239
+ const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
5240
+ const collectAmbientGlobals = (rootDir) => {
5241
+ const globals = /* @__PURE__ */ new Set();
5242
+ const projectFiles = listProjectFiles(rootDir);
5243
+ for (const relativePath of projectFiles) {
5244
+ if (!relativePath.endsWith(".d.ts")) continue;
5245
+ const content = readTextFile$1(path.join(rootDir, relativePath));
5246
+ if (!content) continue;
5247
+ AMBIENT_GLOBAL_RE.lastIndex = 0;
5248
+ let match;
5249
+ while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
5250
+ }
5251
+ const deps = collectPackageNames(rootDir);
5252
+ if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
5253
+ if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
5254
+ "$app",
5255
+ "$config",
5256
+ "$dev",
5257
+ "$interpolate",
5258
+ "$resolve",
5259
+ "$jsonParse",
5260
+ "$jsonStringify",
5261
+ "aws",
5262
+ "cloudflare",
5263
+ "docker",
5264
+ "random",
5265
+ "sst",
5266
+ "vercel",
5267
+ "pulumi"
5268
+ ]) globals.add(name);
5269
+ return [...globals];
5270
+ };
5271
+
5272
+ //#endregion
5273
+ //#region src/engines/lint/oxlint.ts
5274
+ const esmRequire$1 = createRequire(import.meta.url);
5275
+ const OXLINT_EXTENSIONS = new Set([
5276
+ ".ts",
5277
+ ".tsx",
5278
+ ".js",
5279
+ ".jsx",
5280
+ ".mjs",
5281
+ ".cjs"
5282
+ ]);
5283
+ const resolveOxlintBinary = () => {
5284
+ try {
5285
+ const oxlintMainPath = esmRequire$1.resolve("oxlint");
5286
+ const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
5287
+ return path.join(oxlintDir, "bin", "oxlint");
5288
+ } catch {
5289
+ return "oxlint";
5290
+ }
5291
+ };
5292
+ const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
5293
+ const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
4969
5294
  const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
4970
5295
  const isUnderscoreUnusedVar = (rule, message) => {
4971
5296
  if (rule !== "eslint/no-unused-vars") return false;
4972
5297
  const match = UNUSED_VAR_IDENT_RE.exec(message);
4973
5298
  return match ? match[1].startsWith("_") : false;
4974
5299
  };
5300
+ const readTextFile = (filePath) => {
5301
+ try {
5302
+ return fs.readFileSync(filePath, "utf-8");
5303
+ } catch {
5304
+ return null;
5305
+ }
5306
+ };
5307
+ const isSolidRefFalsePositive = (context, diagnostic) => {
5308
+ if (diagnostic.rule !== "eslint/no-unassigned-vars") return false;
5309
+ const name = diagnostic.message.match(/^'([^']+)' is always 'undefined'/)?.[1];
5310
+ if (!name) return false;
5311
+ const content = readTextFile(path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath : path.join(context.rootDirectory, diagnostic.filePath));
5312
+ if (!content) return false;
5313
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5314
+ return new RegExp(`\\bref=\\{\\s*${escaped}\\s*\\}`).test(content);
5315
+ };
5316
+ const isContextualTypeScriptFalsePositive = (diagnostic) => diagnostic.rule === "typescript-eslint/triple-slash-reference" && (diagnostic.filePath.endsWith(".d.ts") || /(?:^|\/)sst\.config\.ts$/.test(diagnostic.filePath));
4975
5317
  const parseRuleCode = (code) => {
4976
5318
  if (!code) return {
4977
5319
  plugin: "eslint",
@@ -5004,6 +5346,7 @@ const detectTestFramework = (rootDir) => {
5004
5346
  } catch {}
5005
5347
  return null;
5006
5348
  };
5349
+ const getOxlintTargets = (context) => getSourceFiles(context).filter((filePath) => OXLINT_EXTENSIONS.has(path.extname(filePath).toLowerCase())).filter((filePath) => !isAutoGenerated(filePath)).map((filePath) => path.relative(context.rootDirectory, filePath).split(path.sep).join("/"));
5007
5350
  const extractUnusedVarName = (message) => {
5008
5351
  const variableMatch = message.match(/Variable '([^']+)' is declared but never used/);
5009
5352
  if (variableMatch?.[1]) return {
@@ -5064,12 +5407,17 @@ const removeDuplicateKeyLines = (rootDirectory, diagnostics) => {
5064
5407
  };
5065
5408
  const runOxlint = async (context) => {
5066
5409
  const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
5410
+ const framework = context.frameworks.find((f) => f !== "none");
5411
+ const testFramework = detectTestFramework(context.rootDirectory);
5412
+ const targets = getOxlintTargets(context);
5413
+ if (targets.length === 0) return [];
5067
5414
  const config = createOxlintConfig({
5068
- framework: context.frameworks.find((f) => f !== "none"),
5069
- testFramework: detectTestFramework(context.rootDirectory)
5415
+ framework,
5416
+ testFramework,
5417
+ globals: collectAmbientGlobals(context.rootDirectory)
5070
5418
  });
5071
5419
  const ambientSources = detectAmbientSources(context.rootDirectory);
5072
- sstReferencedFiles.clear();
5420
+ clearSstReferenceCache();
5073
5421
  try {
5074
5422
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
5075
5423
  const args = [
@@ -5080,7 +5428,7 @@ const runOxlint = async (context) => {
5080
5428
  "json"
5081
5429
  ];
5082
5430
  if (context.languages.includes("typescript") && fs.existsSync(path.join(context.rootDirectory, "tsconfig.json"))) args.push("--tsconfig", "./tsconfig.json");
5083
- args.push(".");
5431
+ args.push(...targets);
5084
5432
  const result = await runSubprocess(process.execPath, args, {
5085
5433
  cwd: context.rootDirectory,
5086
5434
  timeout: 12e4
@@ -5112,6 +5460,8 @@ const runOxlint = async (context) => {
5112
5460
  if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
5113
5461
  if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
5114
5462
  if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
5463
+ if (isSolidRefFalsePositive(context, d)) return false;
5464
+ if (isContextualTypeScriptFalsePositive(d)) return false;
5115
5465
  if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
5116
5466
  if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
5117
5467
  const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
@@ -5126,10 +5476,15 @@ const runOxlint = async (context) => {
5126
5476
  const fixOxlint = async (context, options = {}) => {
5127
5477
  const dangerous = options.force ?? false;
5128
5478
  const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-fix-${process.pid}.json`);
5479
+ const framework = context.frameworks.find((f) => f !== "none");
5480
+ const testFramework = detectTestFramework(context.rootDirectory);
5481
+ const targets = getOxlintTargets(context);
5482
+ if (targets.length === 0) return;
5129
5483
  const config = createOxlintConfig({
5130
- framework: context.frameworks.find((f) => f !== "none"),
5131
- testFramework: detectTestFramework(context.rootDirectory),
5132
- mode: "fix"
5484
+ framework,
5485
+ testFramework,
5486
+ mode: "fix",
5487
+ globals: collectAmbientGlobals(context.rootDirectory)
5133
5488
  });
5134
5489
  try {
5135
5490
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -5141,13 +5496,13 @@ const fixOxlint = async (context, options = {}) => {
5141
5496
  "--fix",
5142
5497
  "--fix-suggestions",
5143
5498
  "--fix-dangerously",
5144
- "."
5499
+ ...targets
5145
5500
  ] : [
5146
5501
  binary,
5147
5502
  "-c",
5148
5503
  configPath,
5149
5504
  "--fix",
5150
- "."
5505
+ ...targets
5151
5506
  ];
5152
5507
  const result = await runSubprocess(process.execPath, args, {
5153
5508
  cwd: context.rootDirectory,
@@ -5739,7 +6094,7 @@ const DB_RECEIVER = "(?:db|database|knex|client|connection|conn|pool|sql|prisma|
5739
6094
  const DB_METHOD = "(?:query|execute|exec|raw|\\$queryRaw|\\$queryRawUnsafe|\\$executeRaw|\\$executeRawUnsafe)";
5740
6095
  const RISKY_PATTERNS = [
5741
6096
  {
5742
- pattern: new RegExp(`\\b${ev}\\s*\\(`, "g"),
6097
+ pattern: new RegExp(`(?<![\\w.>:\\\\])\\b${ev}\\s*\\(`, "g"),
5743
6098
  extensions: [
5744
6099
  ".ts",
5745
6100
  ".tsx",
@@ -5846,6 +6201,16 @@ const RISKY_PATTERNS = [
5846
6201
  help: "Use parameterized queries or an ORM instead of string concatenation"
5847
6202
  }
5848
6203
  ];
6204
+ const hasDangerouslySetInnerHtmlIgnore = (lines, lineIndex) => {
6205
+ const start = Math.max(0, lineIndex - 2);
6206
+ return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
6207
+ };
6208
+ const isStructuredDataScript = (content, matchIndex) => {
6209
+ const before = content.slice(Math.max(0, matchIndex - 300), matchIndex);
6210
+ if (/type=["']application\/ld\+json["']/.test(before)) return true;
6211
+ const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
6212
+ return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
6213
+ };
5849
6214
  const detectRiskyConstructs = async (context) => {
5850
6215
  const files = getSourceFiles(context);
5851
6216
  const diagnostics = [];
@@ -5861,6 +6226,7 @@ const detectRiskyConstructs = async (context) => {
5861
6226
  const normalizedPath = relativePath.split(path.sep).join("/");
5862
6227
  const isMigrationOrSeeder = /(?:^|\/)(migrations|seeders|seeds|migrate)\//.test(normalizedPath);
5863
6228
  const masked = maskStringsAndComments(content, ext);
6229
+ const lines = content.split("\n");
5864
6230
  for (const { pattern, extensions, name, message, help } of RISKY_PATTERNS) {
5865
6231
  if (!extensions.includes(ext)) continue;
5866
6232
  if (isMigrationOrSeeder && name === "sql-injection") continue;
@@ -5872,6 +6238,10 @@ const detectRiskyConstructs = async (context) => {
5872
6238
  const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
5873
6239
  if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
5874
6240
  }
6241
+ if (name === "dangerously-set-innerhtml") {
6242
+ if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
6243
+ if (isStructuredDataScript(content, match.index)) continue;
6244
+ }
5875
6245
  diagnostics.push({
5876
6246
  filePath: relativePath,
5877
6247
  engine: "security",
@@ -5895,7 +6265,8 @@ const detectRiskyConstructs = async (context) => {
5895
6265
  const SECRET_PATTERNS = [
5896
6266
  {
5897
6267
  pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
5898
- name: "API key"
6268
+ name: "API key",
6269
+ keywordPrefixed: true
5899
6270
  },
5900
6271
  {
5901
6272
  pattern: /AKIA[0-9A-Z]{16}/g,
@@ -5903,11 +6274,13 @@ const SECRET_PATTERNS = [
5903
6274
  },
5904
6275
  {
5905
6276
  pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
5906
- name: "AWS Secret Key"
6277
+ name: "AWS Secret Key",
6278
+ keywordPrefixed: true
5907
6279
  },
5908
6280
  {
5909
6281
  pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
5910
- name: "Hardcoded password/secret"
6282
+ name: "Hardcoded password/secret",
6283
+ keywordPrefixed: true
5911
6284
  },
5912
6285
  {
5913
6286
  pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
@@ -5919,7 +6292,8 @@ const SECRET_PATTERNS = [
5919
6292
  },
5920
6293
  {
5921
6294
  pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
5922
- name: "Authentication token"
6295
+ name: "Authentication token",
6296
+ keywordPrefixed: true
5923
6297
  },
5924
6298
  {
5925
6299
  pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
@@ -5934,6 +6308,24 @@ const SECRET_PATTERNS = [
5934
6308
  name: "Database connection string with credentials"
5935
6309
  }
5936
6310
  ];
6311
+ const isInsideStringLiteral = (content, matchIndex) => {
6312
+ const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
6313
+ const prefix = content.slice(lineStart, matchIndex);
6314
+ let inDouble = false;
6315
+ let inSingle = false;
6316
+ let inBacktick = false;
6317
+ for (let i = 0; i < prefix.length; i++) {
6318
+ const ch = prefix[i];
6319
+ if (ch === "\\") {
6320
+ i++;
6321
+ continue;
6322
+ }
6323
+ if (ch === "\"" && !inSingle && !inBacktick) inDouble = !inDouble;
6324
+ else if (ch === "'" && !inDouble && !inBacktick) inSingle = !inSingle;
6325
+ else if (ch === "`" && !inDouble && !inSingle) inBacktick = !inBacktick;
6326
+ }
6327
+ return inDouble || inSingle || inBacktick;
6328
+ };
5937
6329
  const PLACEHOLDER_EXACT = new Set([
5938
6330
  "changeme",
5939
6331
  "password",
@@ -5969,11 +6361,12 @@ const scanSecrets = async (context) => {
5969
6361
  continue;
5970
6362
  }
5971
6363
  const relativePath = path.relative(context.rootDirectory, filePath);
5972
- for (const { pattern, name } of SECRET_PATTERNS) {
6364
+ for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
5973
6365
  const regex = new RegExp(pattern.source, pattern.flags);
5974
6366
  let match;
5975
6367
  while ((match = regex.exec(content)) !== null) {
5976
6368
  if (isPlaceholderValue(match[1] ?? match[0])) continue;
6369
+ if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
5977
6370
  const line = content.slice(0, match.index).split("\n").length;
5978
6371
  diagnostics.push({
5979
6372
  filePath: relativePath,