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/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;
|
|
@@ -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
|
|
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
|
|
4254
|
-
/
|
|
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
|
|
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
|
-
|
|
4266
|
-
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 [];
|
|
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
|
-
|
|
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:
|
|
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
|
};
|