prodlint 0.7.0 → 0.7.2
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 +12 -3
- package/dist/cli.js +136 -15
- package/dist/index.js +136 -15
- package/dist/mcp.js +136 -15
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
The linter for vibe-coded apps.
|
|
8
8
|
|
|
9
|
-
Cursor, v0, Bolt, and Copilot
|
|
9
|
+
Static analysis for vibe-coded apps. Catches the production bugs that Cursor, v0, Bolt, and Copilot write — hallucinated imports, missing auth, hardcoded secrets, unvalidated server actions, and more. Zero config, no LLM, 52 rules, under 100ms.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npx prodlint
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
prodlint v0.
|
|
16
|
+
prodlint v0.7.1
|
|
17
17
|
Scanned 148 files · 3 critical · 5 warnings
|
|
18
18
|
|
|
19
19
|
src/app/api/checkout/route.ts
|
|
@@ -140,7 +140,8 @@ npm i -g prodlint # Global install
|
|
|
140
140
|
|
|
141
141
|
prodlint avoids common false positives:
|
|
142
142
|
|
|
143
|
-
- **AST parsing** — Babel-based analysis for
|
|
143
|
+
- **AST parsing** — Babel-based analysis for 12 rules (imports, catch blocks, redirects, SSRF, path traversal, JWT, HTML injection, hydration, transactions, env leaks, loops, SQL) with regex fallback
|
|
144
|
+
- **Monorepo support** — npm/yarn/pnpm workspace dependencies resolved automatically
|
|
144
145
|
- **Framework awareness** — Prisma, Drizzle, Supabase, Knex, and Sequelize whitelists prevent false SQL injection flags
|
|
145
146
|
- **Middleware detection** — Clerk, NextAuth, Supabase middleware detected — auth findings downgraded
|
|
146
147
|
- **Block comment awareness** — patterns inside `/* */` are ignored
|
|
@@ -218,6 +219,14 @@ claude mcp add prodlint npx prodlint-mcp
|
|
|
218
219
|
|
|
219
220
|
Ask your AI: *"Run prodlint on this project"* and it calls the `scan` tool directly.
|
|
220
221
|
|
|
222
|
+
## For AI Tools
|
|
223
|
+
|
|
224
|
+
- **LLM-friendly docs**: [prodlint.com/llms.txt](https://prodlint.com/llms.txt) — concise project summary for LLMs
|
|
225
|
+
- **Full reference**: [prodlint.com/llms-full.txt](https://prodlint.com/llms-full.txt) — all 52 rules with details
|
|
226
|
+
- **MCP setup guide**: [prodlint.com/mcp](https://prodlint.com/mcp) — detailed editor setup for Claude Code, Cursor, Windsurf
|
|
227
|
+
|
|
228
|
+
prodlint is designed specifically for AI-generated code patterns. Every rule targets bugs that AI coding tools consistently produce — not style nits.
|
|
229
|
+
|
|
221
230
|
## Suppression
|
|
222
231
|
|
|
223
232
|
Suppress a single line:
|
package/dist/cli.js
CHANGED
|
@@ -76,7 +76,10 @@ function isTestFile(relativePath) {
|
|
|
76
76
|
return /\.(test|spec)\.[jt]sx?$/.test(relativePath) || /(?:^|\/)__tests__\//.test(relativePath) || /(?:^|\/)tests?\//.test(relativePath) || /(?:^|\/)fixtures?\//.test(relativePath) || /(?:^|\/)mocks?\//.test(relativePath);
|
|
77
77
|
}
|
|
78
78
|
function isScriptFile(relativePath) {
|
|
79
|
-
|
|
79
|
+
if (/(?:^|\/)scripts?\//.test(relativePath)) return true;
|
|
80
|
+
const name = relativePath.split("/").pop() ?? "";
|
|
81
|
+
const base = name.replace(/\.[^.]+$/, "");
|
|
82
|
+
return /^(seed|migrate|setup|bootstrap|generate|codegen|sync|deploy|cleanup|reset)$/.test(base);
|
|
80
83
|
}
|
|
81
84
|
function isConfigFile(relativePath) {
|
|
82
85
|
const name = relativePath.split("/").pop() ?? "";
|
|
@@ -245,6 +248,25 @@ function findLoopsAST(ast) {
|
|
|
245
248
|
});
|
|
246
249
|
return loops;
|
|
247
250
|
}
|
|
251
|
+
function getImportSources(ast) {
|
|
252
|
+
const sources = [];
|
|
253
|
+
walkAST(ast.program, (node) => {
|
|
254
|
+
if (node.type === "ImportDeclaration") {
|
|
255
|
+
const decl = node;
|
|
256
|
+
sources.push({ source: decl.source.value, line: decl.loc?.start.line ?? 1 });
|
|
257
|
+
}
|
|
258
|
+
if (node.type === "CallExpression") {
|
|
259
|
+
const call = node;
|
|
260
|
+
if (call.callee.type === "Identifier" && call.callee.name === "require" && call.arguments.length === 1 && call.arguments[0].type === "StringLiteral") {
|
|
261
|
+
sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
|
|
262
|
+
}
|
|
263
|
+
if (call.callee.type === "Import" && call.arguments.length >= 1 && call.arguments[0].type === "StringLiteral") {
|
|
264
|
+
sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
return sources;
|
|
269
|
+
}
|
|
248
270
|
function isUserInputNode(node) {
|
|
249
271
|
if (node.type === "MemberExpression") {
|
|
250
272
|
const mem = node;
|
|
@@ -439,6 +461,66 @@ async function readFileContext(root, relativePath) {
|
|
|
439
461
|
return null;
|
|
440
462
|
}
|
|
441
463
|
}
|
|
464
|
+
async function getWorkspacePatterns(root, packageJson) {
|
|
465
|
+
const patterns = [];
|
|
466
|
+
if (packageJson) {
|
|
467
|
+
const workspaces = packageJson.workspaces;
|
|
468
|
+
if (Array.isArray(workspaces)) {
|
|
469
|
+
patterns.push(...workspaces);
|
|
470
|
+
} else if (workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)) {
|
|
471
|
+
patterns.push(...workspaces.packages);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
const raw = await readFile(resolve(root, "pnpm-workspace.yaml"), "utf-8");
|
|
476
|
+
let inPackages = false;
|
|
477
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
478
|
+
const trimmed = line.trim();
|
|
479
|
+
if (/^packages\s*:/.test(trimmed)) {
|
|
480
|
+
inPackages = true;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (inPackages) {
|
|
484
|
+
if (/^-\s+/.test(trimmed)) {
|
|
485
|
+
const glob = trimmed.replace(/^-\s+/, "").replace(/^['"]|['"]$/g, "");
|
|
486
|
+
if (glob) patterns.push(glob);
|
|
487
|
+
} else if (trimmed && !trimmed.startsWith("#")) {
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} catch {
|
|
493
|
+
}
|
|
494
|
+
return patterns;
|
|
495
|
+
}
|
|
496
|
+
async function collectWorkspaceDependencies(root, patterns) {
|
|
497
|
+
const deps = /* @__PURE__ */ new Set();
|
|
498
|
+
const globPatterns = patterns.map((p) => `${p}/package.json`);
|
|
499
|
+
try {
|
|
500
|
+
const pkgFiles = await fg(globPatterns, {
|
|
501
|
+
cwd: root,
|
|
502
|
+
absolute: false,
|
|
503
|
+
ignore: ["**/node_modules/**"]
|
|
504
|
+
});
|
|
505
|
+
for (const pkgFile of pkgFiles) {
|
|
506
|
+
try {
|
|
507
|
+
const raw = await readFile(resolve(root, pkgFile), "utf-8");
|
|
508
|
+
const pkg = JSON.parse(raw);
|
|
509
|
+
if (pkg.name) deps.add(pkg.name);
|
|
510
|
+
for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
511
|
+
if (pkg[key] && typeof pkg[key] === "object") {
|
|
512
|
+
for (const dep of Object.keys(pkg[key])) {
|
|
513
|
+
deps.add(dep);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} catch {
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
return deps;
|
|
523
|
+
}
|
|
442
524
|
async function buildProjectContext(root, files) {
|
|
443
525
|
let packageJson = null;
|
|
444
526
|
let declaredDependencies = /* @__PURE__ */ new Set();
|
|
@@ -457,19 +539,26 @@ async function buildProjectContext(root, files) {
|
|
|
457
539
|
...packageJson?.peerDependencies ?? {}
|
|
458
540
|
};
|
|
459
541
|
declaredDependencies = new Set(Object.keys(deps));
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
const workspacePatterns = await getWorkspacePatterns(root, packageJson);
|
|
545
|
+
if (workspacePatterns.length > 0) {
|
|
546
|
+
const workspaceDeps = await collectWorkspaceDependencies(root, workspacePatterns);
|
|
547
|
+
for (const dep of workspaceDeps) {
|
|
548
|
+
declaredDependencies.add(dep);
|
|
465
549
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
550
|
+
}
|
|
551
|
+
for (const dep of declaredDependencies) {
|
|
552
|
+
const framework = DEPENDENCY_TO_FRAMEWORK[dep];
|
|
553
|
+
if (framework) {
|
|
554
|
+
detectedFrameworks.add(framework);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
for (const framework of detectedFrameworks) {
|
|
558
|
+
if (RATE_LIMIT_FRAMEWORKS.has(framework)) {
|
|
559
|
+
hasRateLimiting = true;
|
|
560
|
+
break;
|
|
471
561
|
}
|
|
472
|
-
} catch {
|
|
473
562
|
}
|
|
474
563
|
try {
|
|
475
564
|
const raw = await readFile(resolve(root, "tsconfig.json"), "utf-8");
|
|
@@ -724,6 +813,32 @@ var hallucinatedImportsRule = {
|
|
|
724
813
|
const findings = [];
|
|
725
814
|
const seen = /* @__PURE__ */ new Set();
|
|
726
815
|
const isNonProd = isTestFile(file.relativePath) || isScriptFile(file.relativePath);
|
|
816
|
+
if (file.ast) {
|
|
817
|
+
try {
|
|
818
|
+
const imports = getImportSources(file.ast);
|
|
819
|
+
for (const { source: importPath, line } of imports) {
|
|
820
|
+
if (importPath.startsWith(".") || importPath.startsWith("/")) continue;
|
|
821
|
+
const pkgName = getPackageName(importPath);
|
|
822
|
+
if (seen.has(pkgName)) continue;
|
|
823
|
+
seen.add(pkgName);
|
|
824
|
+
if (isPathAlias(importPath, project.tsconfigPaths)) continue;
|
|
825
|
+
if (isNodeBuiltin(pkgName)) continue;
|
|
826
|
+
if (IMPLICIT_PACKAGES.has(importPath) || IMPLICIT_PACKAGES.has(pkgName)) continue;
|
|
827
|
+
if (project.declaredDependencies.has(pkgName)) continue;
|
|
828
|
+
findings.push({
|
|
829
|
+
ruleId: "hallucinated-imports",
|
|
830
|
+
file: file.relativePath,
|
|
831
|
+
line,
|
|
832
|
+
column: 1,
|
|
833
|
+
message: `Package "${pkgName}" is imported but not in package.json`,
|
|
834
|
+
severity: isNonProd ? "warning" : "critical",
|
|
835
|
+
category: "reliability"
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return findings;
|
|
839
|
+
} catch {
|
|
840
|
+
}
|
|
841
|
+
}
|
|
727
842
|
for (let i = 0; i < file.lines.length; i++) {
|
|
728
843
|
if (isCommentLine(file.lines, i, file.commentMap)) continue;
|
|
729
844
|
const line = file.lines[i];
|
|
@@ -2255,8 +2370,14 @@ function scoreCatchBody(bodyLines) {
|
|
|
2255
2370
|
const body = bodyLines.join("\n");
|
|
2256
2371
|
let score = 0;
|
|
2257
2372
|
if (/console\.(log|warn|error|info)\s*\(/.test(body)) score = 1;
|
|
2258
|
-
if (/console\.(error|warn)\s*\(/.test(body) && /\b(err|error|e)\b/.test(body)) score = 2;
|
|
2259
|
-
if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body))
|
|
2373
|
+
if ((/console\.(error|warn)\s*\(/.test(body) || /\b(logger|log)\.(error|warn)\s*\(/.test(body)) && /\b(err|error|e)\b/.test(body)) score = 2;
|
|
2374
|
+
if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body) || // Toast notifications (react-toastify, sonner, shadcn)
|
|
2375
|
+
/\btoast\.(error|warn|warning)\s*\(/.test(body) || /\btoast\s*\(\s*\{/.test(body) || // Error monitoring (Sentry, etc.)
|
|
2376
|
+
/\b(Sentry\.)?captureException\s*\(/.test(body) || /\b(Sentry\.)?captureMessage\s*\(/.test(body) || // Structured loggers
|
|
2377
|
+
/\b(logger|log)\.(error|fatal)\s*\(/.test(body) || // Error handler utilities
|
|
2378
|
+
/\b(handleError|reportError|logError|notifyError|showError|onError|trackError)\s*\(/.test(body) || // Express middleware: next(err)
|
|
2379
|
+
/\bnext\s*\(\s*(err|error|e)\s*\)/.test(body) || // Next.js notFound()
|
|
2380
|
+
/\bnotFound\s*\(/.test(body)) {
|
|
2260
2381
|
score = 3;
|
|
2261
2382
|
}
|
|
2262
2383
|
const labels = {
|
|
@@ -2288,7 +2409,7 @@ var shallowCatchRule = {
|
|
|
2288
2409
|
if (!body || !body.loc) return;
|
|
2289
2410
|
const bodyStart = body.loc.start.line - 1;
|
|
2290
2411
|
const bodyEnd = body.loc.end.line - 1;
|
|
2291
|
-
const bodyLines = file.lines.slice(bodyStart + 1, bodyEnd);
|
|
2412
|
+
const bodyLines = bodyStart === bodyEnd ? [file.lines[bodyStart]] : file.lines.slice(bodyStart + 1, bodyEnd);
|
|
2292
2413
|
const { score, label } = scoreCatchBody(bodyLines);
|
|
2293
2414
|
if (score <= 1) {
|
|
2294
2415
|
findings.push({
|
package/dist/index.js
CHANGED
|
@@ -71,7 +71,10 @@ function isTestFile(relativePath) {
|
|
|
71
71
|
return /\.(test|spec)\.[jt]sx?$/.test(relativePath) || /(?:^|\/)__tests__\//.test(relativePath) || /(?:^|\/)tests?\//.test(relativePath) || /(?:^|\/)fixtures?\//.test(relativePath) || /(?:^|\/)mocks?\//.test(relativePath);
|
|
72
72
|
}
|
|
73
73
|
function isScriptFile(relativePath) {
|
|
74
|
-
|
|
74
|
+
if (/(?:^|\/)scripts?\//.test(relativePath)) return true;
|
|
75
|
+
const name = relativePath.split("/").pop() ?? "";
|
|
76
|
+
const base = name.replace(/\.[^.]+$/, "");
|
|
77
|
+
return /^(seed|migrate|setup|bootstrap|generate|codegen|sync|deploy|cleanup|reset)$/.test(base);
|
|
75
78
|
}
|
|
76
79
|
function isConfigFile(relativePath) {
|
|
77
80
|
const name = relativePath.split("/").pop() ?? "";
|
|
@@ -240,6 +243,25 @@ function findLoopsAST(ast) {
|
|
|
240
243
|
});
|
|
241
244
|
return loops;
|
|
242
245
|
}
|
|
246
|
+
function getImportSources(ast) {
|
|
247
|
+
const sources = [];
|
|
248
|
+
walkAST(ast.program, (node) => {
|
|
249
|
+
if (node.type === "ImportDeclaration") {
|
|
250
|
+
const decl = node;
|
|
251
|
+
sources.push({ source: decl.source.value, line: decl.loc?.start.line ?? 1 });
|
|
252
|
+
}
|
|
253
|
+
if (node.type === "CallExpression") {
|
|
254
|
+
const call = node;
|
|
255
|
+
if (call.callee.type === "Identifier" && call.callee.name === "require" && call.arguments.length === 1 && call.arguments[0].type === "StringLiteral") {
|
|
256
|
+
sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
|
|
257
|
+
}
|
|
258
|
+
if (call.callee.type === "Import" && call.arguments.length >= 1 && call.arguments[0].type === "StringLiteral") {
|
|
259
|
+
sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
return sources;
|
|
264
|
+
}
|
|
243
265
|
function isUserInputNode(node) {
|
|
244
266
|
if (node.type === "MemberExpression") {
|
|
245
267
|
const mem = node;
|
|
@@ -434,6 +456,66 @@ async function readFileContext(root, relativePath) {
|
|
|
434
456
|
return null;
|
|
435
457
|
}
|
|
436
458
|
}
|
|
459
|
+
async function getWorkspacePatterns(root, packageJson) {
|
|
460
|
+
const patterns = [];
|
|
461
|
+
if (packageJson) {
|
|
462
|
+
const workspaces = packageJson.workspaces;
|
|
463
|
+
if (Array.isArray(workspaces)) {
|
|
464
|
+
patterns.push(...workspaces);
|
|
465
|
+
} else if (workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)) {
|
|
466
|
+
patterns.push(...workspaces.packages);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
const raw = await readFile(resolve(root, "pnpm-workspace.yaml"), "utf-8");
|
|
471
|
+
let inPackages = false;
|
|
472
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
473
|
+
const trimmed = line.trim();
|
|
474
|
+
if (/^packages\s*:/.test(trimmed)) {
|
|
475
|
+
inPackages = true;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (inPackages) {
|
|
479
|
+
if (/^-\s+/.test(trimmed)) {
|
|
480
|
+
const glob = trimmed.replace(/^-\s+/, "").replace(/^['"]|['"]$/g, "");
|
|
481
|
+
if (glob) patterns.push(glob);
|
|
482
|
+
} else if (trimmed && !trimmed.startsWith("#")) {
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} catch {
|
|
488
|
+
}
|
|
489
|
+
return patterns;
|
|
490
|
+
}
|
|
491
|
+
async function collectWorkspaceDependencies(root, patterns) {
|
|
492
|
+
const deps = /* @__PURE__ */ new Set();
|
|
493
|
+
const globPatterns = patterns.map((p) => `${p}/package.json`);
|
|
494
|
+
try {
|
|
495
|
+
const pkgFiles = await fg(globPatterns, {
|
|
496
|
+
cwd: root,
|
|
497
|
+
absolute: false,
|
|
498
|
+
ignore: ["**/node_modules/**"]
|
|
499
|
+
});
|
|
500
|
+
for (const pkgFile of pkgFiles) {
|
|
501
|
+
try {
|
|
502
|
+
const raw = await readFile(resolve(root, pkgFile), "utf-8");
|
|
503
|
+
const pkg = JSON.parse(raw);
|
|
504
|
+
if (pkg.name) deps.add(pkg.name);
|
|
505
|
+
for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
506
|
+
if (pkg[key] && typeof pkg[key] === "object") {
|
|
507
|
+
for (const dep of Object.keys(pkg[key])) {
|
|
508
|
+
deps.add(dep);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
return deps;
|
|
518
|
+
}
|
|
437
519
|
async function buildProjectContext(root, files) {
|
|
438
520
|
let packageJson = null;
|
|
439
521
|
let declaredDependencies = /* @__PURE__ */ new Set();
|
|
@@ -452,19 +534,26 @@ async function buildProjectContext(root, files) {
|
|
|
452
534
|
...packageJson?.peerDependencies ?? {}
|
|
453
535
|
};
|
|
454
536
|
declaredDependencies = new Set(Object.keys(deps));
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
const workspacePatterns = await getWorkspacePatterns(root, packageJson);
|
|
540
|
+
if (workspacePatterns.length > 0) {
|
|
541
|
+
const workspaceDeps = await collectWorkspaceDependencies(root, workspacePatterns);
|
|
542
|
+
for (const dep of workspaceDeps) {
|
|
543
|
+
declaredDependencies.add(dep);
|
|
460
544
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
545
|
+
}
|
|
546
|
+
for (const dep of declaredDependencies) {
|
|
547
|
+
const framework = DEPENDENCY_TO_FRAMEWORK[dep];
|
|
548
|
+
if (framework) {
|
|
549
|
+
detectedFrameworks.add(framework);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
for (const framework of detectedFrameworks) {
|
|
553
|
+
if (RATE_LIMIT_FRAMEWORKS.has(framework)) {
|
|
554
|
+
hasRateLimiting = true;
|
|
555
|
+
break;
|
|
466
556
|
}
|
|
467
|
-
} catch {
|
|
468
557
|
}
|
|
469
558
|
try {
|
|
470
559
|
const raw = await readFile(resolve(root, "tsconfig.json"), "utf-8");
|
|
@@ -719,6 +808,32 @@ var hallucinatedImportsRule = {
|
|
|
719
808
|
const findings = [];
|
|
720
809
|
const seen = /* @__PURE__ */ new Set();
|
|
721
810
|
const isNonProd = isTestFile(file.relativePath) || isScriptFile(file.relativePath);
|
|
811
|
+
if (file.ast) {
|
|
812
|
+
try {
|
|
813
|
+
const imports = getImportSources(file.ast);
|
|
814
|
+
for (const { source: importPath, line } of imports) {
|
|
815
|
+
if (importPath.startsWith(".") || importPath.startsWith("/")) continue;
|
|
816
|
+
const pkgName = getPackageName(importPath);
|
|
817
|
+
if (seen.has(pkgName)) continue;
|
|
818
|
+
seen.add(pkgName);
|
|
819
|
+
if (isPathAlias(importPath, project.tsconfigPaths)) continue;
|
|
820
|
+
if (isNodeBuiltin(pkgName)) continue;
|
|
821
|
+
if (IMPLICIT_PACKAGES.has(importPath) || IMPLICIT_PACKAGES.has(pkgName)) continue;
|
|
822
|
+
if (project.declaredDependencies.has(pkgName)) continue;
|
|
823
|
+
findings.push({
|
|
824
|
+
ruleId: "hallucinated-imports",
|
|
825
|
+
file: file.relativePath,
|
|
826
|
+
line,
|
|
827
|
+
column: 1,
|
|
828
|
+
message: `Package "${pkgName}" is imported but not in package.json`,
|
|
829
|
+
severity: isNonProd ? "warning" : "critical",
|
|
830
|
+
category: "reliability"
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
return findings;
|
|
834
|
+
} catch {
|
|
835
|
+
}
|
|
836
|
+
}
|
|
722
837
|
for (let i = 0; i < file.lines.length; i++) {
|
|
723
838
|
if (isCommentLine(file.lines, i, file.commentMap)) continue;
|
|
724
839
|
const line = file.lines[i];
|
|
@@ -2250,8 +2365,14 @@ function scoreCatchBody(bodyLines) {
|
|
|
2250
2365
|
const body = bodyLines.join("\n");
|
|
2251
2366
|
let score = 0;
|
|
2252
2367
|
if (/console\.(log|warn|error|info)\s*\(/.test(body)) score = 1;
|
|
2253
|
-
if (/console\.(error|warn)\s*\(/.test(body) && /\b(err|error|e)\b/.test(body)) score = 2;
|
|
2254
|
-
if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body))
|
|
2368
|
+
if ((/console\.(error|warn)\s*\(/.test(body) || /\b(logger|log)\.(error|warn)\s*\(/.test(body)) && /\b(err|error|e)\b/.test(body)) score = 2;
|
|
2369
|
+
if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body) || // Toast notifications (react-toastify, sonner, shadcn)
|
|
2370
|
+
/\btoast\.(error|warn|warning)\s*\(/.test(body) || /\btoast\s*\(\s*\{/.test(body) || // Error monitoring (Sentry, etc.)
|
|
2371
|
+
/\b(Sentry\.)?captureException\s*\(/.test(body) || /\b(Sentry\.)?captureMessage\s*\(/.test(body) || // Structured loggers
|
|
2372
|
+
/\b(logger|log)\.(error|fatal)\s*\(/.test(body) || // Error handler utilities
|
|
2373
|
+
/\b(handleError|reportError|logError|notifyError|showError|onError|trackError)\s*\(/.test(body) || // Express middleware: next(err)
|
|
2374
|
+
/\bnext\s*\(\s*(err|error|e)\s*\)/.test(body) || // Next.js notFound()
|
|
2375
|
+
/\bnotFound\s*\(/.test(body)) {
|
|
2255
2376
|
score = 3;
|
|
2256
2377
|
}
|
|
2257
2378
|
const labels = {
|
|
@@ -2283,7 +2404,7 @@ var shallowCatchRule = {
|
|
|
2283
2404
|
if (!body || !body.loc) return;
|
|
2284
2405
|
const bodyStart = body.loc.start.line - 1;
|
|
2285
2406
|
const bodyEnd = body.loc.end.line - 1;
|
|
2286
|
-
const bodyLines = file.lines.slice(bodyStart + 1, bodyEnd);
|
|
2407
|
+
const bodyLines = bodyStart === bodyEnd ? [file.lines[bodyStart]] : file.lines.slice(bodyStart + 1, bodyEnd);
|
|
2287
2408
|
const { score, label } = scoreCatchBody(bodyLines);
|
|
2288
2409
|
if (score <= 1) {
|
|
2289
2410
|
findings.push({
|
package/dist/mcp.js
CHANGED
|
@@ -80,7 +80,10 @@ function isTestFile(relativePath) {
|
|
|
80
80
|
return /\.(test|spec)\.[jt]sx?$/.test(relativePath) || /(?:^|\/)__tests__\//.test(relativePath) || /(?:^|\/)tests?\//.test(relativePath) || /(?:^|\/)fixtures?\//.test(relativePath) || /(?:^|\/)mocks?\//.test(relativePath);
|
|
81
81
|
}
|
|
82
82
|
function isScriptFile(relativePath) {
|
|
83
|
-
|
|
83
|
+
if (/(?:^|\/)scripts?\//.test(relativePath)) return true;
|
|
84
|
+
const name = relativePath.split("/").pop() ?? "";
|
|
85
|
+
const base = name.replace(/\.[^.]+$/, "");
|
|
86
|
+
return /^(seed|migrate|setup|bootstrap|generate|codegen|sync|deploy|cleanup|reset)$/.test(base);
|
|
84
87
|
}
|
|
85
88
|
function isConfigFile(relativePath) {
|
|
86
89
|
const name = relativePath.split("/").pop() ?? "";
|
|
@@ -249,6 +252,25 @@ function findLoopsAST(ast) {
|
|
|
249
252
|
});
|
|
250
253
|
return loops;
|
|
251
254
|
}
|
|
255
|
+
function getImportSources(ast) {
|
|
256
|
+
const sources = [];
|
|
257
|
+
walkAST(ast.program, (node) => {
|
|
258
|
+
if (node.type === "ImportDeclaration") {
|
|
259
|
+
const decl = node;
|
|
260
|
+
sources.push({ source: decl.source.value, line: decl.loc?.start.line ?? 1 });
|
|
261
|
+
}
|
|
262
|
+
if (node.type === "CallExpression") {
|
|
263
|
+
const call = node;
|
|
264
|
+
if (call.callee.type === "Identifier" && call.callee.name === "require" && call.arguments.length === 1 && call.arguments[0].type === "StringLiteral") {
|
|
265
|
+
sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
|
|
266
|
+
}
|
|
267
|
+
if (call.callee.type === "Import" && call.arguments.length >= 1 && call.arguments[0].type === "StringLiteral") {
|
|
268
|
+
sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
return sources;
|
|
273
|
+
}
|
|
252
274
|
function isUserInputNode(node) {
|
|
253
275
|
if (node.type === "MemberExpression") {
|
|
254
276
|
const mem = node;
|
|
@@ -443,6 +465,66 @@ async function readFileContext(root, relativePath) {
|
|
|
443
465
|
return null;
|
|
444
466
|
}
|
|
445
467
|
}
|
|
468
|
+
async function getWorkspacePatterns(root, packageJson) {
|
|
469
|
+
const patterns = [];
|
|
470
|
+
if (packageJson) {
|
|
471
|
+
const workspaces = packageJson.workspaces;
|
|
472
|
+
if (Array.isArray(workspaces)) {
|
|
473
|
+
patterns.push(...workspaces);
|
|
474
|
+
} else if (workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)) {
|
|
475
|
+
patterns.push(...workspaces.packages);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const raw = await readFile(resolve(root, "pnpm-workspace.yaml"), "utf-8");
|
|
480
|
+
let inPackages = false;
|
|
481
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
482
|
+
const trimmed = line.trim();
|
|
483
|
+
if (/^packages\s*:/.test(trimmed)) {
|
|
484
|
+
inPackages = true;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (inPackages) {
|
|
488
|
+
if (/^-\s+/.test(trimmed)) {
|
|
489
|
+
const glob = trimmed.replace(/^-\s+/, "").replace(/^['"]|['"]$/g, "");
|
|
490
|
+
if (glob) patterns.push(glob);
|
|
491
|
+
} else if (trimmed && !trimmed.startsWith("#")) {
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
return patterns;
|
|
499
|
+
}
|
|
500
|
+
async function collectWorkspaceDependencies(root, patterns) {
|
|
501
|
+
const deps = /* @__PURE__ */ new Set();
|
|
502
|
+
const globPatterns = patterns.map((p) => `${p}/package.json`);
|
|
503
|
+
try {
|
|
504
|
+
const pkgFiles = await fg(globPatterns, {
|
|
505
|
+
cwd: root,
|
|
506
|
+
absolute: false,
|
|
507
|
+
ignore: ["**/node_modules/**"]
|
|
508
|
+
});
|
|
509
|
+
for (const pkgFile of pkgFiles) {
|
|
510
|
+
try {
|
|
511
|
+
const raw = await readFile(resolve(root, pkgFile), "utf-8");
|
|
512
|
+
const pkg = JSON.parse(raw);
|
|
513
|
+
if (pkg.name) deps.add(pkg.name);
|
|
514
|
+
for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
515
|
+
if (pkg[key] && typeof pkg[key] === "object") {
|
|
516
|
+
for (const dep of Object.keys(pkg[key])) {
|
|
517
|
+
deps.add(dep);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
return deps;
|
|
527
|
+
}
|
|
446
528
|
async function buildProjectContext(root, files) {
|
|
447
529
|
let packageJson = null;
|
|
448
530
|
let declaredDependencies = /* @__PURE__ */ new Set();
|
|
@@ -461,19 +543,26 @@ async function buildProjectContext(root, files) {
|
|
|
461
543
|
...packageJson?.peerDependencies ?? {}
|
|
462
544
|
};
|
|
463
545
|
declaredDependencies = new Set(Object.keys(deps));
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
const workspacePatterns = await getWorkspacePatterns(root, packageJson);
|
|
549
|
+
if (workspacePatterns.length > 0) {
|
|
550
|
+
const workspaceDeps = await collectWorkspaceDependencies(root, workspacePatterns);
|
|
551
|
+
for (const dep of workspaceDeps) {
|
|
552
|
+
declaredDependencies.add(dep);
|
|
469
553
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
554
|
+
}
|
|
555
|
+
for (const dep of declaredDependencies) {
|
|
556
|
+
const framework = DEPENDENCY_TO_FRAMEWORK[dep];
|
|
557
|
+
if (framework) {
|
|
558
|
+
detectedFrameworks.add(framework);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
for (const framework of detectedFrameworks) {
|
|
562
|
+
if (RATE_LIMIT_FRAMEWORKS.has(framework)) {
|
|
563
|
+
hasRateLimiting = true;
|
|
564
|
+
break;
|
|
475
565
|
}
|
|
476
|
-
} catch {
|
|
477
566
|
}
|
|
478
567
|
try {
|
|
479
568
|
const raw = await readFile(resolve(root, "tsconfig.json"), "utf-8");
|
|
@@ -728,6 +817,32 @@ var hallucinatedImportsRule = {
|
|
|
728
817
|
const findings = [];
|
|
729
818
|
const seen = /* @__PURE__ */ new Set();
|
|
730
819
|
const isNonProd = isTestFile(file.relativePath) || isScriptFile(file.relativePath);
|
|
820
|
+
if (file.ast) {
|
|
821
|
+
try {
|
|
822
|
+
const imports = getImportSources(file.ast);
|
|
823
|
+
for (const { source: importPath, line } of imports) {
|
|
824
|
+
if (importPath.startsWith(".") || importPath.startsWith("/")) continue;
|
|
825
|
+
const pkgName = getPackageName(importPath);
|
|
826
|
+
if (seen.has(pkgName)) continue;
|
|
827
|
+
seen.add(pkgName);
|
|
828
|
+
if (isPathAlias(importPath, project.tsconfigPaths)) continue;
|
|
829
|
+
if (isNodeBuiltin(pkgName)) continue;
|
|
830
|
+
if (IMPLICIT_PACKAGES.has(importPath) || IMPLICIT_PACKAGES.has(pkgName)) continue;
|
|
831
|
+
if (project.declaredDependencies.has(pkgName)) continue;
|
|
832
|
+
findings.push({
|
|
833
|
+
ruleId: "hallucinated-imports",
|
|
834
|
+
file: file.relativePath,
|
|
835
|
+
line,
|
|
836
|
+
column: 1,
|
|
837
|
+
message: `Package "${pkgName}" is imported but not in package.json`,
|
|
838
|
+
severity: isNonProd ? "warning" : "critical",
|
|
839
|
+
category: "reliability"
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return findings;
|
|
843
|
+
} catch {
|
|
844
|
+
}
|
|
845
|
+
}
|
|
731
846
|
for (let i = 0; i < file.lines.length; i++) {
|
|
732
847
|
if (isCommentLine(file.lines, i, file.commentMap)) continue;
|
|
733
848
|
const line = file.lines[i];
|
|
@@ -2259,8 +2374,14 @@ function scoreCatchBody(bodyLines) {
|
|
|
2259
2374
|
const body = bodyLines.join("\n");
|
|
2260
2375
|
let score = 0;
|
|
2261
2376
|
if (/console\.(log|warn|error|info)\s*\(/.test(body)) score = 1;
|
|
2262
|
-
if (/console\.(error|warn)\s*\(/.test(body) && /\b(err|error|e)\b/.test(body)) score = 2;
|
|
2263
|
-
if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body))
|
|
2377
|
+
if ((/console\.(error|warn)\s*\(/.test(body) || /\b(logger|log)\.(error|warn)\s*\(/.test(body)) && /\b(err|error|e)\b/.test(body)) score = 2;
|
|
2378
|
+
if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body) || // Toast notifications (react-toastify, sonner, shadcn)
|
|
2379
|
+
/\btoast\.(error|warn|warning)\s*\(/.test(body) || /\btoast\s*\(\s*\{/.test(body) || // Error monitoring (Sentry, etc.)
|
|
2380
|
+
/\b(Sentry\.)?captureException\s*\(/.test(body) || /\b(Sentry\.)?captureMessage\s*\(/.test(body) || // Structured loggers
|
|
2381
|
+
/\b(logger|log)\.(error|fatal)\s*\(/.test(body) || // Error handler utilities
|
|
2382
|
+
/\b(handleError|reportError|logError|notifyError|showError|onError|trackError)\s*\(/.test(body) || // Express middleware: next(err)
|
|
2383
|
+
/\bnext\s*\(\s*(err|error|e)\s*\)/.test(body) || // Next.js notFound()
|
|
2384
|
+
/\bnotFound\s*\(/.test(body)) {
|
|
2264
2385
|
score = 3;
|
|
2265
2386
|
}
|
|
2266
2387
|
const labels = {
|
|
@@ -2292,7 +2413,7 @@ var shallowCatchRule = {
|
|
|
2292
2413
|
if (!body || !body.loc) return;
|
|
2293
2414
|
const bodyStart = body.loc.start.line - 1;
|
|
2294
2415
|
const bodyEnd = body.loc.end.line - 1;
|
|
2295
|
-
const bodyLines = file.lines.slice(bodyStart + 1, bodyEnd);
|
|
2416
|
+
const bodyLines = bodyStart === bodyEnd ? [file.lines[bodyStart]] : file.lines.slice(bodyStart + 1, bodyEnd);
|
|
2296
2417
|
const { score, label } = scoreCatchBody(bodyLines);
|
|
2297
2418
|
if (score <= 1) {
|
|
2298
2419
|
findings.push({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prodlint",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "The linter for vibe-coded apps — catch what AI coding tools miss",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "prodlint contributors",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"url": "https://github.com/prodlint/prodlint/issues"
|
|
14
14
|
},
|
|
15
15
|
"homepage": "https://prodlint.com",
|
|
16
|
+
"mcpName": "io.github.Anthony-Marcovecchio/prodlint",
|
|
16
17
|
"bin": {
|
|
17
18
|
"prodlint": "./dist/cli.js",
|
|
18
19
|
"prodlint-mcp": "./dist/mcp.js"
|
|
@@ -78,6 +79,12 @@
|
|
|
78
79
|
"mcp-server",
|
|
79
80
|
"cursor",
|
|
80
81
|
"copilot",
|
|
82
|
+
"v0",
|
|
83
|
+
"bolt",
|
|
84
|
+
"windsurf",
|
|
85
|
+
"claude",
|
|
86
|
+
"github-action",
|
|
87
|
+
"eslint-alternative",
|
|
81
88
|
"devtools"
|
|
82
89
|
]
|
|
83
90
|
}
|