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/README.md CHANGED
@@ -13,7 +13,7 @@ npx prodlint
13
13
  ```
14
14
 
15
15
  ```
16
- prodlint v0.8.0
16
+ prodlint v0.9.0
17
17
  Scanned 148 files · 3 critical · 5 warnings
18
18
 
19
19
  src/app/api/checkout/route.ts
@@ -72,7 +72,7 @@ npm i -g prodlint # Global install
72
72
  | `auth-checks` | API routes with no authentication |
73
73
  | `env-exposure` | `NEXT_PUBLIC_` on server-only secrets |
74
74
  | `input-validation` | Request body used without validation |
75
- | `cors-config` | `Access-Control-Allow-Origin: *` |
75
+ | `cors-config` | `Access-Control-Allow-Origin: *`, wildcard + credentials escalated to critical |
76
76
  | `unsafe-html` | `dangerouslySetInnerHTML` with user data |
77
77
  | `sql-injection` | String-interpolated SQL queries (ORM-aware) |
78
78
  | `open-redirect` | User input passed to `redirect()` |
@@ -121,7 +121,7 @@ npm i -g prodlint # Global install
121
121
  | `no-unbounded-query` | `.findMany()` / `.select('*')` with no limit |
122
122
  | `no-dynamic-import-loop` | `import()` inside loops |
123
123
  | `server-component-fetch-self` | Server components fetching their own API routes |
124
- | `missing-abort-controller` | Fetch calls without timeout or AbortController |
124
+ | `missing-abort-controller` | Fetch/axios calls without timeout or AbortController |
125
125
 
126
126
  ### AI Quality (8 rules)
127
127
 
package/dist/cli.js CHANGED
@@ -749,7 +749,8 @@ var secretsRule = {
749
749
  column: match.index + 1,
750
750
  message: `Hardcoded ${name} detected`,
751
751
  severity: "critical",
752
- category: "security"
752
+ category: "security",
753
+ fix: "Move the secret to an environment variable and access via process.env.SECRET_NAME"
753
754
  });
754
755
  }
755
756
  }
@@ -833,7 +834,8 @@ var hallucinatedImportsRule = {
833
834
  column: 1,
834
835
  message: `Package "${pkgName}" is imported but not in package.json`,
835
836
  severity: isNonProd ? "warning" : "critical",
836
- category: "reliability"
837
+ category: "reliability",
838
+ fix: "Verify the package exists on npm. The AI may have invented this package name."
837
839
  });
838
840
  }
839
841
  return findings;
@@ -861,7 +863,8 @@ var hallucinatedImportsRule = {
861
863
  column: match.index + 1,
862
864
  message: `Package "${pkgName}" is imported but not in package.json`,
863
865
  severity: isNonProd ? "warning" : "critical",
864
- category: "reliability"
866
+ category: "reliability",
867
+ fix: "Verify the package exists on npm. The AI may have invented this package name."
865
868
  });
866
869
  }
867
870
  }
@@ -944,7 +947,8 @@ var authChecksRule = {
944
947
  column: 1,
945
948
  message,
946
949
  severity,
947
- category: "security"
950
+ category: "security",
951
+ fix: "Add authentication check: const session = await auth(); if (!session) return new Response('Unauthorized', { status: 401 })"
948
952
  }];
949
953
  }
950
954
  };
@@ -983,7 +987,8 @@ var envExposureRule = {
983
987
  column: 1,
984
988
  message: ".env is not listed in .gitignore \u2014 secrets may be committed",
985
989
  severity: "critical",
986
- category: "security"
990
+ category: "security",
991
+ fix: "Add .env to .gitignore to prevent committing secrets"
987
992
  });
988
993
  }
989
994
  return findings;
@@ -1005,7 +1010,8 @@ var envExposureRule = {
1005
1010
  column: match.index + 1,
1006
1011
  message: isSensitive ? `Sensitive server env var "${envName}" used in client component` : `Server env var "${envName}" used in client component (will be undefined at runtime)`,
1007
1012
  severity: isSensitive ? "critical" : "warning",
1008
- category: "security"
1013
+ category: "security",
1014
+ fix: "Move to server-only file or use NEXT_PUBLIC_ prefix only for non-sensitive values"
1009
1015
  });
1010
1016
  }
1011
1017
  }
@@ -1043,7 +1049,8 @@ var errorHandlingRule = {
1043
1049
  column: 1,
1044
1050
  message: "API route handler has no try/catch block",
1045
1051
  severity: "warning",
1046
- category: "reliability"
1052
+ category: "reliability",
1053
+ fix: "Wrap the handler body in try/catch and return appropriate error responses"
1047
1054
  }];
1048
1055
  }
1049
1056
  };
@@ -1101,7 +1108,8 @@ var inputValidationRule = {
1101
1108
  column: 1,
1102
1109
  message: "Request body accessed without validation (consider using Zod, Yup, or similar)",
1103
1110
  severity: "warning",
1104
- category: "security"
1111
+ category: "security",
1112
+ fix: "Validate input with Zod or a similar schema library before using it"
1105
1113
  }];
1106
1114
  }
1107
1115
  };
@@ -1158,12 +1166,22 @@ var rateLimitingRule = {
1158
1166
  column: 1,
1159
1167
  message: "No rate limiting \u2014 anyone could spam this endpoint and run up your API costs",
1160
1168
  severity: "info",
1161
- category: "security"
1169
+ category: "security",
1170
+ fix: "Add rate limiting: import { Ratelimit } from '@upstash/ratelimit' or use express-rate-limit"
1162
1171
  }];
1163
1172
  }
1164
1173
  };
1165
1174
 
1166
1175
  // src/rules/cors-config.ts
1176
+ function hasCredentialsNearby(lines, startLine, commentMap) {
1177
+ const end = Math.min(lines.length, startLine + 8);
1178
+ for (let j = startLine; j < end; j++) {
1179
+ if (commentMap[j]) continue;
1180
+ if (/credentials\s*:\s*true/.test(lines[j])) return true;
1181
+ if (/Access-Control-Allow-Credentials['"]\s*[,:]\s*['"]true['"]/.test(lines[j])) return true;
1182
+ }
1183
+ return false;
1184
+ }
1167
1185
  var corsConfigRule = {
1168
1186
  id: "cors-config",
1169
1187
  name: "Permissive CORS",
@@ -1177,14 +1195,16 @@ var corsConfigRule = {
1177
1195
  if (isCommentLine(file.lines, i, file.commentMap)) continue;
1178
1196
  const line = file.lines[i];
1179
1197
  if (/['"]Access-Control-Allow-Origin['"]\s*[,:]\s*['"]\*['"]/.test(line)) {
1198
+ const withCreds = hasCredentialsNearby(file.lines, i, file.commentMap);
1180
1199
  findings.push({
1181
1200
  ruleId: "cors-config",
1182
1201
  file: file.relativePath,
1183
1202
  line: i + 1,
1184
1203
  column: line.indexOf("Access-Control") + 1,
1185
- message: 'Access-Control-Allow-Origin set to "*" allows any domain',
1186
- severity: "warning",
1187
- category: "security"
1204
+ message: withCreds ? "CORS wildcard with credentials allows any site to make authenticated requests" : 'Access-Control-Allow-Origin set to "*" allows any domain',
1205
+ severity: withCreds ? "critical" : "warning",
1206
+ category: "security",
1207
+ fix: withCreds ? "Never use credentials with wildcard origin. Set origin to specific trusted domains." : "Restrict origin to specific domains: origin: ['https://yourdomain.com']"
1188
1208
  });
1189
1209
  }
1190
1210
  if (/cors\(\s*\)/.test(line)) {
@@ -1195,18 +1215,21 @@ var corsConfigRule = {
1195
1215
  column: line.indexOf("cors(") + 1,
1196
1216
  message: "cors() called without config allows all origins",
1197
1217
  severity: "warning",
1198
- category: "security"
1218
+ category: "security",
1219
+ fix: "Pass explicit options: cors({ origin: 'https://yourdomain.com' })"
1199
1220
  });
1200
1221
  }
1201
1222
  if (/origin\s*:\s*['"]\*['"]/.test(line)) {
1223
+ const withCreds = hasCredentialsNearby(file.lines, i, file.commentMap);
1202
1224
  findings.push({
1203
1225
  ruleId: "cors-config",
1204
1226
  file: file.relativePath,
1205
1227
  line: i + 1,
1206
1228
  column: line.indexOf("origin") + 1,
1207
- message: 'CORS origin set to "*" allows any domain',
1208
- severity: "warning",
1209
- category: "security"
1229
+ message: withCreds ? "CORS wildcard with credentials allows any site to make authenticated requests" : 'CORS origin set to "*" allows any domain',
1230
+ severity: withCreds ? "critical" : "warning",
1231
+ category: "security",
1232
+ fix: withCreds ? "Never use credentials with wildcard origin. Set origin to specific trusted domains." : "Restrict origin to specific domains: origin: ['https://yourdomain.com']"
1210
1233
  });
1211
1234
  }
1212
1235
  if (/origin\s*:\s*true/.test(line)) {
@@ -1217,7 +1240,8 @@ var corsConfigRule = {
1217
1240
  column: line.indexOf("origin") + 1,
1218
1241
  message: "CORS origin set to true mirrors any requesting origin",
1219
1242
  severity: "warning",
1220
- category: "security"
1243
+ category: "security",
1244
+ fix: "Set origin to specific domains instead of reflecting the request origin"
1221
1245
  });
1222
1246
  }
1223
1247
  }
@@ -1260,7 +1284,8 @@ var aiSmellsRule = {
1260
1284
  column: line.indexOf(todoMatch[1]) + 1,
1261
1285
  message: `${todoMatch[1]} comment: ${todoMatch[2].trim() || "(no description)"}`,
1262
1286
  severity: "info",
1263
- category: "ai-quality"
1287
+ category: "ai-quality",
1288
+ fix: "Resolve the TODO/FIXME before shipping to production"
1264
1289
  });
1265
1290
  }
1266
1291
  const commentContent = trimmed.slice(2).trim();
@@ -1275,7 +1300,8 @@ var aiSmellsRule = {
1275
1300
  column: 1,
1276
1301
  message: `${COMMENTED_CODE_THRESHOLD}+ consecutive lines of commented-out code`,
1277
1302
  severity: "info",
1278
- category: "ai-quality"
1303
+ category: "ai-quality",
1304
+ fix: "Remove commented-out code \u2014 use version control to recover old code if needed"
1279
1305
  });
1280
1306
  }
1281
1307
  } else {
@@ -1292,7 +1318,8 @@ var aiSmellsRule = {
1292
1318
  column: 1,
1293
1319
  message: 'Placeholder "not implemented" function',
1294
1320
  severity: "warning",
1295
- category: "ai-quality"
1321
+ category: "ai-quality",
1322
+ fix: "Replace with a production-ready implementation or remove the function"
1296
1323
  });
1297
1324
  }
1298
1325
  if (/console\.log\s*\(/.test(line)) {
@@ -1310,7 +1337,8 @@ var aiSmellsRule = {
1310
1337
  column: 1,
1311
1338
  message: `${consoleLogCount} console.log statements (consider a proper logger)`,
1312
1339
  severity: "warning",
1313
- category: "ai-quality"
1340
+ category: "ai-quality",
1341
+ fix: "Replace console.log with a structured logger (e.g., pino, winston) or remove debug logs"
1314
1342
  });
1315
1343
  }
1316
1344
  if (anyTypeCount > ANY_TYPE_THRESHOLD) {
@@ -1321,7 +1349,8 @@ var aiSmellsRule = {
1321
1349
  column: 1,
1322
1350
  message: `${anyTypeCount} uses of "any" type (consider proper typing)`,
1323
1351
  severity: "warning",
1324
- category: "ai-quality"
1352
+ category: "ai-quality",
1353
+ fix: 'Replace "any" with specific types or use "unknown" with type narrowing'
1325
1354
  });
1326
1355
  }
1327
1356
  return findings;
@@ -1363,7 +1392,8 @@ var unsafeHtmlRule = {
1363
1392
  column: file.lines[line - 1].indexOf("dangerouslySetInnerHTML") + 1,
1364
1393
  message: "dangerouslySetInnerHTML is an XSS risk \u2014 sanitize with DOMPurify or similar",
1365
1394
  severity: "critical",
1366
- category: "security"
1395
+ category: "security",
1396
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1367
1397
  });
1368
1398
  }
1369
1399
  }
@@ -1389,7 +1419,8 @@ var unsafeHtmlRule = {
1389
1419
  column: file.lines[line - 1].indexOf("dangerouslySetInnerHTML") + 1,
1390
1420
  message: "dangerouslySetInnerHTML is an XSS risk \u2014 sanitize with DOMPurify or similar",
1391
1421
  severity: "critical",
1392
- category: "security"
1422
+ category: "security",
1423
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1393
1424
  });
1394
1425
  }
1395
1426
  }
@@ -1404,7 +1435,8 @@ var unsafeHtmlRule = {
1404
1435
  column: file.lines[line - 1].indexOf(".innerHTML") + 1,
1405
1436
  message: "Direct innerHTML assignment is an XSS risk",
1406
1437
  severity: "critical",
1407
- category: "security"
1438
+ category: "security",
1439
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1408
1440
  });
1409
1441
  }
1410
1442
  }
@@ -1432,7 +1464,8 @@ var unsafeHtmlRule = {
1432
1464
  column: line.indexOf("dangerouslySetInnerHTML") + 1,
1433
1465
  message: "dangerouslySetInnerHTML is an XSS risk \u2014 sanitize with DOMPurify or similar",
1434
1466
  severity: "critical",
1435
- category: "security"
1467
+ category: "security",
1468
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1436
1469
  });
1437
1470
  }
1438
1471
  if (/\w\.innerHTML\s*=/.test(line)) {
@@ -1443,7 +1476,8 @@ var unsafeHtmlRule = {
1443
1476
  column: line.indexOf(".innerHTML") + 1,
1444
1477
  message: "Direct innerHTML assignment is an XSS risk",
1445
1478
  severity: "critical",
1446
- category: "security"
1479
+ category: "security",
1480
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1447
1481
  });
1448
1482
  }
1449
1483
  }
@@ -1505,7 +1539,8 @@ var sqlInjectionRule = {
1505
1539
  column: 1,
1506
1540
  message,
1507
1541
  severity,
1508
- category: "security"
1542
+ category: "security",
1543
+ fix: "Use parameterized queries: db.query('SELECT * FROM users WHERE id = $1', [id])"
1509
1544
  });
1510
1545
  break;
1511
1546
  }
@@ -1553,7 +1588,8 @@ var placeholderContentRule = {
1553
1588
  column: match.index + 1,
1554
1589
  message: label,
1555
1590
  severity: "info",
1556
- category: "ai-quality"
1591
+ category: "ai-quality",
1592
+ fix: "Replace placeholder content with real production values before deploying"
1557
1593
  });
1558
1594
  break;
1559
1595
  }
@@ -1599,7 +1635,8 @@ var staleFallbackRule = {
1599
1635
  column: match.index + 1,
1600
1636
  message: `${label} \u2014 use environment variable instead`,
1601
1637
  severity: "warning",
1602
- category: "ai-quality"
1638
+ category: "ai-quality",
1639
+ fix: "Replace hardcoded URL with an environment variable: process.env.DATABASE_URL or similar"
1603
1640
  });
1604
1641
  break;
1605
1642
  }
@@ -1645,7 +1682,8 @@ var hallucinatedApiRule = {
1645
1682
  column: match.index + 1,
1646
1683
  message: fix,
1647
1684
  severity: "warning",
1648
- category: "ai-quality"
1685
+ category: "ai-quality",
1686
+ fix
1649
1687
  });
1650
1688
  }
1651
1689
  }
@@ -1727,7 +1765,8 @@ var openRedirectRule = {
1727
1765
  column: col,
1728
1766
  message: "User input in redirect \u2014 validate against an allowlist to prevent open redirect",
1729
1767
  severity: "warning",
1730
- category: "security"
1768
+ category: "security",
1769
+ fix: "Validate the redirect URL against an allowlist of trusted domains"
1731
1770
  });
1732
1771
  return;
1733
1772
  }
@@ -1739,7 +1778,8 @@ var openRedirectRule = {
1739
1778
  column: col,
1740
1779
  message: "Possible user input in redirect \u2014 verify the URL is validated before use",
1741
1780
  severity: "warning",
1742
- category: "security"
1781
+ category: "security",
1782
+ fix: "Validate the redirect URL against an allowlist of trusted domains"
1743
1783
  });
1744
1784
  }
1745
1785
  });
@@ -1760,7 +1800,8 @@ var openRedirectRule = {
1760
1800
  column: match.index + 1,
1761
1801
  message: "User input in redirect \u2014 validate against an allowlist to prevent open redirect",
1762
1802
  severity: "warning",
1763
- category: "security"
1803
+ category: "security",
1804
+ fix: "Validate the redirect URL against an allowlist of trusted domains"
1764
1805
  });
1765
1806
  break;
1766
1807
  }
@@ -1776,7 +1817,8 @@ var openRedirectRule = {
1776
1817
  column: match.index + 1,
1777
1818
  message: "Possible user input in redirect \u2014 verify the URL is validated before use",
1778
1819
  severity: "warning",
1779
- category: "security"
1820
+ category: "security",
1821
+ fix: "Validate the redirect URL against an allowlist of trusted domains"
1780
1822
  });
1781
1823
  break;
1782
1824
  }
@@ -1814,7 +1856,8 @@ var noSyncFsRule = {
1814
1856
  column: match.index + 1,
1815
1857
  message: `Synchronous ${fnName}() blocks the event loop \u2014 use async alternative`,
1816
1858
  severity,
1817
- category: "performance"
1859
+ category: "performance",
1860
+ fix: `Use the async version: fs.promises.${fnName.replace("Sync", "")}() instead of ${fnName}()`
1818
1861
  });
1819
1862
  }
1820
1863
  }
@@ -1872,7 +1915,8 @@ var noNPlusOneRule = {
1872
1915
  column: match.index + 1,
1873
1916
  message: "Database/fetch call inside loop \u2014 potential N+1 query, consider batching",
1874
1917
  severity: "warning",
1875
- category: "performance"
1918
+ category: "performance",
1919
+ fix: "Use eager loading (include/join) or batch the queries outside the loop"
1876
1920
  });
1877
1921
  break;
1878
1922
  }
@@ -1908,7 +1952,8 @@ var noDynamicImportLoopRule = {
1908
1952
  column: match.index + 1,
1909
1953
  message: "Dynamic import() inside loop \u2014 move import outside or use Promise.all",
1910
1954
  severity: "warning",
1911
- category: "performance"
1955
+ category: "performance",
1956
+ fix: "Move the dynamic import outside the loop and call it once"
1912
1957
  });
1913
1958
  }
1914
1959
  }
@@ -1940,7 +1985,8 @@ var noUnboundedQueryRule = {
1940
1985
  column: line.indexOf(".findMany") + 1,
1941
1986
  message: ".findMany() without take/limit \u2014 query may return unbounded results",
1942
1987
  severity: "warning",
1943
- category: "performance"
1988
+ category: "performance",
1989
+ fix: "Add a LIMIT clause or use pagination to prevent unbounded result sets"
1944
1990
  });
1945
1991
  continue;
1946
1992
  }
@@ -1954,7 +2000,8 @@ var noUnboundedQueryRule = {
1954
2000
  column: line.indexOf(".findMany") + 1,
1955
2001
  message: ".findMany() without take \u2014 add pagination or limit",
1956
2002
  severity: "warning",
1957
- category: "performance"
2003
+ category: "performance",
2004
+ fix: "Add a LIMIT clause or use pagination to prevent unbounded result sets"
1958
2005
  });
1959
2006
  }
1960
2007
  continue;
@@ -1970,7 +2017,8 @@ var noUnboundedQueryRule = {
1970
2017
  column: line.indexOf(".select") + 1,
1971
2018
  message: ".select('*') without .limit() or filter \u2014 add pagination or a where clause",
1972
2019
  severity: "warning",
1973
- category: "performance"
2020
+ category: "performance",
2021
+ fix: "Add a LIMIT clause or use pagination to prevent unbounded result sets"
1974
2022
  });
1975
2023
  }
1976
2024
  }
@@ -2049,7 +2097,8 @@ var unhandledPromiseRule = {
2049
2097
  column: col ? col.index + 1 : 1,
2050
2098
  message: "Async call without await, return, or assignment \u2014 promise result is lost",
2051
2099
  severity: "warning",
2052
- category: "reliability"
2100
+ category: "reliability",
2101
+ fix: "Add .catch() handler or use try/catch with await"
2053
2102
  });
2054
2103
  }
2055
2104
  return findings;
@@ -2081,7 +2130,8 @@ var missingLoadingStateRule = {
2081
2130
  column: 1,
2082
2131
  message: "useEffect with fetch but no loading/pending state \u2014 users see empty content during load",
2083
2132
  severity: "info",
2084
- category: "reliability"
2133
+ category: "reliability",
2134
+ fix: "Add a loading state: const [loading, setLoading] = useState(true) and show a spinner/skeleton while loading"
2085
2135
  }];
2086
2136
  }
2087
2137
  }
@@ -2112,7 +2162,8 @@ var missingErrorBoundaryRule = {
2112
2162
  column: 1,
2113
2163
  message: `Layout without error.tsx \u2014 errors in ${dir} will bubble up to parent error boundary`,
2114
2164
  severity: "info",
2115
- category: "reliability"
2165
+ category: "reliability",
2166
+ fix: "Add an error.tsx file in the same route segment to catch rendering errors"
2116
2167
  }];
2117
2168
  }
2118
2169
  };
@@ -2250,7 +2301,8 @@ var codebaseConsistencyRule = {
2250
2301
  column: 1,
2251
2302
  message: `${dim.label}: ${consistency}% consistent \u2014 dominant: ${dominant} (${dominantCount} files), minority: ${minorities.join(", ")}`,
2252
2303
  severity: consistency < 60 ? "warning" : "info",
2253
- category: "ai-quality"
2304
+ category: "ai-quality",
2305
+ fix: "Standardize on one pattern across the codebase for consistency"
2254
2306
  });
2255
2307
  }
2256
2308
  return findings;
@@ -2260,7 +2312,7 @@ var codebaseConsistencyRule = {
2260
2312
  // src/rules/dead-exports.ts
2261
2313
  function isEntryPoint(relativePath) {
2262
2314
  const name = relativePath.split("/").pop() ?? "";
2263
- 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";
2315
+ 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";
2264
2316
  }
2265
2317
  var THRESHOLD = 5;
2266
2318
  var deadExportsRule = {
@@ -2283,8 +2335,31 @@ var deadExportsRule = {
2283
2335
  const importedFiles = /* @__PURE__ */ new Set();
2284
2336
  for (const file of sourceFiles) {
2285
2337
  if (isEntryPoint(file.relativePath)) continue;
2338
+ let inTemplateLiteral = false;
2286
2339
  for (let i = 0; i < file.lines.length; i++) {
2287
2340
  const line = file.lines[i];
2341
+ let backtickCount = 0;
2342
+ for (let j = 0; j < line.length; j++) {
2343
+ if (line[j] === "\\") {
2344
+ j++;
2345
+ continue;
2346
+ }
2347
+ if (line[j] === "`") backtickCount++;
2348
+ }
2349
+ if (backtickCount % 2 === 1) inTemplateLiteral = !inTemplateLiteral;
2350
+ if (inTemplateLiteral && backtickCount % 2 === 0) continue;
2351
+ const exportIdx = line.indexOf("export");
2352
+ if (exportIdx >= 0) {
2353
+ let inStr = false;
2354
+ for (let j = 0; j < exportIdx; j++) {
2355
+ if (line[j] === "\\") {
2356
+ j++;
2357
+ continue;
2358
+ }
2359
+ if (line[j] === "'" || line[j] === '"' || line[j] === "`") inStr = !inStr;
2360
+ }
2361
+ if (inStr) continue;
2362
+ }
2288
2363
  let match;
2289
2364
  const namedRe = /export\s+(?:async\s+)?(?:function|const|let|class|enum)\s+(\w+)/g;
2290
2365
  while ((match = namedRe.exec(line)) !== null) {
@@ -2356,7 +2431,8 @@ var deadExportsRule = {
2356
2431
  column: 1,
2357
2432
  message: `${totalDead} exported symbols never imported \u2014 dead exports pollute AI context. Top files: ${topFiles.join(", ")}`,
2358
2433
  severity: totalDead > 20 ? "warning" : "info",
2359
- category: "ai-quality"
2434
+ category: "ai-quality",
2435
+ fix: "Remove the unused export or add a consumer"
2360
2436
  });
2361
2437
  }
2362
2438
  return findings;
@@ -2420,7 +2496,8 @@ var shallowCatchRule = {
2420
2496
  column: file.lines[catchLine].indexOf("catch") + 1,
2421
2497
  message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
2422
2498
  severity: score === 0 ? "warning" : "info",
2423
- category: "reliability"
2499
+ category: "reliability",
2500
+ fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
2424
2501
  });
2425
2502
  }
2426
2503
  });
@@ -2488,7 +2565,8 @@ var shallowCatchRule = {
2488
2565
  column: file.lines[i].indexOf("catch") + 1,
2489
2566
  message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
2490
2567
  severity: score === 0 ? "warning" : "info",
2491
- category: "reliability"
2568
+ category: "reliability",
2569
+ fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
2492
2570
  });
2493
2571
  }
2494
2572
  i = bodyEnd;
@@ -2540,7 +2618,8 @@ var comprehensionDebtRule = {
2540
2618
  column: 1,
2541
2619
  message: `${fnName}() has ${params} parameters (max ${MAX_PARAMS}) \u2014 hard to call correctly`,
2542
2620
  severity: "info",
2543
- category: "ai-quality"
2621
+ category: "ai-quality",
2622
+ fix: "Group related parameters into an options object"
2544
2623
  });
2545
2624
  }
2546
2625
  }
@@ -2567,7 +2646,8 @@ var comprehensionDebtRule = {
2567
2646
  column: 1,
2568
2647
  message: `${fnName}() is ${length} lines long (max ${MAX_FUNCTION_LENGTH}) \u2014 consider splitting`,
2569
2648
  severity: "info",
2570
- category: "ai-quality"
2649
+ category: "ai-quality",
2650
+ fix: "Break this into smaller, focused functions with clear names"
2571
2651
  });
2572
2652
  }
2573
2653
  if (maxDepthInFn > MAX_NESTING_DEPTH) {
@@ -2578,7 +2658,8 @@ var comprehensionDebtRule = {
2578
2658
  column: 1,
2579
2659
  message: `${fnName}() has nesting depth ${maxDepthInFn} (max ${MAX_NESTING_DEPTH}) \u2014 flatten with early returns`,
2580
2660
  severity: "info",
2581
- category: "ai-quality"
2661
+ category: "ai-quality",
2662
+ fix: "Reduce nesting with early returns, guard clauses, or extracted helper functions"
2582
2663
  });
2583
2664
  }
2584
2665
  inFunction = false;
@@ -2685,7 +2766,8 @@ var phantomDependencyRule = {
2685
2766
  column: 1,
2686
2767
  message: `"${name}" is a commonly hallucinated package name \u2014 verify it exists and is the correct package`,
2687
2768
  severity: "warning",
2688
- category: "security"
2769
+ category: "security",
2770
+ fix: "Add the missing package to dependencies in package.json, or remove the import if the package is hallucinated"
2689
2771
  });
2690
2772
  }
2691
2773
  for (const pattern of SUSPICIOUS_PATTERNS) {
@@ -2697,7 +2779,8 @@ var phantomDependencyRule = {
2697
2779
  column: 1,
2698
2780
  message: `"${name}" has a suspicious package name pattern \u2014 verify it's legitimate`,
2699
2781
  severity: "info",
2700
- category: "security"
2782
+ category: "security",
2783
+ fix: "Verify the package on npm \u2014 suspicious names may indicate typosquatting or hallucination"
2701
2784
  });
2702
2785
  break;
2703
2786
  }
@@ -3450,7 +3533,8 @@ var evalInjectionRule = {
3450
3533
  column: match.index + 1,
3451
3534
  message: msg,
3452
3535
  severity: "critical",
3453
- category: "security"
3536
+ category: "security",
3537
+ fix: "Replace eval/Function constructor with a safe alternative like JSON.parse() or a sandboxed interpreter"
3454
3538
  });
3455
3539
  break;
3456
3540
  }
@@ -3853,6 +3937,8 @@ var hydrationMismatchRule = {
3853
3937
  if (isClientComponent(file.content)) return [];
3854
3938
  if (!/(?:^|\/)(?:src\/)?app\//.test(file.relativePath)) return [];
3855
3939
  if (/route\.[jt]sx?$/.test(file.relativePath)) return [];
3940
+ if (/(?:^|\/)(?:src\/)?app\/(?:lib|utils|helpers|server|actions)\//.test(file.relativePath)) return [];
3941
+ if (/\.[jt]s$/.test(file.relativePath) && !/<[A-Z]/.test(file.content)) return [];
3856
3942
  const findings = [];
3857
3943
  let useEffectRanges = [];
3858
3944
  if (file.ast) {
@@ -4245,44 +4331,57 @@ var clientSideAuthOnlyRule = {
4245
4331
  };
4246
4332
 
4247
4333
  // src/rules/missing-abort-controller.ts
4248
- var FETCH_CALL = /\bfetch\s*\(/;
4334
+ var HTTP_CALL = /\b(fetch|axios)\s*[.(]/;
4249
4335
  var HAS_TIMEOUT = [
4250
4336
  /AbortController/,
4251
4337
  /abort/i,
4252
4338
  /signal\s*:/,
4253
- /timeout/i,
4254
- /setTimeout.*abort/s
4339
+ /timeout\s*:/,
4340
+ /timeout\s*=/,
4341
+ /setTimeout.*abort/s,
4342
+ /axios\.create\s*\([^)]*timeout/s
4255
4343
  ];
4344
+ var BACKGROUND_PATTERN = /\b(setInterval|cron|schedule|createWorker|bullmq|agenda|queue\.process)\b/;
4345
+ function isBackgroundFile(file) {
4346
+ if (BACKGROUND_PATTERN.test(file.content)) return true;
4347
+ const p = file.relativePath.toLowerCase();
4348
+ return /\b(cron|jobs?|workers?|queues?|tasks?|background)\b/.test(p);
4349
+ }
4256
4350
  var missingAbortControllerRule = {
4257
4351
  id: "missing-abort-controller",
4258
4352
  name: "Missing Abort Controller",
4259
- description: "Detects fetch calls in API routes without timeout or AbortController \u2014 requests can hang indefinitely",
4353
+ description: "Detects fetch/axios calls without timeout or AbortController \u2014 requests can hang indefinitely",
4260
4354
  category: "performance",
4261
4355
  severity: "info",
4262
4356
  fileExtensions: ["ts", "tsx", "js", "jsx"],
4263
4357
  check(file, _project) {
4264
4358
  if (isTestFile(file.relativePath)) return [];
4265
- if (!isApiRoute(file.relativePath) && !/['"]use server['"]/.test(file.content)) return [];
4266
- if (!FETCH_CALL.test(file.content)) return [];
4359
+ const isRelevant = isApiRoute(file.relativePath) || /['"]use server['"]/.test(file.content) || isBackgroundFile(file);
4360
+ if (!isRelevant) return [];
4361
+ if (!HTTP_CALL.test(file.content)) return [];
4267
4362
  const hasTimeout = HAS_TIMEOUT.some((p) => p.test(file.content));
4268
4363
  if (hasTimeout) return [];
4269
4364
  let reportLine = 1;
4365
+ let matchedCall = "fetch";
4270
4366
  for (let i = 0; i < file.lines.length; i++) {
4271
4367
  if (isCommentLine(file.lines, i, file.commentMap)) continue;
4272
- if (FETCH_CALL.test(file.lines[i])) {
4368
+ const m = file.lines[i].match(HTTP_CALL);
4369
+ if (m) {
4273
4370
  reportLine = i + 1;
4371
+ matchedCall = m[1];
4274
4372
  break;
4275
4373
  }
4276
4374
  }
4375
+ const isFetch = matchedCall === "fetch";
4277
4376
  return [{
4278
4377
  ruleId: "missing-abort-controller",
4279
4378
  file: file.relativePath,
4280
4379
  line: reportLine,
4281
4380
  column: 1,
4282
- message: "fetch() without timeout or AbortController \u2014 request will hang indefinitely if the upstream server doesn't respond",
4381
+ message: `${matchedCall}() without timeout or AbortController \u2014 request will hang indefinitely if the upstream server doesn't respond`,
4283
4382
  severity: "info",
4284
4383
  category: "performance",
4285
- fix: "Add a timeout: const controller = new AbortController(); setTimeout(() => controller.abort(), 10000); fetch(url, { signal: controller.signal })"
4384
+ 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 })"
4286
4385
  }];
4287
4386
  }
4288
4387
  };