@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,746 @@
|
|
|
1
|
+
import ts from "@stateward/core/typescript";
|
|
2
|
+
import { defineRule } from "@stateward/core";
|
|
3
|
+
import { example, resultShapeOf, shorten } from "./result-type.js";
|
|
4
|
+
const EXPLAIN = `# errors/result-unchecked
|
|
5
|
+
|
|
6
|
+
A call that reports failure in its value, not by throwing, is checked before the code goes on as if it worked.
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
Some calls do not throw when they fail: they resolve to data or an error. An RTK Query trigger without \`.unwrap()\` is the common one. Wrap \`await deletePlan(id)\` in a \`try\` and the \`catch\` never runs: a failed delete still shows "Deleted" and closes the dialog. Store the result and show its error, and the lines after still run: "Saved" appears after a save that failed.
|
|
11
|
+
|
|
12
|
+
## When to use it
|
|
13
|
+
|
|
14
|
+
Use it if you call RTK Query triggers, or functions that return a Result (\`{ ok: true; value } | { ok: false; error }\`, zod's \`safeParse\`, neverthrow), and await them in event handlers. It is a warning in every preset.
|
|
15
|
+
|
|
16
|
+
## What it catches
|
|
17
|
+
|
|
18
|
+
A call whose value is data or an error: a promise with an \`unwrap()\` method that resolves to a union with an \`error\` member (RTK Query's triggers; found by the type, not by a name), or a Result recognised as \`errors/result-ignored\` recognises it. Then:
|
|
19
|
+
|
|
20
|
+
- awaited as a statement inside a \`try\` whose \`catch\` nothing else can reach (no call before it, no other \`await\` or \`throw\` in the \`try\`): the \`catch\` never runs;
|
|
21
|
+
- awaited as a statement inside a \`try\` after another call (an optimistic \`setFolders(…)\`, a \`parse(values)\`), with more lines after it: when it fails, those lines run (\`notify("Folder updated!")\`) and the \`catch\` (the rollback) does not;
|
|
22
|
+
- a trigger awaited as a statement outside a \`try\`, with more lines after it (\`await sendReply(x); setText("")\`);
|
|
23
|
+
- stored, then its data read before it is checked;
|
|
24
|
+
- stored, its error read in an \`if\` that does not \`return\` or \`throw\` (\`"error" in result\`, \`result.error\`, or an \`error\` or \`isError\` taken out of it), with more lines after that \`if\`.
|
|
25
|
+
|
|
26
|
+
### Bad
|
|
27
|
+
|
|
28
|
+
\`\`\`ts
|
|
29
|
+
try {
|
|
30
|
+
await deletePlan(id);
|
|
31
|
+
toast.success("Deleted");
|
|
32
|
+
} catch {
|
|
33
|
+
toast.error("Could not delete");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = await saveSettings(values);
|
|
37
|
+
if ("error" in result) toast.error("Could not save");
|
|
38
|
+
toast.success("Saved");
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
### Good
|
|
42
|
+
|
|
43
|
+
\`\`\`ts
|
|
44
|
+
try {
|
|
45
|
+
await deletePlan(id).unwrap();
|
|
46
|
+
toast.success("Deleted");
|
|
47
|
+
} catch {
|
|
48
|
+
toast.error("Could not delete");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = await saveSettings(values);
|
|
52
|
+
if ("error" in result) {
|
|
53
|
+
toast.error("Could not save");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
toast.success("Saved");
|
|
57
|
+
\`\`\`
|
|
58
|
+
|
|
59
|
+
### Fix
|
|
60
|
+
|
|
61
|
+
Add \`.unwrap()\` so a failure throws (a quick fix where the result is dropped), or check the result and \`return\` (or \`throw\`) when it failed. Code that says what it means is left alone: \`void trigger(…)\`, \`.unwrap()\`, a check (\`"error" in result\`, \`if (result.error) return\`, \`isOk(result)\`, \`const { data, isError } = await dispatch(…); if (isError) throw …\`) before the success path, an \`if\` with an \`else\` for the success path, and a component that reads the trigger's own \`isError\` or \`error\` from its hook (\`const [save, { isError }] = useSaveMutation()\`).
|
|
62
|
+
|
|
63
|
+
## What it does not catch
|
|
64
|
+
|
|
65
|
+
- A Result (\`ok\`, \`success\`, neverthrow) dropped as a statement: that is \`errors/result-ignored\`'s.
|
|
66
|
+
- Lines after the call that only set back a pending state set before it (\`setSaving(true)\` … \`setSaving(false)\`): they run either way on purpose.
|
|
67
|
+
- A \`try\` with another \`await\` or a \`throw\` in it: the \`catch\` may be there for that.
|
|
68
|
+
- A result passed on, returned or stored elsewhere, or checked in a callback.
|
|
69
|
+
- A promise that rejects on failure (react-query's \`mutateAsync\`, \`fetch\`): it has no \`unwrap()\` that resolves to data or an error.
|
|
70
|
+
- An \`if\` that tests the result in a way stateward cannot read (\`if (result.error && isApiError(result.error))\`): it is listed as a place not checked.
|
|
71
|
+
|
|
72
|
+
## Turn it on or off
|
|
73
|
+
|
|
74
|
+
In \`stateward.config.ts\`:
|
|
75
|
+
|
|
76
|
+
\`\`\`ts
|
|
77
|
+
rules: { "errors/result-unchecked": "off" } // or "error"
|
|
78
|
+
groups: { "errors/callers": "off" } // the whole group
|
|
79
|
+
\`\`\`
|
|
80
|
+
`;
|
|
81
|
+
/** The longest call an example line shows before its arguments become `…`. */
|
|
82
|
+
const CALL_MAX = 60;
|
|
83
|
+
/** The longest condition the fix hint quotes. */
|
|
84
|
+
const CONDITION_MAX = 50;
|
|
85
|
+
// `isError` and `isSuccess`: an RTK Query state (`await dispatch(api.endpoints.x.initiate(…))`)
|
|
86
|
+
// carries them next to `data` and `error`, and testing one is as much a check as testing `error`.
|
|
87
|
+
const ALIAS_KINDS = new Map([
|
|
88
|
+
["error", "error"],
|
|
89
|
+
["isError", "error"],
|
|
90
|
+
["data", "data"],
|
|
91
|
+
["value", "data"],
|
|
92
|
+
["ok", "ok"],
|
|
93
|
+
["success", "ok"],
|
|
94
|
+
["isSuccess", "ok"],
|
|
95
|
+
]);
|
|
96
|
+
const DATA_PROPERTIES = new Set(["data", "value"]);
|
|
97
|
+
const SUCCESS_PROPERTIES = new Set(["data", "value", "ok", "success"]);
|
|
98
|
+
const FAILURE_GUARD = /^is(?:Err|Error|Failure|Failed)$/;
|
|
99
|
+
const SUCCESS_GUARD = /^is(?:Ok|Success|Succeeded)$/;
|
|
100
|
+
function strip(expression) {
|
|
101
|
+
return ts.isParenthesizedExpression(expression) ? strip(expression.expression) : expression;
|
|
102
|
+
}
|
|
103
|
+
function flip(polarity) {
|
|
104
|
+
if (polarity === "failure")
|
|
105
|
+
return "success";
|
|
106
|
+
if (polarity === "success")
|
|
107
|
+
return "failure";
|
|
108
|
+
return polarity;
|
|
109
|
+
}
|
|
110
|
+
/** The kind of data-or-error value a call gives, or undefined when it is something else. */
|
|
111
|
+
function kindOf(call, awaited, ctx) {
|
|
112
|
+
const shape = resultShapeOf(call, ctx.types, []);
|
|
113
|
+
if (shape !== undefined)
|
|
114
|
+
return shape === "named" ? undefined : shape;
|
|
115
|
+
return awaited && ctx.types.settlesWithError(call) ? "trigger" : undefined;
|
|
116
|
+
}
|
|
117
|
+
function calleeText(call, ctx) {
|
|
118
|
+
return call.expression.getText(ctx.sourceFile).replace(/\s+/g, "");
|
|
119
|
+
}
|
|
120
|
+
/** The call as an example line shows it: whole, or its callee and `(…)` when long. */
|
|
121
|
+
function callText(call, ctx) {
|
|
122
|
+
const whole = shorten(call.getText(ctx.sourceFile), Number.MAX_SAFE_INTEGER);
|
|
123
|
+
return whole.length > CALL_MAX
|
|
124
|
+
? `${shorten(call.expression.getText(ctx.sourceFile), CALL_MAX)}(…)`
|
|
125
|
+
: whole;
|
|
126
|
+
}
|
|
127
|
+
/** The line that returns when the call failed, in the result's own shape and names. */
|
|
128
|
+
function checkLine(tracked) {
|
|
129
|
+
const errorAlias = [...tracked.aliases].find(([, kind]) => kind === "error")?.[0];
|
|
130
|
+
if (tracked.whole === undefined && errorAlias !== undefined)
|
|
131
|
+
return `if (${errorAlias}) return;`;
|
|
132
|
+
const name = tracked.whole ?? "result";
|
|
133
|
+
switch (tracked.kind) {
|
|
134
|
+
case "trigger":
|
|
135
|
+
return `if ("error" in ${name}) return;`;
|
|
136
|
+
case "ok":
|
|
137
|
+
return `if (!${name}.ok) return;`;
|
|
138
|
+
case "success":
|
|
139
|
+
return `if (!${name}.success) return;`;
|
|
140
|
+
case "isErr":
|
|
141
|
+
return `if (${name}.isErr()) return;`;
|
|
142
|
+
default: {
|
|
143
|
+
const unreachable = tracked.kind;
|
|
144
|
+
return unreachable;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Whether an identifier is a name read as a value (not `x.name`, not a property's key). */
|
|
149
|
+
function isValueName(node) {
|
|
150
|
+
const parent = node.parent;
|
|
151
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node)
|
|
152
|
+
return false;
|
|
153
|
+
if (ts.isPropertyAssignment(parent) && parent.name === node)
|
|
154
|
+
return false;
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
function someNode(node, test) {
|
|
158
|
+
if (test(node))
|
|
159
|
+
return true;
|
|
160
|
+
return ts.forEachChild(node, (child) => someNode(child, test)) === true;
|
|
161
|
+
}
|
|
162
|
+
function findNode(node, test) {
|
|
163
|
+
if (test(node))
|
|
164
|
+
return node;
|
|
165
|
+
return ts.forEachChild(node, (child) => findNode(child, test));
|
|
166
|
+
}
|
|
167
|
+
function mentions(node, tracked) {
|
|
168
|
+
return someNode(node, (one) => ts.isIdentifier(one) &&
|
|
169
|
+
isValueName(one) &&
|
|
170
|
+
(one.text === tracked.whole || tracked.aliases.has(one.text)));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Whether `node` sits where a test of the result has already said the call worked:
|
|
174
|
+
* `ok ? r.data : …`, `"data" in r && r.data`, `r.error || r.data`, or an `if` branch.
|
|
175
|
+
*/
|
|
176
|
+
function guarded(node, stop, tracked) {
|
|
177
|
+
let child = node;
|
|
178
|
+
while (child !== stop) {
|
|
179
|
+
const parent = child.parent;
|
|
180
|
+
let test;
|
|
181
|
+
let when;
|
|
182
|
+
if (ts.isConditionalExpression(parent) && child !== parent.condition) {
|
|
183
|
+
test = parent.condition;
|
|
184
|
+
when = child === parent.whenTrue ? "success" : "failure";
|
|
185
|
+
}
|
|
186
|
+
else if (ts.isIfStatement(parent) && child !== parent.expression) {
|
|
187
|
+
test = parent.expression;
|
|
188
|
+
when = child === parent.thenStatement ? "success" : "failure";
|
|
189
|
+
}
|
|
190
|
+
else if (ts.isBinaryExpression(parent) && child === parent.right) {
|
|
191
|
+
const operator = parent.operatorToken.kind;
|
|
192
|
+
test = parent.left;
|
|
193
|
+
if (operator === ts.SyntaxKind.AmpersandAmpersandToken)
|
|
194
|
+
when = "success";
|
|
195
|
+
if (operator === ts.SyntaxKind.BarBarToken)
|
|
196
|
+
when = "failure";
|
|
197
|
+
}
|
|
198
|
+
if (test !== undefined && when !== undefined && polarity(test, tracked) === when)
|
|
199
|
+
return true;
|
|
200
|
+
child = parent;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* `result.data`, or a `data` destructured from it, read where no test has said the call worked.
|
|
206
|
+
* Only a trigger's data can be read unchecked: a Result's `value` exists only on its success
|
|
207
|
+
* member, so TypeScript itself refuses to read it before a check.
|
|
208
|
+
*/
|
|
209
|
+
function readsData(statement, tracked) {
|
|
210
|
+
if (tracked.kind !== "trigger")
|
|
211
|
+
return false;
|
|
212
|
+
return someNode(statement, (one) => {
|
|
213
|
+
const read = (ts.isPropertyAccessExpression(one) &&
|
|
214
|
+
ts.isIdentifier(one.expression) &&
|
|
215
|
+
one.expression.text === tracked.whole &&
|
|
216
|
+
DATA_PROPERTIES.has(one.name.text)) ||
|
|
217
|
+
(ts.isIdentifier(one) && isValueName(one) && tracked.aliases.get(one.text) === "data");
|
|
218
|
+
return read && !guarded(one, statement, tracked);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function isWhole(expression, tracked) {
|
|
222
|
+
const code = strip(expression);
|
|
223
|
+
return ts.isIdentifier(code) && code.text === tracked.whole;
|
|
224
|
+
}
|
|
225
|
+
/** `undefined`, `null`, `true` or `false` as written, or undefined for anything else. */
|
|
226
|
+
function literalOf(expression) {
|
|
227
|
+
const code = strip(expression);
|
|
228
|
+
if (code.kind === ts.SyntaxKind.NullKeyword)
|
|
229
|
+
return "nullish";
|
|
230
|
+
if (ts.isIdentifier(code) && code.text === "undefined")
|
|
231
|
+
return "nullish";
|
|
232
|
+
if (code.kind === ts.SyntaxKind.TrueKeyword)
|
|
233
|
+
return "true";
|
|
234
|
+
if (code.kind === ts.SyntaxKind.FalseKeyword)
|
|
235
|
+
return "false";
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
const EQUAL = new Set([ts.SyntaxKind.EqualsEqualsEqualsToken, ts.SyntaxKind.EqualsEqualsToken]);
|
|
239
|
+
const NOT_EQUAL = new Set([
|
|
240
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
241
|
+
ts.SyntaxKind.ExclamationEqualsToken,
|
|
242
|
+
]);
|
|
243
|
+
/** `a === undefined`, `a !== null`, `a === false`: the polarity of `a` as a truthy test, turned by the comparison. */
|
|
244
|
+
function comparedPolarity(expression, tracked) {
|
|
245
|
+
const operator = expression.operatorToken.kind;
|
|
246
|
+
const rightLiteral = literalOf(expression.right);
|
|
247
|
+
const literal = rightLiteral ?? literalOf(expression.left);
|
|
248
|
+
const other = rightLiteral === undefined ? expression.left : expression.right;
|
|
249
|
+
if (literal === undefined)
|
|
250
|
+
return mentions(expression, tracked) ? "undecided" : "none";
|
|
251
|
+
const base = polarity(other, tracked);
|
|
252
|
+
if (base !== "failure" && base !== "success")
|
|
253
|
+
return base;
|
|
254
|
+
// `x === true` keeps the test; `x === undefined`, `x === null` and `x === false` turn it.
|
|
255
|
+
const keeps = literal === "true";
|
|
256
|
+
if (EQUAL.has(operator))
|
|
257
|
+
return keeps ? base : flip(base);
|
|
258
|
+
return keeps ? flip(base) : base;
|
|
259
|
+
}
|
|
260
|
+
/** What an `if` condition says about the stored result (design: errors.md, result-unchecked). */
|
|
261
|
+
function polarity(expression, tracked) {
|
|
262
|
+
const code = strip(expression);
|
|
263
|
+
if (ts.isPrefixUnaryExpression(code) && code.operator === ts.SyntaxKind.ExclamationToken) {
|
|
264
|
+
return flip(polarity(code.operand, tracked));
|
|
265
|
+
}
|
|
266
|
+
if (ts.isIdentifier(code)) {
|
|
267
|
+
const alias = tracked.aliases.get(code.text);
|
|
268
|
+
if (alias === "error")
|
|
269
|
+
return "failure";
|
|
270
|
+
if (alias !== undefined)
|
|
271
|
+
return "success";
|
|
272
|
+
return code.text === tracked.whole ? "undecided" : "none";
|
|
273
|
+
}
|
|
274
|
+
if (ts.isPropertyAccessExpression(code) && isWhole(code.expression, tracked)) {
|
|
275
|
+
if (code.name.text === "error")
|
|
276
|
+
return "failure";
|
|
277
|
+
return SUCCESS_PROPERTIES.has(code.name.text) ? "success" : "undecided";
|
|
278
|
+
}
|
|
279
|
+
if (ts.isCallExpression(code)) {
|
|
280
|
+
const callee = strip(code.expression);
|
|
281
|
+
if (ts.isPropertyAccessExpression(callee) && isWhole(callee.expression, tracked)) {
|
|
282
|
+
if (callee.name.text === "isErr")
|
|
283
|
+
return "failure";
|
|
284
|
+
if (callee.name.text === "isOk")
|
|
285
|
+
return "success";
|
|
286
|
+
}
|
|
287
|
+
const [argument] = code.arguments;
|
|
288
|
+
if (ts.isIdentifier(callee) &&
|
|
289
|
+
code.arguments.length === 1 &&
|
|
290
|
+
argument !== undefined &&
|
|
291
|
+
isWhole(argument, tracked)) {
|
|
292
|
+
if (FAILURE_GUARD.test(callee.text))
|
|
293
|
+
return "failure";
|
|
294
|
+
if (SUCCESS_GUARD.test(callee.text))
|
|
295
|
+
return "success";
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (ts.isBinaryExpression(code)) {
|
|
299
|
+
const operator = code.operatorToken.kind;
|
|
300
|
+
if (operator === ts.SyntaxKind.InKeyword &&
|
|
301
|
+
ts.isStringLiteral(code.left) &&
|
|
302
|
+
isWhole(code.right, tracked)) {
|
|
303
|
+
if (code.left.text === "error")
|
|
304
|
+
return "failure";
|
|
305
|
+
return DATA_PROPERTIES.has(code.left.text) ? "success" : "undecided";
|
|
306
|
+
}
|
|
307
|
+
if (EQUAL.has(operator) || NOT_EQUAL.has(operator))
|
|
308
|
+
return comparedPolarity(code, tracked);
|
|
309
|
+
}
|
|
310
|
+
return mentions(code, tracked) ? "undecided" : "none";
|
|
311
|
+
}
|
|
312
|
+
/** Whether a statement always leaves the block it is in: `return`, `throw`, `break`, `continue`. */
|
|
313
|
+
function exits(statement) {
|
|
314
|
+
if (ts.isReturnStatement(statement) ||
|
|
315
|
+
ts.isThrowStatement(statement) ||
|
|
316
|
+
ts.isBreakStatement(statement) ||
|
|
317
|
+
ts.isContinueStatement(statement)) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
if (ts.isBlock(statement))
|
|
321
|
+
return statement.statements.some(exits);
|
|
322
|
+
if (ts.isIfStatement(statement)) {
|
|
323
|
+
return (exits(statement.thenStatement) &&
|
|
324
|
+
statement.elseStatement !== undefined &&
|
|
325
|
+
exits(statement.elseStatement));
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
function isFunctionLike(node) {
|
|
330
|
+
return ts.isFunctionLike(node) || ts.isClassLike(node);
|
|
331
|
+
}
|
|
332
|
+
/** Whether `node` has a descendant passing `test`, without entering nested functions. */
|
|
333
|
+
function inBody(node, test) {
|
|
334
|
+
if (test(node))
|
|
335
|
+
return true;
|
|
336
|
+
return ts.forEachChild(node, (child) => !isFunctionLike(child) && inBody(child, test)) === true;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Whether the component reads the trigger's own failure from its hook: `const [save, { isError }]
|
|
340
|
+
* = useSaveMutation()`, or `const [save, saving] = …` and `saving.error` read. Failure is then
|
|
341
|
+
* handled through the hook's result (stated intent), so code after the call may run either way.
|
|
342
|
+
*/
|
|
343
|
+
function hookStateRead(call, ctx) {
|
|
344
|
+
const callee = strip(call.expression);
|
|
345
|
+
if (!ts.isIdentifier(callee))
|
|
346
|
+
return false;
|
|
347
|
+
const reads = new Set(["isError", "error"]);
|
|
348
|
+
const hook = findNode(ctx.sourceFile, (node) => {
|
|
349
|
+
if (!ts.isVariableDeclaration(node) || !ts.isArrayBindingPattern(node.name))
|
|
350
|
+
return false;
|
|
351
|
+
const [trigger] = node.name.elements;
|
|
352
|
+
return (trigger !== undefined &&
|
|
353
|
+
ts.isBindingElement(trigger) &&
|
|
354
|
+
ts.isIdentifier(trigger.name) &&
|
|
355
|
+
trigger.name.text === callee.text);
|
|
356
|
+
});
|
|
357
|
+
if (hook === undefined || !ts.isVariableDeclaration(hook))
|
|
358
|
+
return false;
|
|
359
|
+
if (!ts.isArrayBindingPattern(hook.name))
|
|
360
|
+
return false;
|
|
361
|
+
const second = hook.name.elements[1];
|
|
362
|
+
if (second === undefined || !ts.isBindingElement(second))
|
|
363
|
+
return false;
|
|
364
|
+
const found = second.name;
|
|
365
|
+
if (ts.isObjectBindingPattern(found)) {
|
|
366
|
+
return found.elements.some((element) => {
|
|
367
|
+
const key = element.propertyName ?? element.name;
|
|
368
|
+
return ts.isIdentifier(key) && reads.has(key.text);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (!ts.isIdentifier(found))
|
|
372
|
+
return false;
|
|
373
|
+
const name = found.text;
|
|
374
|
+
return someNode(ctx.sourceFile, (node) => ts.isPropertyAccessExpression(node) &&
|
|
375
|
+
ts.isIdentifier(node.expression) &&
|
|
376
|
+
node.expression.text === name &&
|
|
377
|
+
reads.has(node.name.text));
|
|
378
|
+
}
|
|
379
|
+
/** `await call(…);` as a statement: the call, or undefined. */
|
|
380
|
+
function droppedAwait(statement) {
|
|
381
|
+
if (!ts.isExpressionStatement(statement))
|
|
382
|
+
return undefined;
|
|
383
|
+
const awaited = strip(statement.expression);
|
|
384
|
+
if (!ts.isAwaitExpression(awaited))
|
|
385
|
+
return undefined;
|
|
386
|
+
const call = strip(awaited.expression);
|
|
387
|
+
return ts.isCallExpression(call) ? call : undefined;
|
|
388
|
+
}
|
|
389
|
+
function reachesCatch(node) {
|
|
390
|
+
return inBody(node, (one) => ts.isAwaitExpression(one) ||
|
|
391
|
+
ts.isThrowStatement(one) ||
|
|
392
|
+
ts.isYieldExpression(one) ||
|
|
393
|
+
(ts.isForOfStatement(one) && one.awaitModifier !== undefined));
|
|
394
|
+
}
|
|
395
|
+
function callsSomething(node) {
|
|
396
|
+
return inBody(node, (one) => ts.isCallExpression(one) || ts.isNewExpression(one) || ts.isTaggedTemplateExpression(one));
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Case (a): `try { await trigger(x); … } catch { … }`. When only the trigger could reach the
|
|
400
|
+
* `catch`, the `catch` never runs. When a call before it could (`schema.parse(values)`, an
|
|
401
|
+
* optimistic `setFolders(…)`), the `catch` may be for that call, but the lines after the trigger
|
|
402
|
+
* still run when it failed, and the `catch` (a rollback) does not.
|
|
403
|
+
*/
|
|
404
|
+
function checkTry(statement, ctx) {
|
|
405
|
+
if (statement.catchClause === undefined)
|
|
406
|
+
return;
|
|
407
|
+
const statements = statement.tryBlock.statements;
|
|
408
|
+
const found = [];
|
|
409
|
+
let callBefore = false;
|
|
410
|
+
for (const inner of statements) {
|
|
411
|
+
const call = droppedAwait(inner);
|
|
412
|
+
if (call !== undefined && kindOf(call, true, ctx) === "trigger") {
|
|
413
|
+
found.push([inner, call]);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (reachesCatch(inner))
|
|
417
|
+
return;
|
|
418
|
+
if (found.length === 0 && callsSomething(inner))
|
|
419
|
+
callBefore = true;
|
|
420
|
+
}
|
|
421
|
+
if (callBefore) {
|
|
422
|
+
for (const [inner, call] of found) {
|
|
423
|
+
const index = statements.indexOf(inner);
|
|
424
|
+
const after = statements.slice(index + 1);
|
|
425
|
+
if (after.every((next) => undoesPending(next, statements.slice(0, index))))
|
|
426
|
+
continue;
|
|
427
|
+
if (hookStateRead(call, ctx))
|
|
428
|
+
continue;
|
|
429
|
+
ctx.report({
|
|
430
|
+
node: inner,
|
|
431
|
+
message: `the lines after this run even when \`${calleeText(call, ctx)}(…)\` failed, and the \`catch\` does not`,
|
|
432
|
+
hint: [
|
|
433
|
+
'add `.unwrap()` so a failure reaches the `catch` (quick fix), or check `"error" in result` and return:',
|
|
434
|
+
example([
|
|
435
|
+
`const result = await ${callText(call, ctx)};`,
|
|
436
|
+
`if ("error" in result) return;`,
|
|
437
|
+
]),
|
|
438
|
+
].join("\n"),
|
|
439
|
+
fixes: [unwrapFix(call, "Add `.unwrap()` so a failure reaches the `catch`", ctx)],
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
for (const [inner, call] of found) {
|
|
445
|
+
ctx.report({
|
|
446
|
+
// The statement, as `errors/result-ignored` reports it, so that rule can yield to this one.
|
|
447
|
+
node: inner,
|
|
448
|
+
message: `\`${calleeText(call, ctx)}(…)\` does not throw when it fails, so this \`catch\` never runs`,
|
|
449
|
+
hint: [
|
|
450
|
+
'the lines after it still run on failure; add `.unwrap()` (quick fix), or check `"error" in result` and return:',
|
|
451
|
+
example([`const result = await ${callText(call, ctx)};`, `if ("error" in result) return;`]),
|
|
452
|
+
].join("\n"),
|
|
453
|
+
fixes: [unwrapFix(call, "Add `.unwrap()` so a failure reaches the `catch`", ctx)],
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function unwrapFix(call, title, ctx) {
|
|
458
|
+
const at = call.getEnd();
|
|
459
|
+
return {
|
|
460
|
+
title,
|
|
461
|
+
edits: [
|
|
462
|
+
{ location: { file: ctx.sourceFile.fileName, start: at, end: at }, newText: ".unwrap()" },
|
|
463
|
+
],
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/** A state setter by its name: `setSaving`, `form.setBusy`. */
|
|
467
|
+
const SETTER = /^set[A-Z_]/;
|
|
468
|
+
/**
|
|
469
|
+
* The target a statement sets, as written: `setSaving` in `setSaving(false)`, `state.busy` in
|
|
470
|
+
* `state.busy = false`. Only a setter counts: `toast("Saving…")` before and `toast("Saved")` after
|
|
471
|
+
* are two messages, not a pending state set back.
|
|
472
|
+
*/
|
|
473
|
+
function setTarget(statement) {
|
|
474
|
+
if (!ts.isExpressionStatement(statement))
|
|
475
|
+
return undefined;
|
|
476
|
+
const code = strip(statement.expression);
|
|
477
|
+
if (ts.isCallExpression(code) && code.arguments.length === 1) {
|
|
478
|
+
const callee = strip(code.expression);
|
|
479
|
+
const name = ts.isPropertyAccessExpression(callee) ? callee.name : callee;
|
|
480
|
+
if (!ts.isIdentifier(name) || !SETTER.test(name.text))
|
|
481
|
+
return undefined;
|
|
482
|
+
return code.expression.getText().replace(/\s+/g, "");
|
|
483
|
+
}
|
|
484
|
+
if (ts.isBinaryExpression(code) && code.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
485
|
+
return code.left.getText().replace(/\s+/g, "");
|
|
486
|
+
}
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Whether a statement after the call only sets back a pending state set before it
|
|
491
|
+
* (`setSaving(true)` … `setSaving(false)`): that runs on success and failure alike on purpose,
|
|
492
|
+
* so it is not the code going on as if the call worked.
|
|
493
|
+
*/
|
|
494
|
+
function undoesPending(statement, before) {
|
|
495
|
+
if (ts.isReturnStatement(statement) && statement.expression === undefined)
|
|
496
|
+
return true;
|
|
497
|
+
const target = setTarget(statement);
|
|
498
|
+
return target !== undefined && before.some((earlier) => setTarget(earlier) === target);
|
|
499
|
+
}
|
|
500
|
+
/** Whether `node` is in the `try` block of a `try` statement, in the same function. */
|
|
501
|
+
function inTry(node) {
|
|
502
|
+
for (let child = node; !ts.isSourceFile(child); child = child.parent) {
|
|
503
|
+
if (isFunctionLike(child))
|
|
504
|
+
return false;
|
|
505
|
+
if (ts.isTryStatement(child.parent) && child.parent.tryBlock === child)
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* `await sendReply(x);` dropped outside a `try`, with lines after it: they run even when it
|
|
512
|
+
* failed (the reply the user typed is cleared). Inside a `try` it is `checkTry`'s.
|
|
513
|
+
*/
|
|
514
|
+
function checkDropped(statement, ctx) {
|
|
515
|
+
const call = droppedAwait(statement);
|
|
516
|
+
if (call === undefined)
|
|
517
|
+
return;
|
|
518
|
+
const parent = statement.parent;
|
|
519
|
+
if (!ts.isBlock(parent) && !ts.isSourceFile(parent) && !ts.isCaseClause(parent))
|
|
520
|
+
return;
|
|
521
|
+
if (inTry(statement))
|
|
522
|
+
return;
|
|
523
|
+
const siblings = parent.statements;
|
|
524
|
+
const index = siblings.indexOf(statement);
|
|
525
|
+
const before = siblings.slice(0, index);
|
|
526
|
+
const after = siblings.slice(index + 1);
|
|
527
|
+
// The type questions after the syntax that rules most statements out.
|
|
528
|
+
if (after.every((next) => undoesPending(next, before)))
|
|
529
|
+
return;
|
|
530
|
+
if (kindOf(call, true, ctx) !== "trigger" || hookStateRead(call, ctx))
|
|
531
|
+
return;
|
|
532
|
+
ctx.report({
|
|
533
|
+
node: statement,
|
|
534
|
+
message: `the lines after this run even when \`${calleeText(call, ctx)}(…)\` failed`,
|
|
535
|
+
hint: [
|
|
536
|
+
'add `.unwrap()` so a failure throws (quick fix), or check `"error" in result` and return:',
|
|
537
|
+
example([`const result = await ${callText(call, ctx)};`, `if ("error" in result) return;`]),
|
|
538
|
+
].join("\n"),
|
|
539
|
+
fixes: [unwrapFix(call, "Add `.unwrap()` so a failure stops this function", ctx)],
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
/** What a stored result is reached by: `const r = …` or `const { data, error } = …`. */
|
|
543
|
+
function trackedOf(declaration, call, kind, ctx) {
|
|
544
|
+
const callee = calleeText(call, ctx);
|
|
545
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
546
|
+
return { whole: declaration.name.text, aliases: new Map(), callee, kind };
|
|
547
|
+
}
|
|
548
|
+
if (!ts.isObjectBindingPattern(declaration.name))
|
|
549
|
+
return undefined;
|
|
550
|
+
const aliases = new Map();
|
|
551
|
+
for (const element of declaration.name.elements) {
|
|
552
|
+
if (element.dotDotDotToken !== undefined || !ts.isIdentifier(element.name))
|
|
553
|
+
return undefined;
|
|
554
|
+
const key = element.propertyName ?? element.name;
|
|
555
|
+
if (!ts.isIdentifier(key))
|
|
556
|
+
return undefined;
|
|
557
|
+
const alias = ALIAS_KINDS.get(key.text);
|
|
558
|
+
if (alias !== undefined)
|
|
559
|
+
aliases.set(element.name.text, alias);
|
|
560
|
+
}
|
|
561
|
+
return aliases.size === 0 ? undefined : { whole: undefined, aliases, callee, kind };
|
|
562
|
+
}
|
|
563
|
+
function goesOnHeadline(tracked, after) {
|
|
564
|
+
const what = after === "if" ? "this `if`" : "this";
|
|
565
|
+
return `the lines after ${what} run even when \`${tracked.callee}(…)\` failed`;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* The statements that run after a check that is the last of its list: those after the enclosing
|
|
569
|
+
* `if` branch, `try` block or plain block, outward, within the same function. It stops at the
|
|
570
|
+
* function, at a loop, a `catch`, a `finally` and a `case`.
|
|
571
|
+
*/
|
|
572
|
+
function followingStatements(statement) {
|
|
573
|
+
let node = statement;
|
|
574
|
+
for (;;) {
|
|
575
|
+
const parent = node.parent;
|
|
576
|
+
if (ts.isIfStatement(parent)) {
|
|
577
|
+
node = parent;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (!ts.isBlock(parent))
|
|
581
|
+
return [];
|
|
582
|
+
const current = node;
|
|
583
|
+
const later = parent.statements.slice(parent.statements.findIndex((one) => one === current) + 1);
|
|
584
|
+
if (later.length > 0)
|
|
585
|
+
return later;
|
|
586
|
+
const owner = parent.parent;
|
|
587
|
+
const walksOut = ts.isIfStatement(owner) ||
|
|
588
|
+
ts.isBlock(owner) ||
|
|
589
|
+
(ts.isTryStatement(owner) && owner.tryBlock === parent);
|
|
590
|
+
if (!walksOut)
|
|
591
|
+
return [];
|
|
592
|
+
node = owner;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/** `return result;` or `return result.data;`: the failure is handed to the caller at once. */
|
|
596
|
+
function returnsResult(statement, tracked) {
|
|
597
|
+
if (!ts.isReturnStatement(statement) || statement.expression === undefined)
|
|
598
|
+
return false;
|
|
599
|
+
const code = strip(statement.expression);
|
|
600
|
+
if (isWhole(code, tracked))
|
|
601
|
+
return true;
|
|
602
|
+
return (ts.isPropertyAccessExpression(code) &&
|
|
603
|
+
isWhole(code.expression, tracked) &&
|
|
604
|
+
DATA_PROPERTIES.has(code.name.text));
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Case (b), read from syntax alone: the statements after a stored result, until it is checked,
|
|
608
|
+
* passed on or read. `tracked.kind` matters only to `readsData` (a trigger's data).
|
|
609
|
+
*/
|
|
610
|
+
function storedOutcome(rest, tracked) {
|
|
611
|
+
for (const [index, next] of rest.entries()) {
|
|
612
|
+
if (ts.isIfStatement(next)) {
|
|
613
|
+
const test = polarity(next.expression, tracked);
|
|
614
|
+
if (test === "undecided")
|
|
615
|
+
return { kind: "undecided", test: next.expression };
|
|
616
|
+
if (test !== "none") {
|
|
617
|
+
const failed = test === "failure" ? next.thenStatement : next.elseStatement;
|
|
618
|
+
const worked = test === "failure" ? next.elseStatement : next.thenStatement;
|
|
619
|
+
if (failed === undefined || worked !== undefined || exits(failed))
|
|
620
|
+
return { kind: "none" };
|
|
621
|
+
const later = rest.slice(index + 1);
|
|
622
|
+
return {
|
|
623
|
+
kind: "goes-on",
|
|
624
|
+
test: next.expression,
|
|
625
|
+
after: later.length > 0 ? later : followingStatements(next),
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (index === 0 && returnsResult(next, tracked))
|
|
630
|
+
return { kind: "none" };
|
|
631
|
+
if (readsData(next, tracked))
|
|
632
|
+
return { kind: "reads-data" };
|
|
633
|
+
if (mentions(next, tracked) || exits(next))
|
|
634
|
+
return { kind: "none" };
|
|
635
|
+
}
|
|
636
|
+
return { kind: "none" };
|
|
637
|
+
}
|
|
638
|
+
/** Case (b): reports what `storedOutcome` found. */
|
|
639
|
+
function followStored(statement, before, rest, tracked, call, ctx) {
|
|
640
|
+
const outcome = storedOutcome(rest, tracked);
|
|
641
|
+
switch (outcome.kind) {
|
|
642
|
+
case "none":
|
|
643
|
+
return;
|
|
644
|
+
case "undecided":
|
|
645
|
+
ctx.unknown({
|
|
646
|
+
node: outcome.test,
|
|
647
|
+
message: `stateward cannot tell whether this \`if\` checks that \`${tracked.callee}(…)\` failed`,
|
|
648
|
+
hint: `test the result itself to make it checkable, as in \`${checkLine(tracked)}\``,
|
|
649
|
+
});
|
|
650
|
+
return;
|
|
651
|
+
case "goes-on": {
|
|
652
|
+
if (outcome.after.every((one) => undoesPending(one, before)) || hookStateRead(call, ctx)) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
ctx.report({
|
|
656
|
+
node: outcome.test,
|
|
657
|
+
message: goesOnHeadline(tracked, "if"),
|
|
658
|
+
hint: [
|
|
659
|
+
"end the branch that handles the failure with `return` (or `throw`):",
|
|
660
|
+
example([
|
|
661
|
+
`if (${shorten(outcome.test.getText(ctx.sourceFile), CONDITION_MAX)}) {`,
|
|
662
|
+
" // … what it shows now",
|
|
663
|
+
" return;",
|
|
664
|
+
"}",
|
|
665
|
+
]),
|
|
666
|
+
].join("\n"),
|
|
667
|
+
});
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
case "reads-data": {
|
|
671
|
+
if (hookStateRead(call, ctx))
|
|
672
|
+
return;
|
|
673
|
+
const data = tracked.whole === undefined
|
|
674
|
+
? ([...tracked.aliases].find(([, kind]) => kind === "data")?.[0] ?? "data")
|
|
675
|
+
: `${tracked.whole}.data`;
|
|
676
|
+
ctx.report({
|
|
677
|
+
node: statement,
|
|
678
|
+
message: goesOnHeadline(tracked, "statement"),
|
|
679
|
+
hint: [
|
|
680
|
+
`check that it worked before reading \`${data}\`, and return when it failed:`,
|
|
681
|
+
example([checkLine(tracked)]),
|
|
682
|
+
].join("\n"),
|
|
683
|
+
});
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
default: {
|
|
687
|
+
const unreachable = outcome;
|
|
688
|
+
return unreachable;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function checkDeclaration(statement, ctx) {
|
|
693
|
+
const parent = statement.parent;
|
|
694
|
+
if (!ts.isBlock(parent) && !ts.isSourceFile(parent) && !ts.isCaseClause(parent))
|
|
695
|
+
return;
|
|
696
|
+
const [declaration] = statement.declarationList.declarations;
|
|
697
|
+
if (declaration?.initializer === undefined)
|
|
698
|
+
return;
|
|
699
|
+
if (statement.declarationList.declarations.length !== 1)
|
|
700
|
+
return;
|
|
701
|
+
const value = strip(declaration.initializer);
|
|
702
|
+
const awaited = ts.isAwaitExpression(value);
|
|
703
|
+
const call = strip(awaited ? value.expression : value);
|
|
704
|
+
if (!ts.isCallExpression(call))
|
|
705
|
+
return;
|
|
706
|
+
const siblings = parent.statements;
|
|
707
|
+
const index = siblings.indexOf(statement);
|
|
708
|
+
const rest = siblings.slice(index + 1);
|
|
709
|
+
// The type question last: most stored calls are read or passed on
|
|
710
|
+
// before any test, and then nothing is said whatever they give. The kind matters only to a
|
|
711
|
+
// trigger's data, which only an awaited call can be; a trigger read finds the most.
|
|
712
|
+
const probe = trackedOf(declaration, call, awaited ? "trigger" : "ok", ctx);
|
|
713
|
+
if (probe === undefined || storedOutcome(rest, probe).kind === "none")
|
|
714
|
+
return;
|
|
715
|
+
const kind = kindOf(call, awaited, ctx);
|
|
716
|
+
if (kind === undefined)
|
|
717
|
+
return;
|
|
718
|
+
const tracked = trackedOf(declaration, call, kind, ctx);
|
|
719
|
+
if (tracked === undefined)
|
|
720
|
+
return;
|
|
721
|
+
followStored(statement, siblings.slice(0, index), rest, tracked, call, ctx);
|
|
722
|
+
}
|
|
723
|
+
export const resultUnchecked = defineRule({
|
|
724
|
+
kind: "file",
|
|
725
|
+
meta: {
|
|
726
|
+
description: "A call that reports failure in its value, not by throwing, is checked before the code goes on as if it worked.",
|
|
727
|
+
defaultSeverity: "warning",
|
|
728
|
+
tier: "core",
|
|
729
|
+
group: "callers",
|
|
730
|
+
heuristic: true,
|
|
731
|
+
explain: EXPLAIN,
|
|
732
|
+
},
|
|
733
|
+
check(ctx) {
|
|
734
|
+
const visit = (node) => {
|
|
735
|
+
if (ts.isTryStatement(node))
|
|
736
|
+
checkTry(node, ctx);
|
|
737
|
+
if (ts.isVariableStatement(node))
|
|
738
|
+
checkDeclaration(node, ctx);
|
|
739
|
+
if (ts.isExpressionStatement(node))
|
|
740
|
+
checkDropped(node, ctx);
|
|
741
|
+
ts.forEachChild(node, visit);
|
|
742
|
+
};
|
|
743
|
+
visit(ctx.sourceFile);
|
|
744
|
+
},
|
|
745
|
+
});
|
|
746
|
+
//# sourceMappingURL=result-unchecked.js.map
|