@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,1324 @@
|
|
|
1
|
+
import ts from "@stateward/core/typescript";
|
|
2
|
+
import { isFunctionBoundary, scanRunning, stripParentheses } from "../syntax.js";
|
|
3
|
+
import { oneLine, shorten } from "../text.js";
|
|
4
|
+
import { ANY_REJECTS, ARRAY_LENGTH, BUILTIN_ERRORS, DEFERRED_CALLERS, FETCH_ABORT, FETCH_REJECTS, MAPPING_METHODS, SYNC_CALLBACKS, THROWING_CONSTRUCTORS, THROWING_GLOBALS, THROWING_MEMBERS, builtinId, isNonThrowingReactHook, } from "./builtins.js";
|
|
5
|
+
/** Why something adds `unknown`, as the hover and the messages show it. */
|
|
6
|
+
export const REASON = {
|
|
7
|
+
noDeclaration: "no declaration",
|
|
8
|
+
noBody: "no body (interface or abstract method)",
|
|
9
|
+
callback: "callback of unknown origin",
|
|
10
|
+
dynamic: "dynamic call",
|
|
11
|
+
thrownValue: "a thrown value of unknown class",
|
|
12
|
+
madeError: "an error made by a call",
|
|
13
|
+
promise: "a promise stateward cannot follow",
|
|
14
|
+
heldPromise: "promise held in a variable",
|
|
15
|
+
};
|
|
16
|
+
/** Why a callback handed to a call of the platform that is not in the tables adds `unknown`. */
|
|
17
|
+
export function mayRunReason(callee) {
|
|
18
|
+
return `callback passed to \`${callee}\` may run during the call`;
|
|
19
|
+
}
|
|
20
|
+
/** The id of a function without a name; the analysis names it by its file and offset. */
|
|
21
|
+
export const ANONYMOUS = "";
|
|
22
|
+
/** A called name's text is cut to this many characters in hovers and messages. */
|
|
23
|
+
const TEXT_MAX = 40;
|
|
24
|
+
/** A class's `extends` chain is followed this deep at most. */
|
|
25
|
+
const MAX_CLASS_DEPTH = 20;
|
|
26
|
+
const TS_LIB = /\/typescript\/lib\/lib\.[^/]*\.d\.ts$/;
|
|
27
|
+
/** A declaration of the platform: TypeScript's lib, or a global in a declaration file. */
|
|
28
|
+
function isPlatform(node, file) {
|
|
29
|
+
if (TS_LIB.test(file))
|
|
30
|
+
return true;
|
|
31
|
+
const sourceFile = node.getSourceFile();
|
|
32
|
+
if (!sourceFile.isDeclarationFile)
|
|
33
|
+
return false;
|
|
34
|
+
const module = ts.findAncestor(node.parent, ts.isModuleDeclaration);
|
|
35
|
+
const enclosing = module === undefined ? undefined : outermostModule(module);
|
|
36
|
+
// `declare module "x"` is a package; `declare global` is the platform; a namespace is what
|
|
37
|
+
// its file is.
|
|
38
|
+
if (enclosing !== undefined && ts.isStringLiteral(enclosing.name))
|
|
39
|
+
return false;
|
|
40
|
+
if (enclosing?.name.text === "global")
|
|
41
|
+
return true;
|
|
42
|
+
return !ts.isExternalModule(sourceFile);
|
|
43
|
+
}
|
|
44
|
+
/** `axios`, `@tanstack/react-query`, `react` for `@types/react`. */
|
|
45
|
+
function packageName(file) {
|
|
46
|
+
const at = file.lastIndexOf("/node_modules/");
|
|
47
|
+
const parts = (at < 0 ? file : file.slice(at + "/node_modules/".length)).split("/");
|
|
48
|
+
const [first = "", second = ""] = parts;
|
|
49
|
+
if (first === "@types")
|
|
50
|
+
return second.includes("__") ? `@${second.replace("__", "/")}` : second;
|
|
51
|
+
return first.startsWith("@") ? `${first}/${second}` : first;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The package a declaration outside the project belongs to: by its node_modules folder, or, for a
|
|
55
|
+
* package linked from elsewhere (a workspace package, whose real path has no node_modules), by
|
|
56
|
+
* the name in its package.json.
|
|
57
|
+
*/
|
|
58
|
+
export function packageNameOf(types, file) {
|
|
59
|
+
return file.includes("/node_modules/")
|
|
60
|
+
? packageName(file)
|
|
61
|
+
: (types.packageOf(file) ?? packageName(file));
|
|
62
|
+
}
|
|
63
|
+
/** The outermost of nested module declarations (`declare global { namespace X { … } }`). */
|
|
64
|
+
function outermostModule(module) {
|
|
65
|
+
const outer = ts.findAncestor(module.parent, ts.isModuleDeclaration);
|
|
66
|
+
return outer === undefined ? module : outermostModule(outer);
|
|
67
|
+
}
|
|
68
|
+
/** The name of the `declare module "x"` a declaration sits in. */
|
|
69
|
+
function ambientModuleName(node) {
|
|
70
|
+
const module = ts.findAncestor(node.parent, ts.isModuleDeclaration);
|
|
71
|
+
if (module === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
const outer = outermostModule(module);
|
|
74
|
+
return ts.isStringLiteral(outer.name) ? outer.name.text : undefined;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* `exports.load = function () {…}` (CommonJS, as a package's JavaScript writes it): the function
|
|
78
|
+
* the assignment's property names. Its declaration is the property access or the assignment.
|
|
79
|
+
*/
|
|
80
|
+
function assignedFunction(node) {
|
|
81
|
+
const assignment = ts.isPropertyAccessExpression(node) ? node.parent : node;
|
|
82
|
+
if (!ts.isBinaryExpression(assignment) ||
|
|
83
|
+
assignment.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
|
84
|
+
!ts.isPropertyAccessExpression(assignment.left) ||
|
|
85
|
+
(ts.isPropertyAccessExpression(node) && assignment.left !== node)) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const value = stripParentheses(assignment.right);
|
|
89
|
+
return ts.isArrowFunction(value) || ts.isFunctionExpression(value) ? value : undefined;
|
|
90
|
+
}
|
|
91
|
+
function functionInitializer(node) {
|
|
92
|
+
const assigned = assignedFunction(node);
|
|
93
|
+
if (assigned !== undefined)
|
|
94
|
+
return assigned;
|
|
95
|
+
if (!ts.isVariableDeclaration(node) &&
|
|
96
|
+
!ts.isPropertyAssignment(node) &&
|
|
97
|
+
!ts.isPropertyDeclaration(node)) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const initializer = node.initializer === undefined ? undefined : stripParentheses(node.initializer);
|
|
101
|
+
return initializer !== undefined &&
|
|
102
|
+
(ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))
|
|
103
|
+
? initializer
|
|
104
|
+
: undefined;
|
|
105
|
+
}
|
|
106
|
+
function isAsync(node) {
|
|
107
|
+
return (ts.canHaveModifiers(node) &&
|
|
108
|
+
ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true);
|
|
109
|
+
}
|
|
110
|
+
function hasModifier(node, kind) {
|
|
111
|
+
return ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((m) => m.kind === kind) === true;
|
|
112
|
+
}
|
|
113
|
+
/** The implementation of an overloaded function or method: a sibling with the same name and a body. */
|
|
114
|
+
function implementationOf(node) {
|
|
115
|
+
if (node.body !== undefined)
|
|
116
|
+
return node;
|
|
117
|
+
const name = node.name?.getText();
|
|
118
|
+
const siblings = ts.isClassLike(node.parent)
|
|
119
|
+
? node.parent.members
|
|
120
|
+
: ts.isSourceFile(node.parent) || ts.isModuleBlock(node.parent) || ts.isBlock(node.parent)
|
|
121
|
+
? node.parent.statements
|
|
122
|
+
: [];
|
|
123
|
+
return siblings.find((sibling) => (ts.isFunctionDeclaration(sibling) || ts.isMethodDeclaration(sibling)) &&
|
|
124
|
+
sibling.body !== undefined &&
|
|
125
|
+
sibling.name?.getText() === name);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The signatures of a function or method declared without a body: itself and its body-less
|
|
129
|
+
* overloads (an `export declare function`, a `.d.ts` function). Empty when it has a body.
|
|
130
|
+
*/
|
|
131
|
+
function signaturesOf(node) {
|
|
132
|
+
if (node.body !== undefined)
|
|
133
|
+
return [];
|
|
134
|
+
const name = node.name?.getText();
|
|
135
|
+
const siblings = ts.isClassLike(node.parent)
|
|
136
|
+
? node.parent.members
|
|
137
|
+
: ts.isSourceFile(node.parent) || ts.isModuleBlock(node.parent) || ts.isBlock(node.parent)
|
|
138
|
+
? node.parent.statements
|
|
139
|
+
: [];
|
|
140
|
+
return siblings.filter((sibling) => (ts.isFunctionDeclaration(sibling) || ts.isMethodDeclaration(sibling)) &&
|
|
141
|
+
sibling.body === undefined &&
|
|
142
|
+
sibling.name?.getText() === name);
|
|
143
|
+
}
|
|
144
|
+
/** Whether a body-less declaration (or one of its overloads) says what it throws with `@throws`. */
|
|
145
|
+
function hasThrowsTag(node) {
|
|
146
|
+
return signaturesOf(node).some((signature) => ts.getJSDocTags(signature).some(ts.isJSDocThrowsTag));
|
|
147
|
+
}
|
|
148
|
+
function isParameterBinding(node) {
|
|
149
|
+
let at = node;
|
|
150
|
+
while (ts.isBindingElement(at) || ts.isObjectBindingPattern(at) || ts.isArrayBindingPattern(at)) {
|
|
151
|
+
at = at.parent;
|
|
152
|
+
}
|
|
153
|
+
return ts.isParameter(at);
|
|
154
|
+
}
|
|
155
|
+
/** What an own declaration is to a call. */
|
|
156
|
+
function ownKind(node, id) {
|
|
157
|
+
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
|
|
158
|
+
if (hasModifier(node, ts.SyntaxKind.AbstractKeyword))
|
|
159
|
+
return { kind: "no-body" };
|
|
160
|
+
const implementation = implementationOf(node);
|
|
161
|
+
if (implementation === undefined || hasModifier(node, ts.SyntaxKind.DeclareKeyword)) {
|
|
162
|
+
// No body to read: it throws what its `@throws` says (`find` makes its fact), else unknown.
|
|
163
|
+
return hasThrowsTag(node)
|
|
164
|
+
? { kind: "function", id, async: false, generator: false }
|
|
165
|
+
: { kind: "no-body" };
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
kind: "function",
|
|
169
|
+
id,
|
|
170
|
+
async: isAsync(implementation),
|
|
171
|
+
generator: implementation.asteriskToken !== undefined,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const initializer = functionInitializer(node);
|
|
175
|
+
if (initializer !== undefined) {
|
|
176
|
+
return {
|
|
177
|
+
kind: "function",
|
|
178
|
+
id,
|
|
179
|
+
async: isAsync(initializer),
|
|
180
|
+
generator: ts.isFunctionExpression(initializer) && initializer.asteriskToken !== undefined,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (ts.isClassDeclaration(node) || ts.isClassExpression(node))
|
|
184
|
+
return { kind: "class", id, node };
|
|
185
|
+
if (ts.isVariableDeclaration(node) && node.initializer !== undefined) {
|
|
186
|
+
const value = stripParentheses(node.initializer);
|
|
187
|
+
if (ts.isClassExpression(value))
|
|
188
|
+
return { kind: "class", id, node: value };
|
|
189
|
+
}
|
|
190
|
+
if (isParameterBinding(node))
|
|
191
|
+
return { kind: "parameter", node };
|
|
192
|
+
if (ts.isMethodSignature(node) ||
|
|
193
|
+
ts.isPropertySignature(node) ||
|
|
194
|
+
ts.isGetAccessorDeclaration(node)) {
|
|
195
|
+
return { kind: "no-body" };
|
|
196
|
+
}
|
|
197
|
+
return { kind: "other" };
|
|
198
|
+
}
|
|
199
|
+
/** The name a call or `new` names: `save` in `save()`, `this.save()` and `api.save()`. */
|
|
200
|
+
function calledName(callee) {
|
|
201
|
+
if (ts.isIdentifier(callee))
|
|
202
|
+
return callee;
|
|
203
|
+
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name))
|
|
204
|
+
return callee.name;
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
function textOf(node, sourceFile) {
|
|
208
|
+
return shorten(oneLine(node.getText(sourceFile)), TEXT_MAX);
|
|
209
|
+
}
|
|
210
|
+
function newScope(async, rejectParameters = new Set()) {
|
|
211
|
+
return { async, returns: [], rejectParameters };
|
|
212
|
+
}
|
|
213
|
+
/** `package:zod#ZodError`: a package's class, by its package and name, the same from its types and its source. */
|
|
214
|
+
export function packageClassId(name, className) {
|
|
215
|
+
return `package:${name}#${className}`;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The id a class named in code has in the summaries (`builtin:TypeError`, `package:zod#ZodError`,
|
|
219
|
+
* or the qualified id of a class of the project), without its facts; undefined when the name
|
|
220
|
+
* resolves to nothing.
|
|
221
|
+
*/
|
|
222
|
+
export function classIdOf(types, name) {
|
|
223
|
+
const symbol = types.symbolAt(name);
|
|
224
|
+
const declaration = symbol === undefined ? undefined : types.declarationOf(symbol);
|
|
225
|
+
if (symbol === undefined || declaration === undefined)
|
|
226
|
+
return undefined;
|
|
227
|
+
if (types.isOwnFile(declaration.file))
|
|
228
|
+
return symbol.id;
|
|
229
|
+
if (isPlatform(declaration.node, declaration.file)) {
|
|
230
|
+
return BUILTIN_ERRORS.has(symbol.name) ? builtinId(symbol.name) : undefined;
|
|
231
|
+
}
|
|
232
|
+
return packageClassId(ambientModuleName(declaration.node) ?? packageNameOf(types, declaration.file), symbol.name);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The throws facts of the file (see summary.ts). With `packages`, a call into a package carries
|
|
236
|
+
* its facts; without (a package's own source), it is unknown.
|
|
237
|
+
*/
|
|
238
|
+
export function summarizeThrows(ctx, packages) {
|
|
239
|
+
const { sourceFile, types } = ctx;
|
|
240
|
+
const classes = new Map();
|
|
241
|
+
const spanOf = (node) => [node.getStart(sourceFile), node.getEnd()];
|
|
242
|
+
const resolve = (name) => resolveWith(types, name);
|
|
243
|
+
/**
|
|
244
|
+
* `const p = load()`: what `p` rejects with, by its declaration, for a later `await p`,
|
|
245
|
+
* `return p`, `Promise.all([p])` or `p.then(…)`. A `const` is never reassigned, so its promise
|
|
246
|
+
* is the one its initializer made, wherever it is used in the file.
|
|
247
|
+
*/
|
|
248
|
+
const held = new Map();
|
|
249
|
+
// ---- classes --------------------------------------------------------------------------------
|
|
250
|
+
const classRef = (node, depth = 0) => {
|
|
251
|
+
const name = ts.isPropertyAccessExpression(node)
|
|
252
|
+
? node.name
|
|
253
|
+
: ts.isQualifiedName(node)
|
|
254
|
+
? node.right
|
|
255
|
+
: node;
|
|
256
|
+
const symbol = types.symbolAt(name);
|
|
257
|
+
const declaration = symbol === undefined ? undefined : types.declarationOf(symbol);
|
|
258
|
+
if (symbol === undefined || declaration === undefined)
|
|
259
|
+
return undefined;
|
|
260
|
+
const own = types.isOwnFile(declaration.file);
|
|
261
|
+
if (!own && isPlatform(declaration.node, declaration.file)) {
|
|
262
|
+
return BUILTIN_ERRORS.has(symbol.name) ? builtinId(symbol.name) : undefined;
|
|
263
|
+
}
|
|
264
|
+
// A package's class is named by its package and name: its types, its source and the config
|
|
265
|
+
// all name it so.
|
|
266
|
+
const id = own
|
|
267
|
+
? symbol.id
|
|
268
|
+
: packageClassId(ambientModuleName(declaration.node) ?? packageNameOf(types, declaration.file), symbol.name);
|
|
269
|
+
let classNode;
|
|
270
|
+
if (ts.isClassDeclaration(declaration.node) || ts.isClassExpression(declaration.node)) {
|
|
271
|
+
classNode = declaration.node;
|
|
272
|
+
}
|
|
273
|
+
else if (ts.isVariableDeclaration(declaration.node) &&
|
|
274
|
+
declaration.node.initializer !== undefined &&
|
|
275
|
+
ts.isClassExpression(stripParentheses(declaration.node.initializer))) {
|
|
276
|
+
const value = stripParentheses(declaration.node.initializer);
|
|
277
|
+
classNode = ts.isClassExpression(value) ? value : undefined;
|
|
278
|
+
}
|
|
279
|
+
if (classNode === undefined)
|
|
280
|
+
return undefined;
|
|
281
|
+
if (!classes.has(id)) {
|
|
282
|
+
// Placeholder first: a class that (wrongly) extends itself stops here.
|
|
283
|
+
classes.set(id, { id, name: symbol.name, parent: null, open: true });
|
|
284
|
+
const base = classNode.heritageClauses
|
|
285
|
+
?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)
|
|
286
|
+
?.types.at(0)?.expression;
|
|
287
|
+
const parent = base === undefined || depth >= MAX_CLASS_DEPTH ? undefined : classRef(base, depth + 1);
|
|
288
|
+
classes.set(id, {
|
|
289
|
+
id,
|
|
290
|
+
name: symbol.name,
|
|
291
|
+
parent: parent ?? null,
|
|
292
|
+
open: base !== undefined && parent === undefined,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return id;
|
|
296
|
+
};
|
|
297
|
+
// ---- leaves ---------------------------------------------------------------------------------
|
|
298
|
+
const unknownLeaf = (reason, text, node) => ({
|
|
299
|
+
k: "unknown",
|
|
300
|
+
reason,
|
|
301
|
+
text,
|
|
302
|
+
at: spanOf(node),
|
|
303
|
+
});
|
|
304
|
+
const classLeaves = (ids, text, node) => ids.map((id) => ({ k: "class", id, text, at: spanOf(node) }));
|
|
305
|
+
/**
|
|
306
|
+
* A call into a package, by its facts: what it throws where it is called (`sync`) and what its
|
|
307
|
+
* promise rejects with. Undefined for one of React's hooks that throw nothing.
|
|
308
|
+
*/
|
|
309
|
+
const packageLeaves = (resolved, text, node) => {
|
|
310
|
+
if (isNonThrowingReactHook(resolved.name, text))
|
|
311
|
+
return undefined;
|
|
312
|
+
// A declaration whose JSDoc was read (a project's own `.d.ts`): what its `@throws` says.
|
|
313
|
+
const tagged = taggedLeaves(resolved.node, text, node);
|
|
314
|
+
if (tagged !== undefined)
|
|
315
|
+
return tagged;
|
|
316
|
+
if (packages === undefined) {
|
|
317
|
+
const leaf = unknownLeaf(`package ${resolved.name}`, text, node);
|
|
318
|
+
return { sync: [leaf], promise: [leaf] };
|
|
319
|
+
}
|
|
320
|
+
const found = packages(resolved);
|
|
321
|
+
for (const fact of found.classes)
|
|
322
|
+
if (!classes.has(fact.id))
|
|
323
|
+
classes.set(fact.id, fact);
|
|
324
|
+
const at = spanOf(node);
|
|
325
|
+
const { facts } = found;
|
|
326
|
+
const side = (part) => facts[part].classes.length > 0 || facts[part].unknown.length > 0
|
|
327
|
+
? [{ k: "package", facts, part, text, at }]
|
|
328
|
+
: [];
|
|
329
|
+
return { sync: side("throws"), promise: side("rejects") };
|
|
330
|
+
};
|
|
331
|
+
/**
|
|
332
|
+
* What a declaration without a body says it throws with `@throws` (on any of its overloads), as
|
|
333
|
+
* leaves at the call; undefined without a tag. On a function that returns a promise the tag
|
|
334
|
+
* is what the promise rejects with.
|
|
335
|
+
*/
|
|
336
|
+
const taggedLeaves = (declaration, text, at) => {
|
|
337
|
+
const signatures = ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration)
|
|
338
|
+
? signaturesOf(declaration)
|
|
339
|
+
: [];
|
|
340
|
+
const doc = docOf(...(signatures.length > 0 ? signatures : [declaration]));
|
|
341
|
+
if (doc === null)
|
|
342
|
+
return undefined;
|
|
343
|
+
const span = spanOf(at);
|
|
344
|
+
const leaves = [
|
|
345
|
+
...doc.entries.map((entry) => entry.id === null
|
|
346
|
+
? {
|
|
347
|
+
k: "unknown",
|
|
348
|
+
reason: "`@throws` names no class stateward can find",
|
|
349
|
+
text: entry.name,
|
|
350
|
+
at: span,
|
|
351
|
+
}
|
|
352
|
+
: { k: "class", id: entry.id, text: `${text}(…)`, at: span }),
|
|
353
|
+
...doc.untyped.map(() => ({
|
|
354
|
+
k: "unknown",
|
|
355
|
+
reason: "`@throws` without a type",
|
|
356
|
+
text: "@throws",
|
|
357
|
+
at: span,
|
|
358
|
+
})),
|
|
359
|
+
];
|
|
360
|
+
return ts.isFunctionLike(declaration) && types.returnsPromiseLike(declaration)
|
|
361
|
+
? { sync: [], promise: leaves }
|
|
362
|
+
: { sync: leaves, promise: [] };
|
|
363
|
+
};
|
|
364
|
+
/** The leaf of a name that cannot be followed, by what it resolved to. */
|
|
365
|
+
const unresolvedLeaf = (resolved, text, node) => {
|
|
366
|
+
switch (resolved?.kind) {
|
|
367
|
+
case undefined:
|
|
368
|
+
return unknownLeaf(REASON.noDeclaration, text, node);
|
|
369
|
+
case "no-body":
|
|
370
|
+
return unknownLeaf(REASON.noBody, text, node);
|
|
371
|
+
case "parameter":
|
|
372
|
+
return unknownLeaf(REASON.callback, text, node);
|
|
373
|
+
case "package":
|
|
374
|
+
// Handled by `packageLeaves` where a call can be one.
|
|
375
|
+
return isNonThrowingReactHook(resolved.name, text)
|
|
376
|
+
? undefined
|
|
377
|
+
: unknownLeaf(`package ${resolved.name}`, text, node);
|
|
378
|
+
case "other":
|
|
379
|
+
return unknownLeaf(REASON.dynamic, text, node);
|
|
380
|
+
case "platform":
|
|
381
|
+
case "function":
|
|
382
|
+
case "class":
|
|
383
|
+
return undefined;
|
|
384
|
+
default: {
|
|
385
|
+
const unreachable = resolved;
|
|
386
|
+
return unreachable;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
// ---- expressions: push what throws now into the sink, return what the promise rejects with --
|
|
391
|
+
const visit = (node, walk) => {
|
|
392
|
+
if (isFunctionBoundary(node))
|
|
393
|
+
return;
|
|
394
|
+
if (ts.isBlock(node) || ts.isStatement(node)) {
|
|
395
|
+
statement(node, walk);
|
|
396
|
+
}
|
|
397
|
+
else if (ts.isExpression(node)) {
|
|
398
|
+
expression(node, walk);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
ts.forEachChild(node, (child) => {
|
|
402
|
+
visit(child, walk);
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const expression = (node, walk) => {
|
|
407
|
+
const e = stripParentheses(node);
|
|
408
|
+
if (ts.isAsExpression(e) ||
|
|
409
|
+
ts.isSatisfiesExpression(e) ||
|
|
410
|
+
ts.isNonNullExpression(e) ||
|
|
411
|
+
ts.isTypeAssertionExpression(e)) {
|
|
412
|
+
return expression(e.expression, walk);
|
|
413
|
+
}
|
|
414
|
+
if (isFunctionBoundary(e))
|
|
415
|
+
return [];
|
|
416
|
+
if (ts.isAwaitExpression(e)) {
|
|
417
|
+
walk.sink.push(...awaited(e.expression, walk));
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
if (ts.isCallExpression(e))
|
|
421
|
+
return call(e, walk, false);
|
|
422
|
+
if (ts.isNewExpression(e))
|
|
423
|
+
return construct(e, walk);
|
|
424
|
+
if (ts.isConditionalExpression(e)) {
|
|
425
|
+
expression(e.condition, walk);
|
|
426
|
+
const branch = { ...walk, conditional: true };
|
|
427
|
+
return [...flowing(e.whenTrue, branch), ...flowing(e.whenFalse, branch)];
|
|
428
|
+
}
|
|
429
|
+
ts.forEachChild(e, (child) => {
|
|
430
|
+
visit(child, walk);
|
|
431
|
+
});
|
|
432
|
+
return [];
|
|
433
|
+
};
|
|
434
|
+
/** What the `const` that `name` refers to holds, when it is one this file summarized. */
|
|
435
|
+
const heldBy = (name) => {
|
|
436
|
+
const symbol = types.symbolAt(name);
|
|
437
|
+
const declaration = symbol === undefined ? undefined : types.declarationOf(symbol);
|
|
438
|
+
return declaration === undefined ? undefined : held.get(declaration.node);
|
|
439
|
+
};
|
|
440
|
+
const unwrap = (node) => {
|
|
441
|
+
const e = stripParentheses(node);
|
|
442
|
+
return ts.isAsExpression(e) ||
|
|
443
|
+
ts.isSatisfiesExpression(e) ||
|
|
444
|
+
ts.isNonNullExpression(e) ||
|
|
445
|
+
ts.isTypeAssertionExpression(e)
|
|
446
|
+
? unwrap(e.expression)
|
|
447
|
+
: e;
|
|
448
|
+
};
|
|
449
|
+
const isHolder = (e) => ts.isIdentifier(e) || ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e);
|
|
450
|
+
/**
|
|
451
|
+
* A value whose promise flows on (returned, awaited, joined, chained): what that promise rejects
|
|
452
|
+
* with. A `const` gives what its initializer rejects with; any other variable, parameter or
|
|
453
|
+
* property that holds a promise cannot be followed to where the promise was made.
|
|
454
|
+
*/
|
|
455
|
+
const flowing = (node, walk) => {
|
|
456
|
+
const e = unwrap(node);
|
|
457
|
+
if (!isHolder(e))
|
|
458
|
+
return expression(e, walk);
|
|
459
|
+
const known = ts.isIdentifier(e) ? heldBy(e) : undefined;
|
|
460
|
+
if (known !== undefined)
|
|
461
|
+
return [...known];
|
|
462
|
+
expression(e, walk);
|
|
463
|
+
return types.isPromiseLike(e)
|
|
464
|
+
? [unknownLeaf(REASON.heldPromise, textOf(e, sourceFile), e)]
|
|
465
|
+
: [];
|
|
466
|
+
};
|
|
467
|
+
/** What `await <operand>` throws. */
|
|
468
|
+
const awaited = (operand, walk) => {
|
|
469
|
+
const inner = unwrap(operand);
|
|
470
|
+
if (!isHolder(inner))
|
|
471
|
+
return expression(inner, walk);
|
|
472
|
+
const known = ts.isIdentifier(inner) ? heldBy(inner) : undefined;
|
|
473
|
+
if (known !== undefined)
|
|
474
|
+
return [...known];
|
|
475
|
+
// Awaited, so taken to be a promise whatever its type says (`any`, a union).
|
|
476
|
+
expression(inner, walk);
|
|
477
|
+
return [unknownLeaf(REASON.heldPromise, textOf(inner, sourceFile), inner)];
|
|
478
|
+
};
|
|
479
|
+
/** A callback's effects: what it throws when called, and what its promise rejects with. */
|
|
480
|
+
const callback = (argument, walk, rejectIndex) => {
|
|
481
|
+
if (argument === undefined)
|
|
482
|
+
return { sync: [], promise: [] };
|
|
483
|
+
const fn = stripParentheses(argument);
|
|
484
|
+
if (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) {
|
|
485
|
+
const reject = rejectIndex === undefined ? undefined : fn.parameters[rejectIndex];
|
|
486
|
+
const scope = newScope(isAsync(fn), new Set(reject === undefined ? [] : [reject]));
|
|
487
|
+
const sink = [];
|
|
488
|
+
const inner = { scope, sink, conditional: false };
|
|
489
|
+
if (ts.isBlock(fn.body))
|
|
490
|
+
statement(fn.body, inner);
|
|
491
|
+
else
|
|
492
|
+
scope.returns.push(...flowing(fn.body, inner));
|
|
493
|
+
return scope.async
|
|
494
|
+
? { sync: [], promise: [...sink, ...scope.returns] }
|
|
495
|
+
: { sync: sink, promise: scope.returns };
|
|
496
|
+
}
|
|
497
|
+
const name = calledName(fn);
|
|
498
|
+
if (name === undefined) {
|
|
499
|
+
expression(fn, walk);
|
|
500
|
+
const leaf = unknownLeaf(REASON.callback, textOf(fn, sourceFile), fn);
|
|
501
|
+
return { sync: [leaf], promise: [leaf] };
|
|
502
|
+
}
|
|
503
|
+
if (ts.isPropertyAccessExpression(fn))
|
|
504
|
+
expression(fn.expression, walk);
|
|
505
|
+
const resolved = resolve(name);
|
|
506
|
+
const text = textOf(fn, sourceFile);
|
|
507
|
+
if (resolved?.kind === "function") {
|
|
508
|
+
if (resolved.generator)
|
|
509
|
+
return { sync: [], promise: [] };
|
|
510
|
+
const at = spanOf(fn);
|
|
511
|
+
const rejects = { k: "call", id: resolved.id, part: "rejects", text, at };
|
|
512
|
+
const throws = { k: "call", id: resolved.id, part: "throws", text, at };
|
|
513
|
+
return { sync: resolved.async ? [] : [throws], promise: [rejects] };
|
|
514
|
+
}
|
|
515
|
+
if (resolved?.kind === "package") {
|
|
516
|
+
const leaves = packageLeaves(resolved, text, fn);
|
|
517
|
+
return leaves === undefined
|
|
518
|
+
? { sync: [], promise: [] }
|
|
519
|
+
: { sync: leaves.sync, promise: [...leaves.sync, ...leaves.promise] };
|
|
520
|
+
}
|
|
521
|
+
const leaf = resolved?.kind === "other"
|
|
522
|
+
? unknownLeaf(REASON.callback, text, fn)
|
|
523
|
+
: unresolvedLeaf(resolved, text, fn);
|
|
524
|
+
return leaf === undefined ? { sync: [], promise: [] } : { sync: [leaf], promise: [leaf] };
|
|
525
|
+
};
|
|
526
|
+
const both = (effects) => [
|
|
527
|
+
...effects.sync,
|
|
528
|
+
...effects.promise,
|
|
529
|
+
];
|
|
530
|
+
const evaluateArguments = (call, walk) => {
|
|
531
|
+
for (const argument of call.arguments ?? [])
|
|
532
|
+
expression(argument, walk);
|
|
533
|
+
};
|
|
534
|
+
/** What the promises of `Promise.all(<argument>)` reject with. */
|
|
535
|
+
const elements = (argument, walk) => {
|
|
536
|
+
if (argument === undefined)
|
|
537
|
+
return [];
|
|
538
|
+
const list = stripParentheses(argument);
|
|
539
|
+
if (ts.isArrayLiteralExpression(list)) {
|
|
540
|
+
return list.elements.flatMap((element) => {
|
|
541
|
+
if (ts.isSpreadElement(element)) {
|
|
542
|
+
expression(element.expression, walk);
|
|
543
|
+
return [unknownLeaf(REASON.promise, textOf(element, sourceFile), element)];
|
|
544
|
+
}
|
|
545
|
+
return flowing(element, walk);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (ts.isCallExpression(list))
|
|
549
|
+
return call(list, walk, true);
|
|
550
|
+
expression(list, walk);
|
|
551
|
+
return [unknownLeaf(REASON.promise, textOf(list, sourceFile), list)];
|
|
552
|
+
};
|
|
553
|
+
/** An argument in a callback's place that is a function (`replace` also takes a string). */
|
|
554
|
+
const isCallback = (argument) => {
|
|
555
|
+
const e = stripParentheses(argument);
|
|
556
|
+
return ts.isArrowFunction(e) || ts.isFunctionExpression(e) || types.isCallable(e);
|
|
557
|
+
};
|
|
558
|
+
/**
|
|
559
|
+
* The functions handed to a call of the platform that is not in the tables: it may run them
|
|
560
|
+
* before it returns, so each whose body throws anything makes the call unknown. Only a function
|
|
561
|
+
* written in place or one of the project's own is looked at; a parameter's body is not known.
|
|
562
|
+
*/
|
|
563
|
+
const mayRun = (args, text, at, walk) => {
|
|
564
|
+
const reason = mayRunReason(text);
|
|
565
|
+
const leaves = [];
|
|
566
|
+
for (const argument of args) {
|
|
567
|
+
const fn = stripParentheses(argument);
|
|
568
|
+
if (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) {
|
|
569
|
+
const body = both(callback(fn, walk));
|
|
570
|
+
if (body.length > 0)
|
|
571
|
+
leaves.push({ k: "maybe", reason, text, body, at: spanOf(at) });
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const name = calledName(fn);
|
|
575
|
+
const resolved = name === undefined ? undefined : resolve(name);
|
|
576
|
+
if (resolved?.kind !== "function" || resolved.generator)
|
|
577
|
+
continue;
|
|
578
|
+
const callText = textOf(fn, sourceFile);
|
|
579
|
+
const body = [
|
|
580
|
+
{ k: "call", id: resolved.id, part: "both", text: callText, at: spanOf(fn) },
|
|
581
|
+
];
|
|
582
|
+
leaves.push({ k: "maybe", reason, text, body, at: spanOf(at) });
|
|
583
|
+
}
|
|
584
|
+
return leaves;
|
|
585
|
+
};
|
|
586
|
+
/**
|
|
587
|
+
* Whether `fetch`'s options surely carry an abort signal: an object literal with a `signal`
|
|
588
|
+
* property, or a value whose type has a `signal` that is neither optional nor `undefined`.
|
|
589
|
+
* `RequestInit`'s own `signal` is optional, so a `RequestInit` passed through adds no
|
|
590
|
+
* `AbortError`: a possible miss rather than an error the caller may never see.
|
|
591
|
+
*/
|
|
592
|
+
const signalOf = (argument) => {
|
|
593
|
+
if (argument === undefined)
|
|
594
|
+
return false;
|
|
595
|
+
const init = stripParentheses(argument);
|
|
596
|
+
if (ts.isObjectLiteralExpression(init)) {
|
|
597
|
+
return init.properties.some((p) => !ts.isSpreadAssignment(p) && p.name.getText(sourceFile) === "signal");
|
|
598
|
+
}
|
|
599
|
+
return types.hasDefinedProperty(init, "signal");
|
|
600
|
+
};
|
|
601
|
+
/**
|
|
602
|
+
* A call: what it throws goes to the sink; returns what its promise rejects with. With
|
|
603
|
+
* `mapping`, an array method returns what the promises its callback returned reject with.
|
|
604
|
+
*/
|
|
605
|
+
const call = (node, walk, mapping) => {
|
|
606
|
+
const callee = stripParentheses(node.expression);
|
|
607
|
+
if (ts.isArrowFunction(callee) || ts.isFunctionExpression(callee)) {
|
|
608
|
+
evaluateArguments(node, walk);
|
|
609
|
+
const effects = callback(callee, walk);
|
|
610
|
+
walk.sink.push(...effects.sync);
|
|
611
|
+
return effects.promise;
|
|
612
|
+
}
|
|
613
|
+
if (callee.kind === ts.SyntaxKind.SuperKeyword) {
|
|
614
|
+
evaluateArguments(node, walk);
|
|
615
|
+
superCall(callee, walk);
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
const name = calledName(callee);
|
|
619
|
+
const text = textOf(callee, sourceFile);
|
|
620
|
+
if (name === undefined) {
|
|
621
|
+
expression(callee, walk);
|
|
622
|
+
evaluateArguments(node, walk);
|
|
623
|
+
const leaf = unknownLeaf(REASON.dynamic, text, callee);
|
|
624
|
+
walk.sink.push(leaf);
|
|
625
|
+
return [leaf];
|
|
626
|
+
}
|
|
627
|
+
const receiver = ts.isPropertyAccessExpression(callee) ? callee.expression : undefined;
|
|
628
|
+
const resolved = resolve(name);
|
|
629
|
+
const args = node.arguments;
|
|
630
|
+
if (resolved?.kind === "platform") {
|
|
631
|
+
const key = resolved.owner === null ? resolved.name : `${resolved.owner}.${resolved.name}`;
|
|
632
|
+
if (resolved.owner === "Promise") {
|
|
633
|
+
const before = receiver === undefined ? [] : flowing(receiver, walk);
|
|
634
|
+
const [first, second] = args;
|
|
635
|
+
switch (resolved.name) {
|
|
636
|
+
case "then":
|
|
637
|
+
return [
|
|
638
|
+
...(second === undefined ? before : both(callback(second, walk))),
|
|
639
|
+
...both(callback(first, walk)),
|
|
640
|
+
];
|
|
641
|
+
case "catch":
|
|
642
|
+
return first === undefined ? before : both(callback(first, walk));
|
|
643
|
+
case "finally":
|
|
644
|
+
return [...before, ...both(callback(first, walk))];
|
|
645
|
+
default:
|
|
646
|
+
evaluateArguments(node, walk);
|
|
647
|
+
return before;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (receiver !== undefined)
|
|
651
|
+
expression(receiver, walk);
|
|
652
|
+
if (resolved.owner === "PromiseConstructor") {
|
|
653
|
+
const [first] = args;
|
|
654
|
+
switch (resolved.name) {
|
|
655
|
+
case "all":
|
|
656
|
+
case "race":
|
|
657
|
+
return elements(first, walk);
|
|
658
|
+
case "allSettled":
|
|
659
|
+
elements(first, walk);
|
|
660
|
+
return [];
|
|
661
|
+
case "any":
|
|
662
|
+
elements(first, walk);
|
|
663
|
+
return classLeaves([ANY_REJECTS], `${text}(…)`, node);
|
|
664
|
+
case "reject":
|
|
665
|
+
return first === undefined ? [] : thrown(first, walk);
|
|
666
|
+
case "resolve":
|
|
667
|
+
return first === undefined ? [] : flowing(first, walk);
|
|
668
|
+
default:
|
|
669
|
+
evaluateArguments(node, walk);
|
|
670
|
+
return [];
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const index = SYNC_CALLBACKS.get(key);
|
|
674
|
+
let mapped = [];
|
|
675
|
+
if (index === undefined) {
|
|
676
|
+
evaluateArguments(node, walk);
|
|
677
|
+
if (!DEFERRED_CALLERS.has(resolved.name)) {
|
|
678
|
+
walk.sink.push(...mayRun(args, text, callee, walk));
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
for (const [i, argument] of args.entries()) {
|
|
683
|
+
if (i === index && isCallback(argument)) {
|
|
684
|
+
const effects = callback(argument, walk);
|
|
685
|
+
walk.sink.push(...effects.sync);
|
|
686
|
+
mapped = effects.promise;
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
expression(argument, walk);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (key === "fetch") {
|
|
694
|
+
return classLeaves(signalOf(args[1]) ? [...FETCH_REJECTS, FETCH_ABORT] : FETCH_REJECTS, `${text}(…)`, node);
|
|
695
|
+
}
|
|
696
|
+
const throws = resolved.owner === null
|
|
697
|
+
? (THROWING_GLOBALS.get(key) ??
|
|
698
|
+
(key === "Array" && args.length === 1 ? ARRAY_LENGTH : []))
|
|
699
|
+
: (THROWING_MEMBERS.get(key) ?? []);
|
|
700
|
+
walk.sink.push(...classLeaves(throws, `${text}(…)`, node));
|
|
701
|
+
return mapping && MAPPING_METHODS.has(key) ? mapped : [];
|
|
702
|
+
}
|
|
703
|
+
if (receiver !== undefined)
|
|
704
|
+
expression(receiver, walk);
|
|
705
|
+
if (resolved?.kind === "parameter" && walk.scope.rejectParameters.has(resolved.node)) {
|
|
706
|
+
evaluateArguments(node, walk);
|
|
707
|
+
const [reason] = args;
|
|
708
|
+
walk.sink.push(...(reason === undefined
|
|
709
|
+
? [unknownLeaf(REASON.thrownValue, text, node)]
|
|
710
|
+
: thrown(reason, { ...walk, sink: [] })));
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
evaluateArguments(node, walk);
|
|
714
|
+
if (resolved?.kind === "function") {
|
|
715
|
+
if (resolved.generator)
|
|
716
|
+
return [];
|
|
717
|
+
const at = spanOf(callee);
|
|
718
|
+
if (!resolved.async) {
|
|
719
|
+
walk.sink.push({ k: "call", id: resolved.id, part: "throws", text, at });
|
|
720
|
+
}
|
|
721
|
+
return [{ k: "call", id: resolved.id, part: "rejects", text, at }];
|
|
722
|
+
}
|
|
723
|
+
if (resolved?.kind === "class")
|
|
724
|
+
return [];
|
|
725
|
+
if (resolved?.kind === "package") {
|
|
726
|
+
const leaves = packageLeaves(resolved, text, callee);
|
|
727
|
+
if (leaves === undefined)
|
|
728
|
+
return [];
|
|
729
|
+
walk.sink.push(...leaves.sync);
|
|
730
|
+
return leaves.promise;
|
|
731
|
+
}
|
|
732
|
+
const leaf = unresolvedLeaf(resolved, text, callee);
|
|
733
|
+
if (leaf === undefined)
|
|
734
|
+
return [];
|
|
735
|
+
walk.sink.push(leaf);
|
|
736
|
+
return [leaf];
|
|
737
|
+
};
|
|
738
|
+
/** `super(…)`: what the constructor of the class this one extends throws. */
|
|
739
|
+
const superCall = (callee, walk) => {
|
|
740
|
+
const owner = ts.findAncestor(callee, ts.isClassLike);
|
|
741
|
+
const base = owner === undefined
|
|
742
|
+
? undefined
|
|
743
|
+
: owner.heritageClauses
|
|
744
|
+
?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)
|
|
745
|
+
?.types.at(0)?.expression;
|
|
746
|
+
const name = base === undefined ? undefined : calledName(stripParentheses(base));
|
|
747
|
+
if (base === undefined || name === undefined)
|
|
748
|
+
return;
|
|
749
|
+
const resolved = resolve(name);
|
|
750
|
+
const text = `super (${textOf(base, sourceFile)})`;
|
|
751
|
+
if (resolved?.kind === "class") {
|
|
752
|
+
if (resolved.node.members.some((m) => ts.isConstructorDeclaration(m) && m.body !== undefined)) {
|
|
753
|
+
walk.sink.push({ k: "call", id: resolved.id, part: "throws", text, at: spanOf(callee) });
|
|
754
|
+
}
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const leaf = unresolvedLeaf(resolved, text, callee);
|
|
758
|
+
if (leaf !== undefined)
|
|
759
|
+
walk.sink.push(leaf);
|
|
760
|
+
};
|
|
761
|
+
/** `new X(…)`: what the constructor throws goes to the sink; returns a `new Promise`'s rejections. */
|
|
762
|
+
const construct = (node, walk) => {
|
|
763
|
+
const callee = stripParentheses(node.expression);
|
|
764
|
+
const name = calledName(callee);
|
|
765
|
+
const text = textOf(callee, sourceFile);
|
|
766
|
+
const args = node.arguments ?? [];
|
|
767
|
+
if (name === undefined) {
|
|
768
|
+
expression(callee, walk);
|
|
769
|
+
evaluateArguments(node, walk);
|
|
770
|
+
walk.sink.push(unknownLeaf(REASON.dynamic, `new ${text}`, callee));
|
|
771
|
+
return [];
|
|
772
|
+
}
|
|
773
|
+
if (ts.isPropertyAccessExpression(callee))
|
|
774
|
+
expression(callee.expression, walk);
|
|
775
|
+
const resolved = resolve(name);
|
|
776
|
+
if (resolved?.kind === "platform" && resolved.owner === null && resolved.name === "Promise") {
|
|
777
|
+
const [executor] = args;
|
|
778
|
+
const fn = executor === undefined ? undefined : stripParentheses(executor);
|
|
779
|
+
if (fn !== undefined && (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) {
|
|
780
|
+
return both(callback(fn, walk, 1));
|
|
781
|
+
}
|
|
782
|
+
evaluateArguments(node, walk);
|
|
783
|
+
return executor === undefined ? [] : [unknownLeaf(REASON.callback, text, executor)];
|
|
784
|
+
}
|
|
785
|
+
evaluateArguments(node, walk);
|
|
786
|
+
switch (resolved?.kind) {
|
|
787
|
+
case "platform": {
|
|
788
|
+
const throws = resolved.owner !== null
|
|
789
|
+
? []
|
|
790
|
+
: (THROWING_CONSTRUCTORS.get(resolved.name) ??
|
|
791
|
+
(resolved.name === "Array" && args.length === 1 ? ARRAY_LENGTH : []));
|
|
792
|
+
walk.sink.push(...classLeaves(throws, `new ${text}(…)`, node));
|
|
793
|
+
if (!DEFERRED_CALLERS.has(resolved.name)) {
|
|
794
|
+
walk.sink.push(...mayRun(args, `new ${text}`, callee, walk));
|
|
795
|
+
}
|
|
796
|
+
return [];
|
|
797
|
+
}
|
|
798
|
+
case "class": {
|
|
799
|
+
const constructor = resolved.node.members.find((member) => ts.isConstructorDeclaration(member) && member.body !== undefined);
|
|
800
|
+
if (constructor !== undefined) {
|
|
801
|
+
walk.sink.push({
|
|
802
|
+
k: "call",
|
|
803
|
+
id: resolved.id,
|
|
804
|
+
part: "throws",
|
|
805
|
+
text: `new ${text}`,
|
|
806
|
+
at: spanOf(callee),
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
return [];
|
|
810
|
+
}
|
|
811
|
+
case "function":
|
|
812
|
+
return [];
|
|
813
|
+
case "package":
|
|
814
|
+
walk.sink.push(...(packageLeaves(resolved, `new ${text}`, callee)?.sync ?? []));
|
|
815
|
+
return [];
|
|
816
|
+
case undefined:
|
|
817
|
+
case "parameter":
|
|
818
|
+
case "no-body":
|
|
819
|
+
case "other": {
|
|
820
|
+
const leaf = unresolvedLeaf(resolved, `new ${text}`, callee);
|
|
821
|
+
if (leaf !== undefined)
|
|
822
|
+
walk.sink.push(leaf);
|
|
823
|
+
return [];
|
|
824
|
+
}
|
|
825
|
+
default: {
|
|
826
|
+
const unreachable = resolved;
|
|
827
|
+
return unreachable;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
/** The variable of the catch clause that `name` refers to, as its start offset. */
|
|
832
|
+
const caughtVariable = (name) => {
|
|
833
|
+
const symbol = types.symbolAt(name);
|
|
834
|
+
const declaration = symbol === undefined ? undefined : types.declarationOf(symbol);
|
|
835
|
+
const node = declaration?.node;
|
|
836
|
+
return node !== undefined && ts.isVariableDeclaration(node) && ts.isCatchClause(node.parent)
|
|
837
|
+
? node.getStart(sourceFile)
|
|
838
|
+
: undefined;
|
|
839
|
+
};
|
|
840
|
+
/** What `throw <value>` (or `reject(value)`) throws; its own calls go to the sink. */
|
|
841
|
+
const thrown = (value, walk) => {
|
|
842
|
+
const e = stripParentheses(value);
|
|
843
|
+
if (ts.isNewExpression(e) || ts.isCallExpression(e)) {
|
|
844
|
+
const id = classRef(stripParentheses(e.expression));
|
|
845
|
+
if (id !== undefined) {
|
|
846
|
+
evaluateArguments(e, walk);
|
|
847
|
+
return [{ k: "class", id, text: null, at: spanOf(e) }];
|
|
848
|
+
}
|
|
849
|
+
if (ts.isCallExpression(e)) {
|
|
850
|
+
call(e, walk, false);
|
|
851
|
+
return [unknownLeaf(REASON.madeError, textOf(e.expression, sourceFile), e)];
|
|
852
|
+
}
|
|
853
|
+
construct(e, walk);
|
|
854
|
+
return [unknownLeaf(REASON.thrownValue, `new ${textOf(e.expression, sourceFile)}`, e)];
|
|
855
|
+
}
|
|
856
|
+
if (ts.isIdentifier(e)) {
|
|
857
|
+
const of = caughtVariable(e);
|
|
858
|
+
if (of !== undefined)
|
|
859
|
+
return [{ k: "rethrow", of, maybe: walk.conditional, at: spanOf(e) }];
|
|
860
|
+
}
|
|
861
|
+
expression(e, walk);
|
|
862
|
+
return [unknownLeaf(REASON.thrownValue, `throw ${textOf(e, sourceFile)}`, e)];
|
|
863
|
+
};
|
|
864
|
+
// ---- statements -----------------------------------------------------------------------------
|
|
865
|
+
const statements = (nodes, walk) => {
|
|
866
|
+
for (const node of nodes)
|
|
867
|
+
statement(node, walk);
|
|
868
|
+
};
|
|
869
|
+
const region = (nodes, scope) => {
|
|
870
|
+
const sink = [];
|
|
871
|
+
statements(nodes, { scope, sink, conditional: false });
|
|
872
|
+
return sink;
|
|
873
|
+
};
|
|
874
|
+
const statement = (node, walk) => {
|
|
875
|
+
if (isFunctionBoundary(node))
|
|
876
|
+
return;
|
|
877
|
+
if (ts.isBlock(node)) {
|
|
878
|
+
statements(node.statements, walk);
|
|
879
|
+
}
|
|
880
|
+
else if (ts.isTryStatement(node)) {
|
|
881
|
+
walk.sink.push(tryOf(node, walk.scope));
|
|
882
|
+
}
|
|
883
|
+
else if (ts.isThrowStatement(node)) {
|
|
884
|
+
walk.sink.push(...thrown(node.expression, walk));
|
|
885
|
+
}
|
|
886
|
+
else if (ts.isReturnStatement(node)) {
|
|
887
|
+
if (node.expression !== undefined) {
|
|
888
|
+
walk.scope.returns.push(...flowing(node.expression, walk));
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
else if (ts.isExpressionStatement(node)) {
|
|
892
|
+
expression(node.expression, walk);
|
|
893
|
+
}
|
|
894
|
+
else if (ts.isVariableStatement(node)) {
|
|
895
|
+
const constant = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
|
|
896
|
+
for (const declaration of node.declarationList.declarations) {
|
|
897
|
+
if (declaration.initializer === undefined)
|
|
898
|
+
continue;
|
|
899
|
+
if (constant && ts.isIdentifier(declaration.name)) {
|
|
900
|
+
held.set(declaration, flowing(declaration.initializer, walk));
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
expression(declaration.initializer, walk);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
else if (ts.isIfStatement(node)) {
|
|
908
|
+
expression(node.expression, walk);
|
|
909
|
+
const branch = { ...walk, conditional: true };
|
|
910
|
+
statement(node.thenStatement, branch);
|
|
911
|
+
if (node.elseStatement !== undefined)
|
|
912
|
+
statement(node.elseStatement, branch);
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
const inner = { ...walk, conditional: true };
|
|
916
|
+
ts.forEachChild(node, (child) => {
|
|
917
|
+
visit(child, inner);
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
const tryOf = (node, scope) => ({
|
|
922
|
+
k: "try",
|
|
923
|
+
at: [node.getStart(sourceFile), node.tryBlock.getStart(sourceFile)],
|
|
924
|
+
body: region(node.tryBlock.statements, scope),
|
|
925
|
+
handler: node.catchClause === undefined ? null : handlerOf(node.catchClause, scope),
|
|
926
|
+
fin: node.finallyBlock === undefined ? [] : region(node.finallyBlock.statements, scope),
|
|
927
|
+
});
|
|
928
|
+
/** The classes an `if` test or `case` names with `instanceof` on the caught variable. */
|
|
929
|
+
const testOf = (condition, variable) => {
|
|
930
|
+
const e = stripParentheses(condition);
|
|
931
|
+
if (ts.isBinaryExpression(e)) {
|
|
932
|
+
const operator = e.operatorToken.kind;
|
|
933
|
+
if (operator === ts.SyntaxKind.InstanceOfKeyword) {
|
|
934
|
+
const subject = stripParentheses(e.left);
|
|
935
|
+
if (ts.isIdentifier(subject) && subject.text === variable) {
|
|
936
|
+
const id = classRef(stripParentheses(e.right));
|
|
937
|
+
return { classes: id === undefined ? [] : [id], exact: id !== undefined, tested: true };
|
|
938
|
+
}
|
|
939
|
+
return { classes: [], exact: false, tested: false };
|
|
940
|
+
}
|
|
941
|
+
if (operator === ts.SyntaxKind.BarBarToken ||
|
|
942
|
+
operator === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
943
|
+
const left = testOf(e.left, variable);
|
|
944
|
+
const right = testOf(e.right, variable);
|
|
945
|
+
return {
|
|
946
|
+
classes: [...left.classes, ...right.classes],
|
|
947
|
+
exact: operator === ts.SyntaxKind.BarBarToken && left.exact && right.exact,
|
|
948
|
+
tested: left.tested || right.tested,
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return { classes: [], exact: false, tested: false };
|
|
953
|
+
};
|
|
954
|
+
const exits = (node) => {
|
|
955
|
+
if (node === undefined)
|
|
956
|
+
return false;
|
|
957
|
+
if (ts.isBlock(node))
|
|
958
|
+
return exits(node.statements.at(-1));
|
|
959
|
+
return ts.isReturnStatement(node) || ts.isThrowStatement(node);
|
|
960
|
+
};
|
|
961
|
+
const throwsIn = (nodes) => nodes.some((node) => {
|
|
962
|
+
let found = false;
|
|
963
|
+
scanRunning(node, (inner) => {
|
|
964
|
+
if (ts.isThrowStatement(inner))
|
|
965
|
+
found = true;
|
|
966
|
+
});
|
|
967
|
+
return found;
|
|
968
|
+
});
|
|
969
|
+
/**
|
|
970
|
+
* Whether the statements use the caught variable declared at `of` (pass it on, log it, store
|
|
971
|
+
* it, read a property), nested functions included; an `instanceof` test of it is no use.
|
|
972
|
+
*/
|
|
973
|
+
const uses = (nodes, name, of) => {
|
|
974
|
+
const visit = (node) => {
|
|
975
|
+
if (ts.isBinaryExpression(node) &&
|
|
976
|
+
node.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword &&
|
|
977
|
+
ts.isIdentifier(stripParentheses(node.left))) {
|
|
978
|
+
return visit(node.right);
|
|
979
|
+
}
|
|
980
|
+
if (ts.isIdentifier(node))
|
|
981
|
+
return node.text === name && caughtVariable(node) === of;
|
|
982
|
+
return ts.forEachChild(node, (child) => (visit(child) ? true : undefined)) === true;
|
|
983
|
+
};
|
|
984
|
+
return nodes.some(visit);
|
|
985
|
+
};
|
|
986
|
+
const asStatements = (node) => ts.isBlock(node) ? node.statements : [node];
|
|
987
|
+
const regionOfParts = (nodes, scope) => {
|
|
988
|
+
const sink = [];
|
|
989
|
+
const walk = { scope, sink, conditional: false };
|
|
990
|
+
for (const node of nodes) {
|
|
991
|
+
if (ts.isExpression(node))
|
|
992
|
+
expression(node, walk);
|
|
993
|
+
else
|
|
994
|
+
statement(node, walk);
|
|
995
|
+
}
|
|
996
|
+
return sink;
|
|
997
|
+
};
|
|
998
|
+
/** A test of the caught variable, as `!(e instanceof A)`: the classes it excludes. */
|
|
999
|
+
const negatedTest = (condition, variable) => {
|
|
1000
|
+
const e = stripParentheses(condition);
|
|
1001
|
+
if (!ts.isPrefixUnaryExpression(e) || e.operator !== ts.SyntaxKind.ExclamationToken) {
|
|
1002
|
+
return undefined;
|
|
1003
|
+
}
|
|
1004
|
+
const test = testOf(e.operand, variable);
|
|
1005
|
+
return test.tested ? test : undefined;
|
|
1006
|
+
};
|
|
1007
|
+
/**
|
|
1008
|
+
* An `if` / `else if` chain or a `switch (true)` that tests the caught variable with
|
|
1009
|
+
* `instanceof` at least once: its links and its final `else` or `default`.
|
|
1010
|
+
*/
|
|
1011
|
+
const dispatchOf = (node, variable, following) => {
|
|
1012
|
+
const links = [];
|
|
1013
|
+
let otherwise;
|
|
1014
|
+
let tested = false;
|
|
1015
|
+
let guard = false;
|
|
1016
|
+
const negated = ts.isIfStatement(node) ? negatedTest(node.expression, variable) : undefined;
|
|
1017
|
+
if (ts.isIfStatement(node) && negated !== undefined) {
|
|
1018
|
+
// Only a guard that leaves the catch: what follows it runs for the named classes alone.
|
|
1019
|
+
if (node.elseStatement !== undefined || !exits(node.thenStatement))
|
|
1020
|
+
return undefined;
|
|
1021
|
+
tested = true;
|
|
1022
|
+
guard = true;
|
|
1023
|
+
links.push({ ...negated, statements: following });
|
|
1024
|
+
otherwise = asStatements(node.thenStatement);
|
|
1025
|
+
}
|
|
1026
|
+
else if (ts.isIfStatement(node)) {
|
|
1027
|
+
let link = node;
|
|
1028
|
+
while (link !== undefined) {
|
|
1029
|
+
const test = testOf(link.expression, variable);
|
|
1030
|
+
tested ||= test.tested;
|
|
1031
|
+
links.push({ ...test, statements: asStatements(link.thenStatement) });
|
|
1032
|
+
const rest = link.elseStatement;
|
|
1033
|
+
link = undefined;
|
|
1034
|
+
if (rest !== undefined && ts.isIfStatement(rest))
|
|
1035
|
+
link = rest;
|
|
1036
|
+
else if (rest !== undefined)
|
|
1037
|
+
otherwise = asStatements(rest);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
else if (ts.isSwitchStatement(node) &&
|
|
1041
|
+
stripParentheses(node.expression).kind === ts.SyntaxKind.TrueKeyword) {
|
|
1042
|
+
// Cases with no statements fall through to the next: their classes join it.
|
|
1043
|
+
let pending;
|
|
1044
|
+
for (const clause of node.caseBlock.clauses) {
|
|
1045
|
+
if (ts.isDefaultClause(clause)) {
|
|
1046
|
+
otherwise = clause.statements;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
const test = testOf(clause.expression, variable);
|
|
1050
|
+
tested ||= test.tested;
|
|
1051
|
+
const joined = {
|
|
1052
|
+
classes: [...(pending?.classes ?? []), ...test.classes],
|
|
1053
|
+
exact: (pending?.exact ?? true) && test.exact,
|
|
1054
|
+
};
|
|
1055
|
+
if (clause.statements.length === 0) {
|
|
1056
|
+
pending = joined;
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
pending = undefined;
|
|
1060
|
+
links.push({ ...joined, statements: clause.statements });
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
else if (ts.isReturnStatement(node) || ts.isExpressionStatement(node)) {
|
|
1064
|
+
const value = node.expression === undefined ? undefined : stripParentheses(node.expression);
|
|
1065
|
+
let cond = value;
|
|
1066
|
+
while (cond !== undefined && ts.isConditionalExpression(cond)) {
|
|
1067
|
+
const test = testOf(cond.condition, variable);
|
|
1068
|
+
tested ||= test.tested;
|
|
1069
|
+
links.push({ ...test, statements: [cond.whenTrue] });
|
|
1070
|
+
const rest = stripParentheses(cond.whenFalse);
|
|
1071
|
+
if (ts.isConditionalExpression(rest)) {
|
|
1072
|
+
cond = rest;
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
otherwise = [rest];
|
|
1076
|
+
cond = undefined;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (links.length === 0 &&
|
|
1080
|
+
value !== undefined &&
|
|
1081
|
+
ts.isBinaryExpression(value) &&
|
|
1082
|
+
value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
1083
|
+
const test = testOf(value.left, variable);
|
|
1084
|
+
tested = test.tested;
|
|
1085
|
+
links.push({ ...test, statements: [value.right] });
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return tested
|
|
1089
|
+
? { links, otherwise, kind: ts.isSwitchStatement(node) ? "switch" : "if", guard }
|
|
1090
|
+
: undefined;
|
|
1091
|
+
};
|
|
1092
|
+
const handlerOf = (clause, scope) => {
|
|
1093
|
+
const declaration = clause.variableDeclaration;
|
|
1094
|
+
const variable = declaration !== undefined && ts.isIdentifier(declaration.name) ? declaration.name.text : null;
|
|
1095
|
+
const of = declaration === undefined ? -1 : declaration.getStart(sourceFile);
|
|
1096
|
+
const at = [clause.getStart(sourceFile), clause.getStart(sourceFile) + "catch".length];
|
|
1097
|
+
const body = clause.block.statements;
|
|
1098
|
+
const block = [clause.block.getStart(sourceFile), clause.block.getEnd()];
|
|
1099
|
+
const plain = () => ({
|
|
1100
|
+
at,
|
|
1101
|
+
block,
|
|
1102
|
+
of,
|
|
1103
|
+
variable,
|
|
1104
|
+
dispatch: "if",
|
|
1105
|
+
pre: [],
|
|
1106
|
+
branches: [],
|
|
1107
|
+
otherwise: null,
|
|
1108
|
+
otherwiseExits: false,
|
|
1109
|
+
after: region(body, scope),
|
|
1110
|
+
restThrows: throwsIn(body),
|
|
1111
|
+
restUsed: false,
|
|
1112
|
+
});
|
|
1113
|
+
if (variable === null)
|
|
1114
|
+
return plain();
|
|
1115
|
+
for (const [index, node] of body.entries()) {
|
|
1116
|
+
const dispatch = dispatchOf(node, variable, body.slice(index + 1));
|
|
1117
|
+
if (dispatch === undefined)
|
|
1118
|
+
continue;
|
|
1119
|
+
const branches = dispatch.links.map((link) => ({
|
|
1120
|
+
classes: link.classes,
|
|
1121
|
+
exact: link.exact,
|
|
1122
|
+
body: regionOfParts(link.statements, scope),
|
|
1123
|
+
exits: exits(link.statements.at(-1)),
|
|
1124
|
+
}));
|
|
1125
|
+
const otherwise = dispatch.otherwise;
|
|
1126
|
+
const otherwiseExits = exits(otherwise?.at(-1));
|
|
1127
|
+
const following = dispatch.guard ? [] : body.slice(index + 1);
|
|
1128
|
+
return {
|
|
1129
|
+
at,
|
|
1130
|
+
block,
|
|
1131
|
+
of,
|
|
1132
|
+
variable,
|
|
1133
|
+
dispatch: dispatch.kind,
|
|
1134
|
+
pre: region(body.slice(0, index), scope),
|
|
1135
|
+
branches,
|
|
1136
|
+
otherwise: otherwise === undefined ? null : regionOfParts(otherwise, scope),
|
|
1137
|
+
otherwiseExits,
|
|
1138
|
+
after: region(following, scope),
|
|
1139
|
+
restThrows: (otherwise !== undefined && throwsIn(otherwise)) ||
|
|
1140
|
+
((otherwise === undefined || !otherwiseExits) && throwsIn(following)),
|
|
1141
|
+
restUsed: (otherwise !== undefined && uses(otherwise, variable, of)) ||
|
|
1142
|
+
((otherwise === undefined || !otherwiseExits) && uses(following, variable, of)),
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
return plain();
|
|
1146
|
+
};
|
|
1147
|
+
// ---- @throws --------------------------------------------------------------------------------
|
|
1148
|
+
const docOf = (...nodes) => {
|
|
1149
|
+
const tags = new Set(nodes.flatMap((node) => ts.getJSDocTags(node)));
|
|
1150
|
+
const entries = [];
|
|
1151
|
+
const untyped = [];
|
|
1152
|
+
let found = false;
|
|
1153
|
+
const add = (type) => {
|
|
1154
|
+
if (ts.isParenthesizedTypeNode(type)) {
|
|
1155
|
+
add(type.type);
|
|
1156
|
+
}
|
|
1157
|
+
else if (ts.isUnionTypeNode(type)) {
|
|
1158
|
+
for (const member of type.types)
|
|
1159
|
+
add(member);
|
|
1160
|
+
}
|
|
1161
|
+
else if (type.kind !== ts.SyntaxKind.NeverKeyword) {
|
|
1162
|
+
const id = ts.isTypeReferenceNode(type) ? classRef(type.typeName) : undefined;
|
|
1163
|
+
entries.push({
|
|
1164
|
+
id: id ?? null,
|
|
1165
|
+
name: shorten(oneLine(type.getText(sourceFile)), TEXT_MAX),
|
|
1166
|
+
at: spanOf(type),
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
for (const tag of tags) {
|
|
1171
|
+
if (!ts.isJSDocThrowsTag(tag))
|
|
1172
|
+
continue;
|
|
1173
|
+
found = true;
|
|
1174
|
+
const type = tag.typeExpression?.type;
|
|
1175
|
+
if (type === undefined)
|
|
1176
|
+
untyped.push(spanOf(tag));
|
|
1177
|
+
else
|
|
1178
|
+
add(type);
|
|
1179
|
+
}
|
|
1180
|
+
return found ? { entries, untyped } : null;
|
|
1181
|
+
};
|
|
1182
|
+
// ---- functions ------------------------------------------------------------------------------
|
|
1183
|
+
const functions = [];
|
|
1184
|
+
/** Function expressions that are a named declaration's value, summarized under its name. */
|
|
1185
|
+
const named = new Set();
|
|
1186
|
+
const factOf = (name, declaration, fn, async) => {
|
|
1187
|
+
// A function without a name (a handler, a callback) has no id: nothing calls it by name,
|
|
1188
|
+
// but its catches are checked (`ANONYMOUS`).
|
|
1189
|
+
const symbol = name === undefined ? { id: ANONYMOUS, name: ANONYMOUS } : types.symbolAt(name);
|
|
1190
|
+
const body = fn.body;
|
|
1191
|
+
if (symbol === undefined || body === undefined)
|
|
1192
|
+
return;
|
|
1193
|
+
const scope = newScope(async);
|
|
1194
|
+
const sink = [];
|
|
1195
|
+
const walk = { scope, sink, conditional: false };
|
|
1196
|
+
if (ts.isBlock(body))
|
|
1197
|
+
statement(body, walk);
|
|
1198
|
+
else
|
|
1199
|
+
scope.returns.push(...flowing(body, walk));
|
|
1200
|
+
const doc = name === undefined ? null : docOf(declaration, fn);
|
|
1201
|
+
functions.push({
|
|
1202
|
+
id: symbol.id,
|
|
1203
|
+
name: symbol.name,
|
|
1204
|
+
at: spanOf(name ?? fn),
|
|
1205
|
+
async,
|
|
1206
|
+
// Asked only when a `@throws` needs it: that is the one place it changes the result.
|
|
1207
|
+
promise: async || (doc !== null && types.returnsPromiseLike(fn)),
|
|
1208
|
+
doc,
|
|
1209
|
+
local: (ts.isVariableDeclaration(declaration) || ts.isFunctionDeclaration(declaration)) &&
|
|
1210
|
+
ts.findAncestor(declaration.parent, ts.isFunctionLike) !== undefined,
|
|
1211
|
+
body: sink,
|
|
1212
|
+
returns: scope.returns,
|
|
1213
|
+
});
|
|
1214
|
+
};
|
|
1215
|
+
/**
|
|
1216
|
+
* A function declared without a body (`declare function`, a `.d.ts`, overloads with no
|
|
1217
|
+
* implementation) that has `@throws`: a fact with no code and the tags of all its signatures,
|
|
1218
|
+
* made once, at the first of them.
|
|
1219
|
+
*/
|
|
1220
|
+
const bodylessFact = (name, node) => {
|
|
1221
|
+
const symbol = types.symbolAt(name);
|
|
1222
|
+
const doc = docOf(...signaturesOf(node));
|
|
1223
|
+
if (symbol === undefined || doc === null)
|
|
1224
|
+
return;
|
|
1225
|
+
functions.push({
|
|
1226
|
+
id: symbol.id,
|
|
1227
|
+
name: symbol.name,
|
|
1228
|
+
at: spanOf(name),
|
|
1229
|
+
async: false,
|
|
1230
|
+
promise: types.returnsPromiseLike(node),
|
|
1231
|
+
doc,
|
|
1232
|
+
local: false,
|
|
1233
|
+
body: [],
|
|
1234
|
+
returns: [],
|
|
1235
|
+
});
|
|
1236
|
+
};
|
|
1237
|
+
const find = (node) => {
|
|
1238
|
+
if ((ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
|
|
1239
|
+
node.asteriskToken === undefined &&
|
|
1240
|
+
!named.has(node)) {
|
|
1241
|
+
factOf(undefined, node, node, isAsync(node));
|
|
1242
|
+
}
|
|
1243
|
+
else if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
|
|
1244
|
+
if (node.name !== undefined && node.body !== undefined && node.asteriskToken === undefined) {
|
|
1245
|
+
factOf(node.name, node, node, isAsync(node));
|
|
1246
|
+
}
|
|
1247
|
+
else if (node.name !== undefined &&
|
|
1248
|
+
node.body === undefined &&
|
|
1249
|
+
node.asteriskToken === undefined &&
|
|
1250
|
+
!hasModifier(node, ts.SyntaxKind.AbstractKeyword) &&
|
|
1251
|
+
implementationOf(node) === undefined &&
|
|
1252
|
+
hasThrowsTag(node) &&
|
|
1253
|
+
signaturesOf(node)[0] === node) {
|
|
1254
|
+
bodylessFact(node.name, node);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
else if (ts.isBinaryExpression(node) &&
|
|
1258
|
+
ts.isPropertyAccessExpression(node.left) &&
|
|
1259
|
+
ts.isIdentifier(node.left.name)) {
|
|
1260
|
+
const assigned = assignedFunction(node);
|
|
1261
|
+
// Named only when the property resolves; otherwise it stays a function without a name.
|
|
1262
|
+
if (assigned !== undefined &&
|
|
1263
|
+
assigned.asteriskToken === undefined &&
|
|
1264
|
+
types.symbolAt(node.left.name) !== undefined) {
|
|
1265
|
+
named.add(assigned);
|
|
1266
|
+
factOf(node.left.name, node, assigned, isAsync(assigned));
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
else if (ts.isConstructorDeclaration(node)) {
|
|
1270
|
+
const owner = node.parent;
|
|
1271
|
+
if (owner.name !== undefined && node.body !== undefined) {
|
|
1272
|
+
factOf(owner.name, node, node, false);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
else {
|
|
1276
|
+
const initializer = functionInitializer(node);
|
|
1277
|
+
if (initializer !== undefined &&
|
|
1278
|
+
(ts.isVariableDeclaration(node) ||
|
|
1279
|
+
ts.isPropertyAssignment(node) ||
|
|
1280
|
+
ts.isPropertyDeclaration(node)) &&
|
|
1281
|
+
(ts.isIdentifier(node.name) || ts.isPrivateIdentifier(node.name)) &&
|
|
1282
|
+
!(ts.isFunctionExpression(initializer) && initializer.asteriskToken !== undefined)) {
|
|
1283
|
+
named.add(initializer);
|
|
1284
|
+
factOf(node.name, node, initializer, isAsync(initializer));
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
ts.forEachChild(node, find);
|
|
1288
|
+
};
|
|
1289
|
+
// The top level first: a `const` promise made there is known to the functions that use it.
|
|
1290
|
+
const module = region(sourceFile.statements, newScope(false));
|
|
1291
|
+
ts.forEachChild(sourceFile, find);
|
|
1292
|
+
return {
|
|
1293
|
+
classes: [...classes.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
|
|
1294
|
+
functions,
|
|
1295
|
+
module,
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
/** What a name resolves to; undefined when it resolves to nothing. */
|
|
1299
|
+
function resolveWith(types, name) {
|
|
1300
|
+
const symbol = types.symbolAt(name);
|
|
1301
|
+
const declaration = symbol === undefined ? undefined : types.declarationOf(symbol);
|
|
1302
|
+
if (symbol === undefined || declaration === undefined)
|
|
1303
|
+
return undefined;
|
|
1304
|
+
const node = declaration.node;
|
|
1305
|
+
if (!types.isOwnFile(declaration.file)) {
|
|
1306
|
+
if (isPlatform(node, declaration.file)) {
|
|
1307
|
+
const parent = node.parent;
|
|
1308
|
+
const owner = (ts.isMethodSignature(node) || ts.isPropertySignature(node)) &&
|
|
1309
|
+
ts.isInterfaceDeclaration(parent)
|
|
1310
|
+
? parent.name.text
|
|
1311
|
+
: null;
|
|
1312
|
+
return { kind: "platform", owner, name: symbol.name };
|
|
1313
|
+
}
|
|
1314
|
+
return {
|
|
1315
|
+
kind: "package",
|
|
1316
|
+
name: ambientModuleName(node) ?? packageNameOf(types, declaration.file),
|
|
1317
|
+
id: symbol.id,
|
|
1318
|
+
node,
|
|
1319
|
+
file: declaration.file,
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
return ownKind(node, symbol.id);
|
|
1323
|
+
}
|
|
1324
|
+
//# sourceMappingURL=summarize.js.map
|