prodlint 0.8.1 → 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 +137 -63
- package/dist/index.js +137 -63
- package/dist/mcp.js +141 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npx prodlint
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
prodlint v0.
|
|
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;
|
|
@@ -2379,7 +2431,8 @@ var deadExportsRule = {
|
|
|
2379
2431
|
column: 1,
|
|
2380
2432
|
message: `${totalDead} exported symbols never imported \u2014 dead exports pollute AI context. Top files: ${topFiles.join(", ")}`,
|
|
2381
2433
|
severity: totalDead > 20 ? "warning" : "info",
|
|
2382
|
-
category: "ai-quality"
|
|
2434
|
+
category: "ai-quality",
|
|
2435
|
+
fix: "Remove the unused export or add a consumer"
|
|
2383
2436
|
});
|
|
2384
2437
|
}
|
|
2385
2438
|
return findings;
|
|
@@ -2443,7 +2496,8 @@ var shallowCatchRule = {
|
|
|
2443
2496
|
column: file.lines[catchLine].indexOf("catch") + 1,
|
|
2444
2497
|
message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
|
|
2445
2498
|
severity: score === 0 ? "warning" : "info",
|
|
2446
|
-
category: "reliability"
|
|
2499
|
+
category: "reliability",
|
|
2500
|
+
fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
|
|
2447
2501
|
});
|
|
2448
2502
|
}
|
|
2449
2503
|
});
|
|
@@ -2511,7 +2565,8 @@ var shallowCatchRule = {
|
|
|
2511
2565
|
column: file.lines[i].indexOf("catch") + 1,
|
|
2512
2566
|
message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
|
|
2513
2567
|
severity: score === 0 ? "warning" : "info",
|
|
2514
|
-
category: "reliability"
|
|
2568
|
+
category: "reliability",
|
|
2569
|
+
fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
|
|
2515
2570
|
});
|
|
2516
2571
|
}
|
|
2517
2572
|
i = bodyEnd;
|
|
@@ -2563,7 +2618,8 @@ var comprehensionDebtRule = {
|
|
|
2563
2618
|
column: 1,
|
|
2564
2619
|
message: `${fnName}() has ${params} parameters (max ${MAX_PARAMS}) \u2014 hard to call correctly`,
|
|
2565
2620
|
severity: "info",
|
|
2566
|
-
category: "ai-quality"
|
|
2621
|
+
category: "ai-quality",
|
|
2622
|
+
fix: "Group related parameters into an options object"
|
|
2567
2623
|
});
|
|
2568
2624
|
}
|
|
2569
2625
|
}
|
|
@@ -2590,7 +2646,8 @@ var comprehensionDebtRule = {
|
|
|
2590
2646
|
column: 1,
|
|
2591
2647
|
message: `${fnName}() is ${length} lines long (max ${MAX_FUNCTION_LENGTH}) \u2014 consider splitting`,
|
|
2592
2648
|
severity: "info",
|
|
2593
|
-
category: "ai-quality"
|
|
2649
|
+
category: "ai-quality",
|
|
2650
|
+
fix: "Break this into smaller, focused functions with clear names"
|
|
2594
2651
|
});
|
|
2595
2652
|
}
|
|
2596
2653
|
if (maxDepthInFn > MAX_NESTING_DEPTH) {
|
|
@@ -2601,7 +2658,8 @@ var comprehensionDebtRule = {
|
|
|
2601
2658
|
column: 1,
|
|
2602
2659
|
message: `${fnName}() has nesting depth ${maxDepthInFn} (max ${MAX_NESTING_DEPTH}) \u2014 flatten with early returns`,
|
|
2603
2660
|
severity: "info",
|
|
2604
|
-
category: "ai-quality"
|
|
2661
|
+
category: "ai-quality",
|
|
2662
|
+
fix: "Reduce nesting with early returns, guard clauses, or extracted helper functions"
|
|
2605
2663
|
});
|
|
2606
2664
|
}
|
|
2607
2665
|
inFunction = false;
|
|
@@ -2708,7 +2766,8 @@ var phantomDependencyRule = {
|
|
|
2708
2766
|
column: 1,
|
|
2709
2767
|
message: `"${name}" is a commonly hallucinated package name \u2014 verify it exists and is the correct package`,
|
|
2710
2768
|
severity: "warning",
|
|
2711
|
-
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"
|
|
2712
2771
|
});
|
|
2713
2772
|
}
|
|
2714
2773
|
for (const pattern of SUSPICIOUS_PATTERNS) {
|
|
@@ -2720,7 +2779,8 @@ var phantomDependencyRule = {
|
|
|
2720
2779
|
column: 1,
|
|
2721
2780
|
message: `"${name}" has a suspicious package name pattern \u2014 verify it's legitimate`,
|
|
2722
2781
|
severity: "info",
|
|
2723
|
-
category: "security"
|
|
2782
|
+
category: "security",
|
|
2783
|
+
fix: "Verify the package on npm \u2014 suspicious names may indicate typosquatting or hallucination"
|
|
2724
2784
|
});
|
|
2725
2785
|
break;
|
|
2726
2786
|
}
|
|
@@ -3473,7 +3533,8 @@ var evalInjectionRule = {
|
|
|
3473
3533
|
column: match.index + 1,
|
|
3474
3534
|
message: msg,
|
|
3475
3535
|
severity: "critical",
|
|
3476
|
-
category: "security"
|
|
3536
|
+
category: "security",
|
|
3537
|
+
fix: "Replace eval/Function constructor with a safe alternative like JSON.parse() or a sandboxed interpreter"
|
|
3477
3538
|
});
|
|
3478
3539
|
break;
|
|
3479
3540
|
}
|
|
@@ -4270,44 +4331,57 @@ var clientSideAuthOnlyRule = {
|
|
|
4270
4331
|
};
|
|
4271
4332
|
|
|
4272
4333
|
// src/rules/missing-abort-controller.ts
|
|
4273
|
-
var
|
|
4334
|
+
var HTTP_CALL = /\b(fetch|axios)\s*[.(]/;
|
|
4274
4335
|
var HAS_TIMEOUT = [
|
|
4275
4336
|
/AbortController/,
|
|
4276
4337
|
/abort/i,
|
|
4277
4338
|
/signal\s*:/,
|
|
4278
|
-
/timeout
|
|
4279
|
-
/
|
|
4339
|
+
/timeout\s*:/,
|
|
4340
|
+
/timeout\s*=/,
|
|
4341
|
+
/setTimeout.*abort/s,
|
|
4342
|
+
/axios\.create\s*\([^)]*timeout/s
|
|
4280
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
|
+
}
|
|
4281
4350
|
var missingAbortControllerRule = {
|
|
4282
4351
|
id: "missing-abort-controller",
|
|
4283
4352
|
name: "Missing Abort Controller",
|
|
4284
|
-
description: "Detects fetch calls
|
|
4353
|
+
description: "Detects fetch/axios calls without timeout or AbortController \u2014 requests can hang indefinitely",
|
|
4285
4354
|
category: "performance",
|
|
4286
4355
|
severity: "info",
|
|
4287
4356
|
fileExtensions: ["ts", "tsx", "js", "jsx"],
|
|
4288
4357
|
check(file, _project) {
|
|
4289
4358
|
if (isTestFile(file.relativePath)) return [];
|
|
4290
|
-
|
|
4291
|
-
if (!
|
|
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 [];
|
|
4292
4362
|
const hasTimeout = HAS_TIMEOUT.some((p) => p.test(file.content));
|
|
4293
4363
|
if (hasTimeout) return [];
|
|
4294
4364
|
let reportLine = 1;
|
|
4365
|
+
let matchedCall = "fetch";
|
|
4295
4366
|
for (let i = 0; i < file.lines.length; i++) {
|
|
4296
4367
|
if (isCommentLine(file.lines, i, file.commentMap)) continue;
|
|
4297
|
-
|
|
4368
|
+
const m = file.lines[i].match(HTTP_CALL);
|
|
4369
|
+
if (m) {
|
|
4298
4370
|
reportLine = i + 1;
|
|
4371
|
+
matchedCall = m[1];
|
|
4299
4372
|
break;
|
|
4300
4373
|
}
|
|
4301
4374
|
}
|
|
4375
|
+
const isFetch = matchedCall === "fetch";
|
|
4302
4376
|
return [{
|
|
4303
4377
|
ruleId: "missing-abort-controller",
|
|
4304
4378
|
file: file.relativePath,
|
|
4305
4379
|
line: reportLine,
|
|
4306
4380
|
column: 1,
|
|
4307
|
-
message:
|
|
4381
|
+
message: `${matchedCall}() without timeout or AbortController \u2014 request will hang indefinitely if the upstream server doesn't respond`,
|
|
4308
4382
|
severity: "info",
|
|
4309
4383
|
category: "performance",
|
|
4310
|
-
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 })"
|
|
4311
4385
|
}];
|
|
4312
4386
|
}
|
|
4313
4387
|
};
|