@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,184 @@
1
+ import ts from "@stateward/core/typescript";
2
+ import { defineRule, option } from "@stateward/core";
3
+ import { hasModifier, isBuiltinGlobal, sameFileFunction, scanRunning, stripParentheses, throwsUncaught, } from "./syntax.js";
4
+ import { HEADLINE_MAX, fitted, oneLine, withExample } from "./text.js";
5
+ /** The group the rule belongs to in the docs and in `stateward status`. */
6
+ export const GROUP = "finally";
7
+ const EXPLAIN = `# errors/finally-may-throw
8
+
9
+ A finally block does not call something that throws, so the error that led to it is not lost.
10
+
11
+ ## Why
12
+
13
+ When code in a \`finally\` throws, its error replaces the one that sent the code there. An upload that fails with a network error then reports a \`SyntaxError\` from parsing the saved state: the real cause, with its message and stack, is gone, and the rest of the cleanup does not run.
14
+
15
+ ## When to use it
16
+
17
+ Use it in any code that cleans up in \`finally\`. It is on as an error in every preset; keep it on. Turn it off only if your cleanup's own errors are meant to win.
18
+
19
+ ## What it catches
20
+
21
+ In a \`finally\` block, outside a nested \`try\` that has a \`catch\`:
22
+
23
+ - a call to a built-in that throws: \`JSON.parse\`, \`JSON.stringify\` (on circular values), \`new URL\`, \`decodeURIComponent\`, \`decodeURI\`, \`structuredClone\`, \`atob\`;
24
+ - a \`throw\`;
25
+ - a call to a function in the same file that has a \`throw\` outside its own \`try\` / \`catch\` (an \`async\` one when the call is awaited).
26
+
27
+ One finding per \`finally\`, on the first of these.
28
+
29
+ ### Bad
30
+
31
+ \`\`\`ts
32
+ try {
33
+ await upload(file);
34
+ } finally {
35
+ const state = JSON.parse(saved);
36
+ restore(state);
37
+ }
38
+ \`\`\`
39
+
40
+ ### Good
41
+
42
+ \`\`\`ts
43
+ try {
44
+ await upload(file);
45
+ } finally {
46
+ try {
47
+ restore(JSON.parse(saved));
48
+ } catch (e) {
49
+ report(e);
50
+ }
51
+ }
52
+ \`\`\`
53
+
54
+ ### Fix
55
+
56
+ Move the call out of the \`finally\`, or wrap it in its own \`try\` / \`catch\`. There is no quick fix: what the cleanup should do when it fails is your call.
57
+
58
+ ## What it does not catch
59
+
60
+ - Functions from other files, and package calls: only the built-ins above and functions in the same file are read.
61
+ - Other built-ins, such as \`fetch\` or \`BigInt\`.
62
+ - A function that throws only inside its own \`try\` / \`catch\`.
63
+ - Anything inside a nested \`try\` that has a \`catch\`.
64
+ - A second throwing call in the same \`finally\`: only the first is reported.
65
+
66
+ ## Turn it on or off
67
+
68
+ In \`stateward.config.ts\`:
69
+
70
+ \`\`\`ts
71
+ rules: { "errors/finally-may-throw": "off" } // or "warning"
72
+ groups: { "errors/finally": "off" } // the whole group
73
+ rules: { "errors/finally-may-throw": ["error", { severities: { builtin: "warning" } }] } // built-ins only warn
74
+ \`\`\`
75
+ `;
76
+ /** Built-in functions that throw on input they cannot handle, as `Owner.name` or `name`. */
77
+ const THROWING_CALLS = new Set([
78
+ "JSON.parse",
79
+ "JSON.stringify",
80
+ "decodeURIComponent",
81
+ "decodeURI",
82
+ "structuredClone",
83
+ "atob",
84
+ ]);
85
+ /** Built-in constructors that throw. */
86
+ const THROWING_CONSTRUCTORS = new Set(["URL"]);
87
+ /** `JSON.parse` or `atob`, when the callee is one of the globals in the table. */
88
+ function builtinCall(call, ctx) {
89
+ const callee = stripParentheses(call.expression);
90
+ if (ts.isIdentifier(callee)) {
91
+ return THROWING_CALLS.has(callee.text) && isBuiltinGlobal(callee, ctx)
92
+ ? callee.text
93
+ : undefined;
94
+ }
95
+ if (!ts.isPropertyAccessExpression(callee))
96
+ return undefined;
97
+ const owner = stripParentheses(callee.expression);
98
+ if (!ts.isIdentifier(owner))
99
+ return undefined;
100
+ const name = `${owner.text}.${callee.name.text}`;
101
+ return THROWING_CALLS.has(name) && isBuiltinGlobal(owner, ctx) ? name : undefined;
102
+ }
103
+ /** The kinds of finding: they differ in how sure the rule is that the call throws. */
104
+ const KINDS = {
105
+ throw: "a `throw` written in the finally: it throws whenever it runs.",
106
+ builtin: "a built-in such as `JSON.parse` or `new URL`: it throws only on input it cannot handle.",
107
+ "own-function": "a function in the same file with a `throw` in it: it throws on some paths.",
108
+ };
109
+ function causeOf(node, awaited, ctx) {
110
+ if (ts.isThrowStatement(node))
111
+ return { node, subject: () => "this `throw` runs", kind: "throw" };
112
+ if (ts.isNewExpression(node)) {
113
+ const callee = stripParentheses(node.expression);
114
+ if (ts.isIdentifier(callee) &&
115
+ THROWING_CONSTRUCTORS.has(callee.text) &&
116
+ isBuiltinGlobal(callee, ctx)) {
117
+ return { node, subject: () => `\`new ${callee.text}(…)\` throws here`, kind: "builtin" };
118
+ }
119
+ return undefined;
120
+ }
121
+ if (!ts.isCallExpression(node))
122
+ return undefined;
123
+ const builtin = builtinCall(node, ctx);
124
+ if (builtin !== undefined) {
125
+ return { node, subject: () => `\`${builtin}(…)\` throws here`, kind: "builtin" };
126
+ }
127
+ const declared = sameFileFunction(node.expression, ctx);
128
+ if (declared === undefined || !throwsUncaught(declared))
129
+ return undefined;
130
+ // An async function turns its throw into a rejected promise, which only an `await` rethrows.
131
+ if (hasModifier(declared, ts.SyntaxKind.AsyncKeyword) && !awaited)
132
+ return undefined;
133
+ const callee = oneLine(stripParentheses(node.expression).getText(ctx.sourceFile));
134
+ return { node, subject: (cut) => `\`${cut(callee)}(…)\` throws here`, kind: "own-function" };
135
+ }
136
+ function headline(cause) {
137
+ return fitted(HEADLINE_MAX, (cut) => `if ${cause.subject(cut)}, the error that led to this finally is lost`);
138
+ }
139
+ const HINT = withExample("move it out of finally, or catch it inside:", [
140
+ "finally {",
141
+ " try { cleanup(); } catch (e) { report(e); }",
142
+ "}",
143
+ ]);
144
+ /** `shown` holds what is already reported: a `finally` inside a `finally` is looked at twice. */
145
+ function checkFinally(block, shown, ctx) {
146
+ let found;
147
+ scanRunning(block, (node, guarded) => {
148
+ if (found !== undefined || guarded)
149
+ return;
150
+ const awaited = ts.isAwaitExpression(node.parent);
151
+ found = causeOf(node, awaited, ctx);
152
+ });
153
+ if (found === undefined || shown.has(found.node))
154
+ return;
155
+ shown.add(found.node);
156
+ ctx.report({ node: found.node, message: headline(found), hint: HINT, findingKind: found.kind });
157
+ }
158
+ export const finallyMayThrow = defineRule({
159
+ kind: "file",
160
+ meta: {
161
+ description: "A finally block does not call something that throws, so the error that led to it is not lost.",
162
+ defaultSeverity: "error",
163
+ tier: "core",
164
+ group: GROUP,
165
+ options: {
166
+ severities: option.severities({
167
+ kinds: KINDS,
168
+ description: "A severity per kind of call, where you trust one kind less.",
169
+ }),
170
+ },
171
+ explain: EXPLAIN,
172
+ },
173
+ check(ctx) {
174
+ const shown = new Set();
175
+ const visit = (node) => {
176
+ if (ts.isTryStatement(node) && node.finallyBlock !== undefined) {
177
+ checkFinally(node.finallyBlock, shown, ctx);
178
+ }
179
+ ts.forEachChild(node, visit);
180
+ };
181
+ visit(ctx.sourceFile);
182
+ },
183
+ });
184
+ //# sourceMappingURL=finally-may-throw.js.map
@@ -0,0 +1,11 @@
1
+ import type { EditorContributions } from "@stateward/core";
2
+ type Hover = NonNullable<EditorContributions["hover"]>;
3
+ /**
4
+ * `throws: ParseError | unknown (via api.get: no declaration)` on a function's name and on a
5
+ * call of it, from the last full check's solve (no recomputation: the solve is kept per
6
+ * summaries map). Nothing when the pack's inference rules did not run, or for a function that is
7
+ * not the project's own. On an error boundary's tag, what it catches (`boundaryLines`).
8
+ */
9
+ export declare const hover: Hover;
10
+ export {};
11
+ //# sourceMappingURL=hover.d.ts.map
package/dist/hover.js ADDED
@@ -0,0 +1,62 @@
1
+ import ts from "@stateward/core/typescript";
2
+ import { FACT_RULES } from "./inference/deps.js";
3
+ import { resultLines } from "./inference/format.js";
4
+ import { analyse } from "./inference/solve.js";
5
+ import { TREE_RULES, boundaryLines, modelOf, throwsOf } from "./react/boundaries.js";
6
+ /** The identifier that covers `offset`, if any. */
7
+ function identifierAt(root, offset) {
8
+ if (offset < root.getStart() || offset >= root.getEnd())
9
+ return undefined;
10
+ if (ts.isIdentifier(root))
11
+ return root;
12
+ return ts.forEachChild(root, (child) => identifierAt(child, offset));
13
+ }
14
+ /** The name of a function's declaration, or the name a call or `new` calls. */
15
+ function isFunctionName(identifier) {
16
+ const parent = identifier.parent;
17
+ if (ts.isFunctionDeclaration(parent) ||
18
+ ts.isMethodDeclaration(parent) ||
19
+ ts.isVariableDeclaration(parent) ||
20
+ ts.isPropertyAssignment(parent) ||
21
+ ts.isPropertyDeclaration(parent)) {
22
+ return parent.name === identifier;
23
+ }
24
+ const callee = ts.isPropertyAccessExpression(parent) && parent.name === identifier ? parent : identifier;
25
+ const owner = callee.parent;
26
+ return (ts.isCallExpression(owner) || ts.isNewExpression(owner)) && owner.expression === callee;
27
+ }
28
+ /**
29
+ * `throws: ParseError | unknown (via api.get: no declaration)` on a function's name and on a
30
+ * call of it, from the last full check's solve (no recomputation: the solve is kept per
31
+ * summaries map). Nothing when the pack's inference rules did not run, or for a function that is
32
+ * not the project's own. On an error boundary's tag, what it catches (`boundaryLines`).
33
+ */
34
+ export const hover = (input) => {
35
+ let trees;
36
+ for (const ruleId of TREE_RULES)
37
+ trees ??= input.projectSummaries(ruleId);
38
+ if (trees !== undefined) {
39
+ const lines = boundaryLines(modelOf(trees), input.sourceFile.fileName, input.offset);
40
+ if (lines !== undefined)
41
+ return lines.join("\n\n");
42
+ }
43
+ let summaries;
44
+ for (const ruleId of FACT_RULES)
45
+ summaries ??= input.projectSummaries(ruleId);
46
+ summaries ??= trees === undefined ? undefined : throwsOf(trees);
47
+ if (summaries === undefined)
48
+ return undefined;
49
+ const identifier = identifierAt(input.sourceFile, input.offset);
50
+ if (identifier === undefined || !isFunctionName(identifier))
51
+ return undefined;
52
+ const symbol = input.types.symbolAt(identifier);
53
+ if (symbol === undefined)
54
+ return undefined;
55
+ const analysis = analyse(summaries);
56
+ const result = analysis.results.get(symbol.id);
57
+ if (result === undefined)
58
+ return undefined;
59
+ // A blank line between them: Markdown joins single-newline lines into one paragraph.
60
+ return resultLines(result, analysis.classes).join("\n\n");
61
+ };
62
+ //# sourceMappingURL=hover.js.map
@@ -0,0 +1,7 @@
1
+ export { errors } from "./plugin.js";
2
+ export type { ThrowsSummary } from "./inference/summary.js";
3
+ export type { ReactSummary } from "./react/boundaries.js";
4
+ export type { TreeSummary } from "./react/tree.js";
5
+ export type { MustHandleSummary } from "./must-handle-error.js";
6
+ export type { NarrowTrySummary } from "./narrow-try.js";
7
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { errors } from "./plugin.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What the language's own functions throw. Anything of the
3
+ * platform (TypeScript's lib files, global declarations) that is not listed here is taken not to
4
+ * throw: errors of the engine itself (reading a property of `undefined`, a stack overflow) are not
5
+ * modelled, as Java does not declare its unchecked exceptions.
6
+ */
7
+ /** The error classes of the platform; their ids are `builtin:<Name>`. */
8
+ export declare const BUILTIN_ERRORS: ReadonlySet<string>;
9
+ export declare function builtinId(name: string): string;
10
+ /** A `DOMException` with one `name` (`AbortError`): caught by `instanceof DOMException`. */
11
+ export declare function domExceptionId(name: string): string;
12
+ export declare function isBuiltin(id: string): boolean;
13
+ /**
14
+ * The class a built-in error class extends: `Error` for every other one, `DOMException` for a
15
+ * named `DOMException`; null for `Error`.
16
+ */
17
+ export declare function builtinParent(id: string): string | null;
18
+ /** How a class is shown: `TypeError`, `AbortError (DOMException)`. */
19
+ export declare function builtinName(id: string): string;
20
+ /** The class an `instanceof` test names to catch it: `DOMException` for `AbortError (DOMException)`. */
21
+ export declare function builtinCatchName(id: string): string;
22
+ /** Methods of the platform's interfaces, as `<interface>.<method>`, that throw synchronously. */
23
+ export declare const THROWING_MEMBERS: ReadonlyMap<string, readonly string[]>;
24
+ /** Global functions that throw synchronously. */
25
+ export declare const THROWING_GLOBALS: ReadonlyMap<string, readonly string[]>;
26
+ /** Global constructors that throw. */
27
+ export declare const THROWING_CONSTRUCTORS: ReadonlyMap<string, readonly string[]>;
28
+ /** `Array(n)` and `new Array(n)` with one argument: a length that is not a valid one. */
29
+ export declare const ARRAY_LENGTH: string[];
30
+ /** What `fetch` rejects with: a network failure, and an abort when a signal is passed. */
31
+ export declare const FETCH_REJECTS: string[];
32
+ export declare const FETCH_ABORT: string;
33
+ /** What `Promise.any` rejects with when every promise rejects. */
34
+ export declare const ANY_REJECTS: string;
35
+ /**
36
+ * Methods and functions of the platform that call a callback before they return, as
37
+ * `<interface>.<method>` (or the global's name), with the index of the argument that is the
38
+ * callback: what it throws, the call throws. `replace`'s second argument and `JSON.stringify`'s
39
+ * replacer may also be a string or a list; only a function there is a callback.
40
+ */
41
+ export declare const SYNC_CALLBACKS: ReadonlyMap<string, number>;
42
+ /** Calls whose result holds what the callback returned (promises, for `Promise.all`). */
43
+ export declare const MAPPING_METHODS: ReadonlySet<string>;
44
+ /**
45
+ * Functions and constructors of the platform, by name, that run their callback later (a timer, an
46
+ * event, an observer) or never (`removeEventListener`): what the callback throws never reaches
47
+ * the caller, so it is left out on purpose. Its own catches are still checked.
48
+ */
49
+ export declare const DEFERRED_CALLERS: ReadonlySet<string>;
50
+ /** Whether a call into package `name`, written `callText`, is one of React's non-throwing hooks. */
51
+ export declare function isNonThrowingReactHook(name: string, callText: string): boolean;
52
+ //# sourceMappingURL=builtins.d.ts.map
@@ -0,0 +1,179 @@
1
+ /**
2
+ * What the language's own functions throw. Anything of the
3
+ * platform (TypeScript's lib files, global declarations) that is not listed here is taken not to
4
+ * throw: errors of the engine itself (reading a property of `undefined`, a stack overflow) are not
5
+ * modelled, as Java does not declare its unchecked exceptions.
6
+ */
7
+ /** The error classes of the platform; their ids are `builtin:<Name>`. */
8
+ export const BUILTIN_ERRORS = new Set([
9
+ "Error",
10
+ "TypeError",
11
+ "RangeError",
12
+ "SyntaxError",
13
+ "URIError",
14
+ "ReferenceError",
15
+ "EvalError",
16
+ "AggregateError",
17
+ "DOMException",
18
+ ]);
19
+ const BUILTIN = "builtin:";
20
+ const DOM_EXCEPTION = `${BUILTIN}DOMException`;
21
+ export function builtinId(name) {
22
+ return `${BUILTIN}${name}`;
23
+ }
24
+ /** A `DOMException` with one `name` (`AbortError`): caught by `instanceof DOMException`. */
25
+ export function domExceptionId(name) {
26
+ return `${DOM_EXCEPTION}:${name}`;
27
+ }
28
+ export function isBuiltin(id) {
29
+ return id.startsWith(BUILTIN);
30
+ }
31
+ /**
32
+ * The class a built-in error class extends: `Error` for every other one, `DOMException` for a
33
+ * named `DOMException`; null for `Error`.
34
+ */
35
+ export function builtinParent(id) {
36
+ if (id === builtinId("Error"))
37
+ return null;
38
+ if (id.startsWith(`${DOM_EXCEPTION}:`))
39
+ return DOM_EXCEPTION;
40
+ return builtinId("Error");
41
+ }
42
+ /** How a class is shown: `TypeError`, `AbortError (DOMException)`. */
43
+ export function builtinName(id) {
44
+ if (id.startsWith(`${DOM_EXCEPTION}:`)) {
45
+ return `${id.slice(DOM_EXCEPTION.length + 1)} (DOMException)`;
46
+ }
47
+ return id.slice(BUILTIN.length);
48
+ }
49
+ /** The class an `instanceof` test names to catch it: `DOMException` for `AbortError (DOMException)`. */
50
+ export function builtinCatchName(id) {
51
+ return id.startsWith(`${DOM_EXCEPTION}:`) ? "DOMException" : id.slice(BUILTIN.length);
52
+ }
53
+ const SYNTAX = builtinId("SyntaxError");
54
+ const TYPE = builtinId("TypeError");
55
+ const RANGE = builtinId("RangeError");
56
+ const URI = builtinId("URIError");
57
+ /** Methods of the platform's interfaces, as `<interface>.<method>`, that throw synchronously. */
58
+ export const THROWING_MEMBERS = new Map([
59
+ ["JSON.parse", [SYNTAX]],
60
+ // A circular value, or a BigInt.
61
+ ["JSON.stringify", [TYPE]],
62
+ // A digit count outside 0 to 100.
63
+ ["Number.toFixed", [RANGE]],
64
+ ]);
65
+ /** Global functions that throw synchronously. */
66
+ export const THROWING_GLOBALS = new Map([
67
+ ["decodeURIComponent", [URI]],
68
+ ["decodeURI", [URI]],
69
+ ["structuredClone", [domExceptionId("DataCloneError")]],
70
+ ["atob", [domExceptionId("InvalidCharacterError")]],
71
+ // A fraction (RangeError), text that is not a number (SyntaxError), a symbol (TypeError).
72
+ ["BigInt", [RANGE, SYNTAX, TYPE]],
73
+ ]);
74
+ /** Global constructors that throw. */
75
+ export const THROWING_CONSTRUCTORS = new Map([
76
+ ["URL", [TYPE]],
77
+ ]);
78
+ /** `Array(n)` and `new Array(n)` with one argument: a length that is not a valid one. */
79
+ export const ARRAY_LENGTH = [RANGE];
80
+ /** What `fetch` rejects with: a network failure, and an abort when a signal is passed. */
81
+ export const FETCH_REJECTS = [TYPE];
82
+ export const FETCH_ABORT = domExceptionId("AbortError");
83
+ /** What `Promise.any` rejects with when every promise rejects. */
84
+ export const ANY_REJECTS = builtinId("AggregateError");
85
+ /**
86
+ * Methods and functions of the platform that call a callback before they return, as
87
+ * `<interface>.<method>` (or the global's name), with the index of the argument that is the
88
+ * callback: what it throws, the call throws. `replace`'s second argument and `JSON.stringify`'s
89
+ * replacer may also be a string or a list; only a function there is a callback.
90
+ */
91
+ export const SYNC_CALLBACKS = new Map([
92
+ ...["Array", "ReadonlyArray"].flatMap((owner) => [
93
+ "map",
94
+ "forEach",
95
+ "filter",
96
+ "reduce",
97
+ "reduceRight",
98
+ "some",
99
+ "every",
100
+ "find",
101
+ "findIndex",
102
+ "findLast",
103
+ "findLastIndex",
104
+ "flatMap",
105
+ "toSorted",
106
+ ].map((method) => [`${owner}.${method}`, 0])),
107
+ ["Array.sort", 0],
108
+ ...["Map", "ReadonlyMap", "Set", "ReadonlySet"].map((owner) => [
109
+ `${owner}.forEach`,
110
+ 0,
111
+ ]),
112
+ ["ArrayConstructor.from", 1],
113
+ ["String.replace", 1],
114
+ ["String.replaceAll", 1],
115
+ ["JSON.parse", 1],
116
+ ["JSON.stringify", 1],
117
+ ["ObjectConstructor.groupBy", 1],
118
+ ["MapConstructor.groupBy", 1],
119
+ ]);
120
+ /** Calls whose result holds what the callback returned (promises, for `Promise.all`). */
121
+ export const MAPPING_METHODS = new Set([
122
+ "Array.map",
123
+ "ReadonlyArray.map",
124
+ "Array.flatMap",
125
+ "ReadonlyArray.flatMap",
126
+ "ArrayConstructor.from",
127
+ ]);
128
+ /**
129
+ * Functions and constructors of the platform, by name, that run their callback later (a timer, an
130
+ * event, an observer) or never (`removeEventListener`): what the callback throws never reaches
131
+ * the caller, so it is left out on purpose. Its own catches are still checked.
132
+ */
133
+ export const DEFERRED_CALLERS = new Set([
134
+ "setTimeout",
135
+ "setInterval",
136
+ "queueMicrotask",
137
+ "requestAnimationFrame",
138
+ "requestIdleCallback",
139
+ "addEventListener",
140
+ "removeEventListener",
141
+ "MutationObserver",
142
+ "ResizeObserver",
143
+ "IntersectionObserver",
144
+ "PerformanceObserver",
145
+ ]);
146
+ /**
147
+ * React's built-in hooks do not throw in correct use (misuse, like a hook called conditionally, is
148
+ * a bug the rules-of-hooks lint finds, not part of their contract), so a call to one adds nothing.
149
+ * Without this, every component would carry "unknown (package react)". `use()` is left out: it
150
+ * rethrows a rejected promise.
151
+ */
152
+ const NON_THROWING_REACT_HOOKS = new Set([
153
+ "useState",
154
+ "useReducer",
155
+ "useEffect",
156
+ "useLayoutEffect",
157
+ "useInsertionEffect",
158
+ "useMemo",
159
+ "useCallback",
160
+ "useRef",
161
+ "useContext",
162
+ "useId",
163
+ "useTransition",
164
+ "useDeferredValue",
165
+ "useImperativeHandle",
166
+ "useDebugValue",
167
+ "useSyncExternalStore",
168
+ "useOptimistic",
169
+ "useActionState",
170
+ "useFormStatus",
171
+ ]);
172
+ /** Whether a call into package `name`, written `callText`, is one of React's non-throwing hooks. */
173
+ export function isNonThrowingReactHook(name, callText) {
174
+ if (name !== "react" && name !== "react-dom")
175
+ return false;
176
+ const callee = /([A-Za-z_$][\w$]*)\s*(?:<[^>]*>)?\s*\(?/.exec(callText.split(".").pop() ?? "")?.[1];
177
+ return callee !== undefined && NON_THROWING_REACT_HOOKS.has(callee);
178
+ }
179
+ //# sourceMappingURL=builtins.js.map
@@ -0,0 +1,9 @@
1
+ import type { Analysis, CatchSeen } from "./solve.js";
2
+ /**
3
+ * Every catch of `file` (of the whole project when `file` is undefined) with what its `try` can
4
+ * throw, whether it branches on `instanceof` or not: the editor's catch-variable hint and
5
+ * the refactoring "Handle the known errors" read these. `catch-misses-error` reads only the catches that branch
6
+ * (`everyCatch`). In source order, one per `catch` keyword.
7
+ */
8
+ export declare function catchesOf(analysis: Analysis, file?: string): readonly CatchSeen[];
9
+ //# sourceMappingURL=catches.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { collected, evaluate, inferred } from "./solve.js";
2
+ /**
3
+ * Every catch of `file` (of the whole project when `file` is undefined) with what its `try` can
4
+ * throw, whether it branches on `instanceof` or not: the editor's catch-variable hint and
5
+ * the refactoring "Handle the known errors" read these. `catch-misses-error` reads only the catches that branch
6
+ * (`everyCatch`). In source order, one per `catch` keyword.
7
+ */
8
+ export function catchesOf(analysis, file) {
9
+ const found = new Map();
10
+ const add = (one) => {
11
+ const key = `${one.file}\u0000${String(one.handler.at[0]).padStart(10, "0")}`;
12
+ if (!found.has(key))
13
+ found.set(key, one);
14
+ };
15
+ for (const entry of analysis.functions.values()) {
16
+ if (file === undefined || entry.file === file)
17
+ inferred(entry, analysis, add);
18
+ }
19
+ for (const module of analysis.modules) {
20
+ if (file !== undefined && module.file !== file)
21
+ continue;
22
+ evaluate(module.effects, {
23
+ file: module.file,
24
+ classes: analysis.classes,
25
+ resultOf: (id) => analysis.results.get(id),
26
+ onCatch: add,
27
+ }, new Map(), collected());
28
+ }
29
+ return [...found.entries()]
30
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
31
+ .map(([, one]) => one);
32
+ }
33
+ //# sourceMappingURL=catches.js.map
@@ -0,0 +1,12 @@
1
+ import type { EditorContributions, Json, LibraryUse } from "@stateward/core";
2
+ type LibraryFacts = NonNullable<EditorContributions["libraryFacts"]>;
3
+ /** The rules whose summaries hold the throws facts; the first that ran is read. */
4
+ export declare const FACT_RULES: readonly ["errors/throws-doc-mismatch"];
5
+ /**
6
+ * Every package function the project calls, with where its throws facts come from and why what
7
+ * is unknown is unknown (`stateward deps`), from the last full check.
8
+ */
9
+ export declare function packagesCalled(summaries: ReadonlyMap<string, Json>): readonly LibraryUse[];
10
+ export declare const libraryFacts: LibraryFacts;
11
+ export {};
12
+ //# sourceMappingURL=deps.d.ts.map
@@ -0,0 +1,79 @@
1
+ import { TREE_RULES, throwsOf } from "../react/boundaries.js";
2
+ import { readSummary } from "./summary.js";
3
+ /** The rules whose summaries hold the throws facts; the first that ran is read. */
4
+ export const FACT_RULES = ["errors/throws-doc-mismatch"];
5
+ function collect(effects, into) {
6
+ for (const effect of effects) {
7
+ if (effect.k === "package")
8
+ into.push(effect.facts);
9
+ else if (effect.k === "try") {
10
+ collect(effect.body, into);
11
+ collect(effect.fin, into);
12
+ const handler = effect.handler;
13
+ if (handler !== null) {
14
+ collect(handler.pre, into);
15
+ for (const branch of handler.branches)
16
+ collect(branch.body, into);
17
+ collect(handler.otherwise ?? [], into);
18
+ collect(handler.after, into);
19
+ }
20
+ }
21
+ }
22
+ }
23
+ function compare(a, b) {
24
+ return a < b ? -1 : a > b ? 1 : 0;
25
+ }
26
+ /**
27
+ * Every package function the project calls, with where its throws facts come from and why what
28
+ * is unknown is unknown (`stateward deps`), from the last full check.
29
+ */
30
+ export function packagesCalled(summaries) {
31
+ const found = [];
32
+ for (const value of summaries.values()) {
33
+ const summary = readSummary(value);
34
+ if (summary === undefined)
35
+ continue;
36
+ for (const fact of summary.functions) {
37
+ collect(fact.body, found);
38
+ collect(fact.returns, found);
39
+ }
40
+ collect(summary.module, found);
41
+ }
42
+ const byPackage = new Map();
43
+ for (const facts of found) {
44
+ const key = `${facts.name}\u0000${facts.version}`;
45
+ const entry = byPackage.get(key) ?? {
46
+ version: facts.version,
47
+ functions: new Map(),
48
+ };
49
+ byPackage.set(key, entry);
50
+ const fn = entry.functions.get(facts.fn) ?? {
51
+ source: facts.source,
52
+ unknown: new Set(),
53
+ };
54
+ entry.functions.set(facts.fn, fn);
55
+ for (const reason of [...facts.throws.unknown, ...facts.rejects.unknown])
56
+ fn.unknown.add(reason);
57
+ }
58
+ return [...byPackage]
59
+ .sort(([a], [b]) => compare(a, b))
60
+ .map(([key, entry]) => ({
61
+ package: key.slice(0, key.indexOf("\u0000")),
62
+ version: entry.version,
63
+ functions: [...entry.functions]
64
+ .sort(([a], [b]) => compare(a, b))
65
+ .map(([name, fn]) => ({ name, source: fn.source, unknown: [...fn.unknown].sort() })),
66
+ }));
67
+ }
68
+ export const libraryFacts = (input) => {
69
+ let summaries;
70
+ for (const ruleId of FACT_RULES)
71
+ summaries ??= input.projectSummaries(ruleId);
72
+ for (const ruleId of TREE_RULES) {
73
+ const trees = summaries === undefined ? input.projectSummaries(ruleId) : undefined;
74
+ if (trees !== undefined)
75
+ summaries = throwsOf(trees);
76
+ }
77
+ return summaries === undefined ? [] : packagesCalled(summaries);
78
+ };
79
+ //# sourceMappingURL=deps.js.map
@@ -0,0 +1,25 @@
1
+ import type { Result, Source, Thrown } from "./solve.js";
2
+ import type { ClassFact } from "./summary.js";
3
+ /** How a class is shown: its name, `AbortError (DOMException)` for a named DOMException. */
4
+ export declare function className(id: string, classes: ReadonlyMap<string, ClassFact>): string;
5
+ /** The name an `instanceof` test or a `@throws` tag uses for the class. */
6
+ export declare function catchName(id: string, classes: ReadonlyMap<string, ClassFact>): string;
7
+ /** `others stateward cannot see (from a: why; b: why; and 3 more)`. */
8
+ export declare function unknownText(sources: readonly Source[], more: boolean): string;
9
+ /**
10
+ * `ParseError | ZodError (zod 3.23.8: declared) | others stateward cannot see (from …)`, or "" for an empty set: a class
11
+ * known from a package's facts names where they come from.
12
+ */
13
+ export declare function thrownText(thrown: Thrown, classes: ReadonlyMap<string, ClassFact>): string;
14
+ /**
15
+ * What a call's promise rejects with, without the unknown parts already listed as thrown: an
16
+ * unknown call is both thrown and returned when stateward cannot tell whether it gives a promise,
17
+ * and is shown once, where it is thrown.
18
+ */
19
+ export declare function rejectsShown(result: Result): Thrown;
20
+ /**
21
+ * The hover's lines: `throws: …` for what a call throws, `rejects: …` for what its promise
22
+ * rejects with, `throws nothing stateward can see` when both are empty.
23
+ */
24
+ export declare function resultLines(result: Result, classes: ReadonlyMap<string, ClassFact>): string[];
25
+ //# sourceMappingURL=format.d.ts.map