@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,575 @@
|
|
|
1
|
+
import { createPropagationMemo, propagate } from "@stateward/core";
|
|
2
|
+
import { builtinParent, isBuiltin } from "./builtins.js";
|
|
3
|
+
import { ANONYMOUS, REASON } from "./summarize.js";
|
|
4
|
+
import { readSummary } from "./summary.js";
|
|
5
|
+
/** `zod 3.23.8`, or `zod` when its version is not known. */
|
|
6
|
+
export function packageLabel(facts) {
|
|
7
|
+
return facts.version === "" ? facts.name : `${facts.name} ${facts.version}`;
|
|
8
|
+
}
|
|
9
|
+
/** At most this many unknown sources are kept per set; `more` says there were others. */
|
|
10
|
+
const MAX_SOURCES = 50;
|
|
11
|
+
const EMPTY = { classes: new Map(), origins: new Map(), unknowns: [], more: false };
|
|
12
|
+
const NOTHING = { throws: EMPTY, rejects: EMPTY };
|
|
13
|
+
export function collected() {
|
|
14
|
+
return { classes: new Map(), unknowns: new Map(), more: false };
|
|
15
|
+
}
|
|
16
|
+
function sourceKey(source) {
|
|
17
|
+
return `${source.file}\u0000${String(source.start)}\u0000${String(source.end)}\u0000${source.reason}`;
|
|
18
|
+
}
|
|
19
|
+
function addClass(into, id, certain, via, from = null) {
|
|
20
|
+
const known = into.classes.get(id);
|
|
21
|
+
if (known === undefined) {
|
|
22
|
+
into.classes.set(id, { certain, via, from });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (certain && !known.certain)
|
|
26
|
+
known.certain = true;
|
|
27
|
+
// The least label: the same whatever order the sets were merged in.
|
|
28
|
+
if (from !== null && (known.from === null || from < known.from))
|
|
29
|
+
known.from = from;
|
|
30
|
+
}
|
|
31
|
+
function addSource(into, source) {
|
|
32
|
+
into.unknowns.set(sourceKey(source), source);
|
|
33
|
+
}
|
|
34
|
+
function merge(into, from, certain = true) {
|
|
35
|
+
for (const [id, info] of from.classes) {
|
|
36
|
+
addClass(into, id, certain && info.certain, info.via, info.from);
|
|
37
|
+
}
|
|
38
|
+
for (const source of from.unknowns.values())
|
|
39
|
+
addSource(into, source);
|
|
40
|
+
into.more ||= from.more;
|
|
41
|
+
}
|
|
42
|
+
function mergeThrown(into, from, via) {
|
|
43
|
+
for (const [id, certain] of from.classes) {
|
|
44
|
+
addClass(into, id, certain, via, from.origins.get(id) ?? null);
|
|
45
|
+
}
|
|
46
|
+
for (const source of from.unknowns)
|
|
47
|
+
addSource(into, source);
|
|
48
|
+
into.more ||= from.more;
|
|
49
|
+
}
|
|
50
|
+
function compareSources(a, b) {
|
|
51
|
+
if (a.file !== b.file)
|
|
52
|
+
return a.file < b.file ? -1 : 1;
|
|
53
|
+
return (a.start - b.start || a.end - b.end || (a.reason < b.reason ? -1 : a.reason > b.reason ? 1 : 0));
|
|
54
|
+
}
|
|
55
|
+
/** The set as a result: classes sorted by id, unknown sources sorted and capped. */
|
|
56
|
+
export function toThrown(from) {
|
|
57
|
+
const ids = [...from.classes.keys()].sort();
|
|
58
|
+
const sources = [...from.unknowns.values()].sort(compareSources);
|
|
59
|
+
const origins = new Map();
|
|
60
|
+
for (const id of ids) {
|
|
61
|
+
const origin = from.classes.get(id)?.from;
|
|
62
|
+
if (origin !== undefined && origin !== null)
|
|
63
|
+
origins.set(id, origin);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
classes: new Map(ids.map((id) => [id, from.classes.get(id)?.certain ?? false])),
|
|
67
|
+
origins,
|
|
68
|
+
unknowns: sources.slice(0, MAX_SOURCES),
|
|
69
|
+
more: from.more || sources.length > MAX_SOURCES,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function sameThrown(a, b) {
|
|
73
|
+
if (a.classes.size !== b.classes.size || a.unknowns.length !== b.unknowns.length)
|
|
74
|
+
return false;
|
|
75
|
+
if (a.more !== b.more)
|
|
76
|
+
return false;
|
|
77
|
+
for (const [id, certain] of a.classes)
|
|
78
|
+
if (b.classes.get(id) !== certain)
|
|
79
|
+
return false;
|
|
80
|
+
if (a.origins.size !== b.origins.size)
|
|
81
|
+
return false;
|
|
82
|
+
for (const [id, origin] of a.origins)
|
|
83
|
+
if (b.origins.get(id) !== origin)
|
|
84
|
+
return false;
|
|
85
|
+
return a.unknowns.every((source, i) => {
|
|
86
|
+
const other = b.unknowns[i];
|
|
87
|
+
return (other !== undefined && sourceKey(source) === sourceKey(other) && source.text === other.text);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export function sameResult(a, b) {
|
|
91
|
+
return sameThrown(a.throws, b.throws) && sameThrown(a.rejects, b.rejects);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Whether `instanceof <handler>` is true for an error of class `cls`: "yes" when `handler` is the
|
|
95
|
+
* class or one it extends, "no" when the chain is known and does not reach it, "undecided" when
|
|
96
|
+
* the chain cannot be followed.
|
|
97
|
+
*/
|
|
98
|
+
export function relation(handler, cls, classes) {
|
|
99
|
+
let current = cls;
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
while (current !== null) {
|
|
102
|
+
if (current === handler)
|
|
103
|
+
return "yes";
|
|
104
|
+
if (seen.has(current))
|
|
105
|
+
return "undecided";
|
|
106
|
+
seen.add(current);
|
|
107
|
+
if (isBuiltin(current)) {
|
|
108
|
+
current = builtinParent(current);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const fact = classes.get(current);
|
|
112
|
+
if (fact === undefined || fact.open)
|
|
113
|
+
return "undecided";
|
|
114
|
+
current = fact.parent;
|
|
115
|
+
}
|
|
116
|
+
return "no";
|
|
117
|
+
}
|
|
118
|
+
function locationOf(file, at) {
|
|
119
|
+
return { file, start: at[0], end: at[1] };
|
|
120
|
+
}
|
|
121
|
+
/** Adds what `effects` throw to `into`; `caught` holds, by catch, what reaches its variable. */
|
|
122
|
+
export function evaluate(effects, context, caught, into) {
|
|
123
|
+
const { file } = context;
|
|
124
|
+
for (const effect of effects) {
|
|
125
|
+
switch (effect.k) {
|
|
126
|
+
case "class":
|
|
127
|
+
addClass(into, effect.id, true, effect.text === null ? null : { ...locationOf(file, effect.at), text: effect.text });
|
|
128
|
+
break;
|
|
129
|
+
case "unknown":
|
|
130
|
+
addSource(into, {
|
|
131
|
+
...locationOf(file, effect.at),
|
|
132
|
+
text: effect.text,
|
|
133
|
+
reason: effect.reason,
|
|
134
|
+
});
|
|
135
|
+
break;
|
|
136
|
+
case "call": {
|
|
137
|
+
const result = context.resultOf(effect.id);
|
|
138
|
+
if (result === undefined) {
|
|
139
|
+
addSource(into, {
|
|
140
|
+
...locationOf(file, effect.at),
|
|
141
|
+
text: effect.text,
|
|
142
|
+
reason: REASON.noDeclaration,
|
|
143
|
+
});
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
const via = { ...locationOf(file, effect.at), text: `${effect.text}(…)` };
|
|
147
|
+
if (effect.part !== "rejects")
|
|
148
|
+
mergeThrown(into, result.throws, via);
|
|
149
|
+
if (effect.part !== "throws")
|
|
150
|
+
mergeThrown(into, result.rejects, via);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "package": {
|
|
154
|
+
const { facts } = effect;
|
|
155
|
+
const side = facts[effect.part];
|
|
156
|
+
const label = packageLabel(facts);
|
|
157
|
+
const via = { ...locationOf(file, effect.at), text: `${effect.text}(…)` };
|
|
158
|
+
for (const id of side.classes)
|
|
159
|
+
addClass(into, id, true, via, `${label}: ${facts.source}`);
|
|
160
|
+
for (const reason of side.unknown) {
|
|
161
|
+
addSource(into, {
|
|
162
|
+
...locationOf(file, effect.at),
|
|
163
|
+
text: effect.text,
|
|
164
|
+
reason: `${label}: ${reason}`,
|
|
165
|
+
package: facts.name,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case "maybe": {
|
|
171
|
+
// What the callback throws, evaluated apart: its own catches are checked as its own function.
|
|
172
|
+
const inner = collected();
|
|
173
|
+
const quiet = {
|
|
174
|
+
file,
|
|
175
|
+
classes: context.classes,
|
|
176
|
+
resultOf: (id) => context.resultOf(id),
|
|
177
|
+
};
|
|
178
|
+
evaluate(effect.body, quiet, caught, inner);
|
|
179
|
+
if (inner.classes.size > 0 || inner.unknowns.size > 0 || inner.more) {
|
|
180
|
+
addSource(into, {
|
|
181
|
+
...locationOf(file, effect.at),
|
|
182
|
+
text: effect.text,
|
|
183
|
+
reason: effect.reason,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case "rethrow": {
|
|
189
|
+
const reaching = caught.get(effect.of);
|
|
190
|
+
if (reaching === undefined) {
|
|
191
|
+
addSource(into, {
|
|
192
|
+
...locationOf(file, effect.at),
|
|
193
|
+
text: "throw",
|
|
194
|
+
reason: "a caught error thrown from elsewhere",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
merge(into, reaching, !effect.maybe);
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case "try": {
|
|
203
|
+
const body = collected();
|
|
204
|
+
evaluate(effect.body, context, caught, body);
|
|
205
|
+
if (effect.handler === null)
|
|
206
|
+
merge(into, body);
|
|
207
|
+
else
|
|
208
|
+
evaluateCatch(effect.handler, body, context, caught, into);
|
|
209
|
+
evaluate(effect.fin, context, caught, into);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
default: {
|
|
213
|
+
const unreachable = effect;
|
|
214
|
+
return unreachable;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function withUnknowns(from) {
|
|
220
|
+
const made = collected();
|
|
221
|
+
for (const [key, source] of from.unknowns)
|
|
222
|
+
made.unknowns.set(key, source);
|
|
223
|
+
made.more = from.more;
|
|
224
|
+
return made;
|
|
225
|
+
}
|
|
226
|
+
function evaluateCatch(handler, thrown, context, caught, into) {
|
|
227
|
+
const bound = (reaching) => new Map([...caught, [handler.of, reaching]]);
|
|
228
|
+
evaluate(handler.pre, context, bound(thrown), into);
|
|
229
|
+
const remaining = new Map([...thrown.classes].map(([id, info]) => [
|
|
230
|
+
id,
|
|
231
|
+
{ certain: info.certain, via: info.via, from: info.from },
|
|
232
|
+
]));
|
|
233
|
+
const afterwards = withUnknowns(thrown);
|
|
234
|
+
for (const branch of handler.branches) {
|
|
235
|
+
const reaching = withUnknowns(thrown);
|
|
236
|
+
for (const [id, info] of [...remaining]) {
|
|
237
|
+
const relations = branch.classes.map((handled) => relation(handled, id, context.classes));
|
|
238
|
+
const verdict = relations.includes("yes")
|
|
239
|
+
? "yes"
|
|
240
|
+
: relations.includes("undecided") || branch.classes.length === 0
|
|
241
|
+
? "undecided"
|
|
242
|
+
: "no";
|
|
243
|
+
if (verdict === "no")
|
|
244
|
+
continue;
|
|
245
|
+
if (verdict === "yes" && branch.exact) {
|
|
246
|
+
addClass(reaching, id, info.certain, info.via, info.from);
|
|
247
|
+
remaining.delete(id);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// It may go to this branch, or on to the next: certain of neither.
|
|
251
|
+
addClass(reaching, id, false, info.via, info.from);
|
|
252
|
+
info.certain = false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
evaluate(branch.body, context, bound(reaching), into);
|
|
256
|
+
if (!branch.exits)
|
|
257
|
+
merge(afterwards, reaching);
|
|
258
|
+
}
|
|
259
|
+
const rest = withUnknowns(thrown);
|
|
260
|
+
for (const [id, info] of remaining)
|
|
261
|
+
addClass(rest, id, info.certain, info.via, info.from);
|
|
262
|
+
if (handler.otherwise !== null) {
|
|
263
|
+
evaluate(handler.otherwise, context, bound(rest), into);
|
|
264
|
+
if (!handler.otherwiseExits)
|
|
265
|
+
merge(afterwards, rest);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
merge(afterwards, rest);
|
|
269
|
+
}
|
|
270
|
+
evaluate(handler.after, context, bound(afterwards), into);
|
|
271
|
+
context.onCatch?.({ file: context.file, handler, thrown, unhandled: remaining });
|
|
272
|
+
}
|
|
273
|
+
/** What the body of a function throws and rejects with, ignoring its `@throws`. */
|
|
274
|
+
export function inferred(entry, analysis, onCatch) {
|
|
275
|
+
const context = {
|
|
276
|
+
file: entry.file,
|
|
277
|
+
classes: analysis.classes,
|
|
278
|
+
resultOf: (id) => analysis.results.get(id),
|
|
279
|
+
...(onCatch === undefined ? {} : { onCatch }),
|
|
280
|
+
};
|
|
281
|
+
return inferWith(entry, context);
|
|
282
|
+
}
|
|
283
|
+
function inferWith(entry, context) {
|
|
284
|
+
const body = collected();
|
|
285
|
+
const returns = collected();
|
|
286
|
+
evaluate(entry.fact.body, context, new Map(), body);
|
|
287
|
+
evaluate(entry.fact.returns, context, new Map(), returns);
|
|
288
|
+
if (!entry.fact.async)
|
|
289
|
+
return { throws: body, rejects: returns };
|
|
290
|
+
merge(body, returns);
|
|
291
|
+
return { throws: collected(), rejects: body };
|
|
292
|
+
}
|
|
293
|
+
/** What a `@throws` declares, as the function's result. */
|
|
294
|
+
function declared(entry) {
|
|
295
|
+
const made = collected();
|
|
296
|
+
const doc = entry.fact.doc;
|
|
297
|
+
if (doc === null)
|
|
298
|
+
return made;
|
|
299
|
+
for (const item of doc.entries) {
|
|
300
|
+
if (item.id !== null) {
|
|
301
|
+
addClass(made, item.id, true, null);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
addSource(made, {
|
|
305
|
+
...locationOf(entry.file, item.at),
|
|
306
|
+
text: item.name,
|
|
307
|
+
reason: "`@throws` names no class stateward can find",
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (const at of doc.untyped) {
|
|
312
|
+
addSource(made, {
|
|
313
|
+
...locationOf(entry.file, at),
|
|
314
|
+
text: entry.fact.name,
|
|
315
|
+
reason: "`@throws` without a type",
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return made;
|
|
319
|
+
}
|
|
320
|
+
/** The ids a function's effects call, each once, in order. */
|
|
321
|
+
const callees = new WeakMap();
|
|
322
|
+
const bodyCallees = new WeakMap();
|
|
323
|
+
/**
|
|
324
|
+
* What the function's result is computed from; with `body`, what its body calls even when a
|
|
325
|
+
* `@throws` decides its result.
|
|
326
|
+
*/
|
|
327
|
+
function calleesOf(fact, body = false) {
|
|
328
|
+
const cache = body ? bodyCallees : callees;
|
|
329
|
+
const known = cache.get(fact);
|
|
330
|
+
if (known !== undefined)
|
|
331
|
+
return known;
|
|
332
|
+
const found = new Set();
|
|
333
|
+
const visit = (effects) => {
|
|
334
|
+
for (const effect of effects) {
|
|
335
|
+
if (effect.k === "call")
|
|
336
|
+
found.add(effect.id);
|
|
337
|
+
else if (effect.k === "maybe")
|
|
338
|
+
visit(effect.body);
|
|
339
|
+
else if (effect.k === "try") {
|
|
340
|
+
visit(effect.body);
|
|
341
|
+
visit(effect.fin);
|
|
342
|
+
const handler = effect.handler;
|
|
343
|
+
if (handler !== null) {
|
|
344
|
+
visit(handler.pre);
|
|
345
|
+
for (const branch of handler.branches)
|
|
346
|
+
visit(branch.body);
|
|
347
|
+
visit(handler.otherwise ?? []);
|
|
348
|
+
visit(handler.after);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
// A function with a `@throws` is what its tag says: it depends on nothing.
|
|
354
|
+
if (fact.doc === null || body) {
|
|
355
|
+
visit(fact.body);
|
|
356
|
+
visit(fact.returns);
|
|
357
|
+
}
|
|
358
|
+
const list = [...found];
|
|
359
|
+
cache.set(fact, list);
|
|
360
|
+
return list;
|
|
361
|
+
}
|
|
362
|
+
/** One input object per facts object and classes text, so the memo compares them by identity. */
|
|
363
|
+
const inputs = new WeakMap();
|
|
364
|
+
function inputOf(fact, classes) {
|
|
365
|
+
const known = inputs.get(fact);
|
|
366
|
+
if (known?.classes === classes)
|
|
367
|
+
return known;
|
|
368
|
+
const made = { fact, classes };
|
|
369
|
+
inputs.set(fact, made);
|
|
370
|
+
return made;
|
|
371
|
+
}
|
|
372
|
+
const texts = new WeakMap();
|
|
373
|
+
function textOf(value) {
|
|
374
|
+
let text = texts.get(value);
|
|
375
|
+
if (text === undefined) {
|
|
376
|
+
text = JSON.stringify(value);
|
|
377
|
+
texts.set(value, text);
|
|
378
|
+
}
|
|
379
|
+
return text;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* The last solve, for early cut-off: in the language server, a full pass after an edit computes
|
|
383
|
+
* only the functions whose facts changed, and their callers only while a result changes.
|
|
384
|
+
* Keeping it here is safe: a result is reused only for equal facts and the same callee results.
|
|
385
|
+
*/
|
|
386
|
+
const memo = createPropagationMemo();
|
|
387
|
+
/** One analysis per summaries map: the engine hands out the same map until they change. */
|
|
388
|
+
const analyses = new WeakMap();
|
|
389
|
+
/**
|
|
390
|
+
* The summaries of the last analysis, by their JSON text. Each rule of the pack (and the hover)
|
|
391
|
+
* receives its own parsed copy of the same facts; mapping equal texts to one object lets the
|
|
392
|
+
* second rule, and the next full pass, find the facts unchanged by identity.
|
|
393
|
+
*/
|
|
394
|
+
let canonical = new Map();
|
|
395
|
+
let last;
|
|
396
|
+
/** Solves what every function of the project throws. */
|
|
397
|
+
export function analyse(summaries) {
|
|
398
|
+
const known = analyses.get(summaries);
|
|
399
|
+
if (known !== undefined)
|
|
400
|
+
return known;
|
|
401
|
+
const files = [];
|
|
402
|
+
const read = [];
|
|
403
|
+
const next = new Map();
|
|
404
|
+
for (const [file, value] of summaries) {
|
|
405
|
+
const parsed = readSummary(value);
|
|
406
|
+
if (parsed === undefined || typeof value !== "object" || value === null)
|
|
407
|
+
continue;
|
|
408
|
+
const text = textOf(value);
|
|
409
|
+
const summary = canonical.get(text) ?? next.get(text) ?? parsed;
|
|
410
|
+
next.set(text, summary);
|
|
411
|
+
files.push(file);
|
|
412
|
+
read.push(summary);
|
|
413
|
+
}
|
|
414
|
+
canonical = next;
|
|
415
|
+
if (last?.files.length === files.length &&
|
|
416
|
+
last.files.every((file, i) => file === files[i] && last?.summaries[i] === read[i])) {
|
|
417
|
+
analyses.set(summaries, last.analysis);
|
|
418
|
+
return last.analysis;
|
|
419
|
+
}
|
|
420
|
+
const functions = new Map();
|
|
421
|
+
const named = [];
|
|
422
|
+
const classes = new Map();
|
|
423
|
+
const modules = [];
|
|
424
|
+
for (const [i, summary] of read.entries()) {
|
|
425
|
+
const file = files[i] ?? "";
|
|
426
|
+
for (const fact of summary.classes)
|
|
427
|
+
classes.set(fact.id, fact);
|
|
428
|
+
for (const fact of summary.functions) {
|
|
429
|
+
if (fact.id === ANONYMOUS) {
|
|
430
|
+
// Nothing calls it by name: its catches are checked, it has no result to propagate.
|
|
431
|
+
functions.set(`${file}#@${String(fact.at[0])}`, { file, fact });
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
functions.set(fact.id, { file, fact });
|
|
435
|
+
named.push(fact.id);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (summary.module.length > 0)
|
|
439
|
+
modules.push({ file, effects: summary.module });
|
|
440
|
+
}
|
|
441
|
+
const freshKey = JSON.stringify([...classes.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)));
|
|
442
|
+
// The last text when equal: every function compares it, and the same string compares at once.
|
|
443
|
+
const classesKey = last?.analysis.classesKey === freshKey ? last.analysis.classesKey : freshKey;
|
|
444
|
+
const { results, computed } = propagate({
|
|
445
|
+
nodes: named,
|
|
446
|
+
successors: (id) => {
|
|
447
|
+
const entry = functions.get(id);
|
|
448
|
+
return entry === undefined ? [] : calleesOf(entry.fact);
|
|
449
|
+
},
|
|
450
|
+
input: (id) => inputOf(functions.get(id)?.fact ?? emptyFact(id), classesKey),
|
|
451
|
+
sameInput: (a, b) => a.classes === b.classes && (a.fact === b.fact || textOf(a.fact) === textOf(b.fact)),
|
|
452
|
+
compute: (id, resultOf) => resultOfEntry(functions.get(id), classes, resultOf),
|
|
453
|
+
initial: NOTHING,
|
|
454
|
+
same: sameResult,
|
|
455
|
+
memo,
|
|
456
|
+
});
|
|
457
|
+
const analysis = { functions, classes, classesKey, results, modules, computed };
|
|
458
|
+
analyses.set(summaries, analysis);
|
|
459
|
+
last = { files, summaries: read, analysis };
|
|
460
|
+
return analysis;
|
|
461
|
+
}
|
|
462
|
+
/** A function's result: what its `@throws` declares, else what its body throws and rejects with. */
|
|
463
|
+
function resultOfEntry(entry, classes, resultOf) {
|
|
464
|
+
if (entry === undefined)
|
|
465
|
+
return NOTHING;
|
|
466
|
+
if (entry.fact.doc !== null) {
|
|
467
|
+
const set = toThrown(declared(entry));
|
|
468
|
+
// On a function that returns a promise, `@throws` says what the promise rejects with.
|
|
469
|
+
return entry.fact.promise ? { throws: EMPTY, rejects: set } : { throws: set, rejects: EMPTY };
|
|
470
|
+
}
|
|
471
|
+
const { throws, rejects } = inferWith(entry, { file: entry.file, classes, resultOf });
|
|
472
|
+
return { throws: toThrown(throws), rejects: toThrown(rejects) };
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Solves a closed set of functions on its own, without the project's memo: a package's functions
|
|
476
|
+
* read from its source. Calls outside the set are unknown.
|
|
477
|
+
*/
|
|
478
|
+
export function solveFunctions(functions, classes) {
|
|
479
|
+
return propagate({
|
|
480
|
+
nodes: [...functions.keys()],
|
|
481
|
+
successors: (id) => {
|
|
482
|
+
const entry = functions.get(id);
|
|
483
|
+
return entry === undefined ? [] : calleesOf(entry.fact);
|
|
484
|
+
},
|
|
485
|
+
input: (id) => functions.get(id),
|
|
486
|
+
sameInput: (a, b) => a === b,
|
|
487
|
+
compute: (id, resultOf) => resultOfEntry(functions.get(id), classes, resultOf),
|
|
488
|
+
initial: NOTHING,
|
|
489
|
+
same: sameResult,
|
|
490
|
+
}).results;
|
|
491
|
+
}
|
|
492
|
+
function emptyFact(id) {
|
|
493
|
+
return {
|
|
494
|
+
id,
|
|
495
|
+
name: "",
|
|
496
|
+
at: [0, 0],
|
|
497
|
+
async: false,
|
|
498
|
+
promise: false,
|
|
499
|
+
doc: null,
|
|
500
|
+
local: false,
|
|
501
|
+
body: [],
|
|
502
|
+
returns: [],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const evaluations = new WeakMap();
|
|
506
|
+
/** The function's catches and inferred result, evaluated with the analysis's callee results. */
|
|
507
|
+
export function evaluated(entry, analysis) {
|
|
508
|
+
const callees = calleesOf(entry.fact, true).map((id) => analysis.results.get(id));
|
|
509
|
+
const byFile = evaluations.get(entry.fact) ?? new Map();
|
|
510
|
+
evaluations.set(entry.fact, byFile);
|
|
511
|
+
const known = byFile.get(entry.file);
|
|
512
|
+
if (known?.classesKey === analysis.classesKey &&
|
|
513
|
+
known.callees.length === callees.length &&
|
|
514
|
+
known.callees.every((result, i) => result === callees[i])) {
|
|
515
|
+
return known;
|
|
516
|
+
}
|
|
517
|
+
const catches = [];
|
|
518
|
+
const body = inferred(entry, analysis, (one) => {
|
|
519
|
+
if (one.handler.branches.length > 0)
|
|
520
|
+
catches.push(one);
|
|
521
|
+
});
|
|
522
|
+
const made = { callees, classesKey: analysis.classesKey, catches, body };
|
|
523
|
+
byFile.set(entry.file, made);
|
|
524
|
+
return made;
|
|
525
|
+
}
|
|
526
|
+
/** Every catch of the project, with what reaches it, for `catch-misses-error`. */
|
|
527
|
+
export function everyCatch(analysis) {
|
|
528
|
+
const seen = [];
|
|
529
|
+
// A callback of a built-in (`items.forEach(…)`) is evaluated where it is called and on its own.
|
|
530
|
+
const where = new Set();
|
|
531
|
+
const add = (one) => {
|
|
532
|
+
const key = `${one.file}\u0000${String(one.handler.at[0])}`;
|
|
533
|
+
if (where.has(key))
|
|
534
|
+
return;
|
|
535
|
+
where.add(key);
|
|
536
|
+
seen.push(one);
|
|
537
|
+
};
|
|
538
|
+
for (const entry of analysis.functions.values()) {
|
|
539
|
+
if (hasDispatch(entry.fact.body) || hasDispatch(entry.fact.returns)) {
|
|
540
|
+
for (const one of evaluated(entry, analysis).catches)
|
|
541
|
+
add(one);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
for (const { file, effects } of analysis.modules) {
|
|
545
|
+
if (!hasDispatch(effects))
|
|
546
|
+
continue;
|
|
547
|
+
evaluate(effects, {
|
|
548
|
+
file,
|
|
549
|
+
classes: analysis.classes,
|
|
550
|
+
resultOf: (id) => analysis.results.get(id),
|
|
551
|
+
onCatch: (one) => {
|
|
552
|
+
if (one.handler.branches.length > 0)
|
|
553
|
+
add(one);
|
|
554
|
+
},
|
|
555
|
+
}, new Map(), collected());
|
|
556
|
+
}
|
|
557
|
+
return seen;
|
|
558
|
+
}
|
|
559
|
+
function hasDispatch(effects) {
|
|
560
|
+
return effects.some((effect) => {
|
|
561
|
+
if (effect.k !== "try")
|
|
562
|
+
return false;
|
|
563
|
+
const handler = effect.handler;
|
|
564
|
+
if (hasDispatch(effect.body) || hasDispatch(effect.fin))
|
|
565
|
+
return true;
|
|
566
|
+
if (handler === null)
|
|
567
|
+
return false;
|
|
568
|
+
return (handler.branches.length > 0 ||
|
|
569
|
+
hasDispatch(handler.pre) ||
|
|
570
|
+
handler.branches.some((branch) => hasDispatch(branch.body)) ||
|
|
571
|
+
hasDispatch(handler.otherwise ?? []) ||
|
|
572
|
+
hasDispatch(handler.after));
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
//# sourceMappingURL=solve.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import ts from "@stateward/core/typescript";
|
|
2
|
+
import type { FileContext, TypeQueries } from "@stateward/core";
|
|
3
|
+
import type { ClassFact, PackageFacts, ThrowsSummary } from "./summary.js";
|
|
4
|
+
/** Why something adds `unknown`, as the hover and the messages show it. */
|
|
5
|
+
export declare const REASON: {
|
|
6
|
+
readonly noDeclaration: "no declaration";
|
|
7
|
+
readonly noBody: "no body (interface or abstract method)";
|
|
8
|
+
readonly callback: "callback of unknown origin";
|
|
9
|
+
readonly dynamic: "dynamic call";
|
|
10
|
+
readonly thrownValue: "a thrown value of unknown class";
|
|
11
|
+
readonly madeError: "an error made by a call";
|
|
12
|
+
readonly promise: "a promise stateward cannot follow";
|
|
13
|
+
readonly heldPromise: "promise held in a variable";
|
|
14
|
+
};
|
|
15
|
+
/** Why a callback handed to a call of the platform that is not in the tables adds `unknown`. */
|
|
16
|
+
export declare function mayRunReason(callee: string): string;
|
|
17
|
+
/** The id of a function without a name; the analysis names it by its file and offset. */
|
|
18
|
+
export declare const ANONYMOUS = "";
|
|
19
|
+
/**
|
|
20
|
+
* The package a declaration outside the project belongs to: by its node_modules folder, or, for a
|
|
21
|
+
* package linked from elsewhere (a workspace package, whose real path has no node_modules), by
|
|
22
|
+
* the name in its package.json.
|
|
23
|
+
*/
|
|
24
|
+
export declare function packageNameOf(types: TypeQueries, file: string): string;
|
|
25
|
+
/** A package's declaration that a call reaches, for its facts (packages.ts). */
|
|
26
|
+
export type PackageCall = {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly node: ts.Node;
|
|
30
|
+
readonly file: string;
|
|
31
|
+
};
|
|
32
|
+
/** Finds what a call into a package throws; its classes, when it read some. */
|
|
33
|
+
export type PackageResolver = (call: PackageCall) => {
|
|
34
|
+
readonly facts: PackageFacts;
|
|
35
|
+
readonly classes: readonly ClassFact[];
|
|
36
|
+
};
|
|
37
|
+
/** `package:zod#ZodError`: a package's class, by its package and name, the same from its types and its source. */
|
|
38
|
+
export declare function packageClassId(name: string, className: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* The id a class named in code has in the summaries (`builtin:TypeError`, `package:zod#ZodError`,
|
|
41
|
+
* or the qualified id of a class of the project), without its facts; undefined when the name
|
|
42
|
+
* resolves to nothing.
|
|
43
|
+
*/
|
|
44
|
+
export declare function classIdOf(types: TypeQueries, name: ts.Node): string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The throws facts of the file (see summary.ts). With `packages`, a call into a package carries
|
|
47
|
+
* its facts; without (a package's own source), it is unknown.
|
|
48
|
+
*/
|
|
49
|
+
export declare function summarizeThrows(ctx: FileContext, packages?: PackageResolver): ThrowsSummary;
|
|
50
|
+
//# sourceMappingURL=summarize.d.ts.map
|