@stateward/plugin-errors 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,191 @@
|
|
|
1
|
+
import type { FactSource, Json } from "@stateward/core";
|
|
2
|
+
/**
|
|
3
|
+
* The throws facts of one file: JSON, so the engine can cache it
|
|
4
|
+
* under the file's dependency fingerprint. Every id is a qualified id (`src/api.ts#Api.get`) or,
|
|
5
|
+
* for the language's own error classes, `builtin:<Name>` (`builtin:TypeError`,
|
|
6
|
+
* `builtin:DOMException:AbortError` for a named `DOMException`).
|
|
7
|
+
*/
|
|
8
|
+
/** A start and end offset in the summarized file. */
|
|
9
|
+
export type Span = readonly [number, number];
|
|
10
|
+
/**
|
|
11
|
+
* A class that is thrown or caught in the file, with the class it extends: `parent` is null for a
|
|
12
|
+
* class that extends nothing, and `open` is true when what it extends cannot be followed (a
|
|
13
|
+
* mixin call, an unresolved name), so whether an `instanceof` test catches it is not known.
|
|
14
|
+
*/
|
|
15
|
+
export type ClassFact = {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly parent: string | null;
|
|
19
|
+
readonly open: boolean;
|
|
20
|
+
};
|
|
21
|
+
/** Which part of a called function's result a call adds: what it throws, rejects, or both. */
|
|
22
|
+
export type Part = "throws" | "rejects" | "both";
|
|
23
|
+
/** One side of a package function's facts: the classes it gives, and why the rest is unknown. */
|
|
24
|
+
export type Side = {
|
|
25
|
+
readonly classes: readonly string[];
|
|
26
|
+
readonly unknown: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* What a call into a package throws and rejects with, and where that is known from
|
|
30
|
+
*: class ids are `builtin:<Name>` or `package:<package>#<Name>`; an
|
|
31
|
+
* unknown is one of the fixed reasons (`UNKNOWN_REASONS`, packages.ts).
|
|
32
|
+
*/
|
|
33
|
+
export type PackageFacts = {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
/** "" when the package's version cannot be read. */
|
|
36
|
+
readonly version: string;
|
|
37
|
+
/** The function's path in the package's types: `remote`, `Client.get`. */
|
|
38
|
+
readonly fn: string;
|
|
39
|
+
readonly source: FactSource;
|
|
40
|
+
readonly throws: Side;
|
|
41
|
+
readonly rejects: Side;
|
|
42
|
+
};
|
|
43
|
+
/** Something that adds to what the code around it throws. */
|
|
44
|
+
export type Leaf =
|
|
45
|
+
/** `throw new X(…)`, or a built-in that throws X; `text` is the call that does, null for a `throw`. */
|
|
46
|
+
{
|
|
47
|
+
readonly k: "class";
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly text: string | null;
|
|
50
|
+
readonly at: Span;
|
|
51
|
+
}
|
|
52
|
+
/** What cannot be followed, and why (`no declaration`, `callback of unknown origin`, …). */
|
|
53
|
+
| {
|
|
54
|
+
readonly k: "unknown";
|
|
55
|
+
readonly reason: string;
|
|
56
|
+
readonly text: string;
|
|
57
|
+
readonly at: Span;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A callback handed to a call of the platform that may or may not run it before it returns:
|
|
61
|
+
* unknown, for `reason`, when `body` (what the callback throws and rejects with) is not empty.
|
|
62
|
+
*/
|
|
63
|
+
| {
|
|
64
|
+
readonly k: "maybe";
|
|
65
|
+
readonly reason: string;
|
|
66
|
+
readonly text: string;
|
|
67
|
+
readonly body: readonly Effect[];
|
|
68
|
+
readonly at: Span;
|
|
69
|
+
}
|
|
70
|
+
/** A call of a function of the project: adds `part` of what it throws. */
|
|
71
|
+
| {
|
|
72
|
+
readonly k: "call";
|
|
73
|
+
readonly id: string;
|
|
74
|
+
readonly part: Part;
|
|
75
|
+
readonly text: string;
|
|
76
|
+
readonly at: Span;
|
|
77
|
+
}
|
|
78
|
+
/** A call into a package: adds `part` of its facts (`throws` where it is called, `rejects` when awaited). */
|
|
79
|
+
| {
|
|
80
|
+
readonly k: "package";
|
|
81
|
+
readonly facts: PackageFacts;
|
|
82
|
+
readonly part: "throws" | "rejects";
|
|
83
|
+
readonly text: string;
|
|
84
|
+
readonly at: Span;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `throw e` of the variable of the catch that starts at `of`: rethrows what reaches it there.
|
|
88
|
+
* `maybe` when the throw sits under a test this analysis does not read.
|
|
89
|
+
*/
|
|
90
|
+
| {
|
|
91
|
+
readonly k: "rethrow";
|
|
92
|
+
readonly of: number;
|
|
93
|
+
readonly maybe: boolean;
|
|
94
|
+
readonly at: Span;
|
|
95
|
+
};
|
|
96
|
+
/** One `if (e instanceof A || e instanceof B)` link, or one `case` of a `switch (true)`. */
|
|
97
|
+
export type Branch = {
|
|
98
|
+
/** The classes the test names; `exact` when the test is only `instanceof` tests joined by `||`. */
|
|
99
|
+
readonly classes: readonly string[];
|
|
100
|
+
readonly exact: boolean;
|
|
101
|
+
readonly body: readonly Effect[];
|
|
102
|
+
/** Ends with `return` or `throw`: the statements after the chain do not run for it. */
|
|
103
|
+
readonly exits: boolean;
|
|
104
|
+
};
|
|
105
|
+
export type Handler = {
|
|
106
|
+
/** The `catch` keyword. */
|
|
107
|
+
readonly at: Span;
|
|
108
|
+
/** The catch's `{ … }` block, braces included (the quick fix wraps what is inside). */
|
|
109
|
+
readonly block: Span;
|
|
110
|
+
/** Where the catch variable is declared, the key of its rethrows; -1 for `catch {`. */
|
|
111
|
+
readonly of: number;
|
|
112
|
+
readonly variable: string | null;
|
|
113
|
+
/** How the catch branches on the error: an `if` chain or a `switch (true)` (`if` when it does not). */
|
|
114
|
+
readonly dispatch: "if" | "switch";
|
|
115
|
+
/** The statements before the `instanceof` chain or switch. */
|
|
116
|
+
readonly pre: readonly Effect[];
|
|
117
|
+
readonly branches: readonly Branch[];
|
|
118
|
+
/** The final `else` or `default`; null when there is none. */
|
|
119
|
+
readonly otherwise: readonly Effect[] | null;
|
|
120
|
+
readonly otherwiseExits: boolean;
|
|
121
|
+
/** The statements after the chain (the whole block when the catch has no chain). */
|
|
122
|
+
readonly after: readonly Effect[];
|
|
123
|
+
/** Whether what the chain does not handle reaches a `throw` (a rethrow or a wrap). */
|
|
124
|
+
readonly restThrows: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Whether what the chain does not handle reaches code that uses the caught variable (passes it
|
|
127
|
+
* on, logs it, stores it, reads a property): a final `else` or the code after the chain.
|
|
128
|
+
*/
|
|
129
|
+
readonly restUsed: boolean;
|
|
130
|
+
};
|
|
131
|
+
export type Try = {
|
|
132
|
+
readonly k: "try";
|
|
133
|
+
readonly at: Span;
|
|
134
|
+
readonly body: readonly Effect[];
|
|
135
|
+
readonly handler: Handler | null;
|
|
136
|
+
readonly fin: readonly Effect[];
|
|
137
|
+
};
|
|
138
|
+
export type Effect = Leaf | Try;
|
|
139
|
+
/** One class a `@throws` tag names: its id, or null when the name resolves to no class. */
|
|
140
|
+
export type DocEntry = {
|
|
141
|
+
readonly id: string | null;
|
|
142
|
+
readonly name: string;
|
|
143
|
+
readonly at: Span;
|
|
144
|
+
};
|
|
145
|
+
/** The `@throws` tags of a function. */
|
|
146
|
+
export type Doc = {
|
|
147
|
+
readonly entries: readonly DocEntry[];
|
|
148
|
+
/** A tag without a type (`@throws when …`): it says the function throws, not what. */
|
|
149
|
+
readonly untyped: readonly Span[];
|
|
150
|
+
};
|
|
151
|
+
export type FunctionFact = {
|
|
152
|
+
readonly id: string;
|
|
153
|
+
readonly name: string;
|
|
154
|
+
/** The function's name. */
|
|
155
|
+
readonly at: Span;
|
|
156
|
+
readonly async: boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Its `@throws` says what the promise it returns rejects with: it is `async`, or it has a
|
|
159
|
+
* `@throws` and returns a promise (declared or inferred). False for an undocumented function
|
|
160
|
+
* that is not `async`, whose body's own throws are synchronous whatever it returns.
|
|
161
|
+
*/
|
|
162
|
+
readonly promise: boolean;
|
|
163
|
+
readonly doc: Doc | null;
|
|
164
|
+
/** A function or arrow declared inside another function: only that function's code can call it. */
|
|
165
|
+
readonly local: boolean;
|
|
166
|
+
/** What runs when it is called: thrown where it happens. */
|
|
167
|
+
readonly body: readonly Effect[];
|
|
168
|
+
/** What the promise it returns rejects with (`return fetch(…)`), outside any `try`. */
|
|
169
|
+
readonly returns: readonly Effect[];
|
|
170
|
+
};
|
|
171
|
+
export type ThrowsSummary = {
|
|
172
|
+
readonly classes: readonly ClassFact[];
|
|
173
|
+
readonly functions: readonly FunctionFact[];
|
|
174
|
+
/** The code at the top of the module, outside any function: its catches are checked too. */
|
|
175
|
+
readonly module: readonly Effect[];
|
|
176
|
+
};
|
|
177
|
+
export type Fields = Readonly<Record<string, Json>>;
|
|
178
|
+
export declare function fieldsOf(value: Json): Fields | undefined;
|
|
179
|
+
export declare function isString(value: Json | undefined): value is string;
|
|
180
|
+
export declare function spanOf(value: Json | undefined): Span | undefined;
|
|
181
|
+
export declare function each<T>(value: Json | undefined, read: (item: Json) => T | undefined): T[] | undefined;
|
|
182
|
+
export declare function readStrings(value: Json | undefined): string[] | undefined;
|
|
183
|
+
/** Package facts as `PackageFacts` writes them; undefined for anything else. */
|
|
184
|
+
export declare function readPackageFacts(value: Json | undefined): PackageFacts | undefined;
|
|
185
|
+
export declare function readClass(value: Json): ClassFact | undefined;
|
|
186
|
+
/**
|
|
187
|
+
* The summary as this pack wrote it, or undefined for anything else (a cache file that does not
|
|
188
|
+
* hold what this version writes): such a file then contributes no facts.
|
|
189
|
+
*/
|
|
190
|
+
export declare function readSummary(value: Json): ThrowsSummary | undefined;
|
|
191
|
+
//# sourceMappingURL=summary.d.ts.map
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
function isList(value) {
|
|
2
|
+
return Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function fieldsOf(value) {
|
|
5
|
+
if (typeof value !== "object" || value === null || isList(value))
|
|
6
|
+
return undefined;
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function listOf(value) {
|
|
10
|
+
return Array.isArray(value) ? value : undefined;
|
|
11
|
+
}
|
|
12
|
+
export function isString(value) {
|
|
13
|
+
return typeof value === "string";
|
|
14
|
+
}
|
|
15
|
+
export function spanOf(value) {
|
|
16
|
+
const list = listOf(value);
|
|
17
|
+
if (list?.length !== 2)
|
|
18
|
+
return undefined;
|
|
19
|
+
const [start, end] = list;
|
|
20
|
+
return typeof start === "number" && typeof end === "number" ? [start, end] : undefined;
|
|
21
|
+
}
|
|
22
|
+
export function each(value, read) {
|
|
23
|
+
const list = listOf(value);
|
|
24
|
+
if (list === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const item of list) {
|
|
28
|
+
const one = read(item);
|
|
29
|
+
if (one === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
out.push(one);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function readPart(value) {
|
|
36
|
+
return value === "throws" || value === "rejects" || value === "both" ? value : undefined;
|
|
37
|
+
}
|
|
38
|
+
export function readStrings(value) {
|
|
39
|
+
return each(value, (item) => (isString(item) ? item : undefined));
|
|
40
|
+
}
|
|
41
|
+
function readSide(value) {
|
|
42
|
+
const f = value === undefined ? undefined : fieldsOf(value);
|
|
43
|
+
const classes = readStrings(f?.classes);
|
|
44
|
+
const unknown = readStrings(f?.unknown);
|
|
45
|
+
return classes === undefined || unknown === undefined ? undefined : { classes, unknown };
|
|
46
|
+
}
|
|
47
|
+
const SOURCES = ["config", "declared", "inferred", "unknown"];
|
|
48
|
+
function readSource(value) {
|
|
49
|
+
return SOURCES.find((source) => source === value);
|
|
50
|
+
}
|
|
51
|
+
/** Package facts as `PackageFacts` writes them; undefined for anything else. */
|
|
52
|
+
export function readPackageFacts(value) {
|
|
53
|
+
const f = value === undefined ? undefined : fieldsOf(value);
|
|
54
|
+
if (f === undefined || !isString(f.name) || !isString(f.version) || !isString(f.fn)) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const source = readSource(f.source);
|
|
58
|
+
const throws = readSide(f.throws);
|
|
59
|
+
const rejects = readSide(f.rejects);
|
|
60
|
+
if (source === undefined || throws === undefined || rejects === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
return { name: f.name, version: f.version, fn: f.fn, source, throws, rejects };
|
|
63
|
+
}
|
|
64
|
+
function readEffect(value) {
|
|
65
|
+
const f = fieldsOf(value);
|
|
66
|
+
const at = spanOf(f?.at);
|
|
67
|
+
if (f === undefined || at === undefined || !isString(f.k))
|
|
68
|
+
return undefined;
|
|
69
|
+
switch (f.k) {
|
|
70
|
+
case "class":
|
|
71
|
+
return isString(f.id) && (f.text === null || isString(f.text))
|
|
72
|
+
? { k: "class", id: f.id, text: f.text, at }
|
|
73
|
+
: undefined;
|
|
74
|
+
case "unknown":
|
|
75
|
+
return isString(f.reason) && isString(f.text)
|
|
76
|
+
? { k: "unknown", reason: f.reason, text: f.text, at }
|
|
77
|
+
: undefined;
|
|
78
|
+
case "call": {
|
|
79
|
+
const part = readPart(f.part);
|
|
80
|
+
return isString(f.id) && isString(f.text) && part !== undefined
|
|
81
|
+
? { k: "call", id: f.id, part, text: f.text, at }
|
|
82
|
+
: undefined;
|
|
83
|
+
}
|
|
84
|
+
case "package": {
|
|
85
|
+
const facts = readPackageFacts(f.facts);
|
|
86
|
+
return facts !== undefined &&
|
|
87
|
+
isString(f.text) &&
|
|
88
|
+
(f.part === "throws" || f.part === "rejects")
|
|
89
|
+
? { k: "package", facts, part: f.part, text: f.text, at }
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
|
92
|
+
case "maybe": {
|
|
93
|
+
const body = each(f.body, readEffect);
|
|
94
|
+
return isString(f.reason) && isString(f.text) && body !== undefined
|
|
95
|
+
? { k: "maybe", reason: f.reason, text: f.text, body, at }
|
|
96
|
+
: undefined;
|
|
97
|
+
}
|
|
98
|
+
case "rethrow":
|
|
99
|
+
return typeof f.of === "number" && typeof f.maybe === "boolean"
|
|
100
|
+
? { k: "rethrow", of: f.of, maybe: f.maybe, at }
|
|
101
|
+
: undefined;
|
|
102
|
+
case "try": {
|
|
103
|
+
const body = each(f.body, readEffect);
|
|
104
|
+
const fin = each(f.fin, readEffect);
|
|
105
|
+
const handler = f.handler === null ? null : readHandler(f.handler);
|
|
106
|
+
return body === undefined || fin === undefined || handler === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: { k: "try", at, body, handler, fin };
|
|
109
|
+
}
|
|
110
|
+
default:
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function readBranch(value) {
|
|
115
|
+
const f = fieldsOf(value);
|
|
116
|
+
if (f === undefined)
|
|
117
|
+
return undefined;
|
|
118
|
+
const classes = each(f.classes, (item) => (isString(item) ? item : undefined));
|
|
119
|
+
const body = each(f.body, readEffect);
|
|
120
|
+
if (classes === undefined || body === undefined)
|
|
121
|
+
return undefined;
|
|
122
|
+
if (typeof f.exact !== "boolean" || typeof f.exits !== "boolean")
|
|
123
|
+
return undefined;
|
|
124
|
+
return { classes, exact: f.exact, body, exits: f.exits };
|
|
125
|
+
}
|
|
126
|
+
function readHandler(value) {
|
|
127
|
+
const f = value === undefined ? undefined : fieldsOf(value);
|
|
128
|
+
const at = spanOf(f?.at);
|
|
129
|
+
const block = spanOf(f?.block);
|
|
130
|
+
if (f === undefined || at === undefined || block === undefined)
|
|
131
|
+
return undefined;
|
|
132
|
+
const pre = each(f.pre, readEffect);
|
|
133
|
+
const branches = each(f.branches, readBranch);
|
|
134
|
+
const after = each(f.after, readEffect);
|
|
135
|
+
const otherwise = f.otherwise === null ? null : each(f.otherwise, readEffect);
|
|
136
|
+
if (pre === undefined || branches === undefined || after === undefined)
|
|
137
|
+
return undefined;
|
|
138
|
+
if (otherwise === undefined || typeof f.of !== "number")
|
|
139
|
+
return undefined;
|
|
140
|
+
if (typeof f.otherwiseExits !== "boolean" || typeof f.restThrows !== "boolean")
|
|
141
|
+
return undefined;
|
|
142
|
+
if (typeof f.restUsed !== "boolean")
|
|
143
|
+
return undefined;
|
|
144
|
+
if (f.variable !== null && !isString(f.variable))
|
|
145
|
+
return undefined;
|
|
146
|
+
if (f.dispatch !== "if" && f.dispatch !== "switch")
|
|
147
|
+
return undefined;
|
|
148
|
+
return {
|
|
149
|
+
at,
|
|
150
|
+
block,
|
|
151
|
+
of: f.of,
|
|
152
|
+
variable: f.variable,
|
|
153
|
+
dispatch: f.dispatch,
|
|
154
|
+
pre,
|
|
155
|
+
branches,
|
|
156
|
+
otherwise,
|
|
157
|
+
otherwiseExits: f.otherwiseExits,
|
|
158
|
+
after,
|
|
159
|
+
restThrows: f.restThrows,
|
|
160
|
+
restUsed: f.restUsed,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function readDoc(value) {
|
|
164
|
+
if (value === null)
|
|
165
|
+
return null;
|
|
166
|
+
const f = value === undefined ? undefined : fieldsOf(value);
|
|
167
|
+
if (f === undefined)
|
|
168
|
+
return undefined;
|
|
169
|
+
const entries = each(f.entries, (item) => {
|
|
170
|
+
const entry = fieldsOf(item);
|
|
171
|
+
const at = spanOf(entry?.at);
|
|
172
|
+
if (entry === undefined || at === undefined || !isString(entry.name))
|
|
173
|
+
return undefined;
|
|
174
|
+
if (entry.id !== null && !isString(entry.id))
|
|
175
|
+
return undefined;
|
|
176
|
+
return { id: entry.id, name: entry.name, at };
|
|
177
|
+
});
|
|
178
|
+
const untyped = each(f.untyped, spanOf);
|
|
179
|
+
return entries === undefined || untyped === undefined ? undefined : { entries, untyped };
|
|
180
|
+
}
|
|
181
|
+
function readFunction(value) {
|
|
182
|
+
const f = fieldsOf(value);
|
|
183
|
+
const at = spanOf(f?.at);
|
|
184
|
+
if (f === undefined || at === undefined || !isString(f.id) || !isString(f.name))
|
|
185
|
+
return undefined;
|
|
186
|
+
const doc = readDoc(f.doc);
|
|
187
|
+
const body = each(f.body, readEffect);
|
|
188
|
+
const returns = each(f.returns, readEffect);
|
|
189
|
+
if (doc === undefined || body === undefined || returns === undefined)
|
|
190
|
+
return undefined;
|
|
191
|
+
if (typeof f.async !== "boolean" || typeof f.promise !== "boolean")
|
|
192
|
+
return undefined;
|
|
193
|
+
if (typeof f.local !== "boolean")
|
|
194
|
+
return undefined;
|
|
195
|
+
return {
|
|
196
|
+
id: f.id,
|
|
197
|
+
name: f.name,
|
|
198
|
+
at,
|
|
199
|
+
async: f.async,
|
|
200
|
+
promise: f.promise,
|
|
201
|
+
doc,
|
|
202
|
+
local: f.local,
|
|
203
|
+
body,
|
|
204
|
+
returns,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
export function readClass(value) {
|
|
208
|
+
const f = fieldsOf(value);
|
|
209
|
+
if (f === undefined || !isString(f.id) || !isString(f.name))
|
|
210
|
+
return undefined;
|
|
211
|
+
if ((f.parent !== null && !isString(f.parent)) || typeof f.open !== "boolean")
|
|
212
|
+
return undefined;
|
|
213
|
+
return { id: f.id, name: f.name, parent: f.parent, open: f.open };
|
|
214
|
+
}
|
|
215
|
+
/** Read once per summary object: the engine hands out the same frozen object while it is unchanged. */
|
|
216
|
+
const read = new WeakMap();
|
|
217
|
+
/**
|
|
218
|
+
* The summary as this pack wrote it, or undefined for anything else (a cache file that does not
|
|
219
|
+
* hold what this version writes): such a file then contributes no facts.
|
|
220
|
+
*/
|
|
221
|
+
export function readSummary(value) {
|
|
222
|
+
const f = fieldsOf(value);
|
|
223
|
+
if (f === undefined)
|
|
224
|
+
return undefined;
|
|
225
|
+
const known = read.get(f);
|
|
226
|
+
if (known !== undefined)
|
|
227
|
+
return known ?? undefined;
|
|
228
|
+
const classes = each(f.classes, readClass);
|
|
229
|
+
const functions = each(f.functions, readFunction);
|
|
230
|
+
const module = each(f.module, readEffect);
|
|
231
|
+
const summary = classes === undefined || functions === undefined || module === undefined
|
|
232
|
+
? undefined
|
|
233
|
+
: { classes, functions, module };
|
|
234
|
+
read.set(f, summary ?? null);
|
|
235
|
+
return summary;
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=summary.js.map
|
package/dist/inlay.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EditorContributions, Json } from "@stateward/core";
|
|
2
|
+
type InlayHints = NonNullable<EditorContributions["inlayHints"]>;
|
|
3
|
+
/** The two kinds of hint, as the `stateward.inlayHints.<kind>` settings name them. */
|
|
4
|
+
export declare const HINT_KINDS: {
|
|
5
|
+
readonly throws: "throws";
|
|
6
|
+
readonly catchVariable: "catchVariable";
|
|
7
|
+
};
|
|
8
|
+
/** The summaries that hold the throws facts, from the rules that ran (as the hover reads them). */
|
|
9
|
+
export declare function factSummaries(input: {
|
|
10
|
+
projectSummaries(ruleId: string): ReadonlyMap<string, Json> | undefined;
|
|
11
|
+
}): ReadonlyMap<string, Json> | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Two kinds of hint, from the last full check's solve (no recomputation, as the hover):
|
|
14
|
+
* `throws A | B` or `throws (unknown from \`fetchAvatar\`)` after a named function's signature (the hover's facts, `rejects` for what its
|
|
15
|
+
* promise rejects with), and `: A | B | unknown` after a catch variable (what the `try` can throw).
|
|
16
|
+
*/
|
|
17
|
+
export declare const inlayHints: InlayHints;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=inlay.d.ts.map
|
package/dist/inlay.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import ts from "@stateward/core/typescript";
|
|
2
|
+
import { FACT_RULES } from "./inference/deps.js";
|
|
3
|
+
import { catchesOf } from "./inference/catches.js";
|
|
4
|
+
import { className, rejectsShown, resultLines, thrownText } from "./inference/format.js";
|
|
5
|
+
import { analyse, toThrown } from "./inference/solve.js";
|
|
6
|
+
import { TREE_RULES, throwsOf } from "./react/boundaries.js";
|
|
7
|
+
/** The two kinds of hint, as the `stateward.inlayHints.<kind>` settings name them. */
|
|
8
|
+
export const HINT_KINDS = { throws: "throws", catchVariable: "catchVariable" };
|
|
9
|
+
/** At most this many class names are listed in a hint; the rest are counted (`+2`). */
|
|
10
|
+
const MAX_NAMES = 3;
|
|
11
|
+
/**
|
|
12
|
+
* Where the unknown part of a signature's hint comes from: `(unknown from \`fetchAvatar\`)`, or
|
|
13
|
+
* `(unknown from \`fetchAvatar\`, +2)` for several calls. `others` when no call is named, and
|
|
14
|
+
* nothing at all when the set has no unknown part.
|
|
15
|
+
*/
|
|
16
|
+
function unknownPart(thrown) {
|
|
17
|
+
if (thrown.unknowns.length === 0 && !thrown.more)
|
|
18
|
+
return undefined;
|
|
19
|
+
const calls = [...new Set(thrown.unknowns.map((source) => source.text).filter((t) => t !== ""))];
|
|
20
|
+
const [first] = calls;
|
|
21
|
+
if (first === undefined)
|
|
22
|
+
return "others";
|
|
23
|
+
const rest = calls.length - 1 + (thrown.more ? 1 : 0);
|
|
24
|
+
const more = rest > 0 ? `, +${String(rest)}` : "";
|
|
25
|
+
return `(unknown from \`${first}\`${more})`;
|
|
26
|
+
}
|
|
27
|
+
/** Whether an unknown error comes from a package's declaration, not from the project's own code. */
|
|
28
|
+
function fromLibrary(source) {
|
|
29
|
+
return source.package !== undefined || source.reason.startsWith("package ");
|
|
30
|
+
}
|
|
31
|
+
function fromLibrariesOnly(thrown) {
|
|
32
|
+
return !thrown.more && thrown.unknowns.length > 0 && thrown.unknowns.every(fromLibrary);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* `A | B | C | +2 | others`: the names in the hover's order, then the unknown part: where it comes
|
|
36
|
+
* from after a signature (`unknownPart`), `unknown` after a catch variable. Empty when the set is
|
|
37
|
+
* empty, and after a signature when nothing in it is named: a bare "throws others" says nothing.
|
|
38
|
+
*/
|
|
39
|
+
function listed(thrown, classes, unknown) {
|
|
40
|
+
const ids = [...thrown.classes.keys()];
|
|
41
|
+
const hoverText = (id) => {
|
|
42
|
+
const origin = thrown.origins.get(id);
|
|
43
|
+
const name = className(id, classes);
|
|
44
|
+
return origin === undefined ? name : `${name} (${origin})`;
|
|
45
|
+
};
|
|
46
|
+
// Sorted by the hover's text, so the order is the hover's.
|
|
47
|
+
const names = [...new Set(ids.map((id) => [hoverText(id), className(id, classes)]))]
|
|
48
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
49
|
+
.map(([, name]) => name);
|
|
50
|
+
const unique = [...new Set(names)];
|
|
51
|
+
const rest = unique.length - MAX_NAMES;
|
|
52
|
+
const hasUnknown = thrown.unknowns.length > 0 || thrown.more;
|
|
53
|
+
const tail = unknown === "unknown" ? (hasUnknown ? "unknown" : undefined) : unknownPart(thrown);
|
|
54
|
+
if (unique.length === 0 && (tail === undefined || tail === "others"))
|
|
55
|
+
return "";
|
|
56
|
+
// Nothing known, and every unknown comes from a library function: the hint says nothing the
|
|
57
|
+
// person can act on (the hover still lists it).
|
|
58
|
+
if (unique.length === 0 && unknown === "sources" && fromLibrariesOnly(thrown))
|
|
59
|
+
return "";
|
|
60
|
+
return [
|
|
61
|
+
...unique.slice(0, MAX_NAMES),
|
|
62
|
+
...(rest > 0 ? [`+${String(rest)}`] : []),
|
|
63
|
+
...(tail === undefined ? [] : [tail]),
|
|
64
|
+
].join(" | ");
|
|
65
|
+
}
|
|
66
|
+
/** The name a function-like is bound to: its own, or the `const` / property it is the value of. */
|
|
67
|
+
function nameOf(node) {
|
|
68
|
+
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
|
|
69
|
+
return node.name !== undefined && ts.isIdentifier(node.name) ? node.name : undefined;
|
|
70
|
+
}
|
|
71
|
+
if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node))
|
|
72
|
+
return undefined;
|
|
73
|
+
const parent = node.parent;
|
|
74
|
+
if ((ts.isVariableDeclaration(parent) ||
|
|
75
|
+
ts.isPropertyAssignment(parent) ||
|
|
76
|
+
ts.isPropertyDeclaration(parent)) &&
|
|
77
|
+
parent.initializer === node &&
|
|
78
|
+
ts.isIdentifier(parent.name)) {
|
|
79
|
+
return parent.name;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
/** Where the signature ends: after the return type, else after the `)` of the parameters. */
|
|
84
|
+
function signatureEnd(node, sourceFile) {
|
|
85
|
+
if (node.type !== undefined)
|
|
86
|
+
return node.type.end;
|
|
87
|
+
return node.getChildren(sourceFile).find((child) => child.kind === ts.SyntaxKind.CloseParenToken)
|
|
88
|
+
?.end;
|
|
89
|
+
}
|
|
90
|
+
function isFunctionWithBody(node) {
|
|
91
|
+
return ((ts.isFunctionDeclaration(node) ||
|
|
92
|
+
ts.isMethodDeclaration(node) ||
|
|
93
|
+
ts.isArrowFunction(node) ||
|
|
94
|
+
ts.isFunctionExpression(node)) &&
|
|
95
|
+
node.body !== undefined);
|
|
96
|
+
}
|
|
97
|
+
/** The summaries that hold the throws facts, from the rules that ran (as the hover reads them). */
|
|
98
|
+
export function factSummaries(input) {
|
|
99
|
+
let summaries;
|
|
100
|
+
for (const ruleId of FACT_RULES)
|
|
101
|
+
summaries ??= input.projectSummaries(ruleId);
|
|
102
|
+
let trees;
|
|
103
|
+
for (const ruleId of TREE_RULES)
|
|
104
|
+
trees ??= input.projectSummaries(ruleId);
|
|
105
|
+
return summaries ?? (trees === undefined ? undefined : throwsOf(trees));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Two kinds of hint, from the last full check's solve (no recomputation, as the hover):
|
|
109
|
+
* `throws A | B` or `throws (unknown from \`fetchAvatar\`)` after a named function's signature (the hover's facts, `rejects` for what its
|
|
110
|
+
* promise rejects with), and `: A | B | unknown` after a catch variable (what the `try` can throw).
|
|
111
|
+
*/
|
|
112
|
+
export const inlayHints = (input) => {
|
|
113
|
+
const summaries = factSummaries(input);
|
|
114
|
+
if (summaries === undefined)
|
|
115
|
+
return [];
|
|
116
|
+
const analysis = analyse(summaries);
|
|
117
|
+
const { sourceFile } = input;
|
|
118
|
+
const hints = [];
|
|
119
|
+
const catches = new Map(catchesOf(analysis, sourceFile.fileName).map((seen) => [seen.handler.at[0], seen]));
|
|
120
|
+
const visit = (node) => {
|
|
121
|
+
if (isFunctionWithBody(node)) {
|
|
122
|
+
const name = nameOf(node);
|
|
123
|
+
const end = signatureEnd(node, sourceFile);
|
|
124
|
+
const symbol = name === undefined ? undefined : input.types.symbolAt(name);
|
|
125
|
+
const result = symbol === undefined ? undefined : analysis.results.get(symbol.id);
|
|
126
|
+
if (result !== undefined && end !== undefined) {
|
|
127
|
+
const thrown = listed(result.throws, analysis.classes, "sources");
|
|
128
|
+
const rejected = listed(rejectsShown(result), analysis.classes, "sources");
|
|
129
|
+
const parts = [
|
|
130
|
+
...(thrown === "" ? [] : [`throws ${thrown}`]),
|
|
131
|
+
...(rejected === "" ? [] : [`rejects ${rejected}`]),
|
|
132
|
+
];
|
|
133
|
+
if (parts.length > 0) {
|
|
134
|
+
hints.push({
|
|
135
|
+
offset: end,
|
|
136
|
+
kind: HINT_KINDS.throws,
|
|
137
|
+
label: parts.join(" · "),
|
|
138
|
+
paddingLeft: true,
|
|
139
|
+
tooltip: resultLines(result, analysis.classes).join("\n\n"),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (ts.isCatchClause(node)) {
|
|
145
|
+
const declaration = node.variableDeclaration;
|
|
146
|
+
const name = declaration?.name;
|
|
147
|
+
const seen = catches.get(node.getStart(sourceFile));
|
|
148
|
+
if (name !== undefined &&
|
|
149
|
+
ts.isIdentifier(name) &&
|
|
150
|
+
declaration?.type === undefined &&
|
|
151
|
+
seen?.handler.variable === name.text) {
|
|
152
|
+
const thrown = toThrown(seen.thrown);
|
|
153
|
+
if (thrown.classes.size > 0) {
|
|
154
|
+
hints.push({
|
|
155
|
+
offset: name.end,
|
|
156
|
+
kind: HINT_KINDS.catchVariable,
|
|
157
|
+
label: `: ${listed(thrown, analysis.classes, "unknown")}`,
|
|
158
|
+
tooltip: `The \`try\` can throw: ${thrownText(thrown, analysis.classes)}`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
ts.forEachChild(node, visit);
|
|
164
|
+
};
|
|
165
|
+
visit(sourceFile);
|
|
166
|
+
return hints;
|
|
167
|
+
};
|
|
168
|
+
//# sourceMappingURL=inlay.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Span, ThrowsSummary } from "./inference/summary.js";
|
|
2
|
+
/** The group the rule belongs to in the docs and in `stateward status`. */
|
|
3
|
+
export declare const GROUP = "callers";
|
|
4
|
+
/** A class marked `@mustHandle`: its id, its name, the author's sentence and the tag's span. */
|
|
5
|
+
type Tag = {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly how: string;
|
|
9
|
+
readonly at: Span;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Where `@throws` goes for a function, by the start of its name: the start of its statement (after
|
|
13
|
+
* any JSDoc), that line's indentation, and its last JSDoc block when it has one.
|
|
14
|
+
*/
|
|
15
|
+
type Anchor = {
|
|
16
|
+
readonly name: number;
|
|
17
|
+
readonly start: number;
|
|
18
|
+
readonly indent: string;
|
|
19
|
+
readonly doc: {
|
|
20
|
+
readonly start: number;
|
|
21
|
+
readonly end: number;
|
|
22
|
+
readonly text: string;
|
|
23
|
+
} | null;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A function whose body starts by testing a parameter with `instanceof` and does not rethrow it
|
|
27
|
+
* there: `function showError(error) { if (error instanceof Declined) return toast(…); … }`. A
|
|
28
|
+
* `catch` that hands its error to it names those classes.
|
|
29
|
+
*/
|
|
30
|
+
type ErrorHelper = {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly parameter: number;
|
|
33
|
+
readonly classes: readonly string[];
|
|
34
|
+
};
|
|
35
|
+
/** A call in a `catch` (by the start of its `catch` keyword) that passes the caught error on. */
|
|
36
|
+
type Pass = {
|
|
37
|
+
readonly at: number;
|
|
38
|
+
readonly callee: string;
|
|
39
|
+
readonly argument: number;
|
|
40
|
+
};
|
|
41
|
+
type MustHandleFacts = {
|
|
42
|
+
readonly tags: readonly Tag[];
|
|
43
|
+
/** `@mustHandle` on something other than a class declaration: not checked yet. */
|
|
44
|
+
readonly untracked: readonly Span[];
|
|
45
|
+
readonly anchors: readonly Anchor[];
|
|
46
|
+
readonly helpers: readonly ErrorHelper[];
|
|
47
|
+
readonly passes: readonly Pass[];
|
|
48
|
+
};
|
|
49
|
+
export type MustHandleSummary = {
|
|
50
|
+
readonly throws: ThrowsSummary;
|
|
51
|
+
readonly mustHandle: MustHandleFacts;
|
|
52
|
+
};
|
|
53
|
+
export declare const mustHandleError: import("@stateward/core").ProjectRule<MustHandleSummary, Readonly<Record<string, never>>, "callers">;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=must-handle-error.d.ts.map
|