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 +3 -3
- package/dist/cli.js +163 -64
- package/dist/index.js +163 -64
- package/dist/mcp.js +167 -66
- package/package.json +1 -1
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
|
|
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
|
|
4249
|
-
/
|
|
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
|
|
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
|
-
|
|
4261
|
-
if (!
|
|
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
|
-
|
|
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:
|
|
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
|
};
|