prodlint 0.8.0 → 0.9.0

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
@@ -744,7 +744,8 @@ var secretsRule = {
744
744
  column: match.index + 1,
745
745
  message: `Hardcoded ${name} detected`,
746
746
  severity: "critical",
747
- category: "security"
747
+ category: "security",
748
+ fix: "Move the secret to an environment variable and access via process.env.SECRET_NAME"
748
749
  });
749
750
  }
750
751
  }
@@ -828,7 +829,8 @@ var hallucinatedImportsRule = {
828
829
  column: 1,
829
830
  message: `Package "${pkgName}" is imported but not in package.json`,
830
831
  severity: isNonProd ? "warning" : "critical",
831
- category: "reliability"
832
+ category: "reliability",
833
+ fix: "Verify the package exists on npm. The AI may have invented this package name."
832
834
  });
833
835
  }
834
836
  return findings;
@@ -856,7 +858,8 @@ var hallucinatedImportsRule = {
856
858
  column: match.index + 1,
857
859
  message: `Package "${pkgName}" is imported but not in package.json`,
858
860
  severity: isNonProd ? "warning" : "critical",
859
- category: "reliability"
861
+ category: "reliability",
862
+ fix: "Verify the package exists on npm. The AI may have invented this package name."
860
863
  });
861
864
  }
862
865
  }
@@ -939,7 +942,8 @@ var authChecksRule = {
939
942
  column: 1,
940
943
  message,
941
944
  severity,
942
- category: "security"
945
+ category: "security",
946
+ fix: "Add authentication check: const session = await auth(); if (!session) return new Response('Unauthorized', { status: 401 })"
943
947
  }];
944
948
  }
945
949
  };
@@ -978,7 +982,8 @@ var envExposureRule = {
978
982
  column: 1,
979
983
  message: ".env is not listed in .gitignore \u2014 secrets may be committed",
980
984
  severity: "critical",
981
- category: "security"
985
+ category: "security",
986
+ fix: "Add .env to .gitignore to prevent committing secrets"
982
987
  });
983
988
  }
984
989
  return findings;
@@ -1000,7 +1005,8 @@ var envExposureRule = {
1000
1005
  column: match.index + 1,
1001
1006
  message: isSensitive ? `Sensitive server env var "${envName}" used in client component` : `Server env var "${envName}" used in client component (will be undefined at runtime)`,
1002
1007
  severity: isSensitive ? "critical" : "warning",
1003
- category: "security"
1008
+ category: "security",
1009
+ fix: "Move to server-only file or use NEXT_PUBLIC_ prefix only for non-sensitive values"
1004
1010
  });
1005
1011
  }
1006
1012
  }
@@ -1038,7 +1044,8 @@ var errorHandlingRule = {
1038
1044
  column: 1,
1039
1045
  message: "API route handler has no try/catch block",
1040
1046
  severity: "warning",
1041
- category: "reliability"
1047
+ category: "reliability",
1048
+ fix: "Wrap the handler body in try/catch and return appropriate error responses"
1042
1049
  }];
1043
1050
  }
1044
1051
  };
@@ -1096,7 +1103,8 @@ var inputValidationRule = {
1096
1103
  column: 1,
1097
1104
  message: "Request body accessed without validation (consider using Zod, Yup, or similar)",
1098
1105
  severity: "warning",
1099
- category: "security"
1106
+ category: "security",
1107
+ fix: "Validate input with Zod or a similar schema library before using it"
1100
1108
  }];
1101
1109
  }
1102
1110
  };
@@ -1153,12 +1161,22 @@ var rateLimitingRule = {
1153
1161
  column: 1,
1154
1162
  message: "No rate limiting \u2014 anyone could spam this endpoint and run up your API costs",
1155
1163
  severity: "info",
1156
- category: "security"
1164
+ category: "security",
1165
+ fix: "Add rate limiting: import { Ratelimit } from '@upstash/ratelimit' or use express-rate-limit"
1157
1166
  }];
1158
1167
  }
1159
1168
  };
1160
1169
 
1161
1170
  // src/rules/cors-config.ts
1171
+ function hasCredentialsNearby(lines, startLine, commentMap) {
1172
+ const end = Math.min(lines.length, startLine + 8);
1173
+ for (let j = startLine; j < end; j++) {
1174
+ if (commentMap[j]) continue;
1175
+ if (/credentials\s*:\s*true/.test(lines[j])) return true;
1176
+ if (/Access-Control-Allow-Credentials['"]\s*[,:]\s*['"]true['"]/.test(lines[j])) return true;
1177
+ }
1178
+ return false;
1179
+ }
1162
1180
  var corsConfigRule = {
1163
1181
  id: "cors-config",
1164
1182
  name: "Permissive CORS",
@@ -1172,14 +1190,16 @@ var corsConfigRule = {
1172
1190
  if (isCommentLine(file.lines, i, file.commentMap)) continue;
1173
1191
  const line = file.lines[i];
1174
1192
  if (/['"]Access-Control-Allow-Origin['"]\s*[,:]\s*['"]\*['"]/.test(line)) {
1193
+ const withCreds = hasCredentialsNearby(file.lines, i, file.commentMap);
1175
1194
  findings.push({
1176
1195
  ruleId: "cors-config",
1177
1196
  file: file.relativePath,
1178
1197
  line: i + 1,
1179
1198
  column: line.indexOf("Access-Control") + 1,
1180
- message: 'Access-Control-Allow-Origin set to "*" allows any domain',
1181
- severity: "warning",
1182
- category: "security"
1199
+ message: withCreds ? "CORS wildcard with credentials allows any site to make authenticated requests" : 'Access-Control-Allow-Origin set to "*" allows any domain',
1200
+ severity: withCreds ? "critical" : "warning",
1201
+ category: "security",
1202
+ fix: withCreds ? "Never use credentials with wildcard origin. Set origin to specific trusted domains." : "Restrict origin to specific domains: origin: ['https://yourdomain.com']"
1183
1203
  });
1184
1204
  }
1185
1205
  if (/cors\(\s*\)/.test(line)) {
@@ -1190,18 +1210,21 @@ var corsConfigRule = {
1190
1210
  column: line.indexOf("cors(") + 1,
1191
1211
  message: "cors() called without config allows all origins",
1192
1212
  severity: "warning",
1193
- category: "security"
1213
+ category: "security",
1214
+ fix: "Pass explicit options: cors({ origin: 'https://yourdomain.com' })"
1194
1215
  });
1195
1216
  }
1196
1217
  if (/origin\s*:\s*['"]\*['"]/.test(line)) {
1218
+ const withCreds = hasCredentialsNearby(file.lines, i, file.commentMap);
1197
1219
  findings.push({
1198
1220
  ruleId: "cors-config",
1199
1221
  file: file.relativePath,
1200
1222
  line: i + 1,
1201
1223
  column: line.indexOf("origin") + 1,
1202
- message: 'CORS origin set to "*" allows any domain',
1203
- severity: "warning",
1204
- category: "security"
1224
+ message: withCreds ? "CORS wildcard with credentials allows any site to make authenticated requests" : 'CORS origin set to "*" allows any domain',
1225
+ severity: withCreds ? "critical" : "warning",
1226
+ category: "security",
1227
+ fix: withCreds ? "Never use credentials with wildcard origin. Set origin to specific trusted domains." : "Restrict origin to specific domains: origin: ['https://yourdomain.com']"
1205
1228
  });
1206
1229
  }
1207
1230
  if (/origin\s*:\s*true/.test(line)) {
@@ -1212,7 +1235,8 @@ var corsConfigRule = {
1212
1235
  column: line.indexOf("origin") + 1,
1213
1236
  message: "CORS origin set to true mirrors any requesting origin",
1214
1237
  severity: "warning",
1215
- category: "security"
1238
+ category: "security",
1239
+ fix: "Set origin to specific domains instead of reflecting the request origin"
1216
1240
  });
1217
1241
  }
1218
1242
  }
@@ -1255,7 +1279,8 @@ var aiSmellsRule = {
1255
1279
  column: line.indexOf(todoMatch[1]) + 1,
1256
1280
  message: `${todoMatch[1]} comment: ${todoMatch[2].trim() || "(no description)"}`,
1257
1281
  severity: "info",
1258
- category: "ai-quality"
1282
+ category: "ai-quality",
1283
+ fix: "Resolve the TODO/FIXME before shipping to production"
1259
1284
  });
1260
1285
  }
1261
1286
  const commentContent = trimmed.slice(2).trim();
@@ -1270,7 +1295,8 @@ var aiSmellsRule = {
1270
1295
  column: 1,
1271
1296
  message: `${COMMENTED_CODE_THRESHOLD}+ consecutive lines of commented-out code`,
1272
1297
  severity: "info",
1273
- category: "ai-quality"
1298
+ category: "ai-quality",
1299
+ fix: "Remove commented-out code \u2014 use version control to recover old code if needed"
1274
1300
  });
1275
1301
  }
1276
1302
  } else {
@@ -1287,7 +1313,8 @@ var aiSmellsRule = {
1287
1313
  column: 1,
1288
1314
  message: 'Placeholder "not implemented" function',
1289
1315
  severity: "warning",
1290
- category: "ai-quality"
1316
+ category: "ai-quality",
1317
+ fix: "Replace with a production-ready implementation or remove the function"
1291
1318
  });
1292
1319
  }
1293
1320
  if (/console\.log\s*\(/.test(line)) {
@@ -1305,7 +1332,8 @@ var aiSmellsRule = {
1305
1332
  column: 1,
1306
1333
  message: `${consoleLogCount} console.log statements (consider a proper logger)`,
1307
1334
  severity: "warning",
1308
- category: "ai-quality"
1335
+ category: "ai-quality",
1336
+ fix: "Replace console.log with a structured logger (e.g., pino, winston) or remove debug logs"
1309
1337
  });
1310
1338
  }
1311
1339
  if (anyTypeCount > ANY_TYPE_THRESHOLD) {
@@ -1316,7 +1344,8 @@ var aiSmellsRule = {
1316
1344
  column: 1,
1317
1345
  message: `${anyTypeCount} uses of "any" type (consider proper typing)`,
1318
1346
  severity: "warning",
1319
- category: "ai-quality"
1347
+ category: "ai-quality",
1348
+ fix: 'Replace "any" with specific types or use "unknown" with type narrowing'
1320
1349
  });
1321
1350
  }
1322
1351
  return findings;
@@ -1358,7 +1387,8 @@ var unsafeHtmlRule = {
1358
1387
  column: file.lines[line - 1].indexOf("dangerouslySetInnerHTML") + 1,
1359
1388
  message: "dangerouslySetInnerHTML is an XSS risk \u2014 sanitize with DOMPurify or similar",
1360
1389
  severity: "critical",
1361
- category: "security"
1390
+ category: "security",
1391
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1362
1392
  });
1363
1393
  }
1364
1394
  }
@@ -1384,7 +1414,8 @@ var unsafeHtmlRule = {
1384
1414
  column: file.lines[line - 1].indexOf("dangerouslySetInnerHTML") + 1,
1385
1415
  message: "dangerouslySetInnerHTML is an XSS risk \u2014 sanitize with DOMPurify or similar",
1386
1416
  severity: "critical",
1387
- category: "security"
1417
+ category: "security",
1418
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1388
1419
  });
1389
1420
  }
1390
1421
  }
@@ -1399,7 +1430,8 @@ var unsafeHtmlRule = {
1399
1430
  column: file.lines[line - 1].indexOf(".innerHTML") + 1,
1400
1431
  message: "Direct innerHTML assignment is an XSS risk",
1401
1432
  severity: "critical",
1402
- category: "security"
1433
+ category: "security",
1434
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1403
1435
  });
1404
1436
  }
1405
1437
  }
@@ -1427,7 +1459,8 @@ var unsafeHtmlRule = {
1427
1459
  column: line.indexOf("dangerouslySetInnerHTML") + 1,
1428
1460
  message: "dangerouslySetInnerHTML is an XSS risk \u2014 sanitize with DOMPurify or similar",
1429
1461
  severity: "critical",
1430
- category: "security"
1462
+ category: "security",
1463
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1431
1464
  });
1432
1465
  }
1433
1466
  if (/\w\.innerHTML\s*=/.test(line)) {
@@ -1438,7 +1471,8 @@ var unsafeHtmlRule = {
1438
1471
  column: line.indexOf(".innerHTML") + 1,
1439
1472
  message: "Direct innerHTML assignment is an XSS risk",
1440
1473
  severity: "critical",
1441
- category: "security"
1474
+ category: "security",
1475
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1442
1476
  });
1443
1477
  }
1444
1478
  }
@@ -1500,7 +1534,8 @@ var sqlInjectionRule = {
1500
1534
  column: 1,
1501
1535
  message,
1502
1536
  severity,
1503
- category: "security"
1537
+ category: "security",
1538
+ fix: "Use parameterized queries: db.query('SELECT * FROM users WHERE id = $1', [id])"
1504
1539
  });
1505
1540
  break;
1506
1541
  }
@@ -1548,7 +1583,8 @@ var placeholderContentRule = {
1548
1583
  column: match.index + 1,
1549
1584
  message: label,
1550
1585
  severity: "info",
1551
- category: "ai-quality"
1586
+ category: "ai-quality",
1587
+ fix: "Replace placeholder content with real production values before deploying"
1552
1588
  });
1553
1589
  break;
1554
1590
  }
@@ -1594,7 +1630,8 @@ var staleFallbackRule = {
1594
1630
  column: match.index + 1,
1595
1631
  message: `${label} \u2014 use environment variable instead`,
1596
1632
  severity: "warning",
1597
- category: "ai-quality"
1633
+ category: "ai-quality",
1634
+ fix: "Replace hardcoded URL with an environment variable: process.env.DATABASE_URL or similar"
1598
1635
  });
1599
1636
  break;
1600
1637
  }
@@ -1640,7 +1677,8 @@ var hallucinatedApiRule = {
1640
1677
  column: match.index + 1,
1641
1678
  message: fix,
1642
1679
  severity: "warning",
1643
- category: "ai-quality"
1680
+ category: "ai-quality",
1681
+ fix
1644
1682
  });
1645
1683
  }
1646
1684
  }
@@ -1722,7 +1760,8 @@ var openRedirectRule = {
1722
1760
  column: col,
1723
1761
  message: "User input in redirect \u2014 validate against an allowlist to prevent open redirect",
1724
1762
  severity: "warning",
1725
- category: "security"
1763
+ category: "security",
1764
+ fix: "Validate the redirect URL against an allowlist of trusted domains"
1726
1765
  });
1727
1766
  return;
1728
1767
  }
@@ -1734,7 +1773,8 @@ var openRedirectRule = {
1734
1773
  column: col,
1735
1774
  message: "Possible user input in redirect \u2014 verify the URL is validated before use",
1736
1775
  severity: "warning",
1737
- category: "security"
1776
+ category: "security",
1777
+ fix: "Validate the redirect URL against an allowlist of trusted domains"
1738
1778
  });
1739
1779
  }
1740
1780
  });
@@ -1755,7 +1795,8 @@ var openRedirectRule = {
1755
1795
  column: match.index + 1,
1756
1796
  message: "User input in redirect \u2014 validate against an allowlist to prevent open redirect",
1757
1797
  severity: "warning",
1758
- category: "security"
1798
+ category: "security",
1799
+ fix: "Validate the redirect URL against an allowlist of trusted domains"
1759
1800
  });
1760
1801
  break;
1761
1802
  }
@@ -1771,7 +1812,8 @@ var openRedirectRule = {
1771
1812
  column: match.index + 1,
1772
1813
  message: "Possible user input in redirect \u2014 verify the URL is validated before use",
1773
1814
  severity: "warning",
1774
- category: "security"
1815
+ category: "security",
1816
+ fix: "Validate the redirect URL against an allowlist of trusted domains"
1775
1817
  });
1776
1818
  break;
1777
1819
  }
@@ -1809,7 +1851,8 @@ var noSyncFsRule = {
1809
1851
  column: match.index + 1,
1810
1852
  message: `Synchronous ${fnName}() blocks the event loop \u2014 use async alternative`,
1811
1853
  severity,
1812
- category: "performance"
1854
+ category: "performance",
1855
+ fix: `Use the async version: fs.promises.${fnName.replace("Sync", "")}() instead of ${fnName}()`
1813
1856
  });
1814
1857
  }
1815
1858
  }
@@ -1867,7 +1910,8 @@ var noNPlusOneRule = {
1867
1910
  column: match.index + 1,
1868
1911
  message: "Database/fetch call inside loop \u2014 potential N+1 query, consider batching",
1869
1912
  severity: "warning",
1870
- category: "performance"
1913
+ category: "performance",
1914
+ fix: "Use eager loading (include/join) or batch the queries outside the loop"
1871
1915
  });
1872
1916
  break;
1873
1917
  }
@@ -1903,7 +1947,8 @@ var noDynamicImportLoopRule = {
1903
1947
  column: match.index + 1,
1904
1948
  message: "Dynamic import() inside loop \u2014 move import outside or use Promise.all",
1905
1949
  severity: "warning",
1906
- category: "performance"
1950
+ category: "performance",
1951
+ fix: "Move the dynamic import outside the loop and call it once"
1907
1952
  });
1908
1953
  }
1909
1954
  }
@@ -1935,7 +1980,8 @@ var noUnboundedQueryRule = {
1935
1980
  column: line.indexOf(".findMany") + 1,
1936
1981
  message: ".findMany() without take/limit \u2014 query may return unbounded results",
1937
1982
  severity: "warning",
1938
- category: "performance"
1983
+ category: "performance",
1984
+ fix: "Add a LIMIT clause or use pagination to prevent unbounded result sets"
1939
1985
  });
1940
1986
  continue;
1941
1987
  }
@@ -1949,7 +1995,8 @@ var noUnboundedQueryRule = {
1949
1995
  column: line.indexOf(".findMany") + 1,
1950
1996
  message: ".findMany() without take \u2014 add pagination or limit",
1951
1997
  severity: "warning",
1952
- category: "performance"
1998
+ category: "performance",
1999
+ fix: "Add a LIMIT clause or use pagination to prevent unbounded result sets"
1953
2000
  });
1954
2001
  }
1955
2002
  continue;
@@ -1965,7 +2012,8 @@ var noUnboundedQueryRule = {
1965
2012
  column: line.indexOf(".select") + 1,
1966
2013
  message: ".select('*') without .limit() or filter \u2014 add pagination or a where clause",
1967
2014
  severity: "warning",
1968
- category: "performance"
2015
+ category: "performance",
2016
+ fix: "Add a LIMIT clause or use pagination to prevent unbounded result sets"
1969
2017
  });
1970
2018
  }
1971
2019
  }
@@ -2044,7 +2092,8 @@ var unhandledPromiseRule = {
2044
2092
  column: col ? col.index + 1 : 1,
2045
2093
  message: "Async call without await, return, or assignment \u2014 promise result is lost",
2046
2094
  severity: "warning",
2047
- category: "reliability"
2095
+ category: "reliability",
2096
+ fix: "Add .catch() handler or use try/catch with await"
2048
2097
  });
2049
2098
  }
2050
2099
  return findings;
@@ -2076,7 +2125,8 @@ var missingLoadingStateRule = {
2076
2125
  column: 1,
2077
2126
  message: "useEffect with fetch but no loading/pending state \u2014 users see empty content during load",
2078
2127
  severity: "info",
2079
- category: "reliability"
2128
+ category: "reliability",
2129
+ fix: "Add a loading state: const [loading, setLoading] = useState(true) and show a spinner/skeleton while loading"
2080
2130
  }];
2081
2131
  }
2082
2132
  }
@@ -2107,7 +2157,8 @@ var missingErrorBoundaryRule = {
2107
2157
  column: 1,
2108
2158
  message: `Layout without error.tsx \u2014 errors in ${dir} will bubble up to parent error boundary`,
2109
2159
  severity: "info",
2110
- category: "reliability"
2160
+ category: "reliability",
2161
+ fix: "Add an error.tsx file in the same route segment to catch rendering errors"
2111
2162
  }];
2112
2163
  }
2113
2164
  };
@@ -2245,7 +2296,8 @@ var codebaseConsistencyRule = {
2245
2296
  column: 1,
2246
2297
  message: `${dim.label}: ${consistency}% consistent \u2014 dominant: ${dominant} (${dominantCount} files), minority: ${minorities.join(", ")}`,
2247
2298
  severity: consistency < 60 ? "warning" : "info",
2248
- category: "ai-quality"
2299
+ category: "ai-quality",
2300
+ fix: "Standardize on one pattern across the codebase for consistency"
2249
2301
  });
2250
2302
  }
2251
2303
  return findings;
@@ -2255,7 +2307,7 @@ var codebaseConsistencyRule = {
2255
2307
  // src/rules/dead-exports.ts
2256
2308
  function isEntryPoint(relativePath) {
2257
2309
  const name = relativePath.split("/").pop() ?? "";
2258
- return /^(page|layout|loading|error|not-found|route|middleware|instrumentation)\.(tsx?|jsx?)$/.test(name) || /^index\.(tsx?|jsx?)$/.test(name) || name === "global-error.tsx" || name === "global-error.jsx";
2310
+ return /^(page|layout|loading|error|not-found|route|middleware|instrumentation|opengraph-image|twitter-image|icon|apple-icon|sitemap|robots|manifest)\.(tsx?|jsx?)$/.test(name) || /^index\.(tsx?|jsx?)$/.test(name) || name === "global-error.tsx" || name === "global-error.jsx";
2259
2311
  }
2260
2312
  var THRESHOLD = 5;
2261
2313
  var deadExportsRule = {
@@ -2278,8 +2330,31 @@ var deadExportsRule = {
2278
2330
  const importedFiles = /* @__PURE__ */ new Set();
2279
2331
  for (const file of sourceFiles) {
2280
2332
  if (isEntryPoint(file.relativePath)) continue;
2333
+ let inTemplateLiteral = false;
2281
2334
  for (let i = 0; i < file.lines.length; i++) {
2282
2335
  const line = file.lines[i];
2336
+ let backtickCount = 0;
2337
+ for (let j = 0; j < line.length; j++) {
2338
+ if (line[j] === "\\") {
2339
+ j++;
2340
+ continue;
2341
+ }
2342
+ if (line[j] === "`") backtickCount++;
2343
+ }
2344
+ if (backtickCount % 2 === 1) inTemplateLiteral = !inTemplateLiteral;
2345
+ if (inTemplateLiteral && backtickCount % 2 === 0) continue;
2346
+ const exportIdx = line.indexOf("export");
2347
+ if (exportIdx >= 0) {
2348
+ let inStr = false;
2349
+ for (let j = 0; j < exportIdx; j++) {
2350
+ if (line[j] === "\\") {
2351
+ j++;
2352
+ continue;
2353
+ }
2354
+ if (line[j] === "'" || line[j] === '"' || line[j] === "`") inStr = !inStr;
2355
+ }
2356
+ if (inStr) continue;
2357
+ }
2283
2358
  let match;
2284
2359
  const namedRe = /export\s+(?:async\s+)?(?:function|const|let|class|enum)\s+(\w+)/g;
2285
2360
  while ((match = namedRe.exec(line)) !== null) {
@@ -2351,7 +2426,8 @@ var deadExportsRule = {
2351
2426
  column: 1,
2352
2427
  message: `${totalDead} exported symbols never imported \u2014 dead exports pollute AI context. Top files: ${topFiles.join(", ")}`,
2353
2428
  severity: totalDead > 20 ? "warning" : "info",
2354
- category: "ai-quality"
2429
+ category: "ai-quality",
2430
+ fix: "Remove the unused export or add a consumer"
2355
2431
  });
2356
2432
  }
2357
2433
  return findings;
@@ -2415,7 +2491,8 @@ var shallowCatchRule = {
2415
2491
  column: file.lines[catchLine].indexOf("catch") + 1,
2416
2492
  message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
2417
2493
  severity: score === 0 ? "warning" : "info",
2418
- category: "reliability"
2494
+ category: "reliability",
2495
+ fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
2419
2496
  });
2420
2497
  }
2421
2498
  });
@@ -2483,7 +2560,8 @@ var shallowCatchRule = {
2483
2560
  column: file.lines[i].indexOf("catch") + 1,
2484
2561
  message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
2485
2562
  severity: score === 0 ? "warning" : "info",
2486
- category: "reliability"
2563
+ category: "reliability",
2564
+ fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
2487
2565
  });
2488
2566
  }
2489
2567
  i = bodyEnd;
@@ -2535,7 +2613,8 @@ var comprehensionDebtRule = {
2535
2613
  column: 1,
2536
2614
  message: `${fnName}() has ${params} parameters (max ${MAX_PARAMS}) \u2014 hard to call correctly`,
2537
2615
  severity: "info",
2538
- category: "ai-quality"
2616
+ category: "ai-quality",
2617
+ fix: "Group related parameters into an options object"
2539
2618
  });
2540
2619
  }
2541
2620
  }
@@ -2562,7 +2641,8 @@ var comprehensionDebtRule = {
2562
2641
  column: 1,
2563
2642
  message: `${fnName}() is ${length} lines long (max ${MAX_FUNCTION_LENGTH}) \u2014 consider splitting`,
2564
2643
  severity: "info",
2565
- category: "ai-quality"
2644
+ category: "ai-quality",
2645
+ fix: "Break this into smaller, focused functions with clear names"
2566
2646
  });
2567
2647
  }
2568
2648
  if (maxDepthInFn > MAX_NESTING_DEPTH) {
@@ -2573,7 +2653,8 @@ var comprehensionDebtRule = {
2573
2653
  column: 1,
2574
2654
  message: `${fnName}() has nesting depth ${maxDepthInFn} (max ${MAX_NESTING_DEPTH}) \u2014 flatten with early returns`,
2575
2655
  severity: "info",
2576
- category: "ai-quality"
2656
+ category: "ai-quality",
2657
+ fix: "Reduce nesting with early returns, guard clauses, or extracted helper functions"
2577
2658
  });
2578
2659
  }
2579
2660
  inFunction = false;
@@ -2680,7 +2761,8 @@ var phantomDependencyRule = {
2680
2761
  column: 1,
2681
2762
  message: `"${name}" is a commonly hallucinated package name \u2014 verify it exists and is the correct package`,
2682
2763
  severity: "warning",
2683
- category: "security"
2764
+ category: "security",
2765
+ fix: "Add the missing package to dependencies in package.json, or remove the import if the package is hallucinated"
2684
2766
  });
2685
2767
  }
2686
2768
  for (const pattern of SUSPICIOUS_PATTERNS) {
@@ -2692,7 +2774,8 @@ var phantomDependencyRule = {
2692
2774
  column: 1,
2693
2775
  message: `"${name}" has a suspicious package name pattern \u2014 verify it's legitimate`,
2694
2776
  severity: "info",
2695
- category: "security"
2777
+ category: "security",
2778
+ fix: "Verify the package on npm \u2014 suspicious names may indicate typosquatting or hallucination"
2696
2779
  });
2697
2780
  break;
2698
2781
  }
@@ -3445,7 +3528,8 @@ var evalInjectionRule = {
3445
3528
  column: match.index + 1,
3446
3529
  message: msg,
3447
3530
  severity: "critical",
3448
- category: "security"
3531
+ category: "security",
3532
+ fix: "Replace eval/Function constructor with a safe alternative like JSON.parse() or a sandboxed interpreter"
3449
3533
  });
3450
3534
  break;
3451
3535
  }
@@ -3848,6 +3932,8 @@ var hydrationMismatchRule = {
3848
3932
  if (isClientComponent(file.content)) return [];
3849
3933
  if (!/(?:^|\/)(?:src\/)?app\//.test(file.relativePath)) return [];
3850
3934
  if (/route\.[jt]sx?$/.test(file.relativePath)) return [];
3935
+ if (/(?:^|\/)(?:src\/)?app\/(?:lib|utils|helpers|server|actions)\//.test(file.relativePath)) return [];
3936
+ if (/\.[jt]s$/.test(file.relativePath) && !/<[A-Z]/.test(file.content)) return [];
3851
3937
  const findings = [];
3852
3938
  let useEffectRanges = [];
3853
3939
  if (file.ast) {
@@ -4240,44 +4326,57 @@ var clientSideAuthOnlyRule = {
4240
4326
  };
4241
4327
 
4242
4328
  // src/rules/missing-abort-controller.ts
4243
- var FETCH_CALL = /\bfetch\s*\(/;
4329
+ var HTTP_CALL = /\b(fetch|axios)\s*[.(]/;
4244
4330
  var HAS_TIMEOUT = [
4245
4331
  /AbortController/,
4246
4332
  /abort/i,
4247
4333
  /signal\s*:/,
4248
- /timeout/i,
4249
- /setTimeout.*abort/s
4334
+ /timeout\s*:/,
4335
+ /timeout\s*=/,
4336
+ /setTimeout.*abort/s,
4337
+ /axios\.create\s*\([^)]*timeout/s
4250
4338
  ];
4339
+ var BACKGROUND_PATTERN = /\b(setInterval|cron|schedule|createWorker|bullmq|agenda|queue\.process)\b/;
4340
+ function isBackgroundFile(file) {
4341
+ if (BACKGROUND_PATTERN.test(file.content)) return true;
4342
+ const p = file.relativePath.toLowerCase();
4343
+ return /\b(cron|jobs?|workers?|queues?|tasks?|background)\b/.test(p);
4344
+ }
4251
4345
  var missingAbortControllerRule = {
4252
4346
  id: "missing-abort-controller",
4253
4347
  name: "Missing Abort Controller",
4254
- description: "Detects fetch calls in API routes without timeout or AbortController \u2014 requests can hang indefinitely",
4348
+ description: "Detects fetch/axios calls without timeout or AbortController \u2014 requests can hang indefinitely",
4255
4349
  category: "performance",
4256
4350
  severity: "info",
4257
4351
  fileExtensions: ["ts", "tsx", "js", "jsx"],
4258
4352
  check(file, _project) {
4259
4353
  if (isTestFile(file.relativePath)) return [];
4260
- if (!isApiRoute(file.relativePath) && !/['"]use server['"]/.test(file.content)) return [];
4261
- if (!FETCH_CALL.test(file.content)) return [];
4354
+ const isRelevant = isApiRoute(file.relativePath) || /['"]use server['"]/.test(file.content) || isBackgroundFile(file);
4355
+ if (!isRelevant) return [];
4356
+ if (!HTTP_CALL.test(file.content)) return [];
4262
4357
  const hasTimeout = HAS_TIMEOUT.some((p) => p.test(file.content));
4263
4358
  if (hasTimeout) return [];
4264
4359
  let reportLine = 1;
4360
+ let matchedCall = "fetch";
4265
4361
  for (let i = 0; i < file.lines.length; i++) {
4266
4362
  if (isCommentLine(file.lines, i, file.commentMap)) continue;
4267
- if (FETCH_CALL.test(file.lines[i])) {
4363
+ const m = file.lines[i].match(HTTP_CALL);
4364
+ if (m) {
4268
4365
  reportLine = i + 1;
4366
+ matchedCall = m[1];
4269
4367
  break;
4270
4368
  }
4271
4369
  }
4370
+ const isFetch = matchedCall === "fetch";
4272
4371
  return [{
4273
4372
  ruleId: "missing-abort-controller",
4274
4373
  file: file.relativePath,
4275
4374
  line: reportLine,
4276
4375
  column: 1,
4277
- message: "fetch() without timeout or AbortController \u2014 request will hang indefinitely if the upstream server doesn't respond",
4376
+ message: `${matchedCall}() without timeout or AbortController \u2014 request will hang indefinitely if the upstream server doesn't respond`,
4278
4377
  severity: "info",
4279
4378
  category: "performance",
4280
- fix: "Add a timeout: const controller = new AbortController(); setTimeout(() => controller.abort(), 10000); fetch(url, { signal: controller.signal })"
4379
+ fix: isFetch ? "Add a timeout: const controller = new AbortController(); setTimeout(() => controller.abort(), 10000); fetch(url, { signal: controller.signal })" : "Add a timeout: axios.get(url, { timeout: 10000 }) or configure in axios.create({ timeout: 10000 })"
4281
4380
  }];
4282
4381
  }
4283
4382
  };