@stateward/plugin-errors 0.0.0-stage → 1.0.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +20 -2
- package/dist/async-handler-unhandled.d.ts +7 -0
- package/dist/async-handler-unhandled.js +342 -0
- package/dist/catch-hides-unexpected.d.ts +6 -0
- package/dist/catch-hides-unexpected.js +260 -0
- package/dist/catch-misses-error.d.ts +6 -0
- package/dist/catch-misses-error.js +208 -0
- package/dist/catch-without-intent.d.ts +10 -0
- package/dist/catch-without-intent.js +590 -0
- package/dist/exhaustive-catch.d.ts +6 -0
- package/dist/exhaustive-catch.js +190 -0
- package/dist/exhaustive-fallback.d.ts +6 -0
- package/dist/exhaustive-fallback.js +157 -0
- package/dist/finally-can-throw.d.ts +6 -0
- package/dist/finally-can-throw.js +184 -0
- package/dist/finally-may-throw.d.ts +6 -0
- package/dist/finally-may-throw.js +184 -0
- package/dist/hover.d.ts +11 -0
- package/dist/hover.js +62 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2 -0
- package/dist/inference/builtins.d.ts +52 -0
- package/dist/inference/builtins.js +179 -0
- package/dist/inference/catches.d.ts +9 -0
- package/dist/inference/catches.js +33 -0
- package/dist/inference/deps.d.ts +12 -0
- package/dist/inference/deps.js +79 -0
- package/dist/inference/format.d.ts +25 -0
- package/dist/inference/format.js +72 -0
- package/dist/inference/packages.d.ts +40 -0
- package/dist/inference/packages.js +723 -0
- package/dist/inference/project.d.ts +8 -0
- package/dist/inference/project.js +22 -0
- package/dist/inference/solve.d.ts +128 -0
- package/dist/inference/solve.js +575 -0
- package/dist/inference/summarize.d.ts +50 -0
- package/dist/inference/summarize.js +1324 -0
- package/dist/inference/summary.d.ts +191 -0
- package/dist/inference/summary.js +237 -0
- package/dist/inlay.d.ts +19 -0
- package/dist/inlay.js +168 -0
- package/dist/must-handle-error.d.ts +55 -0
- package/dist/must-handle-error.js +794 -0
- package/dist/narrow-try.d.ts +52 -0
- package/dist/narrow-try.js +641 -0
- package/dist/no-swallow.d.ts +8 -0
- package/dist/no-swallow.js +320 -0
- package/dist/plugin.d.ts +116 -0
- package/dist/plugin.js +39 -0
- package/dist/react/boundaries.d.ts +83 -0
- package/dist/react/boundaries.js +371 -0
- package/dist/react/fallbacks.d.ts +12 -0
- package/dist/react/fallbacks.js +83 -0
- package/dist/react/render-guards.d.ts +24 -0
- package/dist/react/render-guards.js +271 -0
- package/dist/react/tree.d.ts +98 -0
- package/dist/react/tree.js +863 -0
- package/dist/refactors.d.ts +15 -0
- package/dist/refactors.js +69 -0
- package/dist/render-throw-without-boundary.d.ts +11 -0
- package/dist/render-throw-without-boundary.js +223 -0
- package/dist/render-throws-without-boundary.d.ts +6 -0
- package/dist/render-throws-without-boundary.js +151 -0
- package/dist/result-ignored.d.ts +6 -0
- package/dist/result-ignored.js +167 -0
- package/dist/result-type.d.ts +18 -0
- package/dist/result-type.js +55 -0
- package/dist/result-unchecked.d.ts +2 -0
- package/dist/result-unchecked.js +746 -0
- package/dist/rethrow-unknown.d.ts +6 -0
- package/dist/rethrow-unknown.js +260 -0
- package/dist/rules.meta.d.ts +214 -0
- package/dist/rules.meta.js +212 -0
- package/dist/syntax.d.ts +41 -0
- package/dist/syntax.js +171 -0
- package/dist/text.d.ts +22 -0
- package/dist/text.js +43 -0
- package/dist/throw-in-hook.d.ts +8 -0
- package/dist/throw-in-hook.js +510 -0
- package/dist/throws-doc-mismatch.d.ts +7 -0
- package/dist/throws-doc-mismatch.js +188 -0
- package/dist/throws-matches-doc.d.ts +7 -0
- package/dist/throws-matches-doc.js +188 -0
- package/package.json +34 -3
|
@@ -0,0 +1,8 @@
|
|
|
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
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import ts from "@stateward/core/typescript";
|
|
2
|
+
import { defineRule, matchesName, option } from "@stateward/core";
|
|
3
|
+
import { withExample } from "./text.js";
|
|
4
|
+
import { stripParentheses } from "./syntax.js";
|
|
5
|
+
/** The group the rule belongs to in the docs and in `stateward status`. */
|
|
6
|
+
export const GROUP = "catch";
|
|
7
|
+
const EXPLAIN = `# errors/no-swallow
|
|
8
|
+
|
|
9
|
+
A catch that only logs, or does nothing, and then carries on treats a failure as if it had not happened.
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
In \`load\`, a failed \`fetchUser\` is logged and \`render(user)\` runs anyway, with a \`user\` that was never fetched. The page shows a blank profile instead of an error, and the only trace is a line in the console.
|
|
14
|
+
|
|
15
|
+
## When to use it
|
|
16
|
+
|
|
17
|
+
Use it for code that must never lose a failure: payments, sync, data import; or to review a module's catches once. In most apps many catches that only log are deliberate fallbacks (a setting read from storage, a best-effort cache write), so it is off until you add its add-on (below), then a warning in every preset. Make it an error once that code is clean.
|
|
18
|
+
|
|
19
|
+
## What it catches
|
|
20
|
+
|
|
21
|
+
A \`catch\` that is empty or only logs (\`console.*\`, a call on \`logger\` or \`log\`, or \`.error\`, \`.warn\`, \`.info\`, \`.debug\` on any object; the options below change these lists), when the function carries on after the \`try\`.
|
|
22
|
+
|
|
23
|
+
### Bad
|
|
24
|
+
|
|
25
|
+
\`\`\`ts
|
|
26
|
+
async function load(id: string) {
|
|
27
|
+
let user;
|
|
28
|
+
try {
|
|
29
|
+
user = await fetchUser(id);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error(e);
|
|
32
|
+
}
|
|
33
|
+
return render(user);
|
|
34
|
+
}
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
### Good
|
|
38
|
+
|
|
39
|
+
\`\`\`ts
|
|
40
|
+
async function load(id: string) {
|
|
41
|
+
try {
|
|
42
|
+
return render(await fetchUser(id));
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return err(e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
\`\`\`
|
|
48
|
+
|
|
49
|
+
### Fix
|
|
50
|
+
|
|
51
|
+
Rethrow the error, return a failure the caller can see, or show it to the user. The message shows \`throw e;\`, or \`return err(e);\` when the file imports \`err\` from \`@stateward/values\`. There is no quick fix.
|
|
52
|
+
|
|
53
|
+
## What it does not catch
|
|
54
|
+
|
|
55
|
+
It stays quiet when:
|
|
56
|
+
|
|
57
|
+
- the \`catch\` does anything else (\`return\`, \`throw\`, a call that shows the error);
|
|
58
|
+
- an empty \`catch\` holds a comment, which says it is on purpose;
|
|
59
|
+
- the \`try\` returns its value and the code after it returns a default (\`try { return JSON.parse(stored); } catch (e) { console.warn(e); } return defaults;\`);
|
|
60
|
+
- every variable the \`try\` assigns already had a value to fall back on (\`let flags = DEFAULTS; try { flags = JSON.parse(stored); } catch …\`; a parameter counts, a \`let\` with no value does not);
|
|
61
|
+
- the \`try\` is the last thing the function, or a loop's body, does (a retry loop moves on to the next attempt).
|
|
62
|
+
|
|
63
|
+
A function of your own that only logs is not recognised as logging until you list it in \`logFunctions\`.
|
|
64
|
+
|
|
65
|
+
## Turn it on or off
|
|
66
|
+
|
|
67
|
+
Turn it on in \`stateward.config.ts\`:
|
|
68
|
+
|
|
69
|
+
\`\`\`ts
|
|
70
|
+
addons: ["errors/swallow"]
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
Then change or turn off the rule itself:
|
|
74
|
+
|
|
75
|
+
\`\`\`ts
|
|
76
|
+
rules: { "errors/no-swallow": "off" } // or "error"
|
|
77
|
+
groups: { "errors/catch": "off" } // the whole group
|
|
78
|
+
rules: { "errors/no-swallow": ["warning", { logFunctions: ["logError"] }] } // your own log helpers
|
|
79
|
+
\`\`\`
|
|
80
|
+
`;
|
|
81
|
+
const OPTIONS = {
|
|
82
|
+
loggers: option.strings({
|
|
83
|
+
default: ["console", "logger", "log"],
|
|
84
|
+
description: "Objects whose every method only logs (globs like *Logger): a catch that only calls them swallows the error.",
|
|
85
|
+
}),
|
|
86
|
+
logMethods: option.strings({
|
|
87
|
+
default: ["error", "warn", "info", "debug"],
|
|
88
|
+
description: "Method names that only log, on any object (`Sentry.captureException` is not one).",
|
|
89
|
+
}),
|
|
90
|
+
logFunctions: option.strings({
|
|
91
|
+
default: [],
|
|
92
|
+
description: "Plain functions of your own that only log, like logError (globs allowed).",
|
|
93
|
+
}),
|
|
94
|
+
};
|
|
95
|
+
/** The name an expression ends in: `logger`, `this.logger`, `app.log`. */
|
|
96
|
+
function lastName(expression) {
|
|
97
|
+
const node = stripParentheses(expression);
|
|
98
|
+
if (ts.isIdentifier(node))
|
|
99
|
+
return node.text;
|
|
100
|
+
return ts.isPropertyAccessExpression(node) ? node.name.text : undefined;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A call that only logs: any method of a logger (`console.x()`, `logger.x()`), a log method on any
|
|
104
|
+
* object (`app.warn()`), or one of the team's own log functions (`logError(e)`).
|
|
105
|
+
*/
|
|
106
|
+
function isLoggingCall(expression, logging) {
|
|
107
|
+
const call = stripParentheses(expression);
|
|
108
|
+
if (!ts.isCallExpression(call))
|
|
109
|
+
return false;
|
|
110
|
+
const callee = stripParentheses(call.expression);
|
|
111
|
+
if (ts.isIdentifier(callee))
|
|
112
|
+
return matchesName(callee.text, logging.logFunctions);
|
|
113
|
+
if (!ts.isPropertyAccessExpression(callee))
|
|
114
|
+
return false;
|
|
115
|
+
if (logging.logMethods.includes(callee.name.text))
|
|
116
|
+
return true;
|
|
117
|
+
const name = lastName(stripParentheses(callee.expression));
|
|
118
|
+
return name !== undefined && matchesName(name, logging.loggers);
|
|
119
|
+
}
|
|
120
|
+
/** What the catch does: nothing, or only log. Undefined when it does anything else. */
|
|
121
|
+
function swallowKind(block, sourceFile, logging) {
|
|
122
|
+
const statements = block.statements.filter((statement) => !ts.isEmptyStatement(statement));
|
|
123
|
+
if (statements.length === 0) {
|
|
124
|
+
// A comment inside an empty catch says it is on purpose.
|
|
125
|
+
return hasComment(block, sourceFile) ? undefined : "ignores";
|
|
126
|
+
}
|
|
127
|
+
const onlyLogs = statements.every((statement) => ts.isExpressionStatement(statement) && isLoggingCall(statement.expression, logging));
|
|
128
|
+
return onlyLogs ? "logs" : undefined;
|
|
129
|
+
}
|
|
130
|
+
function hasComment(block, sourceFile) {
|
|
131
|
+
const inner = sourceFile.text.slice(block.getStart(sourceFile) + 1, block.getEnd() - 1);
|
|
132
|
+
return inner.trim() !== "";
|
|
133
|
+
}
|
|
134
|
+
/** Statements that run nothing: a bare `;` and declarations that only exist at compile time. */
|
|
135
|
+
function runsNothing(statement) {
|
|
136
|
+
return (ts.isEmptyStatement(statement) ||
|
|
137
|
+
ts.isFunctionDeclaration(statement) ||
|
|
138
|
+
ts.isInterfaceDeclaration(statement) ||
|
|
139
|
+
ts.isTypeAliasDeclaration(statement));
|
|
140
|
+
}
|
|
141
|
+
/** `return;`, `break;` and `continue;` end the flow: nothing after them carries on. */
|
|
142
|
+
function endsFlow(statement) {
|
|
143
|
+
if (ts.isReturnStatement(statement))
|
|
144
|
+
return statement.expression === undefined;
|
|
145
|
+
return ts.isBreakStatement(statement) || ts.isContinueStatement(statement);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Whether the function runs something after the `try` statement: a statement that follows it in
|
|
149
|
+
* its block or in a block around it, up to the function (or the file, for top-level code). The end
|
|
150
|
+
* of a loop's body runs nothing after it: the loop goes on to its next turn (a retry loop's next
|
|
151
|
+
* attempt).
|
|
152
|
+
*/
|
|
153
|
+
function continuesAfter(tryStatement) {
|
|
154
|
+
let node = tryStatement;
|
|
155
|
+
for (let parent = node.parent;; parent = node.parent) {
|
|
156
|
+
if (ts.isFunctionLike(parent))
|
|
157
|
+
return false;
|
|
158
|
+
if (ts.isBlock(parent) || ts.isSourceFile(parent) || ts.isCaseOrDefaultClause(parent)) {
|
|
159
|
+
const statements = parent.statements;
|
|
160
|
+
const after = statements.slice(statements.findIndex((candidate) => candidate === node) + 1);
|
|
161
|
+
const next = after.find((statement) => !runsNothing(statement));
|
|
162
|
+
if (next !== undefined)
|
|
163
|
+
return !endsFlow(next);
|
|
164
|
+
}
|
|
165
|
+
if (ts.isSourceFile(parent))
|
|
166
|
+
return false;
|
|
167
|
+
if (ts.isIterationStatement(parent, false) && parent.statement === node)
|
|
168
|
+
return false;
|
|
169
|
+
node = parent;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Whether `node` has a `return` with a value that runs in this function (not in a nested one). */
|
|
173
|
+
function returnsValue(node) {
|
|
174
|
+
if (ts.isReturnStatement(node))
|
|
175
|
+
return node.expression !== undefined;
|
|
176
|
+
if (ts.isFunctionLike(node) || ts.isClassLike(node))
|
|
177
|
+
return false;
|
|
178
|
+
return ts.forEachChild(node, (child) => (returnsValue(child) ? true : undefined)) === true;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* A fallback: the `try` returns the value it produced, and the statement right after it returns
|
|
182
|
+
* another one (`try { return JSON.parse(stored); } catch (e) { console.warn(e); } return
|
|
183
|
+
* defaults;`). The caller gets a value either way; nothing runs on a result that was never made.
|
|
184
|
+
*/
|
|
185
|
+
function fallsBackToAValue(tryStatement) {
|
|
186
|
+
const list = tryStatement.parent;
|
|
187
|
+
if (!ts.isBlock(list) && !ts.isSourceFile(list) && !ts.isCaseOrDefaultClause(list))
|
|
188
|
+
return false;
|
|
189
|
+
const after = list.statements.slice(list.statements.indexOf(tryStatement) + 1);
|
|
190
|
+
const next = after.find((statement) => !runsNothing(statement));
|
|
191
|
+
return (next !== undefined &&
|
|
192
|
+
ts.isReturnStatement(next) &&
|
|
193
|
+
next.expression !== undefined &&
|
|
194
|
+
returnsValue(tryStatement.tryBlock));
|
|
195
|
+
}
|
|
196
|
+
/** The names the `try` block assigns with `=` (not in a nested function): `flags = parse(…)`. */
|
|
197
|
+
function assignedNames(block) {
|
|
198
|
+
const names = [];
|
|
199
|
+
const visit = (node) => {
|
|
200
|
+
if (ts.isFunctionLike(node) || ts.isClassLike(node))
|
|
201
|
+
return;
|
|
202
|
+
if (ts.isBinaryExpression(node) &&
|
|
203
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
204
|
+
ts.isIdentifier(stripParentheses(node.left))) {
|
|
205
|
+
const target = stripParentheses(node.left);
|
|
206
|
+
if (ts.isIdentifier(target))
|
|
207
|
+
names.push(target.text);
|
|
208
|
+
}
|
|
209
|
+
ts.forEachChild(node, visit);
|
|
210
|
+
};
|
|
211
|
+
visit(block);
|
|
212
|
+
return names;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Whether `name` already holds a value before the `try`: a `let` or `var` declared with an
|
|
216
|
+
* initializer in a block around it (before it, up to the module), or a parameter of a function
|
|
217
|
+
* around it.
|
|
218
|
+
*/
|
|
219
|
+
function holdsAValueBefore(name, tryStatement) {
|
|
220
|
+
let node = tryStatement;
|
|
221
|
+
for (let parent = node.parent;; parent = node.parent) {
|
|
222
|
+
if (ts.isBlock(parent) || ts.isSourceFile(parent) || ts.isCaseOrDefaultClause(parent)) {
|
|
223
|
+
const before = parent.statements.slice(0, parent.statements.findIndex((s) => s === node));
|
|
224
|
+
for (const statement of before) {
|
|
225
|
+
if (!ts.isVariableStatement(statement))
|
|
226
|
+
continue;
|
|
227
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
228
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.text === name) {
|
|
229
|
+
return declaration.initializer !== undefined;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// A parameter holds the caller's value; past it, a closure's or the module's variable.
|
|
235
|
+
if (ts.isFunctionLike(parent) &&
|
|
236
|
+
parent.parameters.some((parameter) => ts.isIdentifier(parameter.name) && parameter.name.text === name)) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
if (ts.isSourceFile(parent))
|
|
240
|
+
return false;
|
|
241
|
+
node = parent;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* A fallback assigned before the `try`: every variable the `try` assigns already held a value
|
|
246
|
+
* (`let flags = DEFAULTS; try { flags = parse(…); } catch (e) { log(e); } return flags;`). The
|
|
247
|
+
* code after the `try` runs on that value when the `try` fails, which is what the author chose.
|
|
248
|
+
*/
|
|
249
|
+
function replacesAFallback(tryStatement) {
|
|
250
|
+
const names = assignedNames(tryStatement.tryBlock);
|
|
251
|
+
return names.length > 0 && names.every((name) => holdsAValueBefore(name, tryStatement));
|
|
252
|
+
}
|
|
253
|
+
function headline(kind) {
|
|
254
|
+
return `this catch ${kind} the error and carries on as if nothing failed`;
|
|
255
|
+
}
|
|
256
|
+
/** The local name of `err` when the file imports it from `@stateward/values`. */
|
|
257
|
+
function importedErr(sourceFile) {
|
|
258
|
+
for (const statement of sourceFile.statements) {
|
|
259
|
+
if (!ts.isImportDeclaration(statement) ||
|
|
260
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
261
|
+
statement.moduleSpecifier.text !== "@stateward/values" ||
|
|
262
|
+
statement.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const bindings = statement.importClause?.namedBindings;
|
|
266
|
+
if (bindings === undefined || !ts.isNamedImports(bindings))
|
|
267
|
+
continue;
|
|
268
|
+
const element = bindings.elements.find((candidate) => !candidate.isTypeOnly && (candidate.propertyName ?? candidate.name).text === "err");
|
|
269
|
+
if (element !== undefined)
|
|
270
|
+
return element.name.text;
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
/** The example uses only what the file has: `err` when it imports it, else a plain rethrow. */
|
|
275
|
+
function hint(variable, sourceFile) {
|
|
276
|
+
const err = importedErr(sourceFile);
|
|
277
|
+
return withExample("rethrow it, return a failure the caller can see, or show it to the user:", [
|
|
278
|
+
err === undefined ? `throw ${variable};` : `return ${err}(${variable});`,
|
|
279
|
+
]);
|
|
280
|
+
}
|
|
281
|
+
function checkCatch(clause, ctx) {
|
|
282
|
+
const kind = swallowKind(clause.block, ctx.sourceFile, ctx.options);
|
|
283
|
+
if (kind === undefined ||
|
|
284
|
+
!continuesAfter(clause.parent) ||
|
|
285
|
+
fallsBackToAValue(clause.parent) ||
|
|
286
|
+
replacesAFallback(clause.parent)) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const declaration = clause.variableDeclaration;
|
|
290
|
+
const variable = declaration !== undefined && ts.isIdentifier(declaration.name)
|
|
291
|
+
? declaration.name.text
|
|
292
|
+
: "error";
|
|
293
|
+
ctx.report({
|
|
294
|
+
node: clause.getFirstToken(ctx.sourceFile) ?? clause,
|
|
295
|
+
message: headline(kind),
|
|
296
|
+
hint: hint(variable, ctx.sourceFile),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
export const noSwallow = defineRule({
|
|
300
|
+
kind: "file",
|
|
301
|
+
meta: {
|
|
302
|
+
description: "A catch that only logs, or does nothing, and then carries on treats a failure as if it had not happened.",
|
|
303
|
+
defaultSeverity: "warning",
|
|
304
|
+
tier: "addon",
|
|
305
|
+
addon: "swallow",
|
|
306
|
+
group: GROUP,
|
|
307
|
+
heuristic: true,
|
|
308
|
+
options: OPTIONS,
|
|
309
|
+
explain: EXPLAIN,
|
|
310
|
+
},
|
|
311
|
+
check(ctx) {
|
|
312
|
+
const visit = (node) => {
|
|
313
|
+
if (ts.isCatchClause(node))
|
|
314
|
+
checkCatch(node, ctx);
|
|
315
|
+
ts.forEachChild(node, visit);
|
|
316
|
+
};
|
|
317
|
+
visit(ctx.sourceFile);
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
//# sourceMappingURL=no-swallow.js.map
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export declare function errors(): import("@stateward/core").PluginOf<"errors", {
|
|
2
|
+
readonly "catch-hides-unexpected": import("@stateward/core").LazyRule<{
|
|
3
|
+
readonly restUsesError: {
|
|
4
|
+
readonly kind: "boolean";
|
|
5
|
+
readonly default: false;
|
|
6
|
+
readonly description: "The branch for the other errors must use the error (rethrow it, wrap it, report it); a generic message alone does not count.";
|
|
7
|
+
};
|
|
8
|
+
}, "catch">;
|
|
9
|
+
readonly "catch-without-intent": import("@stateward/core").LazyRule<{
|
|
10
|
+
readonly loggers: {
|
|
11
|
+
readonly kind: "strings";
|
|
12
|
+
readonly default: readonly ["console", "logger", "log"];
|
|
13
|
+
readonly description: "Objects whose every method only logs (globs like *Logger): a catch that only calls them swallows the error.";
|
|
14
|
+
};
|
|
15
|
+
readonly logMethods: {
|
|
16
|
+
readonly kind: "strings";
|
|
17
|
+
readonly default: readonly ["error", "warn", "info", "debug"];
|
|
18
|
+
readonly description: "Method names that only log, on any object (`Sentry.captureException` is not one).";
|
|
19
|
+
};
|
|
20
|
+
readonly logFunctions: {
|
|
21
|
+
readonly kind: "strings";
|
|
22
|
+
readonly default: readonly [];
|
|
23
|
+
readonly description: "Plain functions of your own that only log, like logError (globs allowed).";
|
|
24
|
+
};
|
|
25
|
+
readonly edgeHelpers: {
|
|
26
|
+
readonly kind: "strings";
|
|
27
|
+
readonly default: readonly [];
|
|
28
|
+
readonly description: "Your functions that report an error where it ends (to a service, to the user): a catch that calls one is handled, like reportAndShow (globs allowed). Sentry, Bugsnag and Datadog are known without it.";
|
|
29
|
+
};
|
|
30
|
+
readonly swallowHelpers: {
|
|
31
|
+
readonly kind: "strings";
|
|
32
|
+
readonly default: readonly [];
|
|
33
|
+
readonly description: "Functions that mark an error as ignored on purpose, like ignoreError (globs allowed): a catch that calls one is not reported.";
|
|
34
|
+
};
|
|
35
|
+
}, "catch">;
|
|
36
|
+
readonly "finally-can-throw": import("@stateward/core").LazyRule<{
|
|
37
|
+
readonly severities: {
|
|
38
|
+
readonly kind: "severities";
|
|
39
|
+
readonly default: {};
|
|
40
|
+
readonly kinds: {
|
|
41
|
+
readonly throw: "a `throw` written in the finally: it throws whenever it runs.";
|
|
42
|
+
readonly builtin: "a built-in such as `JSON.parse` or `new URL`: it throws only on input it cannot handle.";
|
|
43
|
+
readonly "own-function": "a function in the same file with a `throw` in it: it throws on some paths.";
|
|
44
|
+
};
|
|
45
|
+
readonly description: "A severity per kind of call, where you trust one kind less.";
|
|
46
|
+
};
|
|
47
|
+
}, "catch">;
|
|
48
|
+
readonly "catch-misses-error": import("@stateward/core").LazyRule<{
|
|
49
|
+
readonly ignoreClasses: {
|
|
50
|
+
readonly kind: "strings";
|
|
51
|
+
readonly default: readonly [];
|
|
52
|
+
readonly description: "Error classes a catch never needs a branch for, as its instanceof test names them (SyntaxError, *AbortError).";
|
|
53
|
+
};
|
|
54
|
+
}, "catch">;
|
|
55
|
+
readonly "throws-doc-mismatch": import("@stateward/core").LazyRule<{
|
|
56
|
+
readonly severities: {
|
|
57
|
+
readonly kind: "severities";
|
|
58
|
+
readonly default: {};
|
|
59
|
+
readonly kinds: {
|
|
60
|
+
readonly missing: "the function can throw a class its `@throws` does not list.";
|
|
61
|
+
readonly extra: "the `@throws` lists a class the function cannot throw (stale, not dangerous).";
|
|
62
|
+
};
|
|
63
|
+
readonly description: "A severity per kind of mismatch.";
|
|
64
|
+
};
|
|
65
|
+
readonly ignoreClasses: {
|
|
66
|
+
readonly kind: "strings";
|
|
67
|
+
readonly default: readonly [];
|
|
68
|
+
readonly description: "Error classes a `@throws` never needs to list (TypeError, *AbortError), as a catch names them.";
|
|
69
|
+
};
|
|
70
|
+
}, "callers">;
|
|
71
|
+
readonly "render-throw-without-boundary": import("@stateward/core").LazyRule<{
|
|
72
|
+
readonly skipContextGuards: {
|
|
73
|
+
readonly kind: "boolean";
|
|
74
|
+
readonly default: true;
|
|
75
|
+
readonly description: "Leave out the usual check in a context hook: `if (!ctx) throw new Error(\"useAuth must be used within AuthProvider\")`.";
|
|
76
|
+
};
|
|
77
|
+
readonly skipInputGuards: {
|
|
78
|
+
readonly kind: "boolean";
|
|
79
|
+
readonly default: true;
|
|
80
|
+
readonly description: "Leave out a throw when a prop or a parameter whose type rules it out, or a context hook's value, is missing (a mistake of the caller).";
|
|
81
|
+
};
|
|
82
|
+
readonly rootBoundaryCounts: {
|
|
83
|
+
readonly kind: "boolean";
|
|
84
|
+
readonly default: false;
|
|
85
|
+
readonly description: "Count the error boundary around the whole app as handling the error. Off by default: the whole app is then replaced by its fallback, so the rule asks for a boundary nearer the page.";
|
|
86
|
+
};
|
|
87
|
+
readonly boundaries: {
|
|
88
|
+
readonly kind: "record";
|
|
89
|
+
readonly default: {
|
|
90
|
+
readonly "react-error-boundary": readonly ["ErrorBoundary"];
|
|
91
|
+
};
|
|
92
|
+
readonly description: "Error boundary components from packages, by package: add Sentry's with `{ \"@sentry/react\": [\"ErrorBoundary\"] }` (keep react-error-boundary's to keep it).";
|
|
93
|
+
};
|
|
94
|
+
}, "render">;
|
|
95
|
+
readonly "must-handle-error": import("@stateward/core").LazyRule<Readonly<Record<string, import("@stateward/core").OptionSpec>>, "callers">;
|
|
96
|
+
readonly "narrow-try": import("@stateward/core").LazyRule<Readonly<Record<string, import("@stateward/core").OptionSpec>>, "catch">;
|
|
97
|
+
readonly "result-ignored": import("@stateward/core").LazyRule<{
|
|
98
|
+
readonly allowVoid: {
|
|
99
|
+
readonly kind: "boolean";
|
|
100
|
+
readonly default: false;
|
|
101
|
+
readonly description: "Take `void save()` as dropping the Result on purpose, and stay quiet.";
|
|
102
|
+
};
|
|
103
|
+
readonly resultTypes: {
|
|
104
|
+
readonly kind: "strings";
|
|
105
|
+
readonly default: readonly [];
|
|
106
|
+
readonly description: "Your names for a Result type whose shape stateward does not recognise (`Either`, `Outcome*`; globs allowed).";
|
|
107
|
+
};
|
|
108
|
+
readonly ignore: {
|
|
109
|
+
readonly kind: "strings";
|
|
110
|
+
readonly default: readonly [];
|
|
111
|
+
readonly description: "Functions whose Result may be dropped, by the name they are called with (`logEvent`, `*.track`; globs allowed).";
|
|
112
|
+
};
|
|
113
|
+
}, "callers">;
|
|
114
|
+
readonly "result-unchecked": import("@stateward/core").LazyRule<Readonly<Record<string, import("@stateward/core").OptionSpec>>, "callers">;
|
|
115
|
+
}>;
|
|
116
|
+
//# sourceMappingURL=plugin.d.ts.map
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { definePlugin, lazyRule, moduleLoader } from "@stateward/core";
|
|
2
|
+
import { codeActions } from "./refactors.js";
|
|
3
|
+
import { hover } from "./hover.js";
|
|
4
|
+
import { libraryFacts } from "./inference/deps.js";
|
|
5
|
+
import { inlayHints } from "./inlay.js";
|
|
6
|
+
import { RULES } from "./rules.meta.js";
|
|
7
|
+
/** Each rule's module, imported the first time the rule is on. */
|
|
8
|
+
const load = {
|
|
9
|
+
catchHidesUnexpected: moduleLoader(() => import("./catch-hides-unexpected.js")),
|
|
10
|
+
catchWithoutIntent: moduleLoader(() => import("./catch-without-intent.js")),
|
|
11
|
+
finallyCanThrow: moduleLoader(() => import("./finally-can-throw.js")),
|
|
12
|
+
catchMissesError: moduleLoader(() => import("./catch-misses-error.js")),
|
|
13
|
+
throwsDocMismatch: moduleLoader(() => import("./throws-doc-mismatch.js")),
|
|
14
|
+
renderThrowWithoutBoundary: moduleLoader(() => import("./render-throw-without-boundary.js")),
|
|
15
|
+
mustHandleError: moduleLoader(() => import("./must-handle-error.js")),
|
|
16
|
+
narrowTry: moduleLoader(() => import("./narrow-try.js")),
|
|
17
|
+
resultIgnored: moduleLoader(() => import("./result-ignored.js")),
|
|
18
|
+
resultUnchecked: moduleLoader(() => import("./result-unchecked.js")),
|
|
19
|
+
};
|
|
20
|
+
export function errors() {
|
|
21
|
+
return definePlugin({
|
|
22
|
+
name: "errors",
|
|
23
|
+
description: "Errors are handled where they can happen: nothing swallowed, nothing lost in finally, no Result dropped.",
|
|
24
|
+
rules: {
|
|
25
|
+
"catch-hides-unexpected": lazyRule(RULES["catch-hides-unexpected"], load.catchHidesUnexpected, (module) => module.catchHidesUnexpected),
|
|
26
|
+
"catch-without-intent": lazyRule(RULES["catch-without-intent"], load.catchWithoutIntent, (module) => module.catchWithoutIntent),
|
|
27
|
+
"finally-can-throw": lazyRule(RULES["finally-can-throw"], load.finallyCanThrow, (module) => module.finallyCanThrow),
|
|
28
|
+
"catch-misses-error": lazyRule(RULES["catch-misses-error"], load.catchMissesError, (module) => module.catchMissesError),
|
|
29
|
+
"throws-doc-mismatch": lazyRule(RULES["throws-doc-mismatch"], load.throwsDocMismatch, (module) => module.throwsDocMismatch),
|
|
30
|
+
"render-throw-without-boundary": lazyRule(RULES["render-throw-without-boundary"], load.renderThrowWithoutBoundary, (module) => module.renderThrowWithoutBoundary),
|
|
31
|
+
"must-handle-error": lazyRule(RULES["must-handle-error"], load.mustHandleError, (module) => module.mustHandleError),
|
|
32
|
+
"narrow-try": lazyRule(RULES["narrow-try"], load.narrowTry, (module) => module.narrowTry),
|
|
33
|
+
"result-ignored": lazyRule(RULES["result-ignored"], load.resultIgnored, (module) => module.resultIgnored),
|
|
34
|
+
"result-unchecked": lazyRule(RULES["result-unchecked"], load.resultUnchecked, (module) => module.resultUnchecked),
|
|
35
|
+
},
|
|
36
|
+
editor: { hover, inlayHints, codeActions, libraryFacts },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { FileContext, Json, Location, StringListRecord } from "@stateward/core";
|
|
2
|
+
import type { Analysis, Source } from "../inference/solve.js";
|
|
3
|
+
import type { ThrowsSummary } from "../inference/summary.js";
|
|
4
|
+
import { type GuardSkips } from "./render-guards.js";
|
|
5
|
+
import type { BoundaryElement, TreeFunction, TreeRender, TreeSummary } from "./tree.js";
|
|
6
|
+
/**
|
|
7
|
+
* Errors in React: what each component throws while React
|
|
8
|
+
* renders it or runs its effects, which error boundary is nearest above each render, and what
|
|
9
|
+
* each boundary can catch. Handlers, timers and async code are left out: a boundary never sees
|
|
10
|
+
* their errors.
|
|
11
|
+
*/
|
|
12
|
+
/** The summary of the pack's React rules: the throws facts and the render tree of one file. */
|
|
13
|
+
export type ReactSummary = {
|
|
14
|
+
readonly throws: ThrowsSummary;
|
|
15
|
+
readonly tree: TreeSummary;
|
|
16
|
+
};
|
|
17
|
+
/** The rules whose summaries hold a render tree; the first that ran is read. */
|
|
18
|
+
export declare const TREE_RULES: readonly ["errors/render-throw-without-boundary", "errors/catch-misses-error"];
|
|
19
|
+
export declare function summarizeReact(ctx: FileContext, libraryBoundaries?: StringListRecord, skips?: GuardSkips): ReactSummary;
|
|
20
|
+
/** A render with the file it is written in. */
|
|
21
|
+
export type Site = {
|
|
22
|
+
readonly to: string;
|
|
23
|
+
readonly file: string;
|
|
24
|
+
readonly render: TreeRender;
|
|
25
|
+
};
|
|
26
|
+
/** One class a component throws: whether surely, and the call that brings it. */
|
|
27
|
+
export type Brought = {
|
|
28
|
+
readonly certain: boolean;
|
|
29
|
+
/** `useUser` for `(via <UserPage> → useUser)`; null when the component throws it itself. */
|
|
30
|
+
readonly step: string | null;
|
|
31
|
+
readonly location: Location;
|
|
32
|
+
/** `` `useUser(…)` can throw `NotFoundError` `` once the class's name is known. */
|
|
33
|
+
readonly what: string;
|
|
34
|
+
};
|
|
35
|
+
export type Own = {
|
|
36
|
+
readonly classes: ReadonlyMap<string, Brought>;
|
|
37
|
+
/**
|
|
38
|
+
* The classes it throws only from guards (render-guards.ts): a missing context or input, a
|
|
39
|
+
* mistake of the caller. A boundary still catches them; they do not make a render failure.
|
|
40
|
+
*/
|
|
41
|
+
readonly guarded: ReadonlySet<string>;
|
|
42
|
+
readonly unknowns: readonly Source[];
|
|
43
|
+
readonly more: boolean;
|
|
44
|
+
};
|
|
45
|
+
export type Model = {
|
|
46
|
+
readonly analysis: Analysis;
|
|
47
|
+
readonly functions: ReadonlyMap<string, {
|
|
48
|
+
file: string;
|
|
49
|
+
fn: TreeFunction;
|
|
50
|
+
}>;
|
|
51
|
+
readonly boundaries: ReadonlyMap<string, {
|
|
52
|
+
file: string;
|
|
53
|
+
element: BoundaryElement;
|
|
54
|
+
}>;
|
|
55
|
+
/** Boundaries by file, for the hover. */
|
|
56
|
+
readonly byFile: ReadonlyMap<string, readonly BoundaryElement[]>;
|
|
57
|
+
readonly entries: readonly Site[];
|
|
58
|
+
/** Every render (entries first), by file and offset. */
|
|
59
|
+
readonly sites: readonly Site[];
|
|
60
|
+
/** The renders written in a component, in order. */
|
|
61
|
+
edges(id: string): readonly Site[];
|
|
62
|
+
/** The boundary nearest above a render, by key; undefined when there is none in its component. */
|
|
63
|
+
nearest(site: Site): string | undefined;
|
|
64
|
+
own(id: string): Own;
|
|
65
|
+
/** The classes a component surely throws, with what it renders outside any boundary. */
|
|
66
|
+
needs(id: string): ReadonlySet<string>;
|
|
67
|
+
/** What a boundary catches: from the renders whose nearest boundary it is, down to nested ones. */
|
|
68
|
+
catches(key: string): Omit<Own, "guarded"> & {
|
|
69
|
+
readonly labels: ReadonlyMap<string, string>;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
/** The throws part of each summary: the same facts `catch-misses-error` solves. */
|
|
73
|
+
export declare function throwsOf(summaries: ReadonlyMap<string, Json>): ReadonlyMap<string, Json>;
|
|
74
|
+
/** The pack's view of the app's render tree, built once per summaries map. */
|
|
75
|
+
export declare function modelOf(summaries: ReadonlyMap<string, Json>): Model;
|
|
76
|
+
/** The line every boundary hover ends with: what no boundary ever catches. */
|
|
77
|
+
export declare const NOT_CAUGHT = "not caught here: errors in event handlers and async code";
|
|
78
|
+
/**
|
|
79
|
+
* On a boundary's tag: `catches: NotFoundError (via <UserPage> → useUser) | others stateward
|
|
80
|
+
* cannot see (from …)`, then `NOT_CAUGHT`. Undefined when `offset` is on no boundary's tag.
|
|
81
|
+
*/
|
|
82
|
+
export declare function boundaryLines(model: Model, file: string, offset: number): string[] | undefined;
|
|
83
|
+
//# sourceMappingURL=boundaries.d.ts.map
|