@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.
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,723 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, readdirSync, statSync } from "node:fs";
3
+ import path from "node:path";
4
+ import ts from "@stateward/core/typescript";
5
+ import { findPackageRoot, openPackageSource, packageCodeId, readPackageCache, reparsedWithJsDoc, writePackageCache, } from "@stateward/core";
6
+ import { BUILTIN_ERRORS, builtinId, isBuiltin } from "./builtins.js";
7
+ import { solveFunctions } from "./solve.js";
8
+ import { REASON, packageClassId, summarizeThrows } from "./summarize.js";
9
+ import { readClass } from "./summary.js";
10
+ /**
11
+ * Library facts for the Errors pack: what a call into a
12
+ * package throws, from the first source that answers:
13
+ *
14
+ * 1. the project's config (`facts`);
15
+ * 2. curated fact files shipped with stateward (`curatedFacts` answers nothing yet);
16
+ * 3. `@throws {X}` on the called declaration in the package's `.d.ts`;
17
+ * 4. inferred from the package's JavaScript, with the summarizer of the project's own code, from
18
+ * the called function to what it calls in the package (bounded), cached globally;
19
+ * 5. unknown, with one of the reasons below.
20
+ */
21
+ /** Why a package's function is not (fully) known: fixed strings, shown as they are. */
22
+ export const UNKNOWN_REASONS = [
23
+ // The package as a whole cannot be read.
24
+ "no readable source",
25
+ "not found in its source",
26
+ "native addon",
27
+ "minified",
28
+ "eval",
29
+ "dynamic require",
30
+ "too large to follow",
31
+ // Read, but part of what the function does cannot be followed.
32
+ "untyped @throws",
33
+ "calls a callback",
34
+ "calls code it cannot see",
35
+ "dynamic call",
36
+ "throws a value of unknown class",
37
+ "awaits a promise it cannot follow",
38
+ ];
39
+ /**
40
+ * At most this many functions are read per package function asked for.
41
+ * The bounds count, never time: a verdict by time would differ between machines.
42
+ */
43
+ export const MAX_FUNCTIONS = 200;
44
+ /** A package whose entry reaches more files than this is not parsed further. */
45
+ const MAX_FILES = 500;
46
+ /** Folder entries looked at for a native addon (`.node`, `binding.gyp`). */
47
+ const MAX_SCANNED = 5000;
48
+ /** Average line length above which a file is minified. */
49
+ const MINIFIED_LINE = 500;
50
+ const NOTHING = { classes: [], unknown: [] };
51
+ function isRecord(value) {
52
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53
+ }
54
+ function sha1(text) {
55
+ return createHash("sha1").update(text).digest("hex");
56
+ }
57
+ /** The folder of the installed package a file in node_modules belongs to. */
58
+ function installedRoot(file) {
59
+ const marker = "/node_modules/";
60
+ const at = file.lastIndexOf(marker);
61
+ if (at < 0)
62
+ return undefined;
63
+ const parts = file.slice(at + marker.length).split("/");
64
+ const [first = "", second = ""] = parts;
65
+ const name = first.startsWith("@") ? `${first}/${second}` : first;
66
+ return { root: `${file.slice(0, at + marker.length)}${name}`, name };
67
+ }
68
+ /**
69
+ * What this process read of each package's package.json, with the file's size and time when it
70
+ * was read: a package upgraded or reinstalled while the process runs (a language server) is read
71
+ * again, and its new content hash keys every cache below, so no old fact is served for it.
72
+ */
73
+ const infos = new Map();
74
+ let codeId;
75
+ /** The size and time of a file, to tell a changed one; "" when it cannot be read. */
76
+ function stampOf(file) {
77
+ try {
78
+ const stats = statSync(file);
79
+ return `${String(stats.size)}:${String(stats.mtimeMs)}`;
80
+ }
81
+ catch {
82
+ return "";
83
+ }
84
+ }
85
+ /**
86
+ * The package at `root`, from its package.json. `checked` holds what this resolver already read,
87
+ * so a package.json is looked at (one stat) once per file summarized, not once per call.
88
+ */
89
+ function readInfo(name, root, checked) {
90
+ if (checked.has(root))
91
+ return checked.get(root);
92
+ const manifestFile = path.join(root, "package.json");
93
+ const stamp = stampOf(manifestFile);
94
+ const known = infos.get(root);
95
+ if (known?.stamp === stamp) {
96
+ checked.set(root, known.info);
97
+ return known.info;
98
+ }
99
+ let info;
100
+ try {
101
+ const text = readFileSync(manifestFile, "utf8");
102
+ const parsed = JSON.parse(text);
103
+ const manifest = isRecord(parsed) ? parsed : {};
104
+ const version = typeof manifest.version === "string" ? manifest.version : "";
105
+ codeId ??= packageCodeId(import.meta.url);
106
+ info = {
107
+ name,
108
+ root,
109
+ version,
110
+ manifest,
111
+ key: { name, version, integrity: sha1(text), implementation: codeId },
112
+ };
113
+ }
114
+ catch {
115
+ info = undefined;
116
+ }
117
+ infos.set(root, { stamp, info });
118
+ checked.set(root, info);
119
+ return info;
120
+ }
121
+ /** The per-process caches' key of a package: its folder and its package.json's content. */
122
+ function packageKey(info) {
123
+ return `${info.root}\u0000${info.key.integrity}`;
124
+ }
125
+ /** The package whose JavaScript a call reaches: next to its declaration, or found by name. */
126
+ function packageOf(call, from, checked) {
127
+ const installed = installedRoot(call.file.replaceAll("\\", "/"));
128
+ if (installed?.name === call.name)
129
+ return readInfo(call.name, installed.root, checked);
130
+ // `@types/x`, a linked package, or `declare module "x"` in the project: the package by its
131
+ // name, as Node finds it from the calling file (a package manager's store keeps `@types/x`
132
+ // away from `x`), else from the declaration.
133
+ const root = findPackageRoot(path.dirname(from), call.name) ??
134
+ findPackageRoot(path.dirname(installed?.root ?? call.file), call.name);
135
+ return root === undefined ? undefined : readInfo(call.name, root, checked);
136
+ }
137
+ const JAVASCRIPT_EXTENSIONS = [".js", ".mjs", ".cjs"];
138
+ function existingFile(file) {
139
+ const candidates = [
140
+ file,
141
+ ...JAVASCRIPT_EXTENSIONS.map((extension) => `${file}${extension}`),
142
+ ...JAVASCRIPT_EXTENSIONS.map((extension) => path.join(file, `index${extension}`)),
143
+ ];
144
+ return candidates.find((candidate) => {
145
+ try {
146
+ return readdirSync(path.dirname(candidate)).includes(path.basename(candidate));
147
+ }
148
+ catch {
149
+ return false;
150
+ }
151
+ });
152
+ }
153
+ /** The first file an `exports` value names for `import`, `default`, `require` or `node`. */
154
+ function exportTarget(value) {
155
+ if (typeof value === "string")
156
+ return value;
157
+ if (!isRecord(value))
158
+ return undefined;
159
+ for (const key of [".", "import", "default", "require", "node"]) {
160
+ const found = exportTarget(value[key]);
161
+ if (found !== undefined)
162
+ return found;
163
+ }
164
+ return undefined;
165
+ }
166
+ /** The JavaScript file a declaration file describes: its sibling, else the package's entry. */
167
+ function entryOf(declarationFile, info) {
168
+ const file = declarationFile.replaceAll("\\", "/");
169
+ if (file.startsWith(`${info.root}/`)) {
170
+ const stem = file.replace(/\.d\.[mc]?ts$/, "");
171
+ const sibling = JAVASCRIPT_EXTENSIONS.map((extension) => `${stem}${extension}`).find((candidate) => existingFile(candidate) === candidate);
172
+ if (sibling !== undefined)
173
+ return sibling;
174
+ }
175
+ const { manifest } = info;
176
+ const named = exportTarget(manifest.exports) ??
177
+ (typeof manifest.module === "string" ? manifest.module : undefined) ??
178
+ (typeof manifest.main === "string" ? manifest.main : undefined) ??
179
+ "index.js";
180
+ return existingFile(path.join(info.root, named));
181
+ }
182
+ // ---- the called declaration ---------------------------------------------------------------------
183
+ function declarationName(node) {
184
+ if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) {
185
+ if (node.name !== undefined)
186
+ return node.name.text;
187
+ return node.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) === true
188
+ ? "default"
189
+ : undefined;
190
+ }
191
+ if (ts.isExportAssignment(node))
192
+ return "default";
193
+ if (ts.isModuleDeclaration(node))
194
+ return ts.isIdentifier(node.name) ? node.name.text : undefined;
195
+ if (ts.isMethodDeclaration(node) ||
196
+ ts.isMethodSignature(node) ||
197
+ ts.isPropertyDeclaration(node) ||
198
+ ts.isPropertySignature(node) ||
199
+ ts.isVariableDeclaration(node) ||
200
+ ts.isInterfaceDeclaration(node)) {
201
+ return ts.isIdentifier(node.name) ? node.name.text : undefined;
202
+ }
203
+ return undefined;
204
+ }
205
+ /** The declaration's path in the package's types: `["Client", "get"]`. */
206
+ export function pathOf(node) {
207
+ const names = [];
208
+ for (let at = node; !ts.isSourceFile(at); at = at.parent) {
209
+ const name = declarationName(at);
210
+ if (name !== undefined)
211
+ names.unshift(name);
212
+ }
213
+ return names;
214
+ }
215
+ /** Whether the declaration says it returns a promise: its facts are what the promise rejects with. */
216
+ function returnsPromise(node) {
217
+ let type;
218
+ if (ts.isFunctionDeclaration(node) ||
219
+ ts.isMethodDeclaration(node) ||
220
+ ts.isMethodSignature(node)) {
221
+ type = node.type;
222
+ }
223
+ else if ((ts.isVariableDeclaration(node) ||
224
+ ts.isPropertySignature(node) ||
225
+ ts.isPropertyDeclaration(node)) &&
226
+ node.type !== undefined &&
227
+ ts.isFunctionTypeNode(node.type)) {
228
+ type = node.type.type;
229
+ }
230
+ return (type !== undefined &&
231
+ ts.isTypeReferenceNode(type) &&
232
+ ts.isIdentifier(type.typeName) &&
233
+ (type.typeName.text === "Promise" || type.typeName.text === "PromiseLike"));
234
+ }
235
+ /** A class a fact names: a built-in error, or the package's own class. */
236
+ function classIdOf(packageName, name) {
237
+ return BUILTIN_ERRORS.has(name) ? builtinId(name) : packageClassId(packageName, name);
238
+ }
239
+ function side(classes, unknown = []) {
240
+ return { classes: [...new Set(classes)].sort(), unknown: [...new Set(unknown)].sort() };
241
+ }
242
+ function onSide(set, promise) {
243
+ return promise ? { throws: NOTHING, rejects: set } : { throws: set, rejects: NOTHING };
244
+ }
245
+ // ---- 1. config ----------------------------------------------------------------------------------
246
+ function configured(entry, packageName, promise) {
247
+ const named = (list) => (list ?? []).map((name) => classIdOf(packageName, name));
248
+ const thrown = side(named(entry.throws));
249
+ const rejected = side(named(entry.rejects));
250
+ // A promise's function "throws" what its promise rejects with, as a developer says it.
251
+ return promise
252
+ ? { throws: NOTHING, rejects: side([...thrown.classes, ...rejected.classes]) }
253
+ : { throws: thrown, rejects: rejected };
254
+ }
255
+ // ---- 2. curated ---------------------------------------------------------------------------
256
+ /**
257
+ * Curated fact files shipped with stateward: none ship yet, so nothing answers between the config
258
+ * and the declarations. Reading `facts/<package>@<major>.json` here would add the `curated`
259
+ * source, and the resolver would ask it after the config.
260
+ */
261
+ export function curatedFacts() {
262
+ return undefined;
263
+ }
264
+ // ---- 3. declared --------------------------------------------------------------------------------
265
+ function sameNode(root, target) {
266
+ if (root.pos === target.pos && root.end === target.end && root.kind === target.kind)
267
+ return root;
268
+ return ts.forEachChild(root, (child) => child.pos <= target.pos && target.end <= child.end ? sameNode(child, target) : undefined);
269
+ }
270
+ /** `@throws {X}` on the called declaration in the package's types, or undefined without a tag. */
271
+ function declared(call) {
272
+ const reparsed = reparsedWithJsDoc(call.node.getSourceFile());
273
+ const node = sameNode(reparsed, call.node);
274
+ if (node === undefined)
275
+ return undefined;
276
+ const tags = ts.getJSDocTags(node).filter(ts.isJSDocThrowsTag);
277
+ if (tags.length === 0)
278
+ return undefined;
279
+ const classes = [];
280
+ const unknown = [];
281
+ const add = (type) => {
282
+ if (ts.isParenthesizedTypeNode(type))
283
+ add(type.type);
284
+ else if (ts.isUnionTypeNode(type))
285
+ for (const member of type.types)
286
+ add(member);
287
+ else if (type.kind === ts.SyntaxKind.NeverKeyword)
288
+ return;
289
+ else if (ts.isTypeReferenceNode(type)) {
290
+ const name = ts.isIdentifier(type.typeName) ? type.typeName : type.typeName.right;
291
+ classes.push(classIdOf(call.name, name.text));
292
+ }
293
+ else
294
+ unknown.push("untyped @throws");
295
+ };
296
+ for (const tag of tags) {
297
+ const type = tag.typeExpression?.type;
298
+ if (type === undefined)
299
+ unknown.push("untyped @throws");
300
+ else
301
+ add(type);
302
+ }
303
+ return onSide(side(classes, unknown), returnsPromise(node));
304
+ }
305
+ // ---- 4. inferred --------------------------------------------------------------------------------
306
+ /** What makes a package's code not open to read, file by file. */
307
+ function unreadable(sourceFile) {
308
+ const text = sourceFile.text;
309
+ const lines = text.split("\n").length;
310
+ if (text.length / lines > MINIFIED_LINE)
311
+ return "minified";
312
+ let names = 0;
313
+ let short = 0;
314
+ let found;
315
+ const isLiteral = (node) => node !== undefined && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node));
316
+ const visit = (node) => {
317
+ if (found !== undefined)
318
+ return;
319
+ if ((ts.isVariableDeclaration(node) || ts.isParameter(node) || ts.isFunctionDeclaration(node)) &&
320
+ node.name !== undefined &&
321
+ ts.isIdentifier(node.name)) {
322
+ names++;
323
+ if (node.name.text.length <= 2)
324
+ short++;
325
+ }
326
+ if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
327
+ const callee = node.expression;
328
+ if (ts.isIdentifier(callee) && (callee.text === "eval" || callee.text === "Function")) {
329
+ found = "eval";
330
+ return;
331
+ }
332
+ const dynamicRequire = ts.isIdentifier(callee) && callee.text === "require";
333
+ const dynamicImport = callee.kind === ts.SyntaxKind.ImportKeyword;
334
+ if ((dynamicRequire || dynamicImport) && !isLiteral(node.arguments?.[0])) {
335
+ found = "dynamic require";
336
+ return;
337
+ }
338
+ }
339
+ ts.forEachChild(node, visit);
340
+ };
341
+ visit(sourceFile);
342
+ // Mostly one- and two-letter names: a minifier renamed them.
343
+ if (names >= 20 && short * 2 > names)
344
+ return "minified";
345
+ return found;
346
+ }
347
+ const natives = new Map();
348
+ /** Every entry under `dir`, relative; none when it cannot be read. */
349
+ function listed(dir) {
350
+ try {
351
+ return readdirSync(dir, { recursive: true, encoding: "utf8" });
352
+ }
353
+ catch {
354
+ return [];
355
+ }
356
+ }
357
+ /** Whether the package ships a native addon (a `.node` file or a `binding.gyp`). */
358
+ function hasNativeAddon(info) {
359
+ const known = natives.get(packageKey(info));
360
+ if (known !== undefined)
361
+ return known;
362
+ const entries = listed(info.root).slice(0, MAX_SCANNED);
363
+ const found = entries.some((entry) => !entry.split(path.sep).includes("node_modules") &&
364
+ (entry.endsWith(".node") || path.basename(entry) === "binding.gyp"));
365
+ natives.set(packageKey(info), found);
366
+ return found;
367
+ }
368
+ /**
369
+ * Package sources by package (root and content) and entry, most recently used last. Each holds a
370
+ * program with its type checker, so only the last few stay open: what was inferred from them is
371
+ * kept in `memo`, and a package needed again is opened again (it rarely is: a project's calls
372
+ * into one package are mostly answered from `memo`).
373
+ */
374
+ const opened = new Map();
375
+ /** How many package sources stay open at once. */
376
+ const OPEN_PACKAGES = 4;
377
+ function open(info, entry) {
378
+ const key = `${packageKey(info)}\u0000${entry}`;
379
+ const known = opened.get(key);
380
+ if (known !== undefined) {
381
+ opened.delete(key);
382
+ opened.set(key, known);
383
+ return known;
384
+ }
385
+ const source = openPackageSource(info.root, entry);
386
+ let verdict;
387
+ if (hasNativeAddon(info))
388
+ verdict = "native addon";
389
+ else if (source.files.length > MAX_FILES)
390
+ verdict = "too large to follow";
391
+ else {
392
+ for (const file of source.files) {
393
+ const sourceFile = source.sourceFile(file);
394
+ verdict = sourceFile === undefined ? undefined : unreadable(sourceFile);
395
+ if (verdict !== undefined)
396
+ break;
397
+ }
398
+ }
399
+ const made = { source, verdict, summaries: new Map() };
400
+ opened.set(key, made);
401
+ for (const oldest of opened.keys()) {
402
+ if (opened.size <= OPEN_PACKAGES)
403
+ break;
404
+ opened.delete(oldest);
405
+ }
406
+ return made;
407
+ }
408
+ function summaryOf(state, file) {
409
+ const known = state.summaries.get(file);
410
+ if (known !== undefined)
411
+ return known;
412
+ const sourceFile = state.source.sourceFile(file);
413
+ if (sourceFile === undefined)
414
+ return undefined;
415
+ const ctx = {
416
+ file,
417
+ options: {},
418
+ sourceFile,
419
+ types: state.source.types,
420
+ addons: [],
421
+ facts: {},
422
+ report() {
423
+ // A package's source is read for facts, never reported on.
424
+ },
425
+ unknown() {
426
+ // As above.
427
+ },
428
+ };
429
+ const summary = summarizeThrows(ctx);
430
+ state.summaries.set(file, summary);
431
+ return summary;
432
+ }
433
+ function callIds(effects, into) {
434
+ for (const effect of effects) {
435
+ if (effect.k === "call")
436
+ into.push(effect.id);
437
+ else if (effect.k === "try") {
438
+ callIds(effect.body, into);
439
+ callIds(effect.fin, into);
440
+ const handler = effect.handler;
441
+ if (handler !== null) {
442
+ callIds(handler.pre, into);
443
+ for (const branch of handler.branches)
444
+ callIds(branch.body, into);
445
+ callIds(handler.otherwise ?? [], into);
446
+ callIds(handler.after, into);
447
+ }
448
+ }
449
+ }
450
+ }
451
+ /** Why part of a function read from source is unknown, as one of the fixed reasons. */
452
+ function reasonOf(source) {
453
+ switch (source.reason) {
454
+ case REASON.callback:
455
+ return "calls a callback";
456
+ case REASON.dynamic:
457
+ return "dynamic call";
458
+ case REASON.promise:
459
+ return "awaits a promise it cannot follow";
460
+ case REASON.thrownValue:
461
+ case REASON.madeError:
462
+ return "throws a value of unknown class";
463
+ default:
464
+ return source.reason.includes("@throws") ? "untyped @throws" : "calls code it cannot see";
465
+ }
466
+ }
467
+ /** Reads the function at `names` in the package's source, within the bounds. */
468
+ function inferFrom(info, entry, names) {
469
+ const state = open(info, entry);
470
+ if (state.verdict !== undefined) {
471
+ return { kind: "unknown", reason: state.verdict };
472
+ }
473
+ const notFound = { kind: "unknown", reason: "not found in its source" };
474
+ const target = state.source.exported(entry, names);
475
+ if (target === undefined)
476
+ return notFound;
477
+ const tooLarge = { kind: "unknown", reason: "too large to follow" };
478
+ const functions = new Map();
479
+ const classes = new Map();
480
+ const files = new Set(state.source.files);
481
+ const queue = [target.id];
482
+ for (let id = queue.shift(); id !== undefined; id = queue.shift()) {
483
+ if (functions.has(id))
484
+ continue;
485
+ const file = path.join(info.root, id.slice(0, id.indexOf("#")));
486
+ const summary = files.has(file.replaceAll("\\", "/")) ? summaryOf(state, file) : undefined;
487
+ const fact = summary?.functions.find((one) => one.id === id);
488
+ if (summary === undefined || fact === undefined)
489
+ continue;
490
+ functions.set(id, { file, fact });
491
+ for (const one of summary.classes)
492
+ classes.set(one.id, one);
493
+ if (functions.size > MAX_FUNCTIONS)
494
+ return tooLarge;
495
+ const called = [];
496
+ callIds(fact.body, called);
497
+ callIds(fact.returns, called);
498
+ queue.push(...called);
499
+ }
500
+ const result = solveFunctions(functions, classes).get(target.id);
501
+ if (result === undefined)
502
+ return notFound;
503
+ const mapped = (id) => isBuiltin(id)
504
+ ? id
505
+ : packageClassId(info.name, classes.get(id)?.name ?? id.slice(id.indexOf("#") + 1));
506
+ const sideOf = (thrown) => side([...thrown.classes.keys()].map(mapped), [
507
+ ...thrown.unknowns.map(reasonOf),
508
+ ...(thrown.more ? ["calls code it cannot see"] : []),
509
+ ]);
510
+ const facts = [...classes.values()]
511
+ .map((one) => ({
512
+ id: mapped(one.id),
513
+ name: one.name,
514
+ parent: one.parent === null ? null : mapped(one.parent),
515
+ open: one.open,
516
+ }))
517
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
518
+ return {
519
+ kind: "known",
520
+ sides: { throws: sideOf(result.throws), rejects: sideOf(result.rejects) },
521
+ classes: facts,
522
+ };
523
+ }
524
+ // ---- the global cache ---------------------------------------------------------------------------
525
+ let inferences = 0;
526
+ let cacheHits = 0;
527
+ /** What this process computed and read back, for tests: inference runs and global-cache hits. */
528
+ export function packageFactsStats() {
529
+ return { inferences, cacheHits };
530
+ }
531
+ /** Forgets what this process knows about packages (tests: the next run reads the global cache). */
532
+ export function forgetPackageFacts() {
533
+ infos.clear();
534
+ opened.clear();
535
+ natives.clear();
536
+ memo.clear();
537
+ entries.clear();
538
+ verified.clear();
539
+ }
540
+ /** Inferred facts by package (root and content), entry and function, for this process. */
541
+ const memo = new Map();
542
+ /** The global cache's entry of each package (root and content), read once per process. */
543
+ const entries = new Map();
544
+ /** Whether the files behind a cached digest still hold what they held, by root and digest. */
545
+ const verified = new Map();
546
+ /**
547
+ * The files inference read, by path relative to the package, with a hash of each; and one digest
548
+ * of them all. A package.json says nothing of a linked workspace package's source, or of a
549
+ * patched file in node_modules, so each inferred entry carries the digest of the files it was
550
+ * inferred from, and a file that no longer matches makes the entry a miss.
551
+ */
552
+ function sourcesOf(info, files) {
553
+ const hashes = {};
554
+ for (const file of files) {
555
+ const relative = path.relative(info.root, file).replaceAll("\\", "/");
556
+ hashes[relative] = fileHash(file) ?? "";
557
+ }
558
+ const lines = Object.keys(hashes)
559
+ .sort()
560
+ .map((relative) => `${relative} ${hashes[relative] ?? ""}`);
561
+ return { digest: sha1(lines.join("\n")), hashes };
562
+ }
563
+ function fileHash(file) {
564
+ try {
565
+ return createHash("sha1").update(readFileSync(file)).digest("hex");
566
+ }
567
+ catch {
568
+ return undefined;
569
+ }
570
+ }
571
+ /** Whether every file a cached digest lists still has the hash it had. */
572
+ function stillTheSame(info, digest, cached) {
573
+ const key = `${packageKey(info)}\u0000${digest}`;
574
+ const known = verified.get(key);
575
+ if (known !== undefined)
576
+ return known;
577
+ const hashes = fieldsOf(cached.get(`sources ${digest}`));
578
+ const same = hashes !== undefined &&
579
+ Object.entries(hashes).every(([relative, hash]) => typeof hash === "string" && fileHash(path.join(info.root, relative)) === hash);
580
+ verified.set(key, same);
581
+ return same;
582
+ }
583
+ /** An entry of the global cache; `sources`: the digest of the files it was inferred from. */
584
+ function toJson(inferred, sources) {
585
+ return inferred.kind === "unknown"
586
+ ? { unknown: inferred.reason, sources }
587
+ : {
588
+ sources,
589
+ throws: { classes: inferred.sides.throws.classes, unknown: inferred.sides.throws.unknown },
590
+ rejects: {
591
+ classes: inferred.sides.rejects.classes,
592
+ unknown: inferred.sides.rejects.unknown,
593
+ },
594
+ classes: inferred.classes.map((one) => ({ ...one })),
595
+ };
596
+ }
597
+ function isReason(value) {
598
+ return UNKNOWN_REASONS.some((reason) => reason === value);
599
+ }
600
+ function isList(value) {
601
+ return Array.isArray(value);
602
+ }
603
+ function fieldsOf(value) {
604
+ if (value === undefined || typeof value !== "object" || value === null || isList(value)) {
605
+ return undefined;
606
+ }
607
+ return value;
608
+ }
609
+ function listOf(value) {
610
+ return Array.isArray(value) ? value : undefined;
611
+ }
612
+ function readSide(value) {
613
+ const fields = fieldsOf(value);
614
+ const classes = listOf(fields?.classes);
615
+ const unknown = listOf(fields?.unknown);
616
+ if (classes === undefined || unknown === undefined)
617
+ return undefined;
618
+ const ids = classes.filter((one) => typeof one === "string");
619
+ const reasons = unknown.filter(isReason);
620
+ return ids.length === classes.length && reasons.length === unknown.length
621
+ ? { classes: ids, unknown: reasons }
622
+ : undefined;
623
+ }
624
+ /** A cached entry as `toJson` wrote it; undefined (a miss) for anything else. */
625
+ function fromJson(value) {
626
+ const fields = fieldsOf(value);
627
+ if (fields === undefined)
628
+ return undefined;
629
+ if (isReason(fields.unknown))
630
+ return { kind: "unknown", reason: fields.unknown };
631
+ const throws = readSide(fields.throws);
632
+ const rejects = readSide(fields.rejects);
633
+ const list = listOf(fields.classes);
634
+ if (throws === undefined || rejects === undefined || list === undefined)
635
+ return undefined;
636
+ const classes = [];
637
+ for (const item of list) {
638
+ const one = readClass(item);
639
+ if (one === undefined)
640
+ return undefined;
641
+ classes.push(one);
642
+ }
643
+ return { kind: "known", sides: { throws, rejects }, classes };
644
+ }
645
+ function inferred(info, entry, names) {
646
+ const relative = path.relative(info.root, entry).replaceAll("\\", "/");
647
+ const cacheKey = `throws ${relative} ${names.join(".")}`;
648
+ const key = `${packageKey(info)}\u0000${cacheKey}`;
649
+ const known = memo.get(key);
650
+ if (known !== undefined)
651
+ return known;
652
+ let cached = entries.get(packageKey(info));
653
+ if (cached === undefined) {
654
+ cached = new Map(readPackageCache(info.key));
655
+ entries.set(packageKey(info), cached);
656
+ }
657
+ const stored = cached.get(cacheKey);
658
+ const digest = fieldsOf(stored)?.sources;
659
+ const hit = typeof digest === "string" && stillTheSame(info, digest, cached) ? fromJson(stored) : undefined;
660
+ if (hit !== undefined) {
661
+ cacheHits++;
662
+ memo.set(key, hit);
663
+ return hit;
664
+ }
665
+ inferences++;
666
+ const found = inferFrom(info, entry, names);
667
+ memo.set(key, found);
668
+ const { files } = open(info, entry).source;
669
+ const sources = sourcesOf(info, files.length > MAX_FILES ? [] : files);
670
+ const value = toJson(found, sources.digest);
671
+ const written = new Map([
672
+ [cacheKey, value],
673
+ [`sources ${sources.digest}`, sources.hashes],
674
+ ]);
675
+ for (const [name, one] of written)
676
+ cached.set(name, one);
677
+ verified.set(`${packageKey(info)}\u0000${sources.digest}`, true);
678
+ // A failure to write costs the next run time, not facts.
679
+ writePackageCache(info.key, written);
680
+ return found;
681
+ }
682
+ // ---- the resolver -------------------------------------------------------------------------------
683
+ /** Finds the facts of the calls into packages of the file `ctx` summarizes. */
684
+ export function packageResolver(ctx) {
685
+ const checked = new Map();
686
+ return (call) => {
687
+ const info = packageOf(call, ctx.file, checked);
688
+ const names = pathOf(call.node);
689
+ const fn = names.join(".") || call.id.slice(call.id.indexOf("#") + 1);
690
+ const base = { name: call.name, version: info?.version ?? "", fn };
691
+ const facts = (source, sides) => ({
692
+ ...base,
693
+ source,
694
+ ...sides,
695
+ });
696
+ const unknown = (reason) => {
697
+ const set = side([], [reason]);
698
+ return facts("unknown", { throws: set, rejects: set });
699
+ };
700
+ const fromConfig = ctx.facts[call.name];
701
+ const entry = fromConfig?.[fn] ?? fromConfig?.[names.at(-1) ?? fn];
702
+ if (entry !== undefined) {
703
+ return {
704
+ facts: facts("config", configured(entry, call.name, returnsPromise(call.node))),
705
+ classes: [],
706
+ };
707
+ }
708
+ // 2. Curated fact files: not shipped yet (`curatedFacts`).
709
+ const tagged = declared(call);
710
+ if (tagged !== undefined)
711
+ return { facts: facts("declared", tagged), classes: [] };
712
+ if (info === undefined)
713
+ return { facts: unknown("no readable source"), classes: [] };
714
+ const file = entryOf(call.file, info);
715
+ if (file === undefined)
716
+ return { facts: unknown("no readable source"), classes: [] };
717
+ const read = inferred(info, file, names);
718
+ return read.kind === "unknown"
719
+ ? { facts: unknown(read.reason), classes: [] }
720
+ : { facts: facts("inferred", read.sides), classes: read.classes };
721
+ };
722
+ }
723
+ //# sourceMappingURL=packages.js.map