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/mcp.js
CHANGED
|
@@ -5,7 +5,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { stat as stat2 } from "fs/promises";
|
|
8
|
-
import { resolve as resolve4 } from "path";
|
|
8
|
+
import { resolve as resolve4, sep as sep2 } from "path";
|
|
9
9
|
|
|
10
10
|
// src/utils/file-walker.ts
|
|
11
11
|
import fg from "fast-glob";
|
|
@@ -753,7 +753,8 @@ var secretsRule = {
|
|
|
753
753
|
column: match.index + 1,
|
|
754
754
|
message: `Hardcoded ${name} detected`,
|
|
755
755
|
severity: "critical",
|
|
756
|
-
category: "security"
|
|
756
|
+
category: "security",
|
|
757
|
+
fix: "Move the secret to an environment variable and access via process.env.SECRET_NAME"
|
|
757
758
|
});
|
|
758
759
|
}
|
|
759
760
|
}
|
|
@@ -837,7 +838,8 @@ var hallucinatedImportsRule = {
|
|
|
837
838
|
column: 1,
|
|
838
839
|
message: `Package "${pkgName}" is imported but not in package.json`,
|
|
839
840
|
severity: isNonProd ? "warning" : "critical",
|
|
840
|
-
category: "reliability"
|
|
841
|
+
category: "reliability",
|
|
842
|
+
fix: "Verify the package exists on npm. The AI may have invented this package name."
|
|
841
843
|
});
|
|
842
844
|
}
|
|
843
845
|
return findings;
|
|
@@ -865,7 +867,8 @@ var hallucinatedImportsRule = {
|
|
|
865
867
|
column: match.index + 1,
|
|
866
868
|
message: `Package "${pkgName}" is imported but not in package.json`,
|
|
867
869
|
severity: isNonProd ? "warning" : "critical",
|
|
868
|
-
category: "reliability"
|
|
870
|
+
category: "reliability",
|
|
871
|
+
fix: "Verify the package exists on npm. The AI may have invented this package name."
|
|
869
872
|
});
|
|
870
873
|
}
|
|
871
874
|
}
|
|
@@ -948,7 +951,8 @@ var authChecksRule = {
|
|
|
948
951
|
column: 1,
|
|
949
952
|
message,
|
|
950
953
|
severity,
|
|
951
|
-
category: "security"
|
|
954
|
+
category: "security",
|
|
955
|
+
fix: "Add authentication check: const session = await auth(); if (!session) return new Response('Unauthorized', { status: 401 })"
|
|
952
956
|
}];
|
|
953
957
|
}
|
|
954
958
|
};
|
|
@@ -987,7 +991,8 @@ var envExposureRule = {
|
|
|
987
991
|
column: 1,
|
|
988
992
|
message: ".env is not listed in .gitignore \u2014 secrets may be committed",
|
|
989
993
|
severity: "critical",
|
|
990
|
-
category: "security"
|
|
994
|
+
category: "security",
|
|
995
|
+
fix: "Add .env to .gitignore to prevent committing secrets"
|
|
991
996
|
});
|
|
992
997
|
}
|
|
993
998
|
return findings;
|
|
@@ -1009,7 +1014,8 @@ var envExposureRule = {
|
|
|
1009
1014
|
column: match.index + 1,
|
|
1010
1015
|
message: isSensitive ? `Sensitive server env var "${envName}" used in client component` : `Server env var "${envName}" used in client component (will be undefined at runtime)`,
|
|
1011
1016
|
severity: isSensitive ? "critical" : "warning",
|
|
1012
|
-
category: "security"
|
|
1017
|
+
category: "security",
|
|
1018
|
+
fix: "Move to server-only file or use NEXT_PUBLIC_ prefix only for non-sensitive values"
|
|
1013
1019
|
});
|
|
1014
1020
|
}
|
|
1015
1021
|
}
|
|
@@ -1047,7 +1053,8 @@ var errorHandlingRule = {
|
|
|
1047
1053
|
column: 1,
|
|
1048
1054
|
message: "API route handler has no try/catch block",
|
|
1049
1055
|
severity: "warning",
|
|
1050
|
-
category: "reliability"
|
|
1056
|
+
category: "reliability",
|
|
1057
|
+
fix: "Wrap the handler body in try/catch and return appropriate error responses"
|
|
1051
1058
|
}];
|
|
1052
1059
|
}
|
|
1053
1060
|
};
|
|
@@ -1105,7 +1112,8 @@ var inputValidationRule = {
|
|
|
1105
1112
|
column: 1,
|
|
1106
1113
|
message: "Request body accessed without validation (consider using Zod, Yup, or similar)",
|
|
1107
1114
|
severity: "warning",
|
|
1108
|
-
category: "security"
|
|
1115
|
+
category: "security",
|
|
1116
|
+
fix: "Validate input with Zod or a similar schema library before using it"
|
|
1109
1117
|
}];
|
|
1110
1118
|
}
|
|
1111
1119
|
};
|
|
@@ -1162,12 +1170,22 @@ var rateLimitingRule = {
|
|
|
1162
1170
|
column: 1,
|
|
1163
1171
|
message: "No rate limiting \u2014 anyone could spam this endpoint and run up your API costs",
|
|
1164
1172
|
severity: "info",
|
|
1165
|
-
category: "security"
|
|
1173
|
+
category: "security",
|
|
1174
|
+
fix: "Add rate limiting: import { Ratelimit } from '@upstash/ratelimit' or use express-rate-limit"
|
|
1166
1175
|
}];
|
|
1167
1176
|
}
|
|
1168
1177
|
};
|
|
1169
1178
|
|
|
1170
1179
|
// src/rules/cors-config.ts
|
|
1180
|
+
function hasCredentialsNearby(lines, startLine, commentMap) {
|
|
1181
|
+
const end = Math.min(lines.length, startLine + 8);
|
|
1182
|
+
for (let j = startLine; j < end; j++) {
|
|
1183
|
+
if (commentMap[j]) continue;
|
|
1184
|
+
if (/credentials\s*:\s*true/.test(lines[j])) return true;
|
|
1185
|
+
if (/Access-Control-Allow-Credentials['"]\s*[,:]\s*['"]true['"]/.test(lines[j])) return true;
|
|
1186
|
+
}
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1171
1189
|
var corsConfigRule = {
|
|
1172
1190
|
id: "cors-config",
|
|
1173
1191
|
name: "Permissive CORS",
|
|
@@ -1181,14 +1199,16 @@ var corsConfigRule = {
|
|
|
1181
1199
|
if (isCommentLine(file.lines, i, file.commentMap)) continue;
|
|
1182
1200
|
const line = file.lines[i];
|
|
1183
1201
|
if (/['"]Access-Control-Allow-Origin['"]\s*[,:]\s*['"]\*['"]/.test(line)) {
|
|
1202
|
+
const withCreds = hasCredentialsNearby(file.lines, i, file.commentMap);
|
|
1184
1203
|
findings.push({
|
|
1185
1204
|
ruleId: "cors-config",
|
|
1186
1205
|
file: file.relativePath,
|
|
1187
1206
|
line: i + 1,
|
|
1188
1207
|
column: line.indexOf("Access-Control") + 1,
|
|
1189
|
-
message: 'Access-Control-Allow-Origin set to "*" allows any domain',
|
|
1190
|
-
severity: "warning",
|
|
1191
|
-
category: "security"
|
|
1208
|
+
message: withCreds ? "CORS wildcard with credentials allows any site to make authenticated requests" : 'Access-Control-Allow-Origin set to "*" allows any domain',
|
|
1209
|
+
severity: withCreds ? "critical" : "warning",
|
|
1210
|
+
category: "security",
|
|
1211
|
+
fix: withCreds ? "Never use credentials with wildcard origin. Set origin to specific trusted domains." : "Restrict origin to specific domains: origin: ['https://yourdomain.com']"
|
|
1192
1212
|
});
|
|
1193
1213
|
}
|
|
1194
1214
|
if (/cors\(\s*\)/.test(line)) {
|
|
@@ -1199,18 +1219,21 @@ var corsConfigRule = {
|
|
|
1199
1219
|
column: line.indexOf("cors(") + 1,
|
|
1200
1220
|
message: "cors() called without config allows all origins",
|
|
1201
1221
|
severity: "warning",
|
|
1202
|
-
category: "security"
|
|
1222
|
+
category: "security",
|
|
1223
|
+
fix: "Pass explicit options: cors({ origin: 'https://yourdomain.com' })"
|
|
1203
1224
|
});
|
|
1204
1225
|
}
|
|
1205
1226
|
if (/origin\s*:\s*['"]\*['"]/.test(line)) {
|
|
1227
|
+
const withCreds = hasCredentialsNearby(file.lines, i, file.commentMap);
|
|
1206
1228
|
findings.push({
|
|
1207
1229
|
ruleId: "cors-config",
|
|
1208
1230
|
file: file.relativePath,
|
|
1209
1231
|
line: i + 1,
|
|
1210
1232
|
column: line.indexOf("origin") + 1,
|
|
1211
|
-
message: 'CORS origin set to "*" allows any domain',
|
|
1212
|
-
severity: "warning",
|
|
1213
|
-
category: "security"
|
|
1233
|
+
message: withCreds ? "CORS wildcard with credentials allows any site to make authenticated requests" : 'CORS origin set to "*" allows any domain',
|
|
1234
|
+
severity: withCreds ? "critical" : "warning",
|
|
1235
|
+
category: "security",
|
|
1236
|
+
fix: withCreds ? "Never use credentials with wildcard origin. Set origin to specific trusted domains." : "Restrict origin to specific domains: origin: ['https://yourdomain.com']"
|
|
1214
1237
|
});
|
|
1215
1238
|
}
|
|
1216
1239
|
if (/origin\s*:\s*true/.test(line)) {
|
|
@@ -1221,7 +1244,8 @@ var corsConfigRule = {
|
|
|
1221
1244
|
column: line.indexOf("origin") + 1,
|
|
1222
1245
|
message: "CORS origin set to true mirrors any requesting origin",
|
|
1223
1246
|
severity: "warning",
|
|
1224
|
-
category: "security"
|
|
1247
|
+
category: "security",
|
|
1248
|
+
fix: "Set origin to specific domains instead of reflecting the request origin"
|
|
1225
1249
|
});
|
|
1226
1250
|
}
|
|
1227
1251
|
}
|
|
@@ -1264,7 +1288,8 @@ var aiSmellsRule = {
|
|
|
1264
1288
|
column: line.indexOf(todoMatch[1]) + 1,
|
|
1265
1289
|
message: `${todoMatch[1]} comment: ${todoMatch[2].trim() || "(no description)"}`,
|
|
1266
1290
|
severity: "info",
|
|
1267
|
-
category: "ai-quality"
|
|
1291
|
+
category: "ai-quality",
|
|
1292
|
+
fix: "Resolve the TODO/FIXME before shipping to production"
|
|
1268
1293
|
});
|
|
1269
1294
|
}
|
|
1270
1295
|
const commentContent = trimmed.slice(2).trim();
|
|
@@ -1279,7 +1304,8 @@ var aiSmellsRule = {
|
|
|
1279
1304
|
column: 1,
|
|
1280
1305
|
message: `${COMMENTED_CODE_THRESHOLD}+ consecutive lines of commented-out code`,
|
|
1281
1306
|
severity: "info",
|
|
1282
|
-
category: "ai-quality"
|
|
1307
|
+
category: "ai-quality",
|
|
1308
|
+
fix: "Remove commented-out code \u2014 use version control to recover old code if needed"
|
|
1283
1309
|
});
|
|
1284
1310
|
}
|
|
1285
1311
|
} else {
|
|
@@ -1296,7 +1322,8 @@ var aiSmellsRule = {
|
|
|
1296
1322
|
column: 1,
|
|
1297
1323
|
message: 'Placeholder "not implemented" function',
|
|
1298
1324
|
severity: "warning",
|
|
1299
|
-
category: "ai-quality"
|
|
1325
|
+
category: "ai-quality",
|
|
1326
|
+
fix: "Replace with a production-ready implementation or remove the function"
|
|
1300
1327
|
});
|
|
1301
1328
|
}
|
|
1302
1329
|
if (/console\.log\s*\(/.test(line)) {
|
|
@@ -1314,7 +1341,8 @@ var aiSmellsRule = {
|
|
|
1314
1341
|
column: 1,
|
|
1315
1342
|
message: `${consoleLogCount} console.log statements (consider a proper logger)`,
|
|
1316
1343
|
severity: "warning",
|
|
1317
|
-
category: "ai-quality"
|
|
1344
|
+
category: "ai-quality",
|
|
1345
|
+
fix: "Replace console.log with a structured logger (e.g., pino, winston) or remove debug logs"
|
|
1318
1346
|
});
|
|
1319
1347
|
}
|
|
1320
1348
|
if (anyTypeCount > ANY_TYPE_THRESHOLD) {
|
|
@@ -1325,7 +1353,8 @@ var aiSmellsRule = {
|
|
|
1325
1353
|
column: 1,
|
|
1326
1354
|
message: `${anyTypeCount} uses of "any" type (consider proper typing)`,
|
|
1327
1355
|
severity: "warning",
|
|
1328
|
-
category: "ai-quality"
|
|
1356
|
+
category: "ai-quality",
|
|
1357
|
+
fix: 'Replace "any" with specific types or use "unknown" with type narrowing'
|
|
1329
1358
|
});
|
|
1330
1359
|
}
|
|
1331
1360
|
return findings;
|
|
@@ -1367,7 +1396,8 @@ var unsafeHtmlRule = {
|
|
|
1367
1396
|
column: file.lines[line - 1].indexOf("dangerouslySetInnerHTML") + 1,
|
|
1368
1397
|
message: "dangerouslySetInnerHTML is an XSS risk \u2014 sanitize with DOMPurify or similar",
|
|
1369
1398
|
severity: "critical",
|
|
1370
|
-
category: "security"
|
|
1399
|
+
category: "security",
|
|
1400
|
+
fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
|
|
1371
1401
|
});
|
|
1372
1402
|
}
|
|
1373
1403
|
}
|
|
@@ -1393,7 +1423,8 @@ var unsafeHtmlRule = {
|
|
|
1393
1423
|
column: file.lines[line - 1].indexOf("dangerouslySetInnerHTML") + 1,
|
|
1394
1424
|
message: "dangerouslySetInnerHTML is an XSS risk \u2014 sanitize with DOMPurify or similar",
|
|
1395
1425
|
severity: "critical",
|
|
1396
|
-
category: "security"
|
|
1426
|
+
category: "security",
|
|
1427
|
+
fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
|
|
1397
1428
|
});
|
|
1398
1429
|
}
|
|
1399
1430
|
}
|
|
@@ -1408,7 +1439,8 @@ var unsafeHtmlRule = {
|
|
|
1408
1439
|
column: file.lines[line - 1].indexOf(".innerHTML") + 1,
|
|
1409
1440
|
message: "Direct innerHTML assignment is an XSS risk",
|
|
1410
1441
|
severity: "critical",
|
|
1411
|
-
category: "security"
|
|
1442
|
+
category: "security",
|
|
1443
|
+
fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
|
|
1412
1444
|
});
|
|
1413
1445
|
}
|
|
1414
1446
|
}
|
|
@@ -1436,7 +1468,8 @@ var unsafeHtmlRule = {
|
|
|
1436
1468
|
column: line.indexOf("dangerouslySetInnerHTML") + 1,
|
|
1437
1469
|
message: "dangerouslySetInnerHTML is an XSS risk \u2014 sanitize with DOMPurify or similar",
|
|
1438
1470
|
severity: "critical",
|
|
1439
|
-
category: "security"
|
|
1471
|
+
category: "security",
|
|
1472
|
+
fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
|
|
1440
1473
|
});
|
|
1441
1474
|
}
|
|
1442
1475
|
if (/\w\.innerHTML\s*=/.test(line)) {
|
|
@@ -1447,7 +1480,8 @@ var unsafeHtmlRule = {
|
|
|
1447
1480
|
column: line.indexOf(".innerHTML") + 1,
|
|
1448
1481
|
message: "Direct innerHTML assignment is an XSS risk",
|
|
1449
1482
|
severity: "critical",
|
|
1450
|
-
category: "security"
|
|
1483
|
+
category: "security",
|
|
1484
|
+
fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
|
|
1451
1485
|
});
|
|
1452
1486
|
}
|
|
1453
1487
|
}
|
|
@@ -1509,7 +1543,8 @@ var sqlInjectionRule = {
|
|
|
1509
1543
|
column: 1,
|
|
1510
1544
|
message,
|
|
1511
1545
|
severity,
|
|
1512
|
-
category: "security"
|
|
1546
|
+
category: "security",
|
|
1547
|
+
fix: "Use parameterized queries: db.query('SELECT * FROM users WHERE id = $1', [id])"
|
|
1513
1548
|
});
|
|
1514
1549
|
break;
|
|
1515
1550
|
}
|
|
@@ -1557,7 +1592,8 @@ var placeholderContentRule = {
|
|
|
1557
1592
|
column: match.index + 1,
|
|
1558
1593
|
message: label,
|
|
1559
1594
|
severity: "info",
|
|
1560
|
-
category: "ai-quality"
|
|
1595
|
+
category: "ai-quality",
|
|
1596
|
+
fix: "Replace placeholder content with real production values before deploying"
|
|
1561
1597
|
});
|
|
1562
1598
|
break;
|
|
1563
1599
|
}
|
|
@@ -1603,7 +1639,8 @@ var staleFallbackRule = {
|
|
|
1603
1639
|
column: match.index + 1,
|
|
1604
1640
|
message: `${label} \u2014 use environment variable instead`,
|
|
1605
1641
|
severity: "warning",
|
|
1606
|
-
category: "ai-quality"
|
|
1642
|
+
category: "ai-quality",
|
|
1643
|
+
fix: "Replace hardcoded URL with an environment variable: process.env.DATABASE_URL or similar"
|
|
1607
1644
|
});
|
|
1608
1645
|
break;
|
|
1609
1646
|
}
|
|
@@ -1649,7 +1686,8 @@ var hallucinatedApiRule = {
|
|
|
1649
1686
|
column: match.index + 1,
|
|
1650
1687
|
message: fix,
|
|
1651
1688
|
severity: "warning",
|
|
1652
|
-
category: "ai-quality"
|
|
1689
|
+
category: "ai-quality",
|
|
1690
|
+
fix
|
|
1653
1691
|
});
|
|
1654
1692
|
}
|
|
1655
1693
|
}
|
|
@@ -1731,7 +1769,8 @@ var openRedirectRule = {
|
|
|
1731
1769
|
column: col,
|
|
1732
1770
|
message: "User input in redirect \u2014 validate against an allowlist to prevent open redirect",
|
|
1733
1771
|
severity: "warning",
|
|
1734
|
-
category: "security"
|
|
1772
|
+
category: "security",
|
|
1773
|
+
fix: "Validate the redirect URL against an allowlist of trusted domains"
|
|
1735
1774
|
});
|
|
1736
1775
|
return;
|
|
1737
1776
|
}
|
|
@@ -1743,7 +1782,8 @@ var openRedirectRule = {
|
|
|
1743
1782
|
column: col,
|
|
1744
1783
|
message: "Possible user input in redirect \u2014 verify the URL is validated before use",
|
|
1745
1784
|
severity: "warning",
|
|
1746
|
-
category: "security"
|
|
1785
|
+
category: "security",
|
|
1786
|
+
fix: "Validate the redirect URL against an allowlist of trusted domains"
|
|
1747
1787
|
});
|
|
1748
1788
|
}
|
|
1749
1789
|
});
|
|
@@ -1764,7 +1804,8 @@ var openRedirectRule = {
|
|
|
1764
1804
|
column: match.index + 1,
|
|
1765
1805
|
message: "User input in redirect \u2014 validate against an allowlist to prevent open redirect",
|
|
1766
1806
|
severity: "warning",
|
|
1767
|
-
category: "security"
|
|
1807
|
+
category: "security",
|
|
1808
|
+
fix: "Validate the redirect URL against an allowlist of trusted domains"
|
|
1768
1809
|
});
|
|
1769
1810
|
break;
|
|
1770
1811
|
}
|
|
@@ -1780,7 +1821,8 @@ var openRedirectRule = {
|
|
|
1780
1821
|
column: match.index + 1,
|
|
1781
1822
|
message: "Possible user input in redirect \u2014 verify the URL is validated before use",
|
|
1782
1823
|
severity: "warning",
|
|
1783
|
-
category: "security"
|
|
1824
|
+
category: "security",
|
|
1825
|
+
fix: "Validate the redirect URL against an allowlist of trusted domains"
|
|
1784
1826
|
});
|
|
1785
1827
|
break;
|
|
1786
1828
|
}
|
|
@@ -1818,7 +1860,8 @@ var noSyncFsRule = {
|
|
|
1818
1860
|
column: match.index + 1,
|
|
1819
1861
|
message: `Synchronous ${fnName}() blocks the event loop \u2014 use async alternative`,
|
|
1820
1862
|
severity,
|
|
1821
|
-
category: "performance"
|
|
1863
|
+
category: "performance",
|
|
1864
|
+
fix: `Use the async version: fs.promises.${fnName.replace("Sync", "")}() instead of ${fnName}()`
|
|
1822
1865
|
});
|
|
1823
1866
|
}
|
|
1824
1867
|
}
|
|
@@ -1876,7 +1919,8 @@ var noNPlusOneRule = {
|
|
|
1876
1919
|
column: match.index + 1,
|
|
1877
1920
|
message: "Database/fetch call inside loop \u2014 potential N+1 query, consider batching",
|
|
1878
1921
|
severity: "warning",
|
|
1879
|
-
category: "performance"
|
|
1922
|
+
category: "performance",
|
|
1923
|
+
fix: "Use eager loading (include/join) or batch the queries outside the loop"
|
|
1880
1924
|
});
|
|
1881
1925
|
break;
|
|
1882
1926
|
}
|
|
@@ -1912,7 +1956,8 @@ var noDynamicImportLoopRule = {
|
|
|
1912
1956
|
column: match.index + 1,
|
|
1913
1957
|
message: "Dynamic import() inside loop \u2014 move import outside or use Promise.all",
|
|
1914
1958
|
severity: "warning",
|
|
1915
|
-
category: "performance"
|
|
1959
|
+
category: "performance",
|
|
1960
|
+
fix: "Move the dynamic import outside the loop and call it once"
|
|
1916
1961
|
});
|
|
1917
1962
|
}
|
|
1918
1963
|
}
|
|
@@ -1944,7 +1989,8 @@ var noUnboundedQueryRule = {
|
|
|
1944
1989
|
column: line.indexOf(".findMany") + 1,
|
|
1945
1990
|
message: ".findMany() without take/limit \u2014 query may return unbounded results",
|
|
1946
1991
|
severity: "warning",
|
|
1947
|
-
category: "performance"
|
|
1992
|
+
category: "performance",
|
|
1993
|
+
fix: "Add a LIMIT clause or use pagination to prevent unbounded result sets"
|
|
1948
1994
|
});
|
|
1949
1995
|
continue;
|
|
1950
1996
|
}
|
|
@@ -1958,7 +2004,8 @@ var noUnboundedQueryRule = {
|
|
|
1958
2004
|
column: line.indexOf(".findMany") + 1,
|
|
1959
2005
|
message: ".findMany() without take \u2014 add pagination or limit",
|
|
1960
2006
|
severity: "warning",
|
|
1961
|
-
category: "performance"
|
|
2007
|
+
category: "performance",
|
|
2008
|
+
fix: "Add a LIMIT clause or use pagination to prevent unbounded result sets"
|
|
1962
2009
|
});
|
|
1963
2010
|
}
|
|
1964
2011
|
continue;
|
|
@@ -1974,7 +2021,8 @@ var noUnboundedQueryRule = {
|
|
|
1974
2021
|
column: line.indexOf(".select") + 1,
|
|
1975
2022
|
message: ".select('*') without .limit() or filter \u2014 add pagination or a where clause",
|
|
1976
2023
|
severity: "warning",
|
|
1977
|
-
category: "performance"
|
|
2024
|
+
category: "performance",
|
|
2025
|
+
fix: "Add a LIMIT clause or use pagination to prevent unbounded result sets"
|
|
1978
2026
|
});
|
|
1979
2027
|
}
|
|
1980
2028
|
}
|
|
@@ -2053,7 +2101,8 @@ var unhandledPromiseRule = {
|
|
|
2053
2101
|
column: col ? col.index + 1 : 1,
|
|
2054
2102
|
message: "Async call without await, return, or assignment \u2014 promise result is lost",
|
|
2055
2103
|
severity: "warning",
|
|
2056
|
-
category: "reliability"
|
|
2104
|
+
category: "reliability",
|
|
2105
|
+
fix: "Add .catch() handler or use try/catch with await"
|
|
2057
2106
|
});
|
|
2058
2107
|
}
|
|
2059
2108
|
return findings;
|
|
@@ -2085,7 +2134,8 @@ var missingLoadingStateRule = {
|
|
|
2085
2134
|
column: 1,
|
|
2086
2135
|
message: "useEffect with fetch but no loading/pending state \u2014 users see empty content during load",
|
|
2087
2136
|
severity: "info",
|
|
2088
|
-
category: "reliability"
|
|
2137
|
+
category: "reliability",
|
|
2138
|
+
fix: "Add a loading state: const [loading, setLoading] = useState(true) and show a spinner/skeleton while loading"
|
|
2089
2139
|
}];
|
|
2090
2140
|
}
|
|
2091
2141
|
}
|
|
@@ -2116,7 +2166,8 @@ var missingErrorBoundaryRule = {
|
|
|
2116
2166
|
column: 1,
|
|
2117
2167
|
message: `Layout without error.tsx \u2014 errors in ${dir} will bubble up to parent error boundary`,
|
|
2118
2168
|
severity: "info",
|
|
2119
|
-
category: "reliability"
|
|
2169
|
+
category: "reliability",
|
|
2170
|
+
fix: "Add an error.tsx file in the same route segment to catch rendering errors"
|
|
2120
2171
|
}];
|
|
2121
2172
|
}
|
|
2122
2173
|
};
|
|
@@ -2254,7 +2305,8 @@ var codebaseConsistencyRule = {
|
|
|
2254
2305
|
column: 1,
|
|
2255
2306
|
message: `${dim.label}: ${consistency}% consistent \u2014 dominant: ${dominant} (${dominantCount} files), minority: ${minorities.join(", ")}`,
|
|
2256
2307
|
severity: consistency < 60 ? "warning" : "info",
|
|
2257
|
-
category: "ai-quality"
|
|
2308
|
+
category: "ai-quality",
|
|
2309
|
+
fix: "Standardize on one pattern across the codebase for consistency"
|
|
2258
2310
|
});
|
|
2259
2311
|
}
|
|
2260
2312
|
return findings;
|
|
@@ -2264,7 +2316,7 @@ var codebaseConsistencyRule = {
|
|
|
2264
2316
|
// src/rules/dead-exports.ts
|
|
2265
2317
|
function isEntryPoint(relativePath) {
|
|
2266
2318
|
const name = relativePath.split("/").pop() ?? "";
|
|
2267
|
-
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";
|
|
2319
|
+
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";
|
|
2268
2320
|
}
|
|
2269
2321
|
var THRESHOLD = 5;
|
|
2270
2322
|
var deadExportsRule = {
|
|
@@ -2287,8 +2339,31 @@ var deadExportsRule = {
|
|
|
2287
2339
|
const importedFiles = /* @__PURE__ */ new Set();
|
|
2288
2340
|
for (const file of sourceFiles) {
|
|
2289
2341
|
if (isEntryPoint(file.relativePath)) continue;
|
|
2342
|
+
let inTemplateLiteral = false;
|
|
2290
2343
|
for (let i = 0; i < file.lines.length; i++) {
|
|
2291
2344
|
const line = file.lines[i];
|
|
2345
|
+
let backtickCount = 0;
|
|
2346
|
+
for (let j = 0; j < line.length; j++) {
|
|
2347
|
+
if (line[j] === "\\") {
|
|
2348
|
+
j++;
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
if (line[j] === "`") backtickCount++;
|
|
2352
|
+
}
|
|
2353
|
+
if (backtickCount % 2 === 1) inTemplateLiteral = !inTemplateLiteral;
|
|
2354
|
+
if (inTemplateLiteral && backtickCount % 2 === 0) continue;
|
|
2355
|
+
const exportIdx = line.indexOf("export");
|
|
2356
|
+
if (exportIdx >= 0) {
|
|
2357
|
+
let inStr = false;
|
|
2358
|
+
for (let j = 0; j < exportIdx; j++) {
|
|
2359
|
+
if (line[j] === "\\") {
|
|
2360
|
+
j++;
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
if (line[j] === "'" || line[j] === '"' || line[j] === "`") inStr = !inStr;
|
|
2364
|
+
}
|
|
2365
|
+
if (inStr) continue;
|
|
2366
|
+
}
|
|
2292
2367
|
let match;
|
|
2293
2368
|
const namedRe = /export\s+(?:async\s+)?(?:function|const|let|class|enum)\s+(\w+)/g;
|
|
2294
2369
|
while ((match = namedRe.exec(line)) !== null) {
|
|
@@ -2360,7 +2435,8 @@ var deadExportsRule = {
|
|
|
2360
2435
|
column: 1,
|
|
2361
2436
|
message: `${totalDead} exported symbols never imported \u2014 dead exports pollute AI context. Top files: ${topFiles.join(", ")}`,
|
|
2362
2437
|
severity: totalDead > 20 ? "warning" : "info",
|
|
2363
|
-
category: "ai-quality"
|
|
2438
|
+
category: "ai-quality",
|
|
2439
|
+
fix: "Remove the unused export or add a consumer"
|
|
2364
2440
|
});
|
|
2365
2441
|
}
|
|
2366
2442
|
return findings;
|
|
@@ -2424,7 +2500,8 @@ var shallowCatchRule = {
|
|
|
2424
2500
|
column: file.lines[catchLine].indexOf("catch") + 1,
|
|
2425
2501
|
message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
|
|
2426
2502
|
severity: score === 0 ? "warning" : "info",
|
|
2427
|
-
category: "reliability"
|
|
2503
|
+
category: "reliability",
|
|
2504
|
+
fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
|
|
2428
2505
|
});
|
|
2429
2506
|
}
|
|
2430
2507
|
});
|
|
@@ -2492,7 +2569,8 @@ var shallowCatchRule = {
|
|
|
2492
2569
|
column: file.lines[i].indexOf("catch") + 1,
|
|
2493
2570
|
message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
|
|
2494
2571
|
severity: score === 0 ? "warning" : "info",
|
|
2495
|
-
category: "reliability"
|
|
2572
|
+
category: "reliability",
|
|
2573
|
+
fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
|
|
2496
2574
|
});
|
|
2497
2575
|
}
|
|
2498
2576
|
i = bodyEnd;
|
|
@@ -2544,7 +2622,8 @@ var comprehensionDebtRule = {
|
|
|
2544
2622
|
column: 1,
|
|
2545
2623
|
message: `${fnName}() has ${params} parameters (max ${MAX_PARAMS}) \u2014 hard to call correctly`,
|
|
2546
2624
|
severity: "info",
|
|
2547
|
-
category: "ai-quality"
|
|
2625
|
+
category: "ai-quality",
|
|
2626
|
+
fix: "Group related parameters into an options object"
|
|
2548
2627
|
});
|
|
2549
2628
|
}
|
|
2550
2629
|
}
|
|
@@ -2571,7 +2650,8 @@ var comprehensionDebtRule = {
|
|
|
2571
2650
|
column: 1,
|
|
2572
2651
|
message: `${fnName}() is ${length} lines long (max ${MAX_FUNCTION_LENGTH}) \u2014 consider splitting`,
|
|
2573
2652
|
severity: "info",
|
|
2574
|
-
category: "ai-quality"
|
|
2653
|
+
category: "ai-quality",
|
|
2654
|
+
fix: "Break this into smaller, focused functions with clear names"
|
|
2575
2655
|
});
|
|
2576
2656
|
}
|
|
2577
2657
|
if (maxDepthInFn > MAX_NESTING_DEPTH) {
|
|
@@ -2582,7 +2662,8 @@ var comprehensionDebtRule = {
|
|
|
2582
2662
|
column: 1,
|
|
2583
2663
|
message: `${fnName}() has nesting depth ${maxDepthInFn} (max ${MAX_NESTING_DEPTH}) \u2014 flatten with early returns`,
|
|
2584
2664
|
severity: "info",
|
|
2585
|
-
category: "ai-quality"
|
|
2665
|
+
category: "ai-quality",
|
|
2666
|
+
fix: "Reduce nesting with early returns, guard clauses, or extracted helper functions"
|
|
2586
2667
|
});
|
|
2587
2668
|
}
|
|
2588
2669
|
inFunction = false;
|
|
@@ -2689,7 +2770,8 @@ var phantomDependencyRule = {
|
|
|
2689
2770
|
column: 1,
|
|
2690
2771
|
message: `"${name}" is a commonly hallucinated package name \u2014 verify it exists and is the correct package`,
|
|
2691
2772
|
severity: "warning",
|
|
2692
|
-
category: "security"
|
|
2773
|
+
category: "security",
|
|
2774
|
+
fix: "Add the missing package to dependencies in package.json, or remove the import if the package is hallucinated"
|
|
2693
2775
|
});
|
|
2694
2776
|
}
|
|
2695
2777
|
for (const pattern of SUSPICIOUS_PATTERNS) {
|
|
@@ -2701,7 +2783,8 @@ var phantomDependencyRule = {
|
|
|
2701
2783
|
column: 1,
|
|
2702
2784
|
message: `"${name}" has a suspicious package name pattern \u2014 verify it's legitimate`,
|
|
2703
2785
|
severity: "info",
|
|
2704
|
-
category: "security"
|
|
2786
|
+
category: "security",
|
|
2787
|
+
fix: "Verify the package on npm \u2014 suspicious names may indicate typosquatting or hallucination"
|
|
2705
2788
|
});
|
|
2706
2789
|
break;
|
|
2707
2790
|
}
|
|
@@ -3454,7 +3537,8 @@ var evalInjectionRule = {
|
|
|
3454
3537
|
column: match.index + 1,
|
|
3455
3538
|
message: msg,
|
|
3456
3539
|
severity: "critical",
|
|
3457
|
-
category: "security"
|
|
3540
|
+
category: "security",
|
|
3541
|
+
fix: "Replace eval/Function constructor with a safe alternative like JSON.parse() or a sandboxed interpreter"
|
|
3458
3542
|
});
|
|
3459
3543
|
break;
|
|
3460
3544
|
}
|
|
@@ -3857,6 +3941,8 @@ var hydrationMismatchRule = {
|
|
|
3857
3941
|
if (isClientComponent(file.content)) return [];
|
|
3858
3942
|
if (!/(?:^|\/)(?:src\/)?app\//.test(file.relativePath)) return [];
|
|
3859
3943
|
if (/route\.[jt]sx?$/.test(file.relativePath)) return [];
|
|
3944
|
+
if (/(?:^|\/)(?:src\/)?app\/(?:lib|utils|helpers|server|actions)\//.test(file.relativePath)) return [];
|
|
3945
|
+
if (/\.[jt]s$/.test(file.relativePath) && !/<[A-Z]/.test(file.content)) return [];
|
|
3860
3946
|
const findings = [];
|
|
3861
3947
|
let useEffectRanges = [];
|
|
3862
3948
|
if (file.ast) {
|
|
@@ -4249,44 +4335,57 @@ var clientSideAuthOnlyRule = {
|
|
|
4249
4335
|
};
|
|
4250
4336
|
|
|
4251
4337
|
// src/rules/missing-abort-controller.ts
|
|
4252
|
-
var
|
|
4338
|
+
var HTTP_CALL = /\b(fetch|axios)\s*[.(]/;
|
|
4253
4339
|
var HAS_TIMEOUT = [
|
|
4254
4340
|
/AbortController/,
|
|
4255
4341
|
/abort/i,
|
|
4256
4342
|
/signal\s*:/,
|
|
4257
|
-
/timeout
|
|
4258
|
-
/
|
|
4343
|
+
/timeout\s*:/,
|
|
4344
|
+
/timeout\s*=/,
|
|
4345
|
+
/setTimeout.*abort/s,
|
|
4346
|
+
/axios\.create\s*\([^)]*timeout/s
|
|
4259
4347
|
];
|
|
4348
|
+
var BACKGROUND_PATTERN = /\b(setInterval|cron|schedule|createWorker|bullmq|agenda|queue\.process)\b/;
|
|
4349
|
+
function isBackgroundFile(file) {
|
|
4350
|
+
if (BACKGROUND_PATTERN.test(file.content)) return true;
|
|
4351
|
+
const p = file.relativePath.toLowerCase();
|
|
4352
|
+
return /\b(cron|jobs?|workers?|queues?|tasks?|background)\b/.test(p);
|
|
4353
|
+
}
|
|
4260
4354
|
var missingAbortControllerRule = {
|
|
4261
4355
|
id: "missing-abort-controller",
|
|
4262
4356
|
name: "Missing Abort Controller",
|
|
4263
|
-
description: "Detects fetch calls
|
|
4357
|
+
description: "Detects fetch/axios calls without timeout or AbortController \u2014 requests can hang indefinitely",
|
|
4264
4358
|
category: "performance",
|
|
4265
4359
|
severity: "info",
|
|
4266
4360
|
fileExtensions: ["ts", "tsx", "js", "jsx"],
|
|
4267
4361
|
check(file, _project) {
|
|
4268
4362
|
if (isTestFile(file.relativePath)) return [];
|
|
4269
|
-
|
|
4270
|
-
if (!
|
|
4363
|
+
const isRelevant = isApiRoute(file.relativePath) || /['"]use server['"]/.test(file.content) || isBackgroundFile(file);
|
|
4364
|
+
if (!isRelevant) return [];
|
|
4365
|
+
if (!HTTP_CALL.test(file.content)) return [];
|
|
4271
4366
|
const hasTimeout = HAS_TIMEOUT.some((p) => p.test(file.content));
|
|
4272
4367
|
if (hasTimeout) return [];
|
|
4273
4368
|
let reportLine = 1;
|
|
4369
|
+
let matchedCall = "fetch";
|
|
4274
4370
|
for (let i = 0; i < file.lines.length; i++) {
|
|
4275
4371
|
if (isCommentLine(file.lines, i, file.commentMap)) continue;
|
|
4276
|
-
|
|
4372
|
+
const m = file.lines[i].match(HTTP_CALL);
|
|
4373
|
+
if (m) {
|
|
4277
4374
|
reportLine = i + 1;
|
|
4375
|
+
matchedCall = m[1];
|
|
4278
4376
|
break;
|
|
4279
4377
|
}
|
|
4280
4378
|
}
|
|
4379
|
+
const isFetch = matchedCall === "fetch";
|
|
4281
4380
|
return [{
|
|
4282
4381
|
ruleId: "missing-abort-controller",
|
|
4283
4382
|
file: file.relativePath,
|
|
4284
4383
|
line: reportLine,
|
|
4285
4384
|
column: 1,
|
|
4286
|
-
message:
|
|
4385
|
+
message: `${matchedCall}() without timeout or AbortController \u2014 request will hang indefinitely if the upstream server doesn't respond`,
|
|
4287
4386
|
severity: "info",
|
|
4288
4387
|
category: "performance",
|
|
4289
|
-
fix: "Add a timeout: const controller = new AbortController(); setTimeout(() => controller.abort(), 10000); fetch(url, { signal: controller.signal })"
|
|
4388
|
+
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 })"
|
|
4290
4389
|
}];
|
|
4291
4390
|
}
|
|
4292
4391
|
};
|
|
@@ -4659,7 +4758,9 @@ server.tool(
|
|
|
4659
4758
|
async ({ path, ignore }) => {
|
|
4660
4759
|
const resolved = resolve4(path);
|
|
4661
4760
|
const cwd = process.cwd();
|
|
4662
|
-
|
|
4761
|
+
const normalizedResolved = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
4762
|
+
const normalizedCwd = process.platform === "win32" ? cwd.toLowerCase() : cwd;
|
|
4763
|
+
if (normalizedResolved !== normalizedCwd && !normalizedResolved.startsWith(normalizedCwd + sep2)) {
|
|
4663
4764
|
return {
|
|
4664
4765
|
content: [{ type: "text", text: `Error: Path must be within the current working directory (${cwd})` }],
|
|
4665
4766
|
isError: true
|