@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.
Files changed (46) hide show
  1. package/README.md +60 -3
  2. package/dist/catch-hides-unexpected.js +47 -58
  3. package/dist/catch-misses-error.js +57 -67
  4. package/dist/finally-can-throw.js +45 -54
  5. package/dist/inference/format.d.ts +3 -3
  6. package/dist/inference/format.js +5 -5
  7. package/dist/inference/solve.js +1 -1
  8. package/dist/inference/summarize.d.ts +1 -1
  9. package/dist/inference/summarize.js +2 -2
  10. package/dist/must-handle-error.js +58 -66
  11. package/dist/narrow-try.d.ts +6 -1
  12. package/dist/narrow-try.js +137 -77
  13. package/dist/plugin.d.ts +0 -27
  14. package/dist/plugin.js +1 -3
  15. package/dist/react/boundaries.d.ts +3 -3
  16. package/dist/react/boundaries.js +5 -5
  17. package/dist/react/fallbacks.js +2 -2
  18. package/dist/refactors.d.ts +1 -1
  19. package/dist/refactors.js +1 -1
  20. package/dist/render-throw-without-boundary.js +64 -47
  21. package/dist/result-ignored.js +31 -37
  22. package/dist/result-unchecked.js +58 -61
  23. package/dist/rules.meta.d.ts +22 -47
  24. package/dist/rules.meta.js +19 -47
  25. package/dist/throws-doc-mismatch.js +35 -44
  26. package/package.json +2 -2
  27. package/dist/async-handler-unhandled.d.ts +0 -7
  28. package/dist/async-handler-unhandled.js +0 -342
  29. package/dist/catch-without-intent.d.ts +0 -10
  30. package/dist/catch-without-intent.js +0 -590
  31. package/dist/exhaustive-catch.d.ts +0 -6
  32. package/dist/exhaustive-catch.js +0 -190
  33. package/dist/exhaustive-fallback.d.ts +0 -6
  34. package/dist/exhaustive-fallback.js +0 -157
  35. package/dist/finally-may-throw.d.ts +0 -6
  36. package/dist/finally-may-throw.js +0 -184
  37. package/dist/no-swallow.d.ts +0 -8
  38. package/dist/no-swallow.js +0 -320
  39. package/dist/render-throws-without-boundary.d.ts +0 -6
  40. package/dist/render-throws-without-boundary.js +0 -151
  41. package/dist/rethrow-unknown.d.ts +0 -6
  42. package/dist/rethrow-unknown.js +0 -260
  43. package/dist/throw-in-hook.d.ts +0 -8
  44. package/dist/throw-in-hook.js +0 -510
  45. package/dist/throws-matches-doc.d.ts +0 -7
  46. package/dist/throws-matches-doc.js +0 -188
@@ -1,190 +0,0 @@
1
- import { defineRule, matchesName, option } from "@stateward/core";
2
- import { catchName, className } from "./inference/format.js";
3
- import { analyse, everyCatch } from "./inference/solve.js";
4
- import { summarizeOnce } from "./inference/project.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 = "catch";
8
- const EXPLAIN = `# errors/exhaustive-catch
9
-
10
- A catch that branches with instanceof handles every error class its try can throw, or rethrows the rest.
11
-
12
- ## Why
13
-
14
- A \`catch\` that checks \`e instanceof AuthError\` says which errors it expects. When \`api.send\` can also throw \`RateLimitError\`, a rate limit falls into the generic branch: the user sees a generic error page, and the retry that would have worked never happens. This rule lists, at the \`catch\`, the classes it leaves out.
15
-
16
- ## When to use it
17
-
18
- Use it in code that throws its own error classes and catches them by class. It is on as an error in every preset; where a \`catch\` does not branch on \`instanceof\`, it says nothing. It works best when your functions throw classes and document them with \`@throws {T}\`.
19
-
20
- ## What it catches
21
-
22
- A \`catch\` that branches with \`if\` / \`else if\` or \`switch (true)\` on \`instanceof\`, when its \`try\` can throw an error class that no branch names and nothing passes on.
23
-
24
- What the \`try\` can throw is worked out from \`throw new X(…)\` in the functions it calls, in any file, their \`@throws\` tags, the built-ins stateward knows (\`JSON.parse\`, \`new URL\`, \`fetch\`, …), and the packages it calls. \`stateward deps\` shows how well each package's errors are known.
25
-
26
- What counts as handled:
27
-
28
- - \`e instanceof X\` handles \`X\` and every class that extends it (\`e instanceof Error\` handles them all);
29
- - a last \`else\` or \`default\`, or a statement after the chain, that throws (\`throw e\`, or a new error with \`{ cause: e }\`) passes the rest on;
30
- - a last \`else\` or \`default\`, or code after the chain, that uses the error (\`setError(e)\`, logging it, reading a property) handles the rest. An \`else\` that ignores the error (\`showGenericError()\`) does not.
31
-
32
- ### Bad
33
-
34
- \`\`\`ts
35
- // api.send can throw RateLimitError and AuthError.
36
- try {
37
- await api.send(message);
38
- } catch (e) {
39
- if (e instanceof AuthError) {
40
- redirectToLogin();
41
- } else {
42
- showGenericError();
43
- }
44
- }
45
- \`\`\`
46
-
47
- ### Good
48
-
49
- \`\`\`ts
50
- try {
51
- await api.send(message);
52
- } catch (e) {
53
- if (e instanceof AuthError) {
54
- redirectToLogin();
55
- } else if (e instanceof RateLimitError) {
56
- retryLater();
57
- } else {
58
- throw e;
59
- }
60
- }
61
- \`\`\`
62
-
63
- ### Fix
64
-
65
- Add a branch for each missing class, or rethrow what you do not handle. The message shows the branch in the shape of your \`catch\`, and links to the call that throws each class. There is no quick fix.
66
-
67
- ## What it does not catch
68
-
69
- - A \`catch\` with no \`instanceof\` branches: it treats every error alike on purpose.
70
- - Errors stateward cannot see: from a package it cannot read, a callback, or a dynamic call. Only the classes it can see are checked, and the message notes each call it could not see into, with the reason. \`errors/rethrow-unknown\` makes sure the rest is not dropped. To fill a gap, declare a package's errors under \`facts\` in the config.
71
- - A class whose \`extends\` cannot be followed, or a branch with a test that is not only \`instanceof\` (\`e instanceof A && e.code === 1\`, \`isAbort(e)\`): it does not guess what that branch handles.
72
- - Errors from the JavaScript engine itself, such as reading a property of \`undefined\`.
73
- - Built-ins other than \`JSON.parse\`, \`JSON.stringify\`, \`new URL\`, \`decodeURIComponent\`, \`decodeURI\`, \`structuredClone\`, \`atob\`, \`fetch\`, \`toFixed\`, \`Array(n)\` and \`BigInt\`: they are taken not to throw.
74
- - Promises kept in a \`let\`, a parameter or a property; getters, setters and generators; tests other than \`instanceof\` (\`e.name === "AbortError"\`).
75
-
76
- ## Turn it on or off
77
-
78
- In \`stateward.config.ts\`:
79
-
80
- \`\`\`ts
81
- rules: { "errors/exhaustive-catch": "off" } // or "warning"
82
- groups: { "errors/catch": "off" } // the whole group
83
- facts: { "config-lib": { load: { throws: ["ConfigError"] } } } // declare a package's errors
84
- rules: { "errors/exhaustive-catch": ["error", { ignoreClasses: ["SyntaxError"] }] } // never require a branch for these
85
- \`\`\`
86
- `;
87
- /** At most this many package calls with unknown errors are noted on a finding. */
88
- const MAX_UNKNOWN_NOTES = 2;
89
- function headline(names, via) {
90
- return fitted(HEADLINE_MAX, (cut) => {
91
- const source = via === undefined ? "its `try` block" : `\`${cut(via)}\``;
92
- return `this catch does not handle ${codeList(names, cut)}, which ${source} can throw`;
93
- });
94
- }
95
- /** The fix, in the shape of the catch: a `case`, an `else if`, or an `if` for a chain without `else`. */
96
- function hint(count, variable, handler, shape) {
97
- const branch = count === 1 ? "it" : "each";
98
- switch (shape) {
99
- case "switch":
100
- return withExample(`add a branch for ${branch}, or rethrow what you do not handle:`, [
101
- `case ${variable} instanceof ${handler}:`,
102
- " …",
103
- "default:",
104
- ` throw ${variable};`,
105
- ]);
106
- case "else":
107
- return withExample(`add a branch for ${branch}, or rethrow what you do not handle:`, [
108
- `} else if (${variable} instanceof ${handler}) {`,
109
- " …",
110
- "} else {",
111
- ` throw ${variable};`,
112
- ]);
113
- case "if":
114
- return withExample(`add a branch for ${branch}, or rethrow what you do not handle after the chain:`, [`if (${variable} instanceof ${handler}) {`, " …", "}", `throw ${variable};`]);
115
- default: {
116
- const unreachable = shape;
117
- return unreachable;
118
- }
119
- }
120
- }
121
- function report(seen, classes, ignored, emit) {
122
- const { handler } = seen;
123
- if (handler.restThrows || handler.restUsed || handler.variable === null)
124
- return;
125
- const missing = [];
126
- for (const [id, info] of seen.unhandled) {
127
- if (info.certain && !matchesName(catchName(id, classes), ignored)) {
128
- missing.push({ id, via: info.via });
129
- }
130
- }
131
- const [first] = missing;
132
- if (first === undefined)
133
- return;
134
- const vias = new Set(missing.map((one) => one.via?.text));
135
- const via = vias.size === 1 ? first.via?.text : undefined;
136
- const related = [];
137
- const shown = new Set();
138
- for (const { id, via: from } of missing) {
139
- if (from === null || shown.has(`${String(from.start)}:${from.text}`) || related.length >= 3) {
140
- continue;
141
- }
142
- shown.add(`${String(from.start)}:${from.text}`);
143
- related.push({
144
- location: { file: from.file, start: from.start, end: from.end },
145
- message: `\`${from.text}\` can throw \`${className(id, classes)}\``,
146
- });
147
- }
148
- // A package call whose errors are not all known: the catch passes them on (they reach the
149
- // rethrow, or nothing is reported for them), and the note says which call and why.
150
- const unknowns = [...seen.thrown.unknowns.values()].filter((one) => one.package !== undefined);
151
- for (const source of unknowns.slice(0, MAX_UNKNOWN_NOTES)) {
152
- related.push({
153
- location: { file: source.file, start: source.start, end: source.end },
154
- message: `\`${source.text}\` may also throw errors stateward cannot list (${source.reason}); rethrow what you do not handle`,
155
- });
156
- }
157
- emit({
158
- message: headline(missing.map((one) => className(one.id, classes)), via),
159
- hint: hint(missing.length, handler.variable, catchName(first.id, classes), handler.dispatch === "switch" ? "switch" : handler.otherwise === null ? "if" : "else"),
160
- location: { file: seen.file, start: handler.at[0], end: handler.at[1] },
161
- related,
162
- });
163
- }
164
- export const exhaustiveCatch = defineRule({
165
- kind: "project",
166
- meta: {
167
- description: "A catch that branches with instanceof handles every error class its try can throw, or rethrows the rest.",
168
- defaultSeverity: "error",
169
- tier: "core",
170
- group: GROUP,
171
- needsJsDoc: true,
172
- options: {
173
- ignoreClasses: option.strings({
174
- default: [],
175
- description: "Error classes a catch never needs a branch for, as its instanceof test names them (SyntaxError, *AbortError).",
176
- }),
177
- },
178
- explain: EXPLAIN,
179
- },
180
- summarize: (ctx) => summarizeOnce(ctx),
181
- check(summaries, ctx) {
182
- const analysis = analyse(summaries);
183
- for (const seen of everyCatch(analysis)) {
184
- report(seen, analysis.classes, ctx.options.ignoreClasses, (problem) => {
185
- ctx.report(problem);
186
- });
187
- }
188
- },
189
- });
190
- //# sourceMappingURL=exhaustive-catch.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 = "react";
3
- export declare const exhaustiveFallback: import("@stateward/core").ProjectRule<import("./react/boundaries.js").ReactSummary, {
4
- ignoreClasses: import("@stateward/core").StringsOption;
5
- }, "react">;
6
- //# sourceMappingURL=exhaustive-fallback.d.ts.map
@@ -1,157 +0,0 @@
1
- import { defineRule, matchesName, option } from "@stateward/core";
2
- import { catchName, className } from "./inference/format.js";
3
- import { relation } from "./inference/solve.js";
4
- import { modelOf, summarizeReact } from "./react/boundaries.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/exhaustive-fallback
9
-
10
- An error boundary's fallback that branches on the error with instanceof handles every error class the boundary can catch, or has a branch for the rest.
11
-
12
- ## Why
13
-
14
- A fallback that checks \`error instanceof NotFoundError\` says which errors it expects. When a page under the same boundary can also throw \`ParseError\`, and the fallback ends with \`return null\`, the user gets an empty screen with no message and no way back. This rule lists the classes the boundary can catch that no branch names.
15
-
16
- ## When to use it
17
-
18
- Use it with error boundaries whose fallback shows something different per error class. It is a core rule, a warning in every preset; a fallback that does not branch on \`instanceof\` is left alone.
19
-
20
- ## What it catches
21
-
22
- A fallback that branches with \`if\` / \`else if\`, \`switch (true)\` or \`? :\` on \`error instanceof X\`, has no branch for any other error, and belongs to a boundary that can catch a class no branch names. The fallback is react-error-boundary's \`FallbackComponent\`, \`fallbackRender\` or \`onError\`, or a boundary class's \`getDerivedStateFromError\`. What the boundary can catch is what the components below it throw while rendering and in their effects, down to the next boundary (the hover on the boundary lists it).
23
-
24
- A branch for the rest is a final \`else\` or \`default\`, or code after the chain, that does more than return \`null\` or nothing.
25
-
26
- ### Bad
27
-
28
- \`\`\`tsx
29
- // SettingsPage, under this boundary, can throw ParseError.
30
- function UserErrorPage({ error }: FallbackProps) {
31
- if (error instanceof NotFoundError) return <p>This user does not exist.</p>;
32
- return null;
33
- }
34
- \`\`\`
35
-
36
- ### Good
37
-
38
- \`\`\`tsx
39
- function UserErrorPage({ error }: FallbackProps) {
40
- if (error instanceof NotFoundError) return <p>This user does not exist.</p>;
41
- return <p>Something went wrong: {String(error)}</p>;
42
- }
43
- \`\`\`
44
-
45
- ### Fix
46
-
47
- Add a branch for each class the message lists, or end the chain with a fallback for any other error. There is no quick fix.
48
-
49
- ## What it does not catch
50
-
51
- - A fallback that does not branch on \`instanceof\`, or whose tests are not only \`instanceof\` (\`error instanceof A && error.status === 404\`): it does not guess what such a branch handles.
52
- - Errors stateward cannot see (from a package it cannot read, a callback, a dynamic call): only known classes are checked.
53
- - Errors from event handlers and async code: a boundary never receives them.
54
- - A fallback passed as anything other than a function written in place or the name of a function of the project.
55
-
56
- ## Turn it on or off
57
-
58
- In \`stateward.config.ts\`:
59
-
60
- \`\`\`ts
61
- rules: { "errors/exhaustive-fallback": "off" } // or "error"
62
- groups: { "errors/react": "off" } // the whole group
63
- rules: { "errors/exhaustive-fallback": ["warning", { ignoreClasses: ["ChunkLoadError"] }] } // never require a branch for these
64
- \`\`\`
65
- `;
66
- /** What the fallback does with any other error, in the example: what it returns or does. */
67
- function restLine(kind, variable) {
68
- switch (kind) {
69
- case "FallbackComponent":
70
- case "fallbackRender":
71
- return `return <p>Something went wrong: {String(${variable})}</p>;`;
72
- case "onError":
73
- return `console.error(${variable});`;
74
- case "getDerivedStateFromError":
75
- return `return { error: ${variable} };`;
76
- default: {
77
- const unreachable = kind;
78
- return unreachable;
79
- }
80
- }
81
- }
82
- function hint(fallback, chain, handler, count) {
83
- const line = `add a branch for ${count === 1 ? "it" : "each"}, or end with one for any other error:`;
84
- const rest = restLine(fallback.kind, chain.variable);
85
- return chain.dispatch === "switch"
86
- ? withExample(line, [
87
- `case ${chain.variable} instanceof ${handler}:`,
88
- " …",
89
- "default:",
90
- ` ${rest}`,
91
- ])
92
- : withExample(line, [`if (${chain.variable} instanceof ${handler}) {`, " …", "}", rest]);
93
- }
94
- export const exhaustiveFallback = defineRule({
95
- kind: "project",
96
- meta: {
97
- description: "An error boundary's fallback that branches on the error with instanceof handles every error class the boundary can catch, or has a branch for the rest.",
98
- defaultSeverity: "warning",
99
- tier: "core",
100
- group: GROUP,
101
- needsJsDoc: true,
102
- options: {
103
- ignoreClasses: option.strings({
104
- default: [],
105
- description: "Error classes a fallback never needs a branch for, as its instanceof test names them (globs allowed).",
106
- }),
107
- },
108
- explain: EXPLAIN,
109
- },
110
- summarize: (ctx) => summarizeReact(ctx),
111
- check(summaries, ctx) {
112
- const model = modelOf(summaries);
113
- const classes = model.analysis.classes;
114
- for (const [key, { file, element }] of model.boundaries) {
115
- for (const fallback of element.fallbacks) {
116
- const chain = fallback.chain;
117
- if (chain === null || chain.generic || !chain.exact)
118
- continue;
119
- const handled = [];
120
- let readable = true;
121
- for (const branch of chain.branches) {
122
- if (branch === null)
123
- readable = false;
124
- else
125
- handled.push(...branch);
126
- }
127
- if (!readable)
128
- continue;
129
- const caught = model.catches(key);
130
- const missing = [...caught.classes]
131
- .filter(([id, brought]) => brought.certain &&
132
- !matchesName(catchName(id, classes), ctx.options.ignoreClasses) &&
133
- handled.every((one) => relation(one, id, classes) === "no"))
134
- .map(([id, brought]) => ({ id, brought, name: className(id, classes) }))
135
- .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
136
- const [first] = missing;
137
- if (first === undefined)
138
- continue;
139
- const related = missing.slice(0, 3).map(({ brought, name }) => ({
140
- location: brought.location,
141
- message: `${brought.what} \`${name}\``,
142
- }));
143
- related.push({
144
- location: { file: chain.file, start: chain.at[0], end: chain.at[1] },
145
- message: `\`${fallback.name}\` branches on the error here`,
146
- });
147
- ctx.report({
148
- message: fitted(HEADLINE_MAX, (cut) => `\`${cut(fallback.name)}\` does not handle ${codeList(missing.map((one) => one.name), cut)}, which this boundary can catch`),
149
- hint: hint(fallback, chain, catchName(first.id, classes), missing.length),
150
- location: { file, start: fallback.at[0], end: fallback.at[1] },
151
- related,
152
- });
153
- }
154
- }
155
- },
156
- });
157
- //# sourceMappingURL=exhaustive-fallback.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 = "finally";
3
- export declare const finallyMayThrow: import("@stateward/core").FileRule<{
4
- severities: import("@stateward/core").SeveritiesOption<"throw" | "builtin" | "own-function">;
5
- }, "finally">;
6
- //# sourceMappingURL=finally-may-throw.d.ts.map
@@ -1,184 +0,0 @@
1
- import ts from "@stateward/core/typescript";
2
- import { defineRule, option } from "@stateward/core";
3
- import { hasModifier, isBuiltinGlobal, sameFileFunction, scanRunning, stripParentheses, throwsUncaught, } from "./syntax.js";
4
- import { HEADLINE_MAX, fitted, oneLine, withExample } from "./text.js";
5
- /** The group the rule belongs to in the docs and in `stateward status`. */
6
- export const GROUP = "finally";
7
- const EXPLAIN = `# errors/finally-may-throw
8
-
9
- A finally block does not call something that throws, so the error that led to it is not lost.
10
-
11
- ## Why
12
-
13
- When code in a \`finally\` throws, its error replaces the one that sent the code there. An upload that fails with a network error then reports a \`SyntaxError\` from parsing the saved state: the real cause, with its message and stack, is gone, and the rest of the cleanup does not run.
14
-
15
- ## When to use it
16
-
17
- Use it in any code that cleans up in \`finally\`. It is on as an error in every preset; keep it on. Turn it off only if your cleanup's own errors are meant to win.
18
-
19
- ## What it catches
20
-
21
- In a \`finally\` block, outside a nested \`try\` that has a \`catch\`:
22
-
23
- - a call to a built-in that throws: \`JSON.parse\`, \`JSON.stringify\` (on circular values), \`new URL\`, \`decodeURIComponent\`, \`decodeURI\`, \`structuredClone\`, \`atob\`;
24
- - a \`throw\`;
25
- - a call to a function in the same file that has a \`throw\` outside its own \`try\` / \`catch\` (an \`async\` one when the call is awaited).
26
-
27
- One finding per \`finally\`, on the first of these.
28
-
29
- ### Bad
30
-
31
- \`\`\`ts
32
- try {
33
- await upload(file);
34
- } finally {
35
- const state = JSON.parse(saved);
36
- restore(state);
37
- }
38
- \`\`\`
39
-
40
- ### Good
41
-
42
- \`\`\`ts
43
- try {
44
- await upload(file);
45
- } finally {
46
- try {
47
- restore(JSON.parse(saved));
48
- } catch (e) {
49
- report(e);
50
- }
51
- }
52
- \`\`\`
53
-
54
- ### Fix
55
-
56
- Move the call out of the \`finally\`, or wrap it in its own \`try\` / \`catch\`. There is no quick fix: what the cleanup should do when it fails is your call.
57
-
58
- ## What it does not catch
59
-
60
- - Functions from other files, and package calls: only the built-ins above and functions in the same file are read.
61
- - Other built-ins, such as \`fetch\` or \`BigInt\`.
62
- - A function that throws only inside its own \`try\` / \`catch\`.
63
- - Anything inside a nested \`try\` that has a \`catch\`.
64
- - A second throwing call in the same \`finally\`: only the first is reported.
65
-
66
- ## Turn it on or off
67
-
68
- In \`stateward.config.ts\`:
69
-
70
- \`\`\`ts
71
- rules: { "errors/finally-may-throw": "off" } // or "warning"
72
- groups: { "errors/finally": "off" } // the whole group
73
- rules: { "errors/finally-may-throw": ["error", { severities: { builtin: "warning" } }] } // built-ins only warn
74
- \`\`\`
75
- `;
76
- /** Built-in functions that throw on input they cannot handle, as `Owner.name` or `name`. */
77
- const THROWING_CALLS = new Set([
78
- "JSON.parse",
79
- "JSON.stringify",
80
- "decodeURIComponent",
81
- "decodeURI",
82
- "structuredClone",
83
- "atob",
84
- ]);
85
- /** Built-in constructors that throw. */
86
- const THROWING_CONSTRUCTORS = new Set(["URL"]);
87
- /** `JSON.parse` or `atob`, when the callee is one of the globals in the table. */
88
- function builtinCall(call, ctx) {
89
- const callee = stripParentheses(call.expression);
90
- if (ts.isIdentifier(callee)) {
91
- return THROWING_CALLS.has(callee.text) && isBuiltinGlobal(callee, ctx)
92
- ? callee.text
93
- : undefined;
94
- }
95
- if (!ts.isPropertyAccessExpression(callee))
96
- return undefined;
97
- const owner = stripParentheses(callee.expression);
98
- if (!ts.isIdentifier(owner))
99
- return undefined;
100
- const name = `${owner.text}.${callee.name.text}`;
101
- return THROWING_CALLS.has(name) && isBuiltinGlobal(owner, ctx) ? name : undefined;
102
- }
103
- /** The kinds of finding: they differ in how sure the rule is that the call throws. */
104
- const KINDS = {
105
- throw: "a `throw` written in the finally: it throws whenever it runs.",
106
- builtin: "a built-in such as `JSON.parse` or `new URL`: it throws only on input it cannot handle.",
107
- "own-function": "a function in the same file with a `throw` in it: it throws on some paths.",
108
- };
109
- function causeOf(node, awaited, ctx) {
110
- if (ts.isThrowStatement(node))
111
- return { node, subject: () => "this `throw` runs", kind: "throw" };
112
- if (ts.isNewExpression(node)) {
113
- const callee = stripParentheses(node.expression);
114
- if (ts.isIdentifier(callee) &&
115
- THROWING_CONSTRUCTORS.has(callee.text) &&
116
- isBuiltinGlobal(callee, ctx)) {
117
- return { node, subject: () => `\`new ${callee.text}(…)\` throws here`, kind: "builtin" };
118
- }
119
- return undefined;
120
- }
121
- if (!ts.isCallExpression(node))
122
- return undefined;
123
- const builtin = builtinCall(node, ctx);
124
- if (builtin !== undefined) {
125
- return { node, subject: () => `\`${builtin}(…)\` throws here`, kind: "builtin" };
126
- }
127
- const declared = sameFileFunction(node.expression, ctx);
128
- if (declared === undefined || !throwsUncaught(declared))
129
- return undefined;
130
- // An async function turns its throw into a rejected promise, which only an `await` rethrows.
131
- if (hasModifier(declared, ts.SyntaxKind.AsyncKeyword) && !awaited)
132
- return undefined;
133
- const callee = oneLine(stripParentheses(node.expression).getText(ctx.sourceFile));
134
- return { node, subject: (cut) => `\`${cut(callee)}(…)\` throws here`, kind: "own-function" };
135
- }
136
- function headline(cause) {
137
- return fitted(HEADLINE_MAX, (cut) => `if ${cause.subject(cut)}, the error that led to this finally is lost`);
138
- }
139
- const HINT = withExample("move it out of finally, or catch it inside:", [
140
- "finally {",
141
- " try { cleanup(); } catch (e) { report(e); }",
142
- "}",
143
- ]);
144
- /** `shown` holds what is already reported: a `finally` inside a `finally` is looked at twice. */
145
- function checkFinally(block, shown, ctx) {
146
- let found;
147
- scanRunning(block, (node, guarded) => {
148
- if (found !== undefined || guarded)
149
- return;
150
- const awaited = ts.isAwaitExpression(node.parent);
151
- found = causeOf(node, awaited, ctx);
152
- });
153
- if (found === undefined || shown.has(found.node))
154
- return;
155
- shown.add(found.node);
156
- ctx.report({ node: found.node, message: headline(found), hint: HINT, findingKind: found.kind });
157
- }
158
- export const finallyMayThrow = defineRule({
159
- kind: "file",
160
- meta: {
161
- description: "A finally block does not call something that throws, so the error that led to it is not lost.",
162
- defaultSeverity: "error",
163
- tier: "core",
164
- group: GROUP,
165
- options: {
166
- severities: option.severities({
167
- kinds: KINDS,
168
- description: "A severity per kind of call, where you trust one kind less.",
169
- }),
170
- },
171
- explain: EXPLAIN,
172
- },
173
- check(ctx) {
174
- const shown = new Set();
175
- const visit = (node) => {
176
- if (ts.isTryStatement(node) && node.finallyBlock !== undefined) {
177
- checkFinally(node.finallyBlock, shown, ctx);
178
- }
179
- ts.forEachChild(node, visit);
180
- };
181
- visit(ctx.sourceFile);
182
- },
183
- });
184
- //# sourceMappingURL=finally-may-throw.js.map
@@ -1,8 +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 noSwallow: import("@stateward/core").FileRule<{
4
- loggers: import("@stateward/core").StringsOption;
5
- logMethods: import("@stateward/core").StringsOption;
6
- logFunctions: import("@stateward/core").StringsOption;
7
- }, "catch">;
8
- //# sourceMappingURL=no-swallow.d.ts.map