@stateward/plugin-errors 1.0.1-rc.1 → 1.0.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/README.md +61 -3
- package/package.json +2 -2
- package/dist/async-handler-unhandled.d.ts +0 -7
- package/dist/async-handler-unhandled.js +0 -342
- 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
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { defineRule, highestUncovered, option, shortestPath } from "@stateward/core";
|
|
2
|
-
import { className } from "./inference/format.js";
|
|
3
|
-
import { modelOf, summarizeReact } from "./react/boundaries.js";
|
|
4
|
-
import { LIBRARY_BOUNDARIES } from "./react/tree.js";
|
|
5
|
-
import { HEADLINE_MAX, codeList, fitted, withExample } from "./text.js";
|
|
6
|
-
/** The group the rule belongs to in the docs and in `stateward status`. */
|
|
7
|
-
export const GROUP = "react";
|
|
8
|
-
const EXPLAIN = `# errors/render-throws-without-boundary
|
|
9
|
-
|
|
10
|
-
A component that can throw a known error while rendering has an error boundary above it.
|
|
11
|
-
|
|
12
|
-
## Why
|
|
13
|
-
|
|
14
|
-
When a component throws while React renders it, or in one of its effects, React unmounts everything up to the nearest error boundary. With no boundary above, the whole app goes blank. A \`<UserPage>\` whose \`useUser\` throws \`NotFoundError\` for a wrong id takes the menu, the header and every other route down with it, where a boundary could have shown "this user does not exist".
|
|
15
|
-
|
|
16
|
-
## When to use it
|
|
17
|
-
|
|
18
|
-
Use it in React apps whose code throws its own error classes. It is a core rule, a warning in every preset: whether a page should handle an error itself or let a boundary do it is your call. If every throw in render is a bug you want to see crash in development, turn it off.
|
|
19
|
-
|
|
20
|
-
## What it catches
|
|
21
|
-
|
|
22
|
-
A place that renders a component which can throw a known error while rendering, with no error boundary above it on the way from your entry point (\`createRoot(…).render(…)\` or \`hydrateRoot\`). The component counts what its body and its hooks throw, what its \`useEffect\` and \`useLayoutEffect\` callbacks throw, and what the components it renders outside a boundary throw. A boundary is react-error-boundary's \`<ErrorBoundary>\` (the \`boundaries\` option adds other packages' boundaries), or a class of yours with \`getDerivedStateFromError\` or \`componentDidCatch\`.
|
|
23
|
-
|
|
24
|
-
It is reported once, at the highest place where the boundary is missing, with the path down to the call that throws. With a boundary around the whole app, it says nothing.
|
|
25
|
-
|
|
26
|
-
### Bad
|
|
27
|
-
|
|
28
|
-
\`\`\`tsx
|
|
29
|
-
// useUser throws NotFoundError for an id it does not know.
|
|
30
|
-
createRoot(root).render(<App />); // App renders <UserPage /> for /users/:id
|
|
31
|
-
\`\`\`
|
|
32
|
-
|
|
33
|
-
### Good
|
|
34
|
-
|
|
35
|
-
\`\`\`tsx
|
|
36
|
-
createRoot(root).render(
|
|
37
|
-
<ErrorBoundary FallbackComponent={ErrorPage}>
|
|
38
|
-
<App />
|
|
39
|
-
</ErrorBoundary>,
|
|
40
|
-
);
|
|
41
|
-
\`\`\`
|
|
42
|
-
|
|
43
|
-
### Fix
|
|
44
|
-
|
|
45
|
-
Put an error boundary above the place the message names, or nearer to the component that throws (around one route). There is no quick fix: where the boundary goes, and what it shows, is your call.
|
|
46
|
-
|
|
47
|
-
## What it does not catch
|
|
48
|
-
|
|
49
|
-
- Errors in event handlers, timers and async code: a boundary never catches them, so they are not counted. \`errors/async-handler-unhandled\` checks async handlers.
|
|
50
|
-
- Errors stateward cannot see (a package it cannot read, a callback, a dynamic call): it is about known error classes, so these never make a finding. The hover on a boundary lists them.
|
|
51
|
-
- An app with no \`createRoot(…).render(…)\` or \`hydrateRoot\` it can find: nothing is checked.
|
|
52
|
-
- Routes in \`createBrowserRouter([...])\` route objects: React Router catches their errors itself (\`errorElement\`). \`memo\`, \`forwardRef\` and \`lazy\` components, and boundaries from other packages you have not listed in \`boundaries\`, are not followed.
|
|
53
|
-
- An element passed to a function or stored in an object or array (\`showToast(<UserPage />)\`): where it renders cannot be followed, so it never makes a finding. An element held in a local \`const\` or \`let\` is checked where the variable is rendered.
|
|
54
|
-
- A class that a \`try\` in the component may catch (with a test other than \`instanceof\`): it is not counted.
|
|
55
|
-
|
|
56
|
-
## Turn it on or off
|
|
57
|
-
|
|
58
|
-
In \`stateward.config.ts\`:
|
|
59
|
-
|
|
60
|
-
\`\`\`ts
|
|
61
|
-
rules: { "errors/render-throws-without-boundary": "off" } // or "error"
|
|
62
|
-
groups: { "errors/react": "off" } // the whole group
|
|
63
|
-
rules: {
|
|
64
|
-
"errors/render-throws-without-boundary": [
|
|
65
|
-
"warning",
|
|
66
|
-
{ boundaries: { "react-error-boundary": ["ErrorBoundary"], "@sentry/react": ["ErrorBoundary"] } },
|
|
67
|
-
],
|
|
68
|
-
} // Sentry's boundary counts too
|
|
69
|
-
\`\`\`
|
|
70
|
-
`;
|
|
71
|
-
/** At most this many notes; a longer path keeps its first steps and its last. */
|
|
72
|
-
const MAX_NOTES = 5;
|
|
73
|
-
function headline(component, names) {
|
|
74
|
-
return fitted(HEADLINE_MAX, (cut) => `\`<${cut(component)}>\` can throw ${codeList(names, cut)} while rendering, with no error boundary above it`);
|
|
75
|
-
}
|
|
76
|
-
function hint(component) {
|
|
77
|
-
return withExample("wrap it in an error boundary, here or nearer to what throws; for example with react-error-boundary:", ["<ErrorBoundary FallbackComponent={ErrorPage}>", ` <${component} />`, "</ErrorBoundary>"]);
|
|
78
|
-
}
|
|
79
|
-
/** The renders down from the site to the component that throws `cls`, then the call that throws it. */
|
|
80
|
-
function pathNotes(model, site, cls) {
|
|
81
|
-
const classes = model.analysis.classes;
|
|
82
|
-
const path = shortestPath(site.to, (id) => model.edges(id), (edge) => model.nearest(edge) === undefined && model.needs(edge.to).has(cls), (id) => model.own(id).classes.get(cls)?.certain === true);
|
|
83
|
-
if (path === undefined)
|
|
84
|
-
return [];
|
|
85
|
-
const notes = path.map((edge) => ({
|
|
86
|
-
location: { file: edge.file, start: edge.render.at[0], end: edge.render.at[1] },
|
|
87
|
-
message: `\`<${edge.render.name}>\` is rendered here`,
|
|
88
|
-
}));
|
|
89
|
-
const thrower = path.at(-1)?.to ?? site.to;
|
|
90
|
-
const brought = model.own(thrower).classes.get(cls);
|
|
91
|
-
if (brought !== undefined) {
|
|
92
|
-
notes.push({
|
|
93
|
-
location: brought.location,
|
|
94
|
-
message: `${brought.what} \`${className(cls, classes)}\``,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
if (notes.length <= MAX_NOTES)
|
|
98
|
-
return notes;
|
|
99
|
-
const skipped = notes.length - (MAX_NOTES - 1);
|
|
100
|
-
const last = notes.at(-1);
|
|
101
|
-
const head = notes.slice(0, MAX_NOTES - 2);
|
|
102
|
-
const first = notes[MAX_NOTES - 2];
|
|
103
|
-
return first === undefined || last === undefined
|
|
104
|
-
? head
|
|
105
|
-
: [...head, { location: first.location, message: `… ${String(skipped)} more steps …` }, last];
|
|
106
|
-
}
|
|
107
|
-
export const renderThrowsWithoutBoundary = defineRule({
|
|
108
|
-
kind: "project",
|
|
109
|
-
meta: {
|
|
110
|
-
description: "A component that can throw a known error while rendering has an error boundary above it.",
|
|
111
|
-
defaultSeverity: "warning",
|
|
112
|
-
tier: "core",
|
|
113
|
-
group: GROUP,
|
|
114
|
-
needsJsDoc: true,
|
|
115
|
-
options: {
|
|
116
|
-
boundaries: option.record({
|
|
117
|
-
default: LIBRARY_BOUNDARIES,
|
|
118
|
-
description: 'Error boundary components from packages, by package: add Sentry\'s with `{ "@sentry/react": ["ErrorBoundary"] }` (keep react-error-boundary\'s to keep it).',
|
|
119
|
-
}),
|
|
120
|
-
},
|
|
121
|
-
explain: EXPLAIN,
|
|
122
|
-
},
|
|
123
|
-
summarize: (ctx) => summarizeReact(ctx, ctx.options.boundaries),
|
|
124
|
-
check(summaries, ctx) {
|
|
125
|
-
const model = modelOf(summaries);
|
|
126
|
-
const classes = model.analysis.classes;
|
|
127
|
-
const found = highestUncovered({
|
|
128
|
-
roots: model.entries,
|
|
129
|
-
edges: (id) => model.edges(id),
|
|
130
|
-
covered: (edge) => model.nearest(edge) !== undefined,
|
|
131
|
-
needs: (id) => model.needs(id),
|
|
132
|
-
key: (edge) => `${edge.file}:${String(edge.render.at[0])}`,
|
|
133
|
-
});
|
|
134
|
-
for (const { edge, needs } of found) {
|
|
135
|
-
const named = needs
|
|
136
|
-
.map((id) => ({ id, name: className(id, classes) }))
|
|
137
|
-
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
138
|
-
const [first] = named;
|
|
139
|
-
if (first === undefined)
|
|
140
|
-
continue;
|
|
141
|
-
const component = model.functions.get(edge.to)?.fn.name ?? edge.render.name;
|
|
142
|
-
ctx.report({
|
|
143
|
-
message: headline(component, named.map((one) => one.name)),
|
|
144
|
-
hint: hint(edge.render.name),
|
|
145
|
-
location: { file: edge.file, start: edge.render.at[0], end: edge.render.at[1] },
|
|
146
|
-
related: pathNotes(model, edge, first.id),
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
//# sourceMappingURL=render-throws-without-boundary.js.map
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/** The group the rule belongs to in the docs and in `stateward status`. */
|
|
2
|
-
export declare const GROUP = "catch";
|
|
3
|
-
export declare const rethrowUnknown: import("@stateward/core").FileRule<{
|
|
4
|
-
restUsesError: import("@stateward/core").BooleanOption;
|
|
5
|
-
}, "catch">;
|
|
6
|
-
//# sourceMappingURL=rethrow-unknown.d.ts.map
|
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
|