prodlint 0.7.0 → 0.7.1
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/LICENSE +21 -21
- package/README.md +253 -252
- package/action.yml +152 -152
- package/dist/cli.js +136 -15
- package/dist/index.js +136 -15
- package/dist/mcp.js +136 -15
- package/package.json +83 -83
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,83 +1,83 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "prodlint",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "The linter for vibe-coded apps — catch what AI coding tools miss",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "prodlint contributors",
|
|
7
|
-
"type": "module",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "https://github.com/prodlint/prodlint"
|
|
11
|
-
},
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/prodlint/prodlint/issues"
|
|
14
|
-
},
|
|
15
|
-
"homepage": "https://prodlint.com",
|
|
16
|
-
"bin": {
|
|
17
|
-
"prodlint": "./dist/cli.js",
|
|
18
|
-
"prodlint-mcp": "./dist/mcp.js"
|
|
19
|
-
},
|
|
20
|
-
"main": "./dist/index.js",
|
|
21
|
-
"types": "./dist/index.d.ts",
|
|
22
|
-
"exports": {
|
|
23
|
-
".": {
|
|
24
|
-
"import": "./dist/index.js",
|
|
25
|
-
"types": "./dist/index.d.ts"
|
|
26
|
-
},
|
|
27
|
-
"./mcp": {
|
|
28
|
-
"import": "./dist/mcp.js"
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
"files": [
|
|
32
|
-
"dist/**/*.js",
|
|
33
|
-
"dist/**/*.d.ts",
|
|
34
|
-
"action.yml"
|
|
35
|
-
],
|
|
36
|
-
"scripts": {
|
|
37
|
-
"build": "tsup",
|
|
38
|
-
"dev": "tsup --watch",
|
|
39
|
-
"test": "vitest run",
|
|
40
|
-
"test:watch": "vitest",
|
|
41
|
-
"lint": "node dist/cli.js .",
|
|
42
|
-
"prepublishOnly": "npm run build"
|
|
43
|
-
},
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"@babel/parser": "^7.29.0",
|
|
46
|
-
"@babel/types": "^7.29.0",
|
|
47
|
-
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
48
|
-
"fast-glob": "^3.3.3",
|
|
49
|
-
"picocolors": "^1.1.1",
|
|
50
|
-
"zod": "^4.3.6"
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"@types/node": "^25.2.3",
|
|
54
|
-
"tsup": "^8.4.0",
|
|
55
|
-
"typescript": "^5.7.0",
|
|
56
|
-
"vitest": "^3.0.0"
|
|
57
|
-
},
|
|
58
|
-
"engines": {
|
|
59
|
-
"node": ">=18"
|
|
60
|
-
},
|
|
61
|
-
"keywords": [
|
|
62
|
-
"lint",
|
|
63
|
-
"linter",
|
|
64
|
-
"security",
|
|
65
|
-
"ai",
|
|
66
|
-
"ai-code",
|
|
67
|
-
"vibe-coding",
|
|
68
|
-
"production-readiness",
|
|
69
|
-
"code-quality",
|
|
70
|
-
"code-scanner",
|
|
71
|
-
"next.js",
|
|
72
|
-
"react",
|
|
73
|
-
"typescript",
|
|
74
|
-
"static-analysis",
|
|
75
|
-
"cli",
|
|
76
|
-
"scanner",
|
|
77
|
-
"mcp",
|
|
78
|
-
"mcp-server",
|
|
79
|
-
"cursor",
|
|
80
|
-
"copilot",
|
|
81
|
-
"devtools"
|
|
82
|
-
]
|
|
83
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "prodlint",
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "The linter for vibe-coded apps — catch what AI coding tools miss",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "prodlint contributors",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/prodlint/prodlint"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/prodlint/prodlint/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://prodlint.com",
|
|
16
|
+
"bin": {
|
|
17
|
+
"prodlint": "./dist/cli.js",
|
|
18
|
+
"prodlint-mcp": "./dist/mcp.js"
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts"
|
|
26
|
+
},
|
|
27
|
+
"./mcp": {
|
|
28
|
+
"import": "./dist/mcp.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist/**/*.js",
|
|
33
|
+
"dist/**/*.d.ts",
|
|
34
|
+
"action.yml"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"dev": "tsup --watch",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:watch": "vitest",
|
|
41
|
+
"lint": "node dist/cli.js .",
|
|
42
|
+
"prepublishOnly": "npm run build"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@babel/parser": "^7.29.0",
|
|
46
|
+
"@babel/types": "^7.29.0",
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
48
|
+
"fast-glob": "^3.3.3",
|
|
49
|
+
"picocolors": "^1.1.1",
|
|
50
|
+
"zod": "^4.3.6"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^25.2.3",
|
|
54
|
+
"tsup": "^8.4.0",
|
|
55
|
+
"typescript": "^5.7.0",
|
|
56
|
+
"vitest": "^3.0.0"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=18"
|
|
60
|
+
},
|
|
61
|
+
"keywords": [
|
|
62
|
+
"lint",
|
|
63
|
+
"linter",
|
|
64
|
+
"security",
|
|
65
|
+
"ai",
|
|
66
|
+
"ai-code",
|
|
67
|
+
"vibe-coding",
|
|
68
|
+
"production-readiness",
|
|
69
|
+
"code-quality",
|
|
70
|
+
"code-scanner",
|
|
71
|
+
"next.js",
|
|
72
|
+
"react",
|
|
73
|
+
"typescript",
|
|
74
|
+
"static-analysis",
|
|
75
|
+
"cli",
|
|
76
|
+
"scanner",
|
|
77
|
+
"mcp",
|
|
78
|
+
"mcp-server",
|
|
79
|
+
"cursor",
|
|
80
|
+
"copilot",
|
|
81
|
+
"devtools"
|
|
82
|
+
]
|
|
83
|
+
}
|