@stateward/plugin-errors 1.0.1-rc.1 → 1.1.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 +60 -3
- package/dist/catch-hides-unexpected.js +47 -58
- package/dist/catch-misses-error.js +57 -67
- package/dist/finally-can-throw.js +45 -54
- package/dist/inference/format.d.ts +3 -3
- package/dist/inference/format.js +5 -5
- package/dist/inference/solve.js +1 -1
- package/dist/inference/summarize.d.ts +1 -1
- package/dist/inference/summarize.js +2 -2
- package/dist/must-handle-error.js +58 -66
- package/dist/narrow-try.d.ts +6 -1
- package/dist/narrow-try.js +137 -77
- package/dist/plugin.d.ts +0 -27
- package/dist/plugin.js +1 -3
- package/dist/react/boundaries.d.ts +3 -3
- package/dist/react/boundaries.js +5 -5
- package/dist/react/fallbacks.js +2 -2
- package/dist/refactors.d.ts +1 -1
- package/dist/refactors.js +1 -1
- package/dist/render-throw-without-boundary.js +64 -47
- package/dist/result-ignored.js +31 -37
- package/dist/result-unchecked.js +58 -61
- package/dist/rules.meta.d.ts +22 -47
- package/dist/rules.meta.js +19 -47
- package/dist/throws-doc-mismatch.js +35 -44
- package/package.json +2 -2
- package/dist/async-handler-unhandled.d.ts +0 -7
- package/dist/async-handler-unhandled.js +0 -342
- package/dist/catch-without-intent.d.ts +0 -10
- package/dist/catch-without-intent.js +0 -590
- package/dist/exhaustive-catch.d.ts +0 -6
- package/dist/exhaustive-catch.js +0 -190
- package/dist/exhaustive-fallback.d.ts +0 -6
- package/dist/exhaustive-fallback.js +0 -157
- package/dist/finally-may-throw.d.ts +0 -6
- package/dist/finally-may-throw.js +0 -184
- package/dist/no-swallow.d.ts +0 -8
- package/dist/no-swallow.js +0 -320
- package/dist/render-throws-without-boundary.d.ts +0 -6
- package/dist/render-throws-without-boundary.js +0 -151
- package/dist/rethrow-unknown.d.ts +0 -6
- package/dist/rethrow-unknown.js +0 -260
- package/dist/throw-in-hook.d.ts +0 -8
- package/dist/throw-in-hook.js +0 -510
- package/dist/throws-matches-doc.d.ts +0 -7
- package/dist/throws-matches-doc.js +0 -188
package/dist/rethrow-unknown.js
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import ts from "@stateward/core/typescript";
|
|
2
|
-
import { defineRule, option } from "@stateward/core";
|
|
3
|
-
import { stripParentheses } from "./syntax.js";
|
|
4
|
-
import { HEADLINE_MAX, codeList, fitted, oneLine, withExample } from "./text.js";
|
|
5
|
-
/** The group the rule belongs to in the docs and in `stateward status`. */
|
|
6
|
-
export const GROUP = "catch";
|
|
7
|
-
const EXPLAIN = `# errors/rethrow-unknown
|
|
8
|
-
|
|
9
|
-
A catch that handles some errors with instanceof also rethrows or handles all the others.
|
|
10
|
-
|
|
11
|
-
## Why
|
|
12
|
-
|
|
13
|
-
A \`catch\` that handles \`NotFoundError\` and \`AuthError\` with \`instanceof\`, and has no last branch, silently drops every other error: a bug, a network failure, a string someone threw. \`save(order)\` fails with a \`TypeError\`, neither branch matches, and the code carries on as if the order was saved.
|
|
14
|
-
|
|
15
|
-
## When to use it
|
|
16
|
-
|
|
17
|
-
Use it in any code that catches by class. It is on as an error in every preset; keep it on. It needs no knowledge of what the \`try\` throws, so it works even where \`errors/exhaustive-catch\` cannot see.
|
|
18
|
-
|
|
19
|
-
## What it catches
|
|
20
|
-
|
|
21
|
-
A \`catch (e)\` with an \`if\` / \`else if\` chain or \`switch (true)\` that tests \`e instanceof X\`, and no branch for the other errors. It is fine when:
|
|
22
|
-
|
|
23
|
-
- a last \`else\` or \`default\` does something: rethrows, wraps the error with \`{ cause: e }\`, or handles it;
|
|
24
|
-
- any statement follows the chain in the \`catch\`, since it runs for every error.
|
|
25
|
-
|
|
26
|
-
### Bad
|
|
27
|
-
|
|
28
|
-
\`\`\`ts
|
|
29
|
-
try {
|
|
30
|
-
await save(order);
|
|
31
|
-
} catch (e) {
|
|
32
|
-
if (e instanceof NotFoundError) {
|
|
33
|
-
showMissing();
|
|
34
|
-
} else if (e instanceof AuthError) {
|
|
35
|
-
redirectToLogin();
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
\`\`\`
|
|
39
|
-
|
|
40
|
-
### Good
|
|
41
|
-
|
|
42
|
-
\`\`\`ts
|
|
43
|
-
try {
|
|
44
|
-
await save(order);
|
|
45
|
-
} catch (e) {
|
|
46
|
-
if (e instanceof NotFoundError) {
|
|
47
|
-
showMissing();
|
|
48
|
-
} else if (e instanceof AuthError) {
|
|
49
|
-
redirectToLogin();
|
|
50
|
-
} else {
|
|
51
|
-
throw e;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
\`\`\`
|
|
55
|
-
|
|
56
|
-
### Fix
|
|
57
|
-
|
|
58
|
-
A quick fix adds \`else { throw e; }\` to an \`if\` chain. For a \`switch (true)\`, add \`default: throw e;\` yourself; the message shows it.
|
|
59
|
-
|
|
60
|
-
## What it does not catch
|
|
61
|
-
|
|
62
|
-
- A chain that tests another value, or tests other than \`instanceof\`.
|
|
63
|
-
- A \`catch\` with no parameter, or a destructured one.
|
|
64
|
-
- A last \`else\` that does something but still ignores the error (\`else { showGenericError(); }\`): it counts as handling the rest, unless you set \`restUsesError\` (below). \`errors/exhaustive-catch\` looks at what that \`else\` does with the error.
|
|
65
|
-
- A \`catch\` with no \`instanceof\` chain that ignores the error: that is \`errors/no-swallow\`.
|
|
66
|
-
|
|
67
|
-
## Turn it on or off
|
|
68
|
-
|
|
69
|
-
In \`stateward.config.ts\`:
|
|
70
|
-
|
|
71
|
-
\`\`\`ts
|
|
72
|
-
rules: { "errors/rethrow-unknown": "off" } // or "warning"
|
|
73
|
-
groups: { "errors/catch": "off" } // the whole group
|
|
74
|
-
rules: { "errors/rethrow-unknown": ["error", { restUsesError: true }] } // the last branch must use the error
|
|
75
|
-
\`\`\`
|
|
76
|
-
`;
|
|
77
|
-
/** The statements of a branch or block, without empty statements; a comment is not a statement. */
|
|
78
|
-
const OPTIONS = {
|
|
79
|
-
restUsesError: option.boolean({
|
|
80
|
-
default: false,
|
|
81
|
-
description: "The branch for the other errors must use the error (rethrow it, wrap it, report it); a generic message alone does not count.",
|
|
82
|
-
}),
|
|
83
|
-
};
|
|
84
|
-
function realStatements(node) {
|
|
85
|
-
if (node === undefined)
|
|
86
|
-
return [];
|
|
87
|
-
const statements = ts.isBlock(node) ? node.statements : [node];
|
|
88
|
-
return statements.filter((statement) => !ts.isEmptyStatement(statement));
|
|
89
|
-
}
|
|
90
|
-
/** The classes of the `instanceof` tests on `variable` in a condition, in source order. */
|
|
91
|
-
function instanceOfClasses(condition, variable, sourceFile) {
|
|
92
|
-
const node = stripParentheses(condition);
|
|
93
|
-
if (!ts.isBinaryExpression(node))
|
|
94
|
-
return [];
|
|
95
|
-
const operator = node.operatorToken.kind;
|
|
96
|
-
if (operator === ts.SyntaxKind.InstanceOfKeyword) {
|
|
97
|
-
const subject = stripParentheses(node.left);
|
|
98
|
-
return ts.isIdentifier(subject) && subject.text === variable
|
|
99
|
-
? [oneLine(stripParentheses(node.right).getText(sourceFile))]
|
|
100
|
-
: [];
|
|
101
|
-
}
|
|
102
|
-
if (operator === ts.SyntaxKind.BarBarToken ||
|
|
103
|
-
operator === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
104
|
-
return [
|
|
105
|
-
...instanceOfClasses(node.left, variable, sourceFile),
|
|
106
|
-
...instanceOfClasses(node.right, variable, sourceFile),
|
|
107
|
-
];
|
|
108
|
-
}
|
|
109
|
-
return [];
|
|
110
|
-
}
|
|
111
|
-
/** An `if` / `else if` chain, from its first `if` to its last link. */
|
|
112
|
-
function linksOf(start) {
|
|
113
|
-
const links = [start];
|
|
114
|
-
for (let last = start; last.elseStatement !== undefined && ts.isIfStatement(last.elseStatement);) {
|
|
115
|
-
last = last.elseStatement;
|
|
116
|
-
links.push(last);
|
|
117
|
-
}
|
|
118
|
-
return links;
|
|
119
|
-
}
|
|
120
|
-
function dispatchOf(statement, variable, sourceFile) {
|
|
121
|
-
if (ts.isIfStatement(statement)) {
|
|
122
|
-
const links = linksOf(statement);
|
|
123
|
-
const classes = links.flatMap((link) => instanceOfClasses(link.expression, variable, sourceFile));
|
|
124
|
-
const end = links[links.length - 1];
|
|
125
|
-
if (classes.length === 0 || end === undefined)
|
|
126
|
-
return undefined;
|
|
127
|
-
return {
|
|
128
|
-
start: statement,
|
|
129
|
-
classes,
|
|
130
|
-
rest: realStatements(end.elseStatement),
|
|
131
|
-
openChainEnd: end.elseStatement === undefined ? end : undefined,
|
|
132
|
-
kind: "if",
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
if (ts.isSwitchStatement(statement)) {
|
|
136
|
-
if (stripParentheses(statement.expression).kind !== ts.SyntaxKind.TrueKeyword)
|
|
137
|
-
return undefined;
|
|
138
|
-
const classes = statement.caseBlock.clauses.flatMap((clause) => ts.isCaseClause(clause) ? instanceOfClasses(clause.expression, variable, sourceFile) : []);
|
|
139
|
-
if (classes.length === 0)
|
|
140
|
-
return undefined;
|
|
141
|
-
const fallback = statement.caseBlock.clauses.find((clause) => ts.isDefaultClause(clause));
|
|
142
|
-
return {
|
|
143
|
-
start: statement,
|
|
144
|
-
classes,
|
|
145
|
-
rest: (fallback?.statements ?? []).filter((inner) => !ts.isEmptyStatement(inner)),
|
|
146
|
-
openChainEnd: undefined,
|
|
147
|
-
kind: "switch",
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
return undefined;
|
|
151
|
-
}
|
|
152
|
-
/** Whether any of `nodes` names `variable` (a nested function's own `e` counts too: rare, and safe). */
|
|
153
|
-
function mentions(nodes, variable) {
|
|
154
|
-
const visit = (node) => (ts.isIdentifier(node) && node.text === variable) || ts.forEachChild(node, visit) === true;
|
|
155
|
-
return nodes.some(visit);
|
|
156
|
-
}
|
|
157
|
-
function ignoringHeadline(classes, variable) {
|
|
158
|
-
return fitted(HEADLINE_MAX, (cut) => `errors other than ${codeList(classes, cut)} are handled here without ever using \`${variable}\``);
|
|
159
|
-
}
|
|
160
|
-
function headline(classes) {
|
|
161
|
-
return fitted(HEADLINE_MAX, (cut) => `errors other than ${codeList(classes, cut)} are silently swallowed here`);
|
|
162
|
-
}
|
|
163
|
-
function hint(dispatch, variable) {
|
|
164
|
-
if (dispatch.kind === "switch") {
|
|
165
|
-
return withExample("end the switch with a case for the rest:", [
|
|
166
|
-
"default:",
|
|
167
|
-
` throw ${variable};`,
|
|
168
|
-
]);
|
|
169
|
-
}
|
|
170
|
-
return withExample("end the chain with a branch for the rest:", [
|
|
171
|
-
"} else {",
|
|
172
|
-
` throw ${variable};`,
|
|
173
|
-
"}",
|
|
174
|
-
]);
|
|
175
|
-
}
|
|
176
|
-
/** The whitespace at the start of the line that holds `position`. */
|
|
177
|
-
function indentationAt(position, text) {
|
|
178
|
-
const lineStart = text.lastIndexOf("\n", position - 1) + 1;
|
|
179
|
-
return /^[ \t]*/.exec(text.slice(lineStart, position))?.[0] ?? "";
|
|
180
|
-
}
|
|
181
|
-
/** `else { throw e; }` after the last branch, at the indentation of the chain. */
|
|
182
|
-
function rethrowFix(start, end, variable, sourceFile) {
|
|
183
|
-
const indent = indentationAt(start.getStart(sourceFile), sourceFile.text);
|
|
184
|
-
const step = indent.includes("\t") ? "\t" : " ";
|
|
185
|
-
const at = end.thenStatement.getEnd();
|
|
186
|
-
return {
|
|
187
|
-
title: `Rethrow the other errors with \`else { throw ${variable}; }\``,
|
|
188
|
-
edits: [
|
|
189
|
-
{
|
|
190
|
-
location: { file: sourceFile.fileName, start: at, end: at },
|
|
191
|
-
newText: ` else {\n${indent}${step}throw ${variable};\n${indent}}`,
|
|
192
|
-
},
|
|
193
|
-
],
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
function checkCatch(clause, ctx) {
|
|
197
|
-
const declaration = clause.variableDeclaration;
|
|
198
|
-
if (declaration === undefined || !ts.isIdentifier(declaration.name))
|
|
199
|
-
return;
|
|
200
|
-
const variable = declaration.name.text;
|
|
201
|
-
const statements = clause.block.statements;
|
|
202
|
-
let last;
|
|
203
|
-
const classes = [];
|
|
204
|
-
for (const [index, statement] of statements.entries()) {
|
|
205
|
-
const dispatch = dispatchOf(statement, variable, ctx.sourceFile);
|
|
206
|
-
if (dispatch === undefined)
|
|
207
|
-
continue;
|
|
208
|
-
last = { dispatch, index };
|
|
209
|
-
for (const name of dispatch.classes)
|
|
210
|
-
if (!classes.includes(name))
|
|
211
|
-
classes.push(name);
|
|
212
|
-
}
|
|
213
|
-
if (last === undefined)
|
|
214
|
-
return;
|
|
215
|
-
const { dispatch, index } = last;
|
|
216
|
-
// A branch for the rest, or any statement that runs for every error after the chain.
|
|
217
|
-
const after = statements.slice(index + 1).filter((statement) => !ts.isEmptyStatement(statement));
|
|
218
|
-
const rest = [...dispatch.rest, ...after];
|
|
219
|
-
if (rest.length > 0) {
|
|
220
|
-
if (!ctx.options.restUsesError || mentions(rest, variable))
|
|
221
|
-
return;
|
|
222
|
-
ctx.report({
|
|
223
|
-
node: dispatch.start.getFirstToken(ctx.sourceFile) ?? dispatch.start,
|
|
224
|
-
message: ignoringHeadline(classes, variable),
|
|
225
|
-
hint: withExample("rethrow the rest, or pass the error on where you handle it:", [
|
|
226
|
-
`throw ${variable};`,
|
|
227
|
-
]),
|
|
228
|
-
});
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
const fixes = dispatch.openChainEnd === undefined
|
|
232
|
-
? []
|
|
233
|
-
: [rethrowFix(dispatch.start, dispatch.openChainEnd, variable, ctx.sourceFile)];
|
|
234
|
-
ctx.report({
|
|
235
|
-
node: dispatch.start.getFirstToken(ctx.sourceFile) ?? dispatch.start,
|
|
236
|
-
message: headline(classes),
|
|
237
|
-
hint: hint(dispatch, variable),
|
|
238
|
-
fixes,
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
export const rethrowUnknown = defineRule({
|
|
242
|
-
kind: "file",
|
|
243
|
-
meta: {
|
|
244
|
-
description: "A catch that handles some errors with instanceof also rethrows or handles all the others.",
|
|
245
|
-
defaultSeverity: "error",
|
|
246
|
-
tier: "core",
|
|
247
|
-
group: GROUP,
|
|
248
|
-
options: OPTIONS,
|
|
249
|
-
explain: EXPLAIN,
|
|
250
|
-
},
|
|
251
|
-
check(ctx) {
|
|
252
|
-
const visit = (node) => {
|
|
253
|
-
if (ts.isCatchClause(node))
|
|
254
|
-
checkCatch(node, ctx);
|
|
255
|
-
ts.forEachChild(node, visit);
|
|
256
|
-
};
|
|
257
|
-
visit(ctx.sourceFile);
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
//# sourceMappingURL=rethrow-unknown.js.map
|
package/dist/throw-in-hook.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/** The group the rule belongs to in the docs and in `stateward status`. */
|
|
2
|
-
export declare const GROUP = "react";
|
|
3
|
-
export declare const throwInHook: import("@stateward/core").FileRule<{
|
|
4
|
-
skipContextGuards: import("@stateward/core").BooleanOption;
|
|
5
|
-
skipInputGuards: import("@stateward/core").BooleanOption;
|
|
6
|
-
ignore: import("@stateward/core").StringsOption;
|
|
7
|
-
}, "react">;
|
|
8
|
-
//# sourceMappingURL=throw-in-hook.d.ts.map
|