prodlint 0.10.2 → 0.10.3
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 +1 -1
- package/action.yml +2 -2
- package/dist/cli.js +84 -90
- package/dist/index.js +84 -90
- package/dist/mcp.js +84 -90
- package/package.json +1 -1
package/README.md
CHANGED
package/action.yml
CHANGED
|
@@ -34,7 +34,7 @@ runs:
|
|
|
34
34
|
using: 'composite'
|
|
35
35
|
steps:
|
|
36
36
|
- name: Setup Node.js
|
|
37
|
-
uses: actions/setup-node@
|
|
37
|
+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
38
38
|
with:
|
|
39
39
|
node-version: '22'
|
|
40
40
|
# Automatic caching would key off the scanned repo's package.json; npx needs none.
|
|
@@ -58,7 +58,7 @@ runs:
|
|
|
58
58
|
|
|
59
59
|
# The exact release this action ships with (scripts/sync-versions.cjs bumps it), so
|
|
60
60
|
# pinning the action — @v1, a version tag or a commit SHA — also pins the scanner.
|
|
61
|
-
OUTPUT=$(npx -y prodlint@0.10.
|
|
61
|
+
OUTPUT=$(npx -y prodlint@0.10.3 "${CMD_ARGS[@]}" 2>&1) || true
|
|
62
62
|
|
|
63
63
|
SCORE=$(echo "$OUTPUT" | node -e "
|
|
64
64
|
const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
package/dist/cli.js
CHANGED
|
@@ -2487,15 +2487,16 @@ var deadExportsRule = {
|
|
|
2487
2487
|
};
|
|
2488
2488
|
|
|
2489
2489
|
// src/rules/shallow-catch.ts
|
|
2490
|
-
function
|
|
2491
|
-
if (
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2490
|
+
function reportsError(value, param) {
|
|
2491
|
+
if (/error/i.test(value) || /\berr|Err|status|Response|json/.test(value)) return true;
|
|
2492
|
+
return param !== void 0 && new RegExp(`(?<![\\w$])${param.replace(/\$/g, "\\$")}(?![\\w$])`).test(value);
|
|
2493
|
+
}
|
|
2494
|
+
function scoreCatchBody({ text: body, empty, returned, param }) {
|
|
2495
|
+
if (empty) return { score: 0, label: "empty catch" };
|
|
2495
2496
|
let score = 0;
|
|
2496
2497
|
if (/console\.(log|warn|error|info)\s*\(/.test(body)) score = 1;
|
|
2497
2498
|
if ((/console\.(error|warn)\s*\(/.test(body) || /\b(logger|log)\.(error|warn)\s*\(/.test(body)) && /\b(err|error|e)\b/.test(body)) score = 2;
|
|
2498
|
-
if (/\bthrow\b/.test(body) ||
|
|
2499
|
+
if (/\bthrow\b/.test(body) || returned.some((value) => reportsError(value, param)) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body) || // Toast notifications (react-toastify, sonner, shadcn)
|
|
2499
2500
|
/\btoast\.(error|warn|warning)\s*\(/.test(body) || /\btoast\s*\(\s*\{/.test(body) || // Error monitoring (Sentry, etc.)
|
|
2500
2501
|
/\b(Sentry\.)?captureException\s*\(/.test(body) || /\b(Sentry\.)?captureMessage\s*\(/.test(body) || // Structured loggers
|
|
2501
2502
|
/\b(logger|log)\.(error|fatal)\s*\(/.test(body) || // Error handler utilities
|
|
@@ -2505,13 +2506,56 @@ function scoreCatchBody(bodyLines) {
|
|
|
2505
2506
|
score = 3;
|
|
2506
2507
|
}
|
|
2507
2508
|
const labels = {
|
|
2508
|
-
0: "
|
|
2509
|
+
0: "catch swallows the error",
|
|
2509
2510
|
1: "catch only logs (no recovery or propagation)",
|
|
2510
2511
|
2: "catch logs error but does not propagate or recover",
|
|
2511
2512
|
3: "catch handles error properly"
|
|
2512
2513
|
};
|
|
2513
2514
|
return { score, label: labels[score] };
|
|
2514
2515
|
}
|
|
2516
|
+
function catchFinding(file, catchLine, body) {
|
|
2517
|
+
const { score, label } = scoreCatchBody(body);
|
|
2518
|
+
if (score > 1) return null;
|
|
2519
|
+
return {
|
|
2520
|
+
ruleId: "shallow-catch",
|
|
2521
|
+
file: file.relativePath,
|
|
2522
|
+
line: catchLine + 1,
|
|
2523
|
+
column: file.lines[catchLine].indexOf("catch") + 1,
|
|
2524
|
+
message: body.empty ? "Empty catch block \u2014 errors are silently swallowed" : score === 0 ? "Catch block swallows the error \u2014 it isn't logged, rethrown or returned" : `Decorative error handler: ${label}`,
|
|
2525
|
+
severity: score === 0 ? "warning" : "info",
|
|
2526
|
+
category: "reliability",
|
|
2527
|
+
fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
var NON_CODE = /\/\/.*|\/\*[\s\S]*?\*\/|'(?:[^'\\\n]|\\[\s\S])*'|"(?:[^"\\\n]|\\[\s\S])*"|`(?:[^`\\]|\\[\s\S])*`/g;
|
|
2531
|
+
function blankOut(source, strings) {
|
|
2532
|
+
const blank = (s) => s.replace(/[^\n]/g, " ");
|
|
2533
|
+
return source.replace(NON_CODE, (m) => m[0] === "/" ? blank(m) : strings ? m[0] + blank(m.slice(1, -1)) + m[0] : m);
|
|
2534
|
+
}
|
|
2535
|
+
function closingBrace(code, open) {
|
|
2536
|
+
let depth = 0;
|
|
2537
|
+
for (let i = open; i < code.length; i++) {
|
|
2538
|
+
if (code[i] === "{") depth++;
|
|
2539
|
+
else if (code[i] === "}" && --depth === 0) return i;
|
|
2540
|
+
}
|
|
2541
|
+
return -1;
|
|
2542
|
+
}
|
|
2543
|
+
function returnedValues(code, text) {
|
|
2544
|
+
const values = [];
|
|
2545
|
+
for (const m of code.matchAll(/(?<![\w$.])return\b/g)) {
|
|
2546
|
+
const start = m.index + "return".length;
|
|
2547
|
+
let end = start;
|
|
2548
|
+
for (let depth = 0; end < code.length; end++) {
|
|
2549
|
+
const ch = code[end];
|
|
2550
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
2551
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
2552
|
+
if (depth-- === 0) break;
|
|
2553
|
+
} else if (depth === 0 && (ch === ";" || ch === "\n")) break;
|
|
2554
|
+
}
|
|
2555
|
+
values.push(text.slice(start, end));
|
|
2556
|
+
}
|
|
2557
|
+
return values;
|
|
2558
|
+
}
|
|
2515
2559
|
var shallowCatchRule = {
|
|
2516
2560
|
id: "shallow-catch",
|
|
2517
2561
|
name: "Shallow Error Handler",
|
|
@@ -2528,95 +2572,45 @@ var shallowCatchRule = {
|
|
|
2528
2572
|
walkAST(file.ast.program, (node) => {
|
|
2529
2573
|
if (node.type !== "CatchClause") return;
|
|
2530
2574
|
if (!node.loc) return;
|
|
2531
|
-
const
|
|
2575
|
+
const catchLine2 = node.loc.start.line - 1;
|
|
2532
2576
|
const body = node.body;
|
|
2533
|
-
if (
|
|
2534
|
-
const
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
|
|
2548
|
-
});
|
|
2549
|
-
}
|
|
2577
|
+
if (body?.start == null || body.end == null) return;
|
|
2578
|
+
const returned = [];
|
|
2579
|
+
walkAST(body, (n) => {
|
|
2580
|
+
if (n.type === "ReturnStatement" && n.argument?.start != null && n.argument.end != null) {
|
|
2581
|
+
returned.push(file.content.slice(n.argument.start, n.argument.end));
|
|
2582
|
+
}
|
|
2583
|
+
});
|
|
2584
|
+
const finding = catchFinding(file, catchLine2, {
|
|
2585
|
+
text: file.content.slice(body.start + 1, body.end - 1),
|
|
2586
|
+
empty: body.body.length === 0,
|
|
2587
|
+
returned,
|
|
2588
|
+
param: node.param?.type === "Identifier" ? node.param.name : void 0
|
|
2589
|
+
});
|
|
2590
|
+
if (finding) findings.push(finding);
|
|
2550
2591
|
});
|
|
2551
2592
|
return findings;
|
|
2552
2593
|
} catch {
|
|
2553
2594
|
}
|
|
2554
2595
|
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
const line = file.lines[j];
|
|
2574
|
-
const startPos = j === braceStart ? line.indexOf("{") : 0;
|
|
2575
|
-
for (let k = startPos; k < line.length; k++) {
|
|
2576
|
-
const ch = line[k];
|
|
2577
|
-
const prev = k > 0 ? line[k - 1] : "";
|
|
2578
|
-
const escaped = prev === "\\" && (k < 2 || line[k - 2] !== "\\");
|
|
2579
|
-
if (!escaped) {
|
|
2580
|
-
if (ch === "'" && !inDouble && !inTemplate) {
|
|
2581
|
-
inSingle = !inSingle;
|
|
2582
|
-
continue;
|
|
2583
|
-
}
|
|
2584
|
-
if (ch === '"' && !inSingle && !inTemplate) {
|
|
2585
|
-
inDouble = !inDouble;
|
|
2586
|
-
continue;
|
|
2587
|
-
}
|
|
2588
|
-
if (ch === "`" && !inSingle && !inDouble) {
|
|
2589
|
-
inTemplate = !inTemplate;
|
|
2590
|
-
continue;
|
|
2591
|
-
}
|
|
2592
|
-
}
|
|
2593
|
-
if (inSingle || inDouble || inTemplate) continue;
|
|
2594
|
-
if (ch === "{") depth++;
|
|
2595
|
-
if (ch === "}") {
|
|
2596
|
-
depth--;
|
|
2597
|
-
if (depth === 0) {
|
|
2598
|
-
bodyEnd = j;
|
|
2599
|
-
break;
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
}
|
|
2603
|
-
if (depth === 0) break;
|
|
2604
|
-
}
|
|
2605
|
-
const bodyLines = file.lines.slice(braceStart + 1, bodyEnd);
|
|
2606
|
-
const { score, label } = scoreCatchBody(bodyLines);
|
|
2607
|
-
if (score <= 1) {
|
|
2608
|
-
findings.push({
|
|
2609
|
-
ruleId: "shallow-catch",
|
|
2610
|
-
file: file.relativePath,
|
|
2611
|
-
line: i + 1,
|
|
2612
|
-
column: file.lines[i].indexOf("catch") + 1,
|
|
2613
|
-
message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
|
|
2614
|
-
severity: score === 0 ? "warning" : "info",
|
|
2615
|
-
category: "reliability",
|
|
2616
|
-
fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
|
|
2617
|
-
});
|
|
2618
|
-
}
|
|
2619
|
-
i = bodyEnd;
|
|
2596
|
+
const source = file.lines.join("\n");
|
|
2597
|
+
const code = blankOut(source, true);
|
|
2598
|
+
const noComments = blankOut(source, false);
|
|
2599
|
+
const catchRe = /(?<![\w$.])catch\s*(?:\(([^)]*)\)\s*)?\{/g;
|
|
2600
|
+
let catchLine = 0;
|
|
2601
|
+
let scanned = 0;
|
|
2602
|
+
for (let m = catchRe.exec(code); m; m = catchRe.exec(code)) {
|
|
2603
|
+
for (; scanned < m.index; scanned++) if (code[scanned] === "\n") catchLine++;
|
|
2604
|
+
const open = m.index + m[0].length - 1;
|
|
2605
|
+
const close = closingBrace(code, open);
|
|
2606
|
+
if (close === -1) continue;
|
|
2607
|
+
const finding = catchFinding(file, catchLine, {
|
|
2608
|
+
text: source.slice(open + 1, close),
|
|
2609
|
+
empty: code.slice(open + 1, close).trim() === "",
|
|
2610
|
+
returned: returnedValues(code.slice(open + 1, close), noComments.slice(open + 1, close)),
|
|
2611
|
+
param: m[1]?.match(/^\s*([A-Za-z_$][\w$]*)/)?.[1]
|
|
2612
|
+
});
|
|
2613
|
+
if (finding) findings.push(finding);
|
|
2620
2614
|
}
|
|
2621
2615
|
return findings;
|
|
2622
2616
|
}
|
package/dist/index.js
CHANGED
|
@@ -2481,15 +2481,16 @@ var deadExportsRule = {
|
|
|
2481
2481
|
};
|
|
2482
2482
|
|
|
2483
2483
|
// src/rules/shallow-catch.ts
|
|
2484
|
-
function
|
|
2485
|
-
if (
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2484
|
+
function reportsError(value, param) {
|
|
2485
|
+
if (/error/i.test(value) || /\berr|Err|status|Response|json/.test(value)) return true;
|
|
2486
|
+
return param !== void 0 && new RegExp(`(?<![\\w$])${param.replace(/\$/g, "\\$")}(?![\\w$])`).test(value);
|
|
2487
|
+
}
|
|
2488
|
+
function scoreCatchBody({ text: body, empty, returned, param }) {
|
|
2489
|
+
if (empty) return { score: 0, label: "empty catch" };
|
|
2489
2490
|
let score = 0;
|
|
2490
2491
|
if (/console\.(log|warn|error|info)\s*\(/.test(body)) score = 1;
|
|
2491
2492
|
if ((/console\.(error|warn)\s*\(/.test(body) || /\b(logger|log)\.(error|warn)\s*\(/.test(body)) && /\b(err|error|e)\b/.test(body)) score = 2;
|
|
2492
|
-
if (/\bthrow\b/.test(body) ||
|
|
2493
|
+
if (/\bthrow\b/.test(body) || returned.some((value) => reportsError(value, param)) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body) || // Toast notifications (react-toastify, sonner, shadcn)
|
|
2493
2494
|
/\btoast\.(error|warn|warning)\s*\(/.test(body) || /\btoast\s*\(\s*\{/.test(body) || // Error monitoring (Sentry, etc.)
|
|
2494
2495
|
/\b(Sentry\.)?captureException\s*\(/.test(body) || /\b(Sentry\.)?captureMessage\s*\(/.test(body) || // Structured loggers
|
|
2495
2496
|
/\b(logger|log)\.(error|fatal)\s*\(/.test(body) || // Error handler utilities
|
|
@@ -2499,13 +2500,56 @@ function scoreCatchBody(bodyLines) {
|
|
|
2499
2500
|
score = 3;
|
|
2500
2501
|
}
|
|
2501
2502
|
const labels = {
|
|
2502
|
-
0: "
|
|
2503
|
+
0: "catch swallows the error",
|
|
2503
2504
|
1: "catch only logs (no recovery or propagation)",
|
|
2504
2505
|
2: "catch logs error but does not propagate or recover",
|
|
2505
2506
|
3: "catch handles error properly"
|
|
2506
2507
|
};
|
|
2507
2508
|
return { score, label: labels[score] };
|
|
2508
2509
|
}
|
|
2510
|
+
function catchFinding(file, catchLine, body) {
|
|
2511
|
+
const { score, label } = scoreCatchBody(body);
|
|
2512
|
+
if (score > 1) return null;
|
|
2513
|
+
return {
|
|
2514
|
+
ruleId: "shallow-catch",
|
|
2515
|
+
file: file.relativePath,
|
|
2516
|
+
line: catchLine + 1,
|
|
2517
|
+
column: file.lines[catchLine].indexOf("catch") + 1,
|
|
2518
|
+
message: body.empty ? "Empty catch block \u2014 errors are silently swallowed" : score === 0 ? "Catch block swallows the error \u2014 it isn't logged, rethrown or returned" : `Decorative error handler: ${label}`,
|
|
2519
|
+
severity: score === 0 ? "warning" : "info",
|
|
2520
|
+
category: "reliability",
|
|
2521
|
+
fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
var NON_CODE = /\/\/.*|\/\*[\s\S]*?\*\/|'(?:[^'\\\n]|\\[\s\S])*'|"(?:[^"\\\n]|\\[\s\S])*"|`(?:[^`\\]|\\[\s\S])*`/g;
|
|
2525
|
+
function blankOut(source, strings) {
|
|
2526
|
+
const blank = (s) => s.replace(/[^\n]/g, " ");
|
|
2527
|
+
return source.replace(NON_CODE, (m) => m[0] === "/" ? blank(m) : strings ? m[0] + blank(m.slice(1, -1)) + m[0] : m);
|
|
2528
|
+
}
|
|
2529
|
+
function closingBrace(code, open) {
|
|
2530
|
+
let depth = 0;
|
|
2531
|
+
for (let i = open; i < code.length; i++) {
|
|
2532
|
+
if (code[i] === "{") depth++;
|
|
2533
|
+
else if (code[i] === "}" && --depth === 0) return i;
|
|
2534
|
+
}
|
|
2535
|
+
return -1;
|
|
2536
|
+
}
|
|
2537
|
+
function returnedValues(code, text) {
|
|
2538
|
+
const values = [];
|
|
2539
|
+
for (const m of code.matchAll(/(?<![\w$.])return\b/g)) {
|
|
2540
|
+
const start = m.index + "return".length;
|
|
2541
|
+
let end = start;
|
|
2542
|
+
for (let depth = 0; end < code.length; end++) {
|
|
2543
|
+
const ch = code[end];
|
|
2544
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
2545
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
2546
|
+
if (depth-- === 0) break;
|
|
2547
|
+
} else if (depth === 0 && (ch === ";" || ch === "\n")) break;
|
|
2548
|
+
}
|
|
2549
|
+
values.push(text.slice(start, end));
|
|
2550
|
+
}
|
|
2551
|
+
return values;
|
|
2552
|
+
}
|
|
2509
2553
|
var shallowCatchRule = {
|
|
2510
2554
|
id: "shallow-catch",
|
|
2511
2555
|
name: "Shallow Error Handler",
|
|
@@ -2522,95 +2566,45 @@ var shallowCatchRule = {
|
|
|
2522
2566
|
walkAST(file.ast.program, (node) => {
|
|
2523
2567
|
if (node.type !== "CatchClause") return;
|
|
2524
2568
|
if (!node.loc) return;
|
|
2525
|
-
const
|
|
2569
|
+
const catchLine2 = node.loc.start.line - 1;
|
|
2526
2570
|
const body = node.body;
|
|
2527
|
-
if (
|
|
2528
|
-
const
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
|
|
2542
|
-
});
|
|
2543
|
-
}
|
|
2571
|
+
if (body?.start == null || body.end == null) return;
|
|
2572
|
+
const returned = [];
|
|
2573
|
+
walkAST(body, (n) => {
|
|
2574
|
+
if (n.type === "ReturnStatement" && n.argument?.start != null && n.argument.end != null) {
|
|
2575
|
+
returned.push(file.content.slice(n.argument.start, n.argument.end));
|
|
2576
|
+
}
|
|
2577
|
+
});
|
|
2578
|
+
const finding = catchFinding(file, catchLine2, {
|
|
2579
|
+
text: file.content.slice(body.start + 1, body.end - 1),
|
|
2580
|
+
empty: body.body.length === 0,
|
|
2581
|
+
returned,
|
|
2582
|
+
param: node.param?.type === "Identifier" ? node.param.name : void 0
|
|
2583
|
+
});
|
|
2584
|
+
if (finding) findings.push(finding);
|
|
2544
2585
|
});
|
|
2545
2586
|
return findings;
|
|
2546
2587
|
} catch {
|
|
2547
2588
|
}
|
|
2548
2589
|
}
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
const line = file.lines[j];
|
|
2568
|
-
const startPos = j === braceStart ? line.indexOf("{") : 0;
|
|
2569
|
-
for (let k = startPos; k < line.length; k++) {
|
|
2570
|
-
const ch = line[k];
|
|
2571
|
-
const prev = k > 0 ? line[k - 1] : "";
|
|
2572
|
-
const escaped = prev === "\\" && (k < 2 || line[k - 2] !== "\\");
|
|
2573
|
-
if (!escaped) {
|
|
2574
|
-
if (ch === "'" && !inDouble && !inTemplate) {
|
|
2575
|
-
inSingle = !inSingle;
|
|
2576
|
-
continue;
|
|
2577
|
-
}
|
|
2578
|
-
if (ch === '"' && !inSingle && !inTemplate) {
|
|
2579
|
-
inDouble = !inDouble;
|
|
2580
|
-
continue;
|
|
2581
|
-
}
|
|
2582
|
-
if (ch === "`" && !inSingle && !inDouble) {
|
|
2583
|
-
inTemplate = !inTemplate;
|
|
2584
|
-
continue;
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
if (inSingle || inDouble || inTemplate) continue;
|
|
2588
|
-
if (ch === "{") depth++;
|
|
2589
|
-
if (ch === "}") {
|
|
2590
|
-
depth--;
|
|
2591
|
-
if (depth === 0) {
|
|
2592
|
-
bodyEnd = j;
|
|
2593
|
-
break;
|
|
2594
|
-
}
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
if (depth === 0) break;
|
|
2598
|
-
}
|
|
2599
|
-
const bodyLines = file.lines.slice(braceStart + 1, bodyEnd);
|
|
2600
|
-
const { score, label } = scoreCatchBody(bodyLines);
|
|
2601
|
-
if (score <= 1) {
|
|
2602
|
-
findings.push({
|
|
2603
|
-
ruleId: "shallow-catch",
|
|
2604
|
-
file: file.relativePath,
|
|
2605
|
-
line: i + 1,
|
|
2606
|
-
column: file.lines[i].indexOf("catch") + 1,
|
|
2607
|
-
message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
|
|
2608
|
-
severity: score === 0 ? "warning" : "info",
|
|
2609
|
-
category: "reliability",
|
|
2610
|
-
fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
|
|
2611
|
-
});
|
|
2612
|
-
}
|
|
2613
|
-
i = bodyEnd;
|
|
2590
|
+
const source = file.lines.join("\n");
|
|
2591
|
+
const code = blankOut(source, true);
|
|
2592
|
+
const noComments = blankOut(source, false);
|
|
2593
|
+
const catchRe = /(?<![\w$.])catch\s*(?:\(([^)]*)\)\s*)?\{/g;
|
|
2594
|
+
let catchLine = 0;
|
|
2595
|
+
let scanned = 0;
|
|
2596
|
+
for (let m = catchRe.exec(code); m; m = catchRe.exec(code)) {
|
|
2597
|
+
for (; scanned < m.index; scanned++) if (code[scanned] === "\n") catchLine++;
|
|
2598
|
+
const open = m.index + m[0].length - 1;
|
|
2599
|
+
const close = closingBrace(code, open);
|
|
2600
|
+
if (close === -1) continue;
|
|
2601
|
+
const finding = catchFinding(file, catchLine, {
|
|
2602
|
+
text: source.slice(open + 1, close),
|
|
2603
|
+
empty: code.slice(open + 1, close).trim() === "",
|
|
2604
|
+
returned: returnedValues(code.slice(open + 1, close), noComments.slice(open + 1, close)),
|
|
2605
|
+
param: m[1]?.match(/^\s*([A-Za-z_$][\w$]*)/)?.[1]
|
|
2606
|
+
});
|
|
2607
|
+
if (finding) findings.push(finding);
|
|
2614
2608
|
}
|
|
2615
2609
|
return findings;
|
|
2616
2610
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -2490,15 +2490,16 @@ var deadExportsRule = {
|
|
|
2490
2490
|
};
|
|
2491
2491
|
|
|
2492
2492
|
// src/rules/shallow-catch.ts
|
|
2493
|
-
function
|
|
2494
|
-
if (
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2493
|
+
function reportsError(value, param) {
|
|
2494
|
+
if (/error/i.test(value) || /\berr|Err|status|Response|json/.test(value)) return true;
|
|
2495
|
+
return param !== void 0 && new RegExp(`(?<![\\w$])${param.replace(/\$/g, "\\$")}(?![\\w$])`).test(value);
|
|
2496
|
+
}
|
|
2497
|
+
function scoreCatchBody({ text: body, empty, returned, param }) {
|
|
2498
|
+
if (empty) return { score: 0, label: "empty catch" };
|
|
2498
2499
|
let score = 0;
|
|
2499
2500
|
if (/console\.(log|warn|error|info)\s*\(/.test(body)) score = 1;
|
|
2500
2501
|
if ((/console\.(error|warn)\s*\(/.test(body) || /\b(logger|log)\.(error|warn)\s*\(/.test(body)) && /\b(err|error|e)\b/.test(body)) score = 2;
|
|
2501
|
-
if (/\bthrow\b/.test(body) ||
|
|
2502
|
+
if (/\bthrow\b/.test(body) || returned.some((value) => reportsError(value, param)) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body) || // Toast notifications (react-toastify, sonner, shadcn)
|
|
2502
2503
|
/\btoast\.(error|warn|warning)\s*\(/.test(body) || /\btoast\s*\(\s*\{/.test(body) || // Error monitoring (Sentry, etc.)
|
|
2503
2504
|
/\b(Sentry\.)?captureException\s*\(/.test(body) || /\b(Sentry\.)?captureMessage\s*\(/.test(body) || // Structured loggers
|
|
2504
2505
|
/\b(logger|log)\.(error|fatal)\s*\(/.test(body) || // Error handler utilities
|
|
@@ -2508,13 +2509,56 @@ function scoreCatchBody(bodyLines) {
|
|
|
2508
2509
|
score = 3;
|
|
2509
2510
|
}
|
|
2510
2511
|
const labels = {
|
|
2511
|
-
0: "
|
|
2512
|
+
0: "catch swallows the error",
|
|
2512
2513
|
1: "catch only logs (no recovery or propagation)",
|
|
2513
2514
|
2: "catch logs error but does not propagate or recover",
|
|
2514
2515
|
3: "catch handles error properly"
|
|
2515
2516
|
};
|
|
2516
2517
|
return { score, label: labels[score] };
|
|
2517
2518
|
}
|
|
2519
|
+
function catchFinding(file, catchLine, body) {
|
|
2520
|
+
const { score, label } = scoreCatchBody(body);
|
|
2521
|
+
if (score > 1) return null;
|
|
2522
|
+
return {
|
|
2523
|
+
ruleId: "shallow-catch",
|
|
2524
|
+
file: file.relativePath,
|
|
2525
|
+
line: catchLine + 1,
|
|
2526
|
+
column: file.lines[catchLine].indexOf("catch") + 1,
|
|
2527
|
+
message: body.empty ? "Empty catch block \u2014 errors are silently swallowed" : score === 0 ? "Catch block swallows the error \u2014 it isn't logged, rethrown or returned" : `Decorative error handler: ${label}`,
|
|
2528
|
+
severity: score === 0 ? "warning" : "info",
|
|
2529
|
+
category: "reliability",
|
|
2530
|
+
fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
var NON_CODE = /\/\/.*|\/\*[\s\S]*?\*\/|'(?:[^'\\\n]|\\[\s\S])*'|"(?:[^"\\\n]|\\[\s\S])*"|`(?:[^`\\]|\\[\s\S])*`/g;
|
|
2534
|
+
function blankOut(source, strings) {
|
|
2535
|
+
const blank = (s) => s.replace(/[^\n]/g, " ");
|
|
2536
|
+
return source.replace(NON_CODE, (m) => m[0] === "/" ? blank(m) : strings ? m[0] + blank(m.slice(1, -1)) + m[0] : m);
|
|
2537
|
+
}
|
|
2538
|
+
function closingBrace(code, open) {
|
|
2539
|
+
let depth = 0;
|
|
2540
|
+
for (let i = open; i < code.length; i++) {
|
|
2541
|
+
if (code[i] === "{") depth++;
|
|
2542
|
+
else if (code[i] === "}" && --depth === 0) return i;
|
|
2543
|
+
}
|
|
2544
|
+
return -1;
|
|
2545
|
+
}
|
|
2546
|
+
function returnedValues(code, text) {
|
|
2547
|
+
const values = [];
|
|
2548
|
+
for (const m of code.matchAll(/(?<![\w$.])return\b/g)) {
|
|
2549
|
+
const start = m.index + "return".length;
|
|
2550
|
+
let end = start;
|
|
2551
|
+
for (let depth = 0; end < code.length; end++) {
|
|
2552
|
+
const ch = code[end];
|
|
2553
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
2554
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
2555
|
+
if (depth-- === 0) break;
|
|
2556
|
+
} else if (depth === 0 && (ch === ";" || ch === "\n")) break;
|
|
2557
|
+
}
|
|
2558
|
+
values.push(text.slice(start, end));
|
|
2559
|
+
}
|
|
2560
|
+
return values;
|
|
2561
|
+
}
|
|
2518
2562
|
var shallowCatchRule = {
|
|
2519
2563
|
id: "shallow-catch",
|
|
2520
2564
|
name: "Shallow Error Handler",
|
|
@@ -2531,95 +2575,45 @@ var shallowCatchRule = {
|
|
|
2531
2575
|
walkAST(file.ast.program, (node) => {
|
|
2532
2576
|
if (node.type !== "CatchClause") return;
|
|
2533
2577
|
if (!node.loc) return;
|
|
2534
|
-
const
|
|
2578
|
+
const catchLine2 = node.loc.start.line - 1;
|
|
2535
2579
|
const body = node.body;
|
|
2536
|
-
if (
|
|
2537
|
-
const
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
|
|
2551
|
-
});
|
|
2552
|
-
}
|
|
2580
|
+
if (body?.start == null || body.end == null) return;
|
|
2581
|
+
const returned = [];
|
|
2582
|
+
walkAST(body, (n) => {
|
|
2583
|
+
if (n.type === "ReturnStatement" && n.argument?.start != null && n.argument.end != null) {
|
|
2584
|
+
returned.push(file.content.slice(n.argument.start, n.argument.end));
|
|
2585
|
+
}
|
|
2586
|
+
});
|
|
2587
|
+
const finding = catchFinding(file, catchLine2, {
|
|
2588
|
+
text: file.content.slice(body.start + 1, body.end - 1),
|
|
2589
|
+
empty: body.body.length === 0,
|
|
2590
|
+
returned,
|
|
2591
|
+
param: node.param?.type === "Identifier" ? node.param.name : void 0
|
|
2592
|
+
});
|
|
2593
|
+
if (finding) findings.push(finding);
|
|
2553
2594
|
});
|
|
2554
2595
|
return findings;
|
|
2555
2596
|
} catch {
|
|
2556
2597
|
}
|
|
2557
2598
|
}
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
const line = file.lines[j];
|
|
2577
|
-
const startPos = j === braceStart ? line.indexOf("{") : 0;
|
|
2578
|
-
for (let k = startPos; k < line.length; k++) {
|
|
2579
|
-
const ch = line[k];
|
|
2580
|
-
const prev = k > 0 ? line[k - 1] : "";
|
|
2581
|
-
const escaped = prev === "\\" && (k < 2 || line[k - 2] !== "\\");
|
|
2582
|
-
if (!escaped) {
|
|
2583
|
-
if (ch === "'" && !inDouble && !inTemplate) {
|
|
2584
|
-
inSingle = !inSingle;
|
|
2585
|
-
continue;
|
|
2586
|
-
}
|
|
2587
|
-
if (ch === '"' && !inSingle && !inTemplate) {
|
|
2588
|
-
inDouble = !inDouble;
|
|
2589
|
-
continue;
|
|
2590
|
-
}
|
|
2591
|
-
if (ch === "`" && !inSingle && !inDouble) {
|
|
2592
|
-
inTemplate = !inTemplate;
|
|
2593
|
-
continue;
|
|
2594
|
-
}
|
|
2595
|
-
}
|
|
2596
|
-
if (inSingle || inDouble || inTemplate) continue;
|
|
2597
|
-
if (ch === "{") depth++;
|
|
2598
|
-
if (ch === "}") {
|
|
2599
|
-
depth--;
|
|
2600
|
-
if (depth === 0) {
|
|
2601
|
-
bodyEnd = j;
|
|
2602
|
-
break;
|
|
2603
|
-
}
|
|
2604
|
-
}
|
|
2605
|
-
}
|
|
2606
|
-
if (depth === 0) break;
|
|
2607
|
-
}
|
|
2608
|
-
const bodyLines = file.lines.slice(braceStart + 1, bodyEnd);
|
|
2609
|
-
const { score, label } = scoreCatchBody(bodyLines);
|
|
2610
|
-
if (score <= 1) {
|
|
2611
|
-
findings.push({
|
|
2612
|
-
ruleId: "shallow-catch",
|
|
2613
|
-
file: file.relativePath,
|
|
2614
|
-
line: i + 1,
|
|
2615
|
-
column: file.lines[i].indexOf("catch") + 1,
|
|
2616
|
-
message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
|
|
2617
|
-
severity: score === 0 ? "warning" : "info",
|
|
2618
|
-
category: "reliability",
|
|
2619
|
-
fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
|
|
2620
|
-
});
|
|
2621
|
-
}
|
|
2622
|
-
i = bodyEnd;
|
|
2599
|
+
const source = file.lines.join("\n");
|
|
2600
|
+
const code = blankOut(source, true);
|
|
2601
|
+
const noComments = blankOut(source, false);
|
|
2602
|
+
const catchRe = /(?<![\w$.])catch\s*(?:\(([^)]*)\)\s*)?\{/g;
|
|
2603
|
+
let catchLine = 0;
|
|
2604
|
+
let scanned = 0;
|
|
2605
|
+
for (let m = catchRe.exec(code); m; m = catchRe.exec(code)) {
|
|
2606
|
+
for (; scanned < m.index; scanned++) if (code[scanned] === "\n") catchLine++;
|
|
2607
|
+
const open = m.index + m[0].length - 1;
|
|
2608
|
+
const close = closingBrace(code, open);
|
|
2609
|
+
if (close === -1) continue;
|
|
2610
|
+
const finding = catchFinding(file, catchLine, {
|
|
2611
|
+
text: source.slice(open + 1, close),
|
|
2612
|
+
empty: code.slice(open + 1, close).trim() === "",
|
|
2613
|
+
returned: returnedValues(code.slice(open + 1, close), noComments.slice(open + 1, close)),
|
|
2614
|
+
param: m[1]?.match(/^\s*([A-Za-z_$][\w$]*)/)?.[1]
|
|
2615
|
+
});
|
|
2616
|
+
if (finding) findings.push(finding);
|
|
2623
2617
|
}
|
|
2624
2618
|
return findings;
|
|
2625
2619
|
}
|