@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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -2
  3. package/dist/async-handler-unhandled.d.ts +7 -0
  4. package/dist/async-handler-unhandled.js +342 -0
  5. package/dist/catch-hides-unexpected.d.ts +6 -0
  6. package/dist/catch-hides-unexpected.js +260 -0
  7. package/dist/catch-misses-error.d.ts +6 -0
  8. package/dist/catch-misses-error.js +208 -0
  9. package/dist/catch-without-intent.d.ts +10 -0
  10. package/dist/catch-without-intent.js +590 -0
  11. package/dist/exhaustive-catch.d.ts +6 -0
  12. package/dist/exhaustive-catch.js +190 -0
  13. package/dist/exhaustive-fallback.d.ts +6 -0
  14. package/dist/exhaustive-fallback.js +157 -0
  15. package/dist/finally-can-throw.d.ts +6 -0
  16. package/dist/finally-can-throw.js +184 -0
  17. package/dist/finally-may-throw.d.ts +6 -0
  18. package/dist/finally-may-throw.js +184 -0
  19. package/dist/hover.d.ts +11 -0
  20. package/dist/hover.js +62 -0
  21. package/dist/index.d.ts +7 -0
  22. package/dist/index.js +2 -0
  23. package/dist/inference/builtins.d.ts +52 -0
  24. package/dist/inference/builtins.js +179 -0
  25. package/dist/inference/catches.d.ts +9 -0
  26. package/dist/inference/catches.js +33 -0
  27. package/dist/inference/deps.d.ts +12 -0
  28. package/dist/inference/deps.js +79 -0
  29. package/dist/inference/format.d.ts +25 -0
  30. package/dist/inference/format.js +72 -0
  31. package/dist/inference/packages.d.ts +40 -0
  32. package/dist/inference/packages.js +723 -0
  33. package/dist/inference/project.d.ts +8 -0
  34. package/dist/inference/project.js +22 -0
  35. package/dist/inference/solve.d.ts +128 -0
  36. package/dist/inference/solve.js +575 -0
  37. package/dist/inference/summarize.d.ts +50 -0
  38. package/dist/inference/summarize.js +1324 -0
  39. package/dist/inference/summary.d.ts +191 -0
  40. package/dist/inference/summary.js +237 -0
  41. package/dist/inlay.d.ts +19 -0
  42. package/dist/inlay.js +168 -0
  43. package/dist/must-handle-error.d.ts +55 -0
  44. package/dist/must-handle-error.js +794 -0
  45. package/dist/narrow-try.d.ts +52 -0
  46. package/dist/narrow-try.js +641 -0
  47. package/dist/no-swallow.d.ts +8 -0
  48. package/dist/no-swallow.js +320 -0
  49. package/dist/plugin.d.ts +116 -0
  50. package/dist/plugin.js +39 -0
  51. package/dist/react/boundaries.d.ts +83 -0
  52. package/dist/react/boundaries.js +371 -0
  53. package/dist/react/fallbacks.d.ts +12 -0
  54. package/dist/react/fallbacks.js +83 -0
  55. package/dist/react/render-guards.d.ts +24 -0
  56. package/dist/react/render-guards.js +271 -0
  57. package/dist/react/tree.d.ts +98 -0
  58. package/dist/react/tree.js +863 -0
  59. package/dist/refactors.d.ts +15 -0
  60. package/dist/refactors.js +69 -0
  61. package/dist/render-throw-without-boundary.d.ts +11 -0
  62. package/dist/render-throw-without-boundary.js +223 -0
  63. package/dist/render-throws-without-boundary.d.ts +6 -0
  64. package/dist/render-throws-without-boundary.js +151 -0
  65. package/dist/result-ignored.d.ts +6 -0
  66. package/dist/result-ignored.js +167 -0
  67. package/dist/result-type.d.ts +18 -0
  68. package/dist/result-type.js +55 -0
  69. package/dist/result-unchecked.d.ts +2 -0
  70. package/dist/result-unchecked.js +746 -0
  71. package/dist/rethrow-unknown.d.ts +6 -0
  72. package/dist/rethrow-unknown.js +260 -0
  73. package/dist/rules.meta.d.ts +214 -0
  74. package/dist/rules.meta.js +212 -0
  75. package/dist/syntax.d.ts +41 -0
  76. package/dist/syntax.js +171 -0
  77. package/dist/text.d.ts +22 -0
  78. package/dist/text.js +43 -0
  79. package/dist/throw-in-hook.d.ts +8 -0
  80. package/dist/throw-in-hook.js +510 -0
  81. package/dist/throws-doc-mismatch.d.ts +7 -0
  82. package/dist/throws-doc-mismatch.js +188 -0
  83. package/dist/throws-matches-doc.d.ts +7 -0
  84. package/dist/throws-matches-doc.js +188 -0
  85. package/package.json +34 -3
@@ -0,0 +1,271 @@
1
+ import ts from "@stateward/core/typescript";
2
+ import { scanRunning, stripParentheses } from "../syntax.js";
3
+ /** Both left out: the default. */
4
+ export const ALL_GUARDS = { contextGuards: true, inputGuards: true };
5
+ /** `useContext(X)` or `React.useContext(X)`. */
6
+ function isUseContextCall(expression) {
7
+ const node = stripParentheses(expression);
8
+ if (!ts.isCallExpression(node))
9
+ return false;
10
+ const callee = stripParentheses(node.expression);
11
+ const name = ts.isPropertyAccessExpression(callee) ? callee.name : callee;
12
+ return ts.isIdentifier(name) && name.text === "useContext";
13
+ }
14
+ /**
15
+ * Whether an expression is the value a context gives: a `useContext(X)` call, or a local `const`
16
+ * initialized with one. A property of it (`ctx.user`) is the app's state, not the Provider.
17
+ */
18
+ function isContextValue(expression, ctx) {
19
+ const node = stripParentheses(expression);
20
+ if (isUseContextCall(node))
21
+ return true;
22
+ if (!ts.isIdentifier(node))
23
+ return false;
24
+ const symbol = ctx.types.symbolAt(node);
25
+ const declaration = symbol === undefined ? undefined : ctx.types.declarationOf(symbol)?.node;
26
+ if (declaration === undefined || !ts.isVariableDeclaration(declaration))
27
+ return false;
28
+ const list = declaration.parent;
29
+ return (ts.isVariableDeclarationList(list) &&
30
+ (list.flags & ts.NodeFlags.Const) !== 0 &&
31
+ declaration.initializer !== undefined &&
32
+ isUseContextCall(declaration.initializer));
33
+ }
34
+ function isNullish(expression) {
35
+ const node = stripParentheses(expression);
36
+ return (node.kind === ts.SyntaxKind.NullKeyword || (ts.isIdentifier(node) && node.text === "undefined"));
37
+ }
38
+ /**
39
+ * Whether a condition holds when a context value is missing: `!ctx`, `ctx == null`,
40
+ * `ctx === undefined` (either side), or an `||` of such checks.
41
+ */
42
+ function checksMissingContext(condition, ctx) {
43
+ const node = stripParentheses(condition);
44
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) {
45
+ return isContextValue(node.operand, ctx);
46
+ }
47
+ if (!ts.isBinaryExpression(node))
48
+ return false;
49
+ const operator = node.operatorToken.kind;
50
+ if (operator === ts.SyntaxKind.BarBarToken) {
51
+ return checksMissingContext(node.left, ctx) && checksMissingContext(node.right, ctx);
52
+ }
53
+ if (operator !== ts.SyntaxKind.EqualsEqualsToken &&
54
+ operator !== ts.SyntaxKind.EqualsEqualsEqualsToken) {
55
+ return false;
56
+ }
57
+ return ((isMissingMarker(node.right, ctx) && isContextValue(node.left, ctx)) ||
58
+ (isMissingMarker(node.left, ctx) && isContextValue(node.right, ctx)));
59
+ }
60
+ /**
61
+ * What a context gives when its Provider is missing: `null`, `undefined`, or a sentinel, a
62
+ * `const` declared at the top of the file (`const missing = {}` as the context's default).
63
+ */
64
+ function isMissingMarker(expression, ctx) {
65
+ if (isNullish(expression))
66
+ return true;
67
+ const node = stripParentheses(expression);
68
+ if (!ts.isIdentifier(node))
69
+ return false;
70
+ const symbol = ctx.types.symbolAt(node);
71
+ const declaration = symbol === undefined ? undefined : ctx.types.declarationOf(symbol)?.node;
72
+ if (declaration === undefined || !ts.isVariableDeclaration(declaration))
73
+ return false;
74
+ const list = declaration.parent;
75
+ return (ts.isVariableDeclarationList(list) &&
76
+ (list.flags & ts.NodeFlags.Const) !== 0 &&
77
+ ts.isVariableStatement(list.parent) &&
78
+ ts.isSourceFile(list.parent.parent));
79
+ }
80
+ /** Whether a declaration is a parameter of `fn`, or a name bound by destructuring one. */
81
+ function isParameterOf(declaration, fn) {
82
+ for (let node = declaration; node !== fn; node = node.parent) {
83
+ if (ts.isParameter(node))
84
+ return node.parent === fn;
85
+ if (!ts.isBindingElement(node) &&
86
+ !ts.isObjectBindingPattern(node) &&
87
+ !ts.isArrayBindingPattern(node)) {
88
+ return false;
89
+ }
90
+ }
91
+ return false;
92
+ }
93
+ /**
94
+ * A hook that hands over a context: named `use…Context`, or one whose body is `useContext(X)`
95
+ * (`const usePreferencesEditor = () => useContext(EditorContext)`).
96
+ */
97
+ function isContextHook(callee, ctx) {
98
+ const node = stripParentheses(callee);
99
+ if (!ts.isIdentifier(node))
100
+ return false;
101
+ if (/^use\w*Context$/.test(node.text))
102
+ return true;
103
+ const symbol = ctx.types.symbolAt(node);
104
+ const declaration = symbol === undefined ? undefined : ctx.types.declarationOf(symbol)?.node;
105
+ if (declaration === undefined)
106
+ return false;
107
+ const fn = ts.isVariableDeclaration(declaration) && declaration.initializer !== undefined
108
+ ? stripParentheses(declaration.initializer)
109
+ : declaration;
110
+ if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn) && !ts.isFunctionDeclaration(fn)) {
111
+ return false;
112
+ }
113
+ const body = fn.body;
114
+ if (body === undefined)
115
+ return false;
116
+ if (!ts.isBlock(body))
117
+ return isUseContextCall(body);
118
+ // A wrapper that reads a context, maybe guards it, and returns it (`usePreferencesEditor`).
119
+ let reads = false;
120
+ scanRunning(body, (node) => {
121
+ if (ts.isCallExpression(node) && isUseContextCall(node))
122
+ reads = true;
123
+ });
124
+ return reads;
125
+ }
126
+ /**
127
+ * A name destructured from a parameter in the body: `const { children, render } = props;`.
128
+ */
129
+ function isDestructuredParameter(declaration, render, ctx) {
130
+ if (!ts.isBindingElement(declaration))
131
+ return false;
132
+ let pattern = declaration.parent;
133
+ while (ts.isBindingElement(pattern.parent))
134
+ pattern = pattern.parent.parent;
135
+ const variable = pattern.parent;
136
+ if (!ts.isVariableDeclaration(variable) || variable.initializer === undefined)
137
+ return false;
138
+ const source = stripParentheses(variable.initializer);
139
+ return ts.isIdentifier(source) && isParameterName(source, render, ctx);
140
+ }
141
+ /**
142
+ * Who answers for an input: `parameter` for a parameter (or a prop destructured from one, or
143
+ * `props.x`), `context` for a value read through a context hook; undefined for anything else.
144
+ */
145
+ function callerInput(expression, render, ctx) {
146
+ const node = stripParentheses(expression);
147
+ if (ts.isPropertyAccessExpression(node)) {
148
+ const owner = stripParentheses(node.expression);
149
+ // `props.title`; a property of a context value is the app's state (`ctx.user`), not a prop.
150
+ return ts.isIdentifier(owner) && isParameterName(owner, render, ctx) ? "parameter" : undefined;
151
+ }
152
+ if (!ts.isIdentifier(node))
153
+ return undefined;
154
+ const symbol = ctx.types.symbolAt(node);
155
+ const declaration = symbol === undefined ? undefined : ctx.types.declarationOf(symbol)?.node;
156
+ if (declaration === undefined)
157
+ return undefined;
158
+ if (isParameterOf(declaration, render.fn) || isDestructuredParameter(declaration, render, ctx)) {
159
+ return "parameter";
160
+ }
161
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer === undefined) {
162
+ return undefined;
163
+ }
164
+ const list = declaration.parent;
165
+ if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) {
166
+ return undefined;
167
+ }
168
+ const init = stripParentheses(declaration.initializer);
169
+ return ts.isCallExpression(init) && isContextHook(init.expression, ctx) ? "context" : undefined;
170
+ }
171
+ const EITHER = { undefined: true, null: true };
172
+ /**
173
+ * Whether a check for a missing input names a caller's mistake. A parameter's check does only
174
+ * when its type rules the value out: a prop typed `title?: string` can be left out by any caller,
175
+ * so its check is a render failure. A context hook's value is missing only
176
+ * when a Provider is, which is the caller's mistake.
177
+ */
178
+ function isCallerMistake(expression, missing, render, ctx) {
179
+ const input = callerInput(expression, render, ctx);
180
+ if (input !== "parameter")
181
+ return input === "context";
182
+ const node = stripParentheses(expression);
183
+ return (!(missing.undefined && ctx.types.canBeUndefined(node)) &&
184
+ !(missing.null && ctx.types.canBeNull(node)));
185
+ }
186
+ function isParameterName(node, render, ctx) {
187
+ const symbol = ctx.types.symbolAt(node);
188
+ const declaration = symbol === undefined ? undefined : ctx.types.declarationOf(symbol)?.node;
189
+ return declaration !== undefined && isParameterOf(declaration, render.fn);
190
+ }
191
+ /** What a comparison with `undefined` or `null` tests for; undefined for any other operand. */
192
+ function missingTested(operand, loose) {
193
+ const node = stripParentheses(operand);
194
+ if (node.kind === ts.SyntaxKind.NullKeyword)
195
+ return loose ? EITHER : { undefined: false, null: true };
196
+ if (ts.isIdentifier(node) && node.text === "undefined") {
197
+ return loose ? EITHER : { undefined: true, null: false };
198
+ }
199
+ return undefined;
200
+ }
201
+ /**
202
+ * Whether a condition holds only when an input is missing through a caller's mistake: `!x`,
203
+ * `x == null`, `x === undefined` (either side), and `&&` / `||` of such checks.
204
+ */
205
+ function checksMissingInput(condition, render, ctx) {
206
+ const node = stripParentheses(condition);
207
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) {
208
+ return isCallerMistake(node.operand, EITHER, render, ctx);
209
+ }
210
+ if (!ts.isBinaryExpression(node))
211
+ return false;
212
+ const operator = node.operatorToken.kind;
213
+ if (operator === ts.SyntaxKind.BarBarToken ||
214
+ operator === ts.SyntaxKind.AmpersandAmpersandToken) {
215
+ return (checksMissingInput(node.left, render, ctx) && checksMissingInput(node.right, render, ctx));
216
+ }
217
+ if (operator !== ts.SyntaxKind.EqualsEqualsToken &&
218
+ operator !== ts.SyntaxKind.EqualsEqualsEqualsToken) {
219
+ return false;
220
+ }
221
+ const loose = operator === ts.SyntaxKind.EqualsEqualsToken;
222
+ const right = missingTested(node.right, loose);
223
+ const left = missingTested(node.left, loose);
224
+ return ((right !== undefined && isCallerMistake(node.left, right, render, ctx)) ||
225
+ (left !== undefined && isCallerMistake(node.right, left, render, ctx)));
226
+ }
227
+ /** An invariant: `if (!render && !children) throw …`, a programming error of the caller. */
228
+ function guardsInput(statement, render, ctx) {
229
+ for (const guard of enclosingIfs(statement, render.body)) {
230
+ if (checksMissingInput(guard.expression, render, ctx))
231
+ return true;
232
+ }
233
+ return false;
234
+ }
235
+ /** The `if` whose `then` branch holds `node`, nearest first, up to the render body. */
236
+ function* enclosingIfs(node, body) {
237
+ let child = node;
238
+ for (let parent = node.parent; child !== body; child = parent, parent = parent.parent) {
239
+ if (ts.isIfStatement(parent) && parent.thenStatement === child)
240
+ yield parent;
241
+ }
242
+ }
243
+ /** The guard of a context hook: `if (!ctx) throw new Error("useX must be used within XProvider")`. */
244
+ function guardsContext(statement, body, ctx) {
245
+ for (const guard of enclosingIfs(statement, body)) {
246
+ if (checksMissingContext(guard.expression, ctx))
247
+ return true;
248
+ }
249
+ return false;
250
+ }
251
+ /**
252
+ * The start of the thrown expression of every guard `throw` in the render function's own body
253
+ * (not in a nested function), as the throws summary records it (`new Error(…)`).
254
+ */
255
+ export function guardThrows(fn, ctx, skips) {
256
+ const body = fn.body;
257
+ if (body === undefined || (!skips.contextGuards && !skips.inputGuards))
258
+ return [];
259
+ const render = { fn, body };
260
+ const found = [];
261
+ scanRunning(body, (node) => {
262
+ if (!ts.isThrowStatement(node))
263
+ return;
264
+ const guard = (skips.contextGuards && guardsContext(node, body, ctx)) ||
265
+ (skips.inputGuards && guardsInput(node, render, ctx));
266
+ if (guard)
267
+ found.push(stripParentheses(node.expression).getStart(ctx.sourceFile));
268
+ });
269
+ return found;
270
+ }
271
+ //# sourceMappingURL=render-guards.js.map
@@ -0,0 +1,98 @@
1
+ import type { FileContext, Json, StringListRecord } from "@stateward/core";
2
+ import { type GuardSkips } from "./render-guards.js";
3
+ /**
4
+ * What one file renders, for the boundary rules and hover:
5
+ * its components and hooks, the components each renders and the error boundaries around each
6
+ * render, the app's entry points, and every boundary element with its fallbacks. Plain JSON, so
7
+ * the engine caches it with the file.
8
+ */
9
+ /** A start and end offset in the summarized file. */
10
+ export type Span = readonly [number, number];
11
+ /**
12
+ * What is around a render inside the component that writes it, innermost first: a boundary
13
+ * element (by key), or an own component whose `children` it is passed as (by id), which may put
14
+ * them under a boundary of its own.
15
+ */
16
+ export type Around = {
17
+ readonly boundary: string;
18
+ } | {
19
+ readonly via: string;
20
+ };
21
+ export type TreeRender = {
22
+ /** The id of the own component or class the tag names. */
23
+ readonly target: string;
24
+ /** The tag as written. */
25
+ readonly name: string;
26
+ readonly at: Span;
27
+ readonly around: readonly Around[];
28
+ /**
29
+ * The element goes where stateward cannot follow it (passed to a function, stored in an
30
+ * object): which boundary is above it is not known, so nothing is reported for it.
31
+ */
32
+ readonly unknown: boolean;
33
+ };
34
+ /** The parts of a boundary that receive the error it catches. */
35
+ export type FallbackKind = "FallbackComponent" | "fallbackRender" | "onError" | "getDerivedStateFromError";
36
+ export declare const FALLBACK_KINDS: readonly FallbackKind[];
37
+ /** How a fallback branches on the error with `instanceof`. */
38
+ export type Chain = {
39
+ /** The file of the fallback's code (a `FallbackComponent` may live in another file). */
40
+ readonly file: string;
41
+ /** The first `if`, `switch` or `?:` of the chain. */
42
+ readonly at: Span;
43
+ /** How the error is written in the tests: `error`, `props.error`. */
44
+ readonly variable: string;
45
+ readonly dispatch: "if" | "switch";
46
+ /** The classes each branch names (ids); a branch with a class that resolves to nothing is `null`. */
47
+ readonly branches: readonly (readonly string[] | null)[];
48
+ /** Every test is only `instanceof` tests joined by `||`. */
49
+ readonly exact: boolean;
50
+ /** A final `else`, `default`, or code after the chain that does more than return nothing. */
51
+ readonly generic: boolean;
52
+ };
53
+ export type Fallback = {
54
+ readonly kind: FallbackKind;
55
+ /** The prop on the element, or the element's tag for a class's `getDerivedStateFromError`. */
56
+ readonly at: Span;
57
+ /** How messages name it: `UserError`, `fallbackRender`, `AppBoundary.getDerivedStateFromError`. */
58
+ readonly name: string;
59
+ readonly chain: Chain | null;
60
+ };
61
+ export type BoundaryElement = {
62
+ /** `<file>:<offset of the element>`. */
63
+ readonly key: string;
64
+ readonly name: string;
65
+ /** The tag name. */
66
+ readonly at: Span;
67
+ readonly fallbacks: readonly Fallback[];
68
+ };
69
+ /** An effect callback, by the offset its function starts at (how the throws facts name it). */
70
+ export type TreeFunction = {
71
+ readonly id: string;
72
+ readonly name: string;
73
+ readonly at: Span;
74
+ readonly kind: "component" | "hook" | "class";
75
+ readonly renders: readonly TreeRender[];
76
+ /** Own hooks it calls, by id. */
77
+ readonly hooks: readonly string[];
78
+ /** The offsets of the `useEffect` / `useLayoutEffect` callbacks written in it. */
79
+ readonly effects: readonly number[];
80
+ /** A class's render and lifecycle methods, by id. */
81
+ readonly methods: readonly string[];
82
+ /** The boundaries around every place it renders `children`, innermost first; null for none. */
83
+ readonly children: readonly string[] | null;
84
+ /** Where the guard throws in its body start (render-guards.ts): not counted as render failures. */
85
+ readonly guards: readonly number[];
86
+ };
87
+ export type TreeSummary = {
88
+ readonly functions: readonly TreeFunction[];
89
+ /** What `createRoot(…).render(…)` and `hydrateRoot(…)` render. */
90
+ readonly entries: readonly TreeRender[];
91
+ readonly boundaries: readonly BoundaryElement[];
92
+ };
93
+ /** The error boundaries from packages that are known without a config: package to components. */
94
+ export declare const LIBRARY_BOUNDARIES: StringListRecord;
95
+ export declare function summarizeTree(ctx: FileContext, libraryBoundaries?: StringListRecord, skips?: GuardSkips): TreeSummary;
96
+ /** The tree as `summarizeTree` wrote it, or undefined for anything else (an old cache file). */
97
+ export declare function readTree(value: Json | undefined): TreeSummary | undefined;
98
+ //# sourceMappingURL=tree.d.ts.map