@cplieger/deadset-ts 8.0.0-dev.2 → 8.0.0-dev.4
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/README.md +1 -1
- package/dist/src/analysis.js +61 -10
- package/dist/src/augmentations.js +63 -1
- package/dist/src/build-scripts.js +437 -0
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +56 -9
- package/dist/src/convention-rows.js +39 -2
- package/dist/src/conventions.js +15 -0
- package/dist/src/dependencies.js +98 -1
- package/dist/src/discover.js +14 -82
- package/dist/src/entry-points.js +9 -18
- package/dist/src/exempt.js +8 -0
- package/dist/src/findings/completion.js +66 -28
- package/dist/src/findings/intra-function.js +4 -2
- package/dist/src/findings/reads-and-writes.js +7 -6
- package/dist/src/findings/unused-declarations.js +4 -1
- package/dist/src/graph.js +5 -3
- package/dist/src/intra-function-parts.js +45 -17
- package/dist/src/kinds.js +8 -8
- package/dist/src/manifest.js +18 -0
- package/dist/src/matrix.js +38 -4
- package/dist/src/query.js +1 -0
- package/dist/src/report.js +23 -4
- package/dist/src/reporters/annotations.js +15 -8
- package/dist/src/reporters/sarif.js +12 -8
- package/dist/src/roots.js +197 -42
- package/dist/src/serialization-contract.js +590 -12
- package/dist/src/sweep.js +7 -9
- package/dist/src/test-globals.js +234 -0
- package/dist/src/type-errors.js +11 -3
- package/dist/src/unseen-loads.js +107 -0
- package/dist/src/verbs/analyze.js +24 -3
- package/dist/src/workflow-steps.js +32 -15
- package/dist/src/yaml-document.js +355 -0
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +109 -12
- package/src/augmentations.ts +77 -2
- package/src/build-scripts.ts +564 -0
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +64 -12
- package/src/convention-rows.ts +47 -2
- package/src/conventions.ts +16 -0
- package/src/dependencies.ts +110 -1
- package/src/discover.ts +14 -94
- package/src/entry-points.ts +10 -25
- package/src/exempt.ts +16 -0
- package/src/file-facts.ts +3 -0
- package/src/findings/boundary.ts +2 -0
- package/src/findings/completion.ts +76 -30
- package/src/findings/intra-function.ts +4 -2
- package/src/findings/reads-and-writes.ts +7 -6
- package/src/findings/unused-declarations.ts +6 -1
- package/src/graph.ts +5 -3
- package/src/intra-function-parts.ts +55 -16
- package/src/kinds.ts +8 -8
- package/src/manifest.ts +21 -3
- package/src/matrix.ts +51 -4
- package/src/query.ts +11 -0
- package/src/report.ts +56 -16
- package/src/reporters/annotations.ts +21 -10
- package/src/reporters/reporter.ts +1 -3
- package/src/reporters/sarif.ts +16 -8
- package/src/roots.ts +226 -46
- package/src/serialization-contract.ts +758 -12
- package/src/sweep.ts +23 -9
- package/src/test-globals.ts +275 -0
- package/src/type-errors.ts +14 -3
- package/src/unseen-loads.ts +145 -0
- package/src/verbs/analyze.ts +28 -3
- package/src/workflow-steps.ts +35 -17
- package/src/yaml-document.ts +377 -0
|
@@ -11,26 +11,36 @@ import {
|
|
|
11
11
|
isArrayLiteralExpression,
|
|
12
12
|
isArrowFunction,
|
|
13
13
|
isAsExpression,
|
|
14
|
+
isAwaitExpression,
|
|
14
15
|
isCallExpression,
|
|
15
16
|
isClassDeclaration,
|
|
16
17
|
isClassExpression,
|
|
17
18
|
isClassStaticBlockDeclaration,
|
|
19
|
+
isElementAccessExpression,
|
|
20
|
+
isExpressionStatement,
|
|
18
21
|
isFunctionDeclaration,
|
|
19
22
|
isFunctionExpression,
|
|
23
|
+
isFunctionLikeDeclaration,
|
|
20
24
|
isGetAccessorDeclaration,
|
|
21
25
|
isIdentifier,
|
|
22
26
|
isInterfaceDeclaration,
|
|
23
27
|
isMethodDeclaration,
|
|
28
|
+
isNonNullExpression,
|
|
24
29
|
isNoSubstitutionTemplateLiteral,
|
|
25
30
|
isNumericLiteral,
|
|
26
31
|
isObjectLiteralExpression,
|
|
27
32
|
isParenthesizedExpression,
|
|
33
|
+
isForInStatement,
|
|
28
34
|
isPropertyAccessExpression,
|
|
29
35
|
isPropertyDeclaration,
|
|
36
|
+
isReturnStatement,
|
|
30
37
|
isSatisfiesExpression,
|
|
31
38
|
isSetAccessorDeclaration,
|
|
39
|
+
isSourceFile,
|
|
40
|
+
isSpreadAssignment,
|
|
32
41
|
isSpreadElement,
|
|
33
42
|
isStringLiteral,
|
|
43
|
+
isTypeAssertion,
|
|
34
44
|
isTypeLiteralNode,
|
|
35
45
|
isVariableDeclaration,
|
|
36
46
|
ModifierFlags,
|
|
@@ -40,6 +50,8 @@ import {
|
|
|
40
50
|
type Expression,
|
|
41
51
|
type Node,
|
|
42
52
|
type ParameterDeclaration,
|
|
53
|
+
type SourceFile,
|
|
54
|
+
type SpreadAssignment,
|
|
43
55
|
} from "@typescript/native/ast";
|
|
44
56
|
import {
|
|
45
57
|
SignatureKind,
|
|
@@ -62,8 +74,8 @@ import {
|
|
|
62
74
|
} from "./configured-declarations.ts";
|
|
63
75
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
64
76
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
65
|
-
import { renderPosition } from "./position.ts";
|
|
66
|
-
import { must } from "./query.ts";
|
|
77
|
+
import { renderPosition, type Position } from "./position.ts";
|
|
78
|
+
import { must, Unanswerable } from "./query.ts";
|
|
67
79
|
import type { ProjectView } from "./session.ts";
|
|
68
80
|
import { erasedStores, type ErasedStores } from "./erased-stores.ts";
|
|
69
81
|
import { checkedTypes, CONVERSION_MEMBERS, valueReach, type Reached } from "./value-reach.ts";
|
|
@@ -80,6 +92,9 @@ const SERIALIZED_CONVERSION = "toJSON";
|
|
|
80
92
|
/** The parameter types that keep nothing of the value they are given. */
|
|
81
93
|
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
82
94
|
|
|
95
|
+
/** The standard library's promise types, whose value is their one type argument. */
|
|
96
|
+
const PROMISES: ReadonlySet<string> = new Set(["Promise", "PromiseLike"]);
|
|
97
|
+
|
|
83
98
|
/** How deep a callee's result is searched for a type parameter: `Promise<Box<T>>` is two levels. */
|
|
84
99
|
const MENTION_DEPTH = 3;
|
|
85
100
|
|
|
@@ -259,6 +274,8 @@ function throughMethod(call: CallExpression): MethodForm | undefined {
|
|
|
259
274
|
interface Walked {
|
|
260
275
|
readonly sites: readonly Site[];
|
|
261
276
|
readonly parameters: readonly Parameter[];
|
|
277
|
+
/** The class properties whose declaration writes an initializer, by identifier. */
|
|
278
|
+
readonly initialized: ReadonlySet<string>;
|
|
262
279
|
}
|
|
263
280
|
|
|
264
281
|
/**
|
|
@@ -269,9 +286,16 @@ interface Walked {
|
|
|
269
286
|
function walkProject<Brand>(project: ProjectView<Brand>, held: Inventory): Walked {
|
|
270
287
|
const sites: Site[] = [];
|
|
271
288
|
const parameters: Parameter[] = [];
|
|
289
|
+
const initialized = new Set<string>();
|
|
272
290
|
for (const file of project.ownSourceFiles()) {
|
|
273
291
|
const visit = (node: Node, self: string | undefined): void => {
|
|
274
292
|
let inner = self;
|
|
293
|
+
if (isPropertyDeclaration(node) && node.initializer !== undefined) {
|
|
294
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
295
|
+
if (id !== undefined) {
|
|
296
|
+
initialized.add(id);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
275
299
|
if (isClassDeclaration(node) || isClassExpression(node)) {
|
|
276
300
|
inner = held.declarations.get(nodeKey(file, node));
|
|
277
301
|
} else if (resetsThis(node)) {
|
|
@@ -304,12 +328,38 @@ function walkProject<Brand>(project: ProjectView<Brand>, held: Inventory): Walke
|
|
|
304
328
|
visit(child, undefined);
|
|
305
329
|
});
|
|
306
330
|
}
|
|
307
|
-
return { sites, parameters };
|
|
331
|
+
return { sites, parameters, initialized };
|
|
308
332
|
}
|
|
309
333
|
|
|
334
|
+
/** The calls of `Object` that read every own property of their argument by name. */
|
|
335
|
+
const ENUMERATIONS: ReadonlySet<string> = new Set([
|
|
336
|
+
"keys",
|
|
337
|
+
"entries",
|
|
338
|
+
"values",
|
|
339
|
+
"getOwnPropertyNames",
|
|
340
|
+
"assign",
|
|
341
|
+
]);
|
|
342
|
+
|
|
310
343
|
/**
|
|
311
|
-
*
|
|
312
|
-
* `any`,
|
|
344
|
+
* Whether a parameter type keeps no member name of the value it is given beyond the
|
|
345
|
+
* properties it names: `unknown`, `any`, or an object type of a string index signature,
|
|
346
|
+
* under which every other member of the value is held.
|
|
347
|
+
*/
|
|
348
|
+
function keepsNoName<Brand>(project: ProjectView<Brand>, type: Type): boolean {
|
|
349
|
+
if ((type.flags & ERASED) !== 0) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
if ((type.flags & TypeFlags.Object) === 0) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
return must(project.queries.indexInfosOf(type)).some(
|
|
356
|
+
(info) => (info.keyType.flags & TypeFlags.String) !== 0,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* The parameters that keep no member name of the value they are given, as `keepsNoName`
|
|
362
|
+
* reads it, and a rest parameter whose elements keep none, each by its symbol.
|
|
313
363
|
*/
|
|
314
364
|
function erasedParameters<Brand>(
|
|
315
365
|
project: ProjectView<Brand>,
|
|
@@ -325,10 +375,10 @@ function erasedParameters<Brand>(
|
|
|
325
375
|
return false;
|
|
326
376
|
}
|
|
327
377
|
if (!parameter.rest) {
|
|
328
|
-
return (type
|
|
378
|
+
return keepsNoName(project, type);
|
|
329
379
|
}
|
|
330
380
|
const [element] = must(project.queries.typeArguments(type));
|
|
331
|
-
return element !== undefined && (element
|
|
381
|
+
return element !== undefined && keepsNoName(project, element);
|
|
332
382
|
});
|
|
333
383
|
const bySymbol = new Map<number, Parameter>();
|
|
334
384
|
project
|
|
@@ -343,6 +393,124 @@ function erasedParameters<Brand>(
|
|
|
343
393
|
return bySymbol;
|
|
344
394
|
}
|
|
345
395
|
|
|
396
|
+
/** Whether one node is a function, whose body runs only where something calls it. */
|
|
397
|
+
function isFunctionNode(node: Node): boolean {
|
|
398
|
+
return (
|
|
399
|
+
isFunctionDeclaration(node) ||
|
|
400
|
+
isFunctionExpression(node) ||
|
|
401
|
+
isArrowFunction(node) ||
|
|
402
|
+
isMethodDeclaration(node) ||
|
|
403
|
+
isGetAccessorDeclaration(node) ||
|
|
404
|
+
isSetAccessorDeclaration(node)
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** The name a function nested in another is called by: its own, or the variable it initializes. */
|
|
409
|
+
function nestedName(fn: Node): string | undefined {
|
|
410
|
+
if (isFunctionDeclaration(fn)) {
|
|
411
|
+
return fn.name?.text;
|
|
412
|
+
}
|
|
413
|
+
let at = fn.parent;
|
|
414
|
+
while (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
|
|
415
|
+
at = at.parent;
|
|
416
|
+
}
|
|
417
|
+
return isVariableDeclaration(at) && isIdentifier(at.name) ? at.name.text : undefined;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** The names one function's body writes as identifiers, other than the names its declarations bind. */
|
|
421
|
+
function namesWritten(fn: Node): ReadonlySet<string> {
|
|
422
|
+
const found = new Set<string>();
|
|
423
|
+
const visit = (node: Node): void => {
|
|
424
|
+
const declares =
|
|
425
|
+
(isFunctionDeclaration(node.parent) || isVariableDeclaration(node.parent)) &&
|
|
426
|
+
node.parent.name === node;
|
|
427
|
+
if (isIdentifier(node) && !declares) {
|
|
428
|
+
found.add(node.text);
|
|
429
|
+
}
|
|
430
|
+
node.forEachChild(visit);
|
|
431
|
+
};
|
|
432
|
+
fn.forEachChild(visit);
|
|
433
|
+
return found;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The erased parameters their own function enumerates: one an `Object.keys`, `entries`,
|
|
438
|
+
* `values`, `getOwnPropertyNames` or `assign` call takes, `Object` the standard library's,
|
|
439
|
+
* or a `for…in` statement walks, which reads every property of the value by its name. A
|
|
440
|
+
* nested function is read as the function's own only where its name is written in it, or
|
|
441
|
+
* it has none, a callback or a function called where it is written.
|
|
442
|
+
*/
|
|
443
|
+
function enumeratedParameters<Brand>(
|
|
444
|
+
project: ProjectView<Brand>,
|
|
445
|
+
erased: ReadonlyMap<number, Parameter>,
|
|
446
|
+
): readonly Parameter[] {
|
|
447
|
+
const asked: {
|
|
448
|
+
readonly node: Node;
|
|
449
|
+
readonly parameter: Parameter;
|
|
450
|
+
readonly object: Node | undefined;
|
|
451
|
+
}[] = [];
|
|
452
|
+
for (const parameter of erased.values()) {
|
|
453
|
+
const fn = parameter.name.parent.parent;
|
|
454
|
+
const spelled = parameter.name.getText();
|
|
455
|
+
const written = namesWritten(fn);
|
|
456
|
+
const visit = (node: Node): void => {
|
|
457
|
+
if (isFunctionNode(node)) {
|
|
458
|
+
const name = nestedName(node);
|
|
459
|
+
if (name !== undefined && !written.has(name)) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const read: Expression[] = [];
|
|
464
|
+
let object: Node | undefined;
|
|
465
|
+
if (isCallExpression(node)) {
|
|
466
|
+
const callee = node.expression;
|
|
467
|
+
if (
|
|
468
|
+
isPropertyAccessExpression(callee) &&
|
|
469
|
+
isIdentifier(callee.expression) &&
|
|
470
|
+
callee.expression.text === "Object" &&
|
|
471
|
+
ENUMERATIONS.has(callee.name.text)
|
|
472
|
+
) {
|
|
473
|
+
object = callee.expression;
|
|
474
|
+
// `Object.assign` reads its sources, every argument after the target.
|
|
475
|
+
read.push(...node.arguments.slice(...(callee.name.text === "assign" ? [1] : [0, 1])));
|
|
476
|
+
}
|
|
477
|
+
} else if (isForInStatement(node)) {
|
|
478
|
+
read.push(node.expression);
|
|
479
|
+
}
|
|
480
|
+
for (const value of read.map(evaluated)) {
|
|
481
|
+
if (isIdentifier(value) && value.text === spelled) {
|
|
482
|
+
asked.push({ node: value, parameter, object });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
node.forEachChild(visit);
|
|
486
|
+
};
|
|
487
|
+
fn.forEachChild(visit);
|
|
488
|
+
}
|
|
489
|
+
const symbols = project.symbolsAt(asked.map((one) => project.handle(one.node))).map(must);
|
|
490
|
+
const objects = asked.flatMap((one) => (one.object === undefined ? [] : [one.object]));
|
|
491
|
+
const own = project.ownPaths();
|
|
492
|
+
const standard = new Set(
|
|
493
|
+
project
|
|
494
|
+
.symbolsAt(objects.map((node) => project.handle(node)))
|
|
495
|
+
.map(must)
|
|
496
|
+
.flatMap((symbol, at) =>
|
|
497
|
+
symbol?.declarations.every((handle) => !own.has(handle.path)) === true ? [objects[at]] : [],
|
|
498
|
+
),
|
|
499
|
+
);
|
|
500
|
+
return [
|
|
501
|
+
...new Set(
|
|
502
|
+
asked.flatMap((one, at) => {
|
|
503
|
+
const symbol = symbols[at];
|
|
504
|
+
return symbol !== undefined &&
|
|
505
|
+
erased.get(symbol.id) === one.parameter &&
|
|
506
|
+
(one.object === undefined || standard.has(one.object))
|
|
507
|
+
? [one.parameter]
|
|
508
|
+
: [];
|
|
509
|
+
}),
|
|
510
|
+
),
|
|
511
|
+
];
|
|
512
|
+
}
|
|
513
|
+
|
|
346
514
|
/** Each place a call's argument passes one of the erased parameters it can see. */
|
|
347
515
|
function handedOn<Brand>(
|
|
348
516
|
project: ProjectView<Brand>,
|
|
@@ -452,8 +620,8 @@ interface Destinations {
|
|
|
452
620
|
* callee is a serializer an entry names; at the arguments its resolved signature types
|
|
453
621
|
* `unknown` or `any` where its callee is declared outside the analysis or only ambiently
|
|
454
622
|
* with no implementation in it; and, where its callee is a function of the project, at each
|
|
455
|
-
* parameter the function hands on to a destination, holding back the widest
|
|
456
|
-
*
|
|
623
|
+
* parameter the function enumerates or hands on to a destination, holding back the widest
|
|
624
|
+
* of what those hold back, settled by widening until no parameter moves.
|
|
457
625
|
*/
|
|
458
626
|
function destinations<Brand>(
|
|
459
627
|
input: DetectorInput<Brand>,
|
|
@@ -674,6 +842,9 @@ function destinations<Brand>(
|
|
|
674
842
|
return undefined;
|
|
675
843
|
};
|
|
676
844
|
|
|
845
|
+
for (const parameter of enumeratedParameters(project, erased)) {
|
|
846
|
+
forwards.set(keyOf(parameter), "properties");
|
|
847
|
+
}
|
|
677
848
|
const forwarded = handedOn(project, walked.sites, erased, invocation);
|
|
678
849
|
for (let moved = true; moved;) {
|
|
679
850
|
moved = false;
|
|
@@ -776,6 +947,19 @@ function flowsOf<Brand>(
|
|
|
776
947
|
},
|
|
777
948
|
];
|
|
778
949
|
});
|
|
950
|
+
const generic = typeParameterFlows(
|
|
951
|
+
project,
|
|
952
|
+
reach,
|
|
953
|
+
sites,
|
|
954
|
+
arguments_.flatMap((one) => {
|
|
955
|
+
const type = typeOf.get(one.value);
|
|
956
|
+
const parameters = type === undefined ? [] : typeParametersOf(project, type, 0);
|
|
957
|
+
const destination = parameters.length === 0 ? undefined : to.at(one.site.call, one.argument);
|
|
958
|
+
return destination === undefined
|
|
959
|
+
? []
|
|
960
|
+
: parameters.map((parameter) => ({ parameter, to: destination }));
|
|
961
|
+
}),
|
|
962
|
+
);
|
|
779
963
|
const left = stores.outside.flatMap((store) => {
|
|
780
964
|
const reached = reach.reachedFrom(store.type);
|
|
781
965
|
return reached.classes.size === 0 && reached.members.length === 0
|
|
@@ -790,7 +974,178 @@ function flowsOf<Brand>(
|
|
|
790
974
|
},
|
|
791
975
|
];
|
|
792
976
|
});
|
|
793
|
-
return [...passed, ...left];
|
|
977
|
+
return [...passed, ...generic, ...left];
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** The type parameters one type names, through constituents and type arguments. */
|
|
981
|
+
function typeParametersOf<Brand>(project: ProjectView<Brand>, type: Type, depth: number): Type[] {
|
|
982
|
+
if (type.isTypeParameter()) {
|
|
983
|
+
return [type];
|
|
984
|
+
}
|
|
985
|
+
if (depth >= MENTION_DEPTH) {
|
|
986
|
+
return [];
|
|
987
|
+
}
|
|
988
|
+
const { queries } = project;
|
|
989
|
+
return [
|
|
990
|
+
...(type.isUnionType() || type.isIntersectionType() ? must(queries.constituents(type)) : []),
|
|
991
|
+
...must(queries.typeArguments(type)),
|
|
992
|
+
].flatMap((one) => typeParametersOf(project, one, depth + 1));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* The type parameters one type names, by identifier: through constituents, type arguments,
|
|
997
|
+
* an alias's arguments and the parameters and results of its call signatures, so a callback
|
|
998
|
+
* parameter's result names the parameter its callee's result does.
|
|
999
|
+
*/
|
|
1000
|
+
function namedTypeParameters<Brand>(
|
|
1001
|
+
project: ProjectView<Brand>,
|
|
1002
|
+
type: Type,
|
|
1003
|
+
depth: number,
|
|
1004
|
+
): readonly number[] {
|
|
1005
|
+
const { queries } = project;
|
|
1006
|
+
const aliased = (type as { readonly aliasTypeArguments?: readonly number[] }).aliasTypeArguments;
|
|
1007
|
+
if (type.isTypeParameter()) {
|
|
1008
|
+
return [type.id];
|
|
1009
|
+
}
|
|
1010
|
+
if (depth >= MENTION_DEPTH) {
|
|
1011
|
+
return aliased ?? [];
|
|
1012
|
+
}
|
|
1013
|
+
return [
|
|
1014
|
+
...(aliased ?? []),
|
|
1015
|
+
...[
|
|
1016
|
+
...(type.isUnionType() || type.isIntersectionType() ? must(queries.constituents(type)) : []),
|
|
1017
|
+
...must(queries.typeArguments(type)),
|
|
1018
|
+
...must(queries.signaturesOf(type, SignatureKind.Call)).flatMap((signature) => [
|
|
1019
|
+
...signature.parameters.flatMap((_parameter, at) => [
|
|
1020
|
+
must(queries.typeAtPosition(signature, at)),
|
|
1021
|
+
]),
|
|
1022
|
+
...[must(queries.returnTypeOf(signature))].filter((one) => one !== undefined),
|
|
1023
|
+
]),
|
|
1024
|
+
].flatMap((one) => namedTypeParameters(project, one, depth + 1)),
|
|
1025
|
+
];
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* The types an instantiated type puts where a declared type names `parameter`: matched
|
|
1030
|
+
* through type arguments, and in a union every constituent the declared union does not
|
|
1031
|
+
* itself name.
|
|
1032
|
+
*/
|
|
1033
|
+
function bound<Brand>(
|
|
1034
|
+
project: ProjectView<Brand>,
|
|
1035
|
+
declared: Type,
|
|
1036
|
+
instantiated: Type,
|
|
1037
|
+
parameter: Type,
|
|
1038
|
+
depth: number,
|
|
1039
|
+
): Type[] {
|
|
1040
|
+
if (declared.id === parameter.id) {
|
|
1041
|
+
return [instantiated];
|
|
1042
|
+
}
|
|
1043
|
+
if (depth >= MENTION_DEPTH) {
|
|
1044
|
+
return [];
|
|
1045
|
+
}
|
|
1046
|
+
const { queries } = project;
|
|
1047
|
+
if (declared.isUnionType()) {
|
|
1048
|
+
const parts = must(queries.constituents(declared));
|
|
1049
|
+
if (parts.some((part) => part.id === parameter.id)) {
|
|
1050
|
+
const kept = new Set(parts.map((part) => part.id));
|
|
1051
|
+
const given = instantiated.isUnionType()
|
|
1052
|
+
? must(queries.constituents(instantiated))
|
|
1053
|
+
: [instantiated];
|
|
1054
|
+
return given.filter((part) => !kept.has(part.id));
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const declaredArguments = must(queries.typeArguments(declared));
|
|
1058
|
+
const givenArguments = must(queries.typeArguments(instantiated));
|
|
1059
|
+
return declaredArguments.length !== givenArguments.length
|
|
1060
|
+
? []
|
|
1061
|
+
: declaredArguments.flatMap((one, at) => {
|
|
1062
|
+
const given = givenArguments[at];
|
|
1063
|
+
return given === undefined ? [] : bound(project, one, given, parameter, depth + 1);
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* The flows a type parameter of a function of the project carries where a value its type
|
|
1069
|
+
* names reaches a destination: each call of the function passes its type argument there,
|
|
1070
|
+
* written or inferred from the arguments, and a type argument that is a type parameter
|
|
1071
|
+
* of the calling function is followed on to that function's calls, to a fixpoint.
|
|
1072
|
+
*/
|
|
1073
|
+
function typeParameterFlows<Brand>(
|
|
1074
|
+
project: ProjectView<Brand>,
|
|
1075
|
+
reach: ReturnType<typeof valueReach>,
|
|
1076
|
+
sites: readonly Site[],
|
|
1077
|
+
pending: { readonly parameter: Type; readonly to: Destination }[],
|
|
1078
|
+
): readonly Flow[] {
|
|
1079
|
+
const { queries } = project;
|
|
1080
|
+
const byName = new Map<string, Site[]>();
|
|
1081
|
+
for (const site of sites) {
|
|
1082
|
+
const name = calleeName(site.call.expression)?.getText();
|
|
1083
|
+
if (name !== undefined) {
|
|
1084
|
+
byName.set(name, [...(byName.get(name) ?? []), site]);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const found: Flow[] = [];
|
|
1088
|
+
const seen = new Set<number>();
|
|
1089
|
+
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
1090
|
+
const { parameter, to } = next;
|
|
1091
|
+
if (seen.has(parameter.id)) {
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
seen.add(parameter.id);
|
|
1095
|
+
const symbol = must(queries.symbolOfType(parameter));
|
|
1096
|
+
const [handle] = symbol?.declarations ?? [];
|
|
1097
|
+
const declaration = handle === undefined ? undefined : project.declarationAt(handle)?.node;
|
|
1098
|
+
const fn = declaration?.parent;
|
|
1099
|
+
const index =
|
|
1100
|
+
fn === undefined || !isFunctionLikeDeclaration(fn)
|
|
1101
|
+
? -1
|
|
1102
|
+
: (fn.typeParameters ?? []).findIndex((one) => one === declaration);
|
|
1103
|
+
const name =
|
|
1104
|
+
fn === undefined
|
|
1105
|
+
? undefined
|
|
1106
|
+
: isMethodDeclaration(fn) && isIdentifier(fn.name)
|
|
1107
|
+
? fn.name.text
|
|
1108
|
+
: nestedName(fn);
|
|
1109
|
+
if (symbol === undefined || fn === undefined || index < 0 || name === undefined) {
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
for (const site of byName.get(name) ?? []) {
|
|
1113
|
+
const resolved = must(queries.resolvedSignature(site.call));
|
|
1114
|
+
const declared = resolved === undefined ? undefined : must(queries.genericOf(resolved));
|
|
1115
|
+
const called =
|
|
1116
|
+
declared?.declaration === undefined
|
|
1117
|
+
? undefined
|
|
1118
|
+
: project.declarationAt(declared.declaration)?.node;
|
|
1119
|
+
if (resolved === undefined || declared === undefined || called !== fn) {
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
const written = site.call.typeArguments?.[index];
|
|
1123
|
+
const given =
|
|
1124
|
+
written === undefined
|
|
1125
|
+
? site.call.arguments.flatMap((_argument, at) => {
|
|
1126
|
+
const from = must(queries.parameterType(declared, at));
|
|
1127
|
+
const into = must(queries.parameterType(resolved, at));
|
|
1128
|
+
return from === undefined || into === undefined
|
|
1129
|
+
? []
|
|
1130
|
+
: bound(project, from, into, parameter, 0);
|
|
1131
|
+
})
|
|
1132
|
+
: typesAt(project, [written]).filter((type) => type !== undefined);
|
|
1133
|
+
for (const type of given) {
|
|
1134
|
+
const reached = reach.reachedFrom(type);
|
|
1135
|
+
if (reached.classes.size > 0 || reached.members.length > 0) {
|
|
1136
|
+
found.push({
|
|
1137
|
+
site: site.call,
|
|
1138
|
+
classes: reached.classes,
|
|
1139
|
+
members: reached.members,
|
|
1140
|
+
to,
|
|
1141
|
+
detail: `passed to ${to.callee} through the type parameter ${symbol.name}`,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
pending.push(...typeParametersOf(project, type, 0).map((one) => ({ parameter: one, to })));
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return found;
|
|
794
1149
|
}
|
|
795
1150
|
|
|
796
1151
|
/**
|
|
@@ -826,6 +1181,11 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
|
|
|
826
1181
|
(flags & SymbolFlags.Property) !== 0 ||
|
|
827
1182
|
((flags & CONVERSION_MEMBERS) !== 0 &&
|
|
828
1183
|
(name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
|
|
1184
|
+
// The receiver reads only what the program wrote, so a data member holds while written.
|
|
1185
|
+
const evidence = (id: string, flags: SymbolFlags, detail: string, site: Position): Evidence =>
|
|
1186
|
+
(flags & SymbolFlags.Property) !== 0 && !walked.initialized.has(id)
|
|
1187
|
+
? { id, detail, site, whileWritten: true }
|
|
1188
|
+
: { id, detail, site };
|
|
829
1189
|
const found: Evidence[] = [];
|
|
830
1190
|
for (const flow of flows) {
|
|
831
1191
|
const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
|
|
@@ -834,16 +1194,402 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
|
|
|
834
1194
|
const symbol = byId.get(member.id);
|
|
835
1195
|
const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
|
|
836
1196
|
if (holdsBack(member.flags, name, flow.to)) {
|
|
837
|
-
found.push(
|
|
1197
|
+
found.push(evidence(member.id, member.flags, detail, site));
|
|
838
1198
|
}
|
|
839
1199
|
}
|
|
840
1200
|
for (const member of flow.members) {
|
|
841
1201
|
if (holdsBack(member.flags, member.name, flow.to)) {
|
|
842
1202
|
for (const id of chains.declarationsOf(member)) {
|
|
843
|
-
found.push(
|
|
1203
|
+
found.push(evidence(id, member.flags, detail, site));
|
|
844
1204
|
}
|
|
845
1205
|
}
|
|
846
1206
|
}
|
|
847
1207
|
}
|
|
848
1208
|
return found;
|
|
849
1209
|
}
|
|
1210
|
+
|
|
1211
|
+
/** The data members the decodes and copies of a project's production code fill. */
|
|
1212
|
+
export interface Decoded {
|
|
1213
|
+
/**
|
|
1214
|
+
* Those a generic declared outside the analysis fills, which it may read, and those of
|
|
1215
|
+
* a value another call or a property types `any` or `unknown`, whose maker it cannot see.
|
|
1216
|
+
*/
|
|
1217
|
+
readonly filled: readonly string[];
|
|
1218
|
+
/** Those a `JSON.parse` call of the standard library fills. */
|
|
1219
|
+
readonly parsed: readonly string[];
|
|
1220
|
+
/** Those a spread or `Object.assign` copies from a value whose type has a member of the name. */
|
|
1221
|
+
readonly copied: readonly string[];
|
|
1222
|
+
/** Every data member of the project, where the checker left a question unanswered. */
|
|
1223
|
+
readonly unanswered: readonly string[];
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* The data members the decodes and copies of `files` fill, as {@link Decoded} names them.
|
|
1228
|
+
* A generic declared outside the analysis, or only ambiently, fills the type that a type
|
|
1229
|
+
* parameter of its own no parameter names binds, where the awaited result carries that
|
|
1230
|
+
* type through its data members rather than only naming it, as a store's methods do: no
|
|
1231
|
+
* argument hands the value over, so it fills it where the analysis cannot see, and may
|
|
1232
|
+
* read it. Where the checker leaves a question unanswered, every data member is `unanswered`.
|
|
1233
|
+
*/
|
|
1234
|
+
export function decodedMembers<Brand>(
|
|
1235
|
+
project: ProjectView<Brand>,
|
|
1236
|
+
held: Inventory,
|
|
1237
|
+
files: readonly SourceFile[],
|
|
1238
|
+
consumers: readonly string[],
|
|
1239
|
+
): Decoded {
|
|
1240
|
+
try {
|
|
1241
|
+
return { ...decodedOf(project, held, files, consumers), unanswered: [] };
|
|
1242
|
+
} catch (error: unknown) {
|
|
1243
|
+
if (!(error instanceof Unanswerable)) {
|
|
1244
|
+
throw error;
|
|
1245
|
+
}
|
|
1246
|
+
return {
|
|
1247
|
+
filled: [],
|
|
1248
|
+
parsed: [],
|
|
1249
|
+
copied: [],
|
|
1250
|
+
unanswered: held.symbols
|
|
1251
|
+
.filter((symbol) => symbol.kind === "class-member" || symbol.kind === "type-member")
|
|
1252
|
+
.map((symbol) => symbol.id),
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function decodedOf<Brand>(
|
|
1258
|
+
project: ProjectView<Brand>,
|
|
1259
|
+
held: Inventory,
|
|
1260
|
+
files: readonly SourceFile[],
|
|
1261
|
+
consumers: readonly string[],
|
|
1262
|
+
): Omit<Decoded, "unanswered"> {
|
|
1263
|
+
const { queries } = project;
|
|
1264
|
+
const own = project.ownPaths();
|
|
1265
|
+
const inAnalysis = (path: string): boolean =>
|
|
1266
|
+
own.has(path) || consumers.some((root) => path === root || path.startsWith(`${root}/`));
|
|
1267
|
+
const calls: CallExpression[] = [];
|
|
1268
|
+
const untyped: Expression[] = [];
|
|
1269
|
+
const spreads: SpreadAssignment[] = [];
|
|
1270
|
+
const noteRead = (node: Expression): void => {
|
|
1271
|
+
const at = readAs(node);
|
|
1272
|
+
if (at.tops.length > 0 || at.asserted.length > 0) {
|
|
1273
|
+
untyped.push(node);
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
const visit = (node: Node): void => {
|
|
1277
|
+
if (isCallExpression(node)) {
|
|
1278
|
+
calls.push(node);
|
|
1279
|
+
noteRead(node);
|
|
1280
|
+
} else if (isPropertyAccessExpression(node) || isElementAccessExpression(node)) {
|
|
1281
|
+
noteRead(node);
|
|
1282
|
+
} else if (isSpreadAssignment(node)) {
|
|
1283
|
+
spreads.push(node);
|
|
1284
|
+
}
|
|
1285
|
+
node.forEachChild(visit);
|
|
1286
|
+
};
|
|
1287
|
+
for (const file of files) {
|
|
1288
|
+
file.forEachChild(visit);
|
|
1289
|
+
}
|
|
1290
|
+
const filled: { readonly carried: Type; readonly types: readonly Type[] }[] = [];
|
|
1291
|
+
const resolved = queries.resolvedSignatures(calls).map(must);
|
|
1292
|
+
for (const signature of resolved) {
|
|
1293
|
+
const declared =
|
|
1294
|
+
signature?.target === undefined ? undefined : must(queries.genericOf(signature));
|
|
1295
|
+
const declaration = declared?.declaration;
|
|
1296
|
+
const generic = declared?.typeParameters ?? [];
|
|
1297
|
+
if (signature === undefined || declared === undefined || generic.length === 0) {
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
const node = declaration === undefined ? undefined : project.declarationAt(declaration)?.node;
|
|
1301
|
+
if (
|
|
1302
|
+
declaration === undefined ||
|
|
1303
|
+
(inAnalysis(declaration.path) &&
|
|
1304
|
+
(node === undefined || (node.flags & NodeFlags.Ambient) === 0))
|
|
1305
|
+
) {
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
const result = must(queries.returnTypeOf(declared));
|
|
1309
|
+
const given = must(queries.returnTypeOf(signature));
|
|
1310
|
+
if (result === undefined || given === undefined) {
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
const passed = new Set(
|
|
1314
|
+
declared.parameters.flatMap((_parameter, at) => {
|
|
1315
|
+
const type = must(queries.parameterType(declared, at));
|
|
1316
|
+
return type === undefined ? [] : namedTypeParameters(project, type, 0);
|
|
1317
|
+
}),
|
|
1318
|
+
);
|
|
1319
|
+
const types = typeParametersOf(project, result, 0)
|
|
1320
|
+
.filter((parameter) => generic.includes(parameter.id) && !passed.has(parameter.id))
|
|
1321
|
+
.flatMap((parameter) => bound(project, result, given, parameter, 0));
|
|
1322
|
+
if (types.length > 0) {
|
|
1323
|
+
filled.push({ carried: awaited(project, given), types });
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const read = readTypes(project, untyped);
|
|
1327
|
+
const copies = copiedNames(project, spreads, calls);
|
|
1328
|
+
if (
|
|
1329
|
+
filled.length === 0 &&
|
|
1330
|
+
read.parsed.length === 0 &&
|
|
1331
|
+
read.made.length === 0 &&
|
|
1332
|
+
copies.length === 0
|
|
1333
|
+
) {
|
|
1334
|
+
return { filled: [], parsed: [], copied: [] };
|
|
1335
|
+
}
|
|
1336
|
+
const reach = valueReach(project, held);
|
|
1337
|
+
const chains = aliasChains(project, held);
|
|
1338
|
+
const members = (type: Type): readonly string[] => {
|
|
1339
|
+
const reached = reach.reachedFrom(type);
|
|
1340
|
+
const ofClasses = classMembers(project, held, reached.classes, "instance");
|
|
1341
|
+
return [
|
|
1342
|
+
...[...reached.classes].flatMap((id) =>
|
|
1343
|
+
(ofClasses.get(id) ?? []).flatMap((member) =>
|
|
1344
|
+
(member.flags & SymbolFlags.Property) !== 0 ? [member.id] : [],
|
|
1345
|
+
),
|
|
1346
|
+
),
|
|
1347
|
+
...reached.members.flatMap((member) =>
|
|
1348
|
+
(member.flags & SymbolFlags.Property) !== 0 ? chains.declarationsOf(member) : [],
|
|
1349
|
+
),
|
|
1350
|
+
];
|
|
1351
|
+
};
|
|
1352
|
+
return {
|
|
1353
|
+
filled: [
|
|
1354
|
+
...new Set([
|
|
1355
|
+
...filled.flatMap(({ carried, types }) => {
|
|
1356
|
+
const carries = new Set(members(carried));
|
|
1357
|
+
return types.flatMap((type) => members(type).filter((id) => carries.has(id)));
|
|
1358
|
+
}),
|
|
1359
|
+
...read.made.flatMap(members),
|
|
1360
|
+
]),
|
|
1361
|
+
],
|
|
1362
|
+
parsed: [...new Set(read.parsed.flatMap(members))],
|
|
1363
|
+
copied: [
|
|
1364
|
+
...new Set(
|
|
1365
|
+
copies.flatMap(({ into, names }) =>
|
|
1366
|
+
must(queries.constituents(into)).flatMap((part) =>
|
|
1367
|
+
must(queries.propertiesOf(part)).flatMap((property) =>
|
|
1368
|
+
(property.flags & SymbolFlags.Property) !== 0 && names.has(property.name)
|
|
1369
|
+
? chains.declarationsOf(property)
|
|
1370
|
+
: [],
|
|
1371
|
+
),
|
|
1372
|
+
),
|
|
1373
|
+
),
|
|
1374
|
+
),
|
|
1375
|
+
],
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/** Where a value is read as a type: the assertions around it, and the positions it is written in. */
|
|
1380
|
+
interface ReadAs {
|
|
1381
|
+
/** The type nodes of the `as` and angle-bracket assertions around the value. */
|
|
1382
|
+
readonly asserted: readonly Node[];
|
|
1383
|
+
/** The outermost expressions whose contextual types the value is read as. */
|
|
1384
|
+
readonly tops: readonly Expression[];
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Where one value is read as a type: each `as` and angle-bracket assertion around it,
|
|
1389
|
+
* through parentheses, `await` and `!`, and the outermost expression unless a statement
|
|
1390
|
+
* of its own or the object or callee of a further access holds it. A `satisfies` check
|
|
1391
|
+
* is stepped through and types nothing, its expression's type being its operand's. A
|
|
1392
|
+
* callback's result is read as well where the call taking the callback is, which may
|
|
1393
|
+
* hand it on inside its result, as `map` does.
|
|
1394
|
+
*/
|
|
1395
|
+
function readAs(value: Expression, depth = 0): ReadAs {
|
|
1396
|
+
const asserted: Node[] = [];
|
|
1397
|
+
let at: Expression = value;
|
|
1398
|
+
for (;;) {
|
|
1399
|
+
const parent = at.parent;
|
|
1400
|
+
if (isAsExpression(parent) || isTypeAssertion(parent)) {
|
|
1401
|
+
asserted.push(parent.type);
|
|
1402
|
+
} else if (
|
|
1403
|
+
!isParenthesizedExpression(parent) &&
|
|
1404
|
+
!isAwaitExpression(parent) &&
|
|
1405
|
+
!isNonNullExpression(parent) &&
|
|
1406
|
+
!isSatisfiesExpression(parent)
|
|
1407
|
+
) {
|
|
1408
|
+
break;
|
|
1409
|
+
}
|
|
1410
|
+
at = parent;
|
|
1411
|
+
}
|
|
1412
|
+
const parent = at.parent;
|
|
1413
|
+
const unused =
|
|
1414
|
+
isExpressionStatement(parent) ||
|
|
1415
|
+
((isPropertyAccessExpression(parent) ||
|
|
1416
|
+
isElementAccessExpression(parent) ||
|
|
1417
|
+
isCallExpression(parent)) &&
|
|
1418
|
+
parent.expression === at);
|
|
1419
|
+
const call = depth < MENTION_DEPTH ? callTaking(at) : undefined;
|
|
1420
|
+
const outer = call === undefined ? undefined : readAs(call, depth + 1);
|
|
1421
|
+
return {
|
|
1422
|
+
asserted: [...asserted, ...(outer?.asserted ?? [])],
|
|
1423
|
+
tops: [...(unused ? [] : [at]), ...(outer?.tops ?? [])],
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/** The call one value is returned to as a callback's result, an argument of that call. */
|
|
1428
|
+
function callTaking(value: Expression): CallExpression | undefined {
|
|
1429
|
+
const parent = value.parent;
|
|
1430
|
+
let callback: Node | undefined;
|
|
1431
|
+
if (isArrowFunction(parent) && parent.body === value) {
|
|
1432
|
+
callback = parent;
|
|
1433
|
+
} else if (isReturnStatement(parent)) {
|
|
1434
|
+
let at: Node = parent.parent;
|
|
1435
|
+
while (!isFunctionLikeDeclaration(at) && !isSourceFile(at)) {
|
|
1436
|
+
at = at.parent;
|
|
1437
|
+
}
|
|
1438
|
+
callback = isArrowFunction(at) || isFunctionExpression(at) ? at : undefined;
|
|
1439
|
+
}
|
|
1440
|
+
let at = callback?.parent;
|
|
1441
|
+
while (at !== undefined && isParenthesizedExpression(at)) {
|
|
1442
|
+
at = at.parent;
|
|
1443
|
+
}
|
|
1444
|
+
return at !== undefined && isCallExpression(at) && at.expression !== callback ? at : undefined;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/** The global the standard library declares one name of `JSON` or `Object` as. */
|
|
1448
|
+
function standardGlobal(callee: Expression, global: string, name: string): Node | undefined {
|
|
1449
|
+
return isPropertyAccessExpression(callee) &&
|
|
1450
|
+
isIdentifier(callee.expression) &&
|
|
1451
|
+
callee.expression.text === global &&
|
|
1452
|
+
callee.name.text === name
|
|
1453
|
+
? callee.expression
|
|
1454
|
+
: undefined;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* The types each value of `values` typed `any` or `unknown` is read as ({@link readAs}):
|
|
1459
|
+
* `parsed` for a `JSON.parse` call of the standard library, `made` for any other call or
|
|
1460
|
+
* property, whose maker the analysis cannot see. `JSON` must be the standard library's
|
|
1461
|
+
* global.
|
|
1462
|
+
*/
|
|
1463
|
+
function readTypes<Brand>(
|
|
1464
|
+
project: ProjectView<Brand>,
|
|
1465
|
+
values: readonly Expression[],
|
|
1466
|
+
): { readonly parsed: readonly Type[]; readonly made: readonly Type[] } {
|
|
1467
|
+
if (values.length === 0) {
|
|
1468
|
+
return { parsed: [], made: [] };
|
|
1469
|
+
}
|
|
1470
|
+
const own = project.ownPaths();
|
|
1471
|
+
const parses = values.map((value) =>
|
|
1472
|
+
isCallExpression(value) ? standardGlobal(value.expression, "JSON", "parse") : undefined,
|
|
1473
|
+
);
|
|
1474
|
+
const globals = parses.flatMap((global) => (global === undefined ? [] : [global]));
|
|
1475
|
+
const standard = new Set(
|
|
1476
|
+
project
|
|
1477
|
+
.symbolsAt(globals.map((global) => project.handle(global)))
|
|
1478
|
+
.map(must)
|
|
1479
|
+
.flatMap((symbol, at) =>
|
|
1480
|
+
symbol?.declarations.every((handle) => !own.has(handle.path)) === true ? [globals[at]] : [],
|
|
1481
|
+
),
|
|
1482
|
+
);
|
|
1483
|
+
// An `await` around the value reads the value it resolves to.
|
|
1484
|
+
const valueTypes = typesAt(
|
|
1485
|
+
project,
|
|
1486
|
+
values.map((value) => (isAwaitExpression(value.parent) ? value.parent : value)),
|
|
1487
|
+
);
|
|
1488
|
+
const sites = values.flatMap((value, at) => {
|
|
1489
|
+
const parse = parses[at];
|
|
1490
|
+
const parsed = parse !== undefined && standard.has(parse);
|
|
1491
|
+
const type = valueTypes[at];
|
|
1492
|
+
const untyped = type !== undefined && (type.flags & (TypeFlags.Any | TypeFlags.Unknown)) !== 0;
|
|
1493
|
+
return parsed || untyped ? [{ ...readAs(value), parsed }] : [];
|
|
1494
|
+
});
|
|
1495
|
+
// Each type node and each outermost expression, with whether a `JSON.parse` call is read as it.
|
|
1496
|
+
const typed = sites.flatMap((site) =>
|
|
1497
|
+
site.asserted.map((node) => ({ node, parsed: site.parsed })),
|
|
1498
|
+
);
|
|
1499
|
+
const placed = sites.flatMap((site) => site.tops.map((node) => ({ node, parsed: site.parsed })));
|
|
1500
|
+
const read = [
|
|
1501
|
+
...typesAt(
|
|
1502
|
+
project,
|
|
1503
|
+
typed.map((one) => one.node),
|
|
1504
|
+
).map((type, at) => ({ type, parsed: typed[at]?.parsed })),
|
|
1505
|
+
...project.queries
|
|
1506
|
+
.contextualTypes(placed.map((one) => one.node))
|
|
1507
|
+
.map(must)
|
|
1508
|
+
.map((type, at) => ({ type, parsed: placed[at]?.parsed })),
|
|
1509
|
+
];
|
|
1510
|
+
const typesWhere = (parsed: boolean): Type[] =>
|
|
1511
|
+
read.flatMap((one) => (one.type !== undefined && one.parsed === parsed ? [one.type] : []));
|
|
1512
|
+
return { parsed: typesWhere(true), made: typesWhere(false) };
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* The copies the spreads and calls of `files` make: for each spread in an object literal,
|
|
1517
|
+
* the type the literal is read as, and for each `Object.assign` call of the standard
|
|
1518
|
+
* library, its first argument's type, with the member names of the copied values' types.
|
|
1519
|
+
*/
|
|
1520
|
+
function copiedNames<Brand>(
|
|
1521
|
+
project: ProjectView<Brand>,
|
|
1522
|
+
spreads: readonly SpreadAssignment[],
|
|
1523
|
+
calls: readonly CallExpression[],
|
|
1524
|
+
): readonly { readonly into: Type; readonly names: ReadonlySet<string> }[] {
|
|
1525
|
+
const own = project.ownPaths();
|
|
1526
|
+
const assigns = calls.flatMap((call) => {
|
|
1527
|
+
const global = standardGlobal(call.expression, "Object", "assign");
|
|
1528
|
+
const [target, ...sources] = call.arguments;
|
|
1529
|
+
return global === undefined || target === undefined || sources.length === 0
|
|
1530
|
+
? []
|
|
1531
|
+
: [{ global, target, sources: sources.filter((source) => !isSpreadElement(source)) }];
|
|
1532
|
+
});
|
|
1533
|
+
const standard = project
|
|
1534
|
+
.symbolsAt(assigns.map((one) => project.handle(one.global)))
|
|
1535
|
+
.map(must)
|
|
1536
|
+
.map((symbol) => symbol?.declarations.every((handle) => !own.has(handle.path)) === true);
|
|
1537
|
+
const assigned = assigns.filter((_one, at) => standard[at] === true);
|
|
1538
|
+
const literals = spreads.flatMap((spread) =>
|
|
1539
|
+
isObjectLiteralExpression(spread.parent)
|
|
1540
|
+
? [{ literal: spread.parent, source: spread.expression }]
|
|
1541
|
+
: [],
|
|
1542
|
+
);
|
|
1543
|
+
const contexts = project.queries.contextualTypes(literals.map((one) => one.literal)).map(must);
|
|
1544
|
+
const targets = typesAt(
|
|
1545
|
+
project,
|
|
1546
|
+
assigned.map((one) => one.target),
|
|
1547
|
+
);
|
|
1548
|
+
const spreadTypes = typesAt(
|
|
1549
|
+
project,
|
|
1550
|
+
literals.map((one) => one.source),
|
|
1551
|
+
);
|
|
1552
|
+
const sourceTypes = assigned.map((one) => typesAt(project, one.sources));
|
|
1553
|
+
const namesOf = (types: readonly (Type | undefined)[]): ReadonlySet<string> =>
|
|
1554
|
+
new Set(
|
|
1555
|
+
types.flatMap((type) =>
|
|
1556
|
+
type === undefined
|
|
1557
|
+
? []
|
|
1558
|
+
: must(project.queries.propertiesOf(type)).map((property) => property.name),
|
|
1559
|
+
),
|
|
1560
|
+
);
|
|
1561
|
+
const found: { readonly into: Type; readonly names: ReadonlySet<string> }[] = [];
|
|
1562
|
+
const add = (into: Type | undefined, names: ReadonlySet<string>): void => {
|
|
1563
|
+
if (into !== undefined && names.size > 0) {
|
|
1564
|
+
found.push({ into, names });
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
contexts.forEach((into, at) => {
|
|
1568
|
+
add(into, namesOf([spreadTypes[at]]));
|
|
1569
|
+
});
|
|
1570
|
+
targets.forEach((into, at) => {
|
|
1571
|
+
add(into, namesOf(sourceTypes[at] ?? []));
|
|
1572
|
+
});
|
|
1573
|
+
return found;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
/** The value a promise of the standard library resolves to, through nested promises. */
|
|
1577
|
+
function awaited<Brand>(project: ProjectView<Brand>, type: Type): Type {
|
|
1578
|
+
const { queries } = project;
|
|
1579
|
+
let at = type;
|
|
1580
|
+
for (let depth = 0; depth < MENTION_DEPTH; depth += 1) {
|
|
1581
|
+
const symbol = must(queries.symbolOfType(at));
|
|
1582
|
+
const [inner] = must(queries.typeArguments(at));
|
|
1583
|
+
const standard =
|
|
1584
|
+
symbol !== undefined &&
|
|
1585
|
+
PROMISES.has(symbol.name) &&
|
|
1586
|
+
symbol.declarations.every(
|
|
1587
|
+
(handle) => project.program.getSourceFileMetadata(handle.path)?.isDefaultLibrary === true,
|
|
1588
|
+
);
|
|
1589
|
+
if (!standard || inner === undefined) {
|
|
1590
|
+
break;
|
|
1591
|
+
}
|
|
1592
|
+
at = inner;
|
|
1593
|
+
}
|
|
1594
|
+
return at;
|
|
1595
|
+
}
|