@cplieger/deadset-ts 3.13.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +119 -23
- package/dist/src/calls.js +23 -22
- package/dist/src/class-members.js +10 -11
- package/dist/src/configured-declarations.js +22 -9
- package/dist/src/dependencies.js +34 -17
- package/dist/src/deprecation.js +35 -24
- package/dist/src/destructuring.js +37 -29
- package/dist/src/discover.js +105 -28
- package/dist/src/entry-points.js +7 -0
- package/dist/src/enum-group.js +46 -40
- package/dist/src/exempt.js +42 -1
- package/dist/src/findings/emitters.js +15 -1
- package/dist/src/findings/interfaces.js +7 -1
- package/dist/src/framework-lifecycle.js +8 -6
- package/dist/src/implementations.js +45 -23
- package/dist/src/interface-satisfaction.js +30 -27
- package/dist/src/intra-function-parts.js +37 -20
- package/dist/src/inventory.js +85 -4
- package/dist/src/json-chunks.js +76 -0
- package/dist/src/partial.js +23 -0
- package/dist/src/query.js +207 -0
- package/dist/src/references.js +92 -20
- package/dist/src/reflective-lookup.js +18 -9
- package/dist/src/report.js +3 -1
- package/dist/src/reporters/annotations.js +9 -5
- package/dist/src/reporters/json.js +3 -2
- package/dist/src/reporters/sarif.js +2 -1
- package/dist/src/reporters/template.js +48 -7
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- package/dist/src/serialization-contract.js +12 -10
- package/dist/src/session.js +79 -39
- package/dist/src/suppress-file.js +2 -1
- package/dist/src/sweep.js +4 -1
- package/dist/src/value-reach.js +14 -19
- package/dist/src/verbs/analyze.js +20 -3
- package/dist/src/verbs/explain.js +22 -9
- package/dist/src/verbs/print-projects.js +40 -7
- package/dist/src/verbs/print-retained.js +6 -2
- package/dist/src/verbs/print-roots.js +5 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +75 -11
- package/src/analysis.ts +223 -61
- package/src/calls.ts +10 -9
- package/src/class-members.ts +10 -11
- package/src/configured-declarations.ts +29 -13
- package/src/dependencies.ts +42 -20
- package/src/deprecation.ts +53 -27
- package/src/destructuring.ts +50 -39
- package/src/discover.ts +177 -33
- package/src/entry-points.ts +7 -0
- package/src/enum-group.ts +56 -42
- package/src/exempt.ts +50 -2
- package/src/findings/emitters.ts +18 -1
- package/src/findings/interfaces.ts +7 -1
- package/src/framework-lifecycle.ts +10 -6
- package/src/host.ts +5 -3
- package/src/implementations.ts +59 -23
- package/src/interface-satisfaction.ts +30 -27
- package/src/intra-function-parts.ts +43 -25
- package/src/inventory.ts +97 -4
- package/src/json-chunks.ts +87 -0
- package/src/partial.ts +40 -0
- package/src/query.ts +522 -0
- package/src/references.ts +119 -38
- package/src/reflective-lookup.ts +21 -11
- package/src/report.ts +14 -2
- package/src/reporters/annotations.ts +23 -21
- package/src/reporters/json.ts +3 -2
- package/src/reporters/reporter.ts +2 -2
- package/src/reporters/sarif.ts +3 -2
- package/src/reporters/template.ts +50 -8
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- package/src/serialization-contract.ts +12 -10
- package/src/session.ts +146 -58
- package/src/suppress-file.ts +6 -2
- package/src/sweep.ts +9 -4
- package/src/value-reach.ts +20 -23
- package/src/verbs/analyze.ts +25 -3
- package/src/verbs/explain.ts +20 -9
- package/src/verbs/print-projects.ts +53 -7
- package/src/verbs/print-retained.ts +6 -2
- package/src/verbs/print-roots.ts +5 -1
package/src/class-members.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { isClassDeclaration, isClassExpression, type Node } from "@typescript/native/unstable/ast";
|
|
8
8
|
import type { Symbol as TSSymbol, SymbolFlags, Type } from "@typescript/native/unstable/sync";
|
|
9
9
|
import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { must } from "./query.ts";
|
|
11
11
|
import type { ProjectView } from "./session.ts";
|
|
12
12
|
|
|
13
13
|
/** One member of a class's value that the target declares. */
|
|
@@ -44,16 +44,15 @@ export function classNames<Brand>(
|
|
|
44
44
|
return names;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* The types of a batch of nodes, for a pass that cannot go on past a type the checker
|
|
49
|
+
* did not answer: it stops there, as {@link must} stops.
|
|
50
|
+
*/
|
|
48
51
|
export function typesAt<Brand>(
|
|
49
52
|
project: ProjectView<Brand>,
|
|
50
53
|
nodes: readonly Node[],
|
|
51
54
|
): (Type | undefined)[] {
|
|
52
|
-
|
|
53
|
-
for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
|
|
54
|
-
types.push(...project.checker.getTypeAtLocation(nodes.slice(from, from + DEFAULT_BATCH_CAP)));
|
|
55
|
-
}
|
|
56
|
-
return types;
|
|
55
|
+
return project.queries.typesAt(nodes).map(must);
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
/**
|
|
@@ -68,9 +67,9 @@ function sideTypes<Brand>(
|
|
|
68
67
|
if (side === "instance") {
|
|
69
68
|
return typesAt(project, nodes);
|
|
70
69
|
}
|
|
71
|
-
const symbols = project.symbolsAt(nodes.map((node) => project.handle(node)));
|
|
70
|
+
const symbols = project.symbolsAt(nodes.map((node) => project.handle(node))).map(must);
|
|
72
71
|
const present = symbols.filter((symbol): symbol is TSSymbol => symbol !== undefined);
|
|
73
|
-
const types =
|
|
72
|
+
const types = project.queries.typesOfSymbols(present).map(must);
|
|
74
73
|
let next = 0;
|
|
75
74
|
return symbols.map((symbol) => (symbol === undefined ? undefined : types[next++]));
|
|
76
75
|
}
|
|
@@ -87,7 +86,7 @@ export function classMembers<Brand>(
|
|
|
87
86
|
side: ClassSide,
|
|
88
87
|
): ReadonlyMap<string, readonly ClassMember[]> {
|
|
89
88
|
const named = [...classNames(project, held)].filter(([id]) => classIds.has(id));
|
|
90
|
-
const own =
|
|
89
|
+
const own = project.ownPaths();
|
|
91
90
|
const types = sideTypes(
|
|
92
91
|
project,
|
|
93
92
|
named.map(([, node]) => node),
|
|
@@ -100,7 +99,7 @@ export function classMembers<Brand>(
|
|
|
100
99
|
return;
|
|
101
100
|
}
|
|
102
101
|
const found: ClassMember[] = [];
|
|
103
|
-
for (const property of project.
|
|
102
|
+
for (const property of must(project.queries.propertiesOf(type))) {
|
|
104
103
|
for (const handle of property.declarations) {
|
|
105
104
|
if (!own.has(handle.path)) {
|
|
106
105
|
continue;
|
|
@@ -25,6 +25,7 @@ import type { AliasChains } from "./alias-chain.ts";
|
|
|
25
25
|
import { resolvedTargets } from "./calls.ts";
|
|
26
26
|
import type { Config, DeclarationEntry } from "./config.ts";
|
|
27
27
|
import type { Inventory } from "./inventory.ts";
|
|
28
|
+
import { must, Unanswerable } from "./query.ts";
|
|
28
29
|
import { nameComponent } from "./ref.ts";
|
|
29
30
|
import type { ProjectView } from "./session.ts";
|
|
30
31
|
|
|
@@ -90,7 +91,7 @@ function unaliased<Brand>(project: ProjectView<Brand>, symbol: TSSymbol): TSSymb
|
|
|
90
91
|
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
91
92
|
return symbol;
|
|
92
93
|
}
|
|
93
|
-
const target = project.
|
|
94
|
+
const target = must(project.queries.aliased(symbol));
|
|
94
95
|
return target.declarations.length === 0 ? undefined : target;
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -105,14 +106,18 @@ function isClassMember(symbol: TSSymbol): boolean {
|
|
|
105
106
|
* with the static suffix names one of the first and a component without it names an
|
|
106
107
|
* instance member or one of the second.
|
|
107
108
|
*/
|
|
108
|
-
function candidatesOf(
|
|
109
|
-
|
|
109
|
+
function candidatesOf<Brand>(
|
|
110
|
+
project: ProjectView<Brand>,
|
|
111
|
+
container: TSSymbol,
|
|
112
|
+
isStatic: boolean,
|
|
113
|
+
): readonly TSSymbol[] {
|
|
114
|
+
const exported = [...must(project.queries.exportsOf(container)).values()];
|
|
110
115
|
if (isStatic) {
|
|
111
116
|
return exported.filter(isClassMember);
|
|
112
117
|
}
|
|
113
118
|
const isClass = (container.flags & SymbolFlags.Class) !== 0;
|
|
114
119
|
return [
|
|
115
|
-
...
|
|
120
|
+
...must(project.queries.membersOf(container)).values(),
|
|
116
121
|
...exported.filter((member) => !isClass || !isClassMember(member)),
|
|
117
122
|
];
|
|
118
123
|
}
|
|
@@ -124,7 +129,7 @@ function memberOf<Brand>(
|
|
|
124
129
|
component: string,
|
|
125
130
|
isStatic: boolean,
|
|
126
131
|
): TSSymbol | undefined {
|
|
127
|
-
const member = candidatesOf(container, isStatic).find(
|
|
132
|
+
const member = candidatesOf(project, container, isStatic).find(
|
|
128
133
|
(candidate) => nameComponent(candidate.name) === component,
|
|
129
134
|
);
|
|
130
135
|
return member === undefined ? undefined : unaliased(project, member);
|
|
@@ -190,7 +195,7 @@ function modulesNamed<Brand>(
|
|
|
190
195
|
file.forEachChild(visit);
|
|
191
196
|
}
|
|
192
197
|
const specified = [...written];
|
|
193
|
-
const symbols = project.symbolsAt(specified.map(([, node]) => project.handle(node)));
|
|
198
|
+
const symbols = project.symbolsAt(specified.map(([, node]) => project.handle(node))).map(must);
|
|
194
199
|
const modules = new Map<string, TSSymbol>();
|
|
195
200
|
specified.forEach(([name], index) => {
|
|
196
201
|
const symbol = symbols[index];
|
|
@@ -238,7 +243,7 @@ export function resolveEntries<Brand>(
|
|
|
238
243
|
const module = modules.get(entry.module);
|
|
239
244
|
start = module === undefined ? undefined : memberOf(project, module, first, false);
|
|
240
245
|
} else if (nameComponent(first) === first) {
|
|
241
|
-
const global = project.
|
|
246
|
+
const global = must(project.queries.resolveName(first, ANY_MEANING, undefined, false));
|
|
242
247
|
start = global === undefined ? undefined : unaliased(project, global);
|
|
243
248
|
}
|
|
244
249
|
const declared = walkPath(project, start, rest, isStatic);
|
|
@@ -334,17 +339,28 @@ export function configuredDeclarations(config: Config): readonly ConfiguredDecla
|
|
|
334
339
|
];
|
|
335
340
|
}
|
|
336
341
|
|
|
337
|
-
/**
|
|
342
|
+
/**
|
|
343
|
+
* Whether each configured entry names a declaration in one project, in the entries'
|
|
344
|
+
* order. Where the checker leaves a question of the resolution unanswered, every entry
|
|
345
|
+
* counts as naming one, so no entry is reported as naming nothing on a gap.
|
|
346
|
+
*/
|
|
338
347
|
export function declarationsNamed<Brand>(
|
|
339
348
|
project: ProjectView<Brand>,
|
|
340
349
|
held: Inventory,
|
|
341
350
|
configured: readonly ConfiguredDeclaration[],
|
|
342
351
|
): readonly boolean[] {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
352
|
+
try {
|
|
353
|
+
return resolveEntries(
|
|
354
|
+
project,
|
|
355
|
+
held,
|
|
356
|
+
configured.map((one) => one.entry),
|
|
357
|
+
).map(namesADeclaration);
|
|
358
|
+
} catch (error: unknown) {
|
|
359
|
+
if (!(error instanceof Unanswerable)) {
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
return configured.map(() => true);
|
|
363
|
+
}
|
|
348
364
|
}
|
|
349
365
|
|
|
350
366
|
/**
|
package/src/dependencies.ts
CHANGED
|
@@ -17,16 +17,16 @@ import {
|
|
|
17
17
|
isStringLiteral,
|
|
18
18
|
isNoSubstitutionTemplateLiteral,
|
|
19
19
|
SyntaxKind,
|
|
20
|
+
type Identifier,
|
|
20
21
|
type Node,
|
|
21
22
|
type SourceFile,
|
|
22
23
|
} from "@typescript/native/unstable/ast";
|
|
23
|
-
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
24
24
|
import type { FindingPosition } from "./finding.ts";
|
|
25
25
|
import type { Host } from "./host.ts";
|
|
26
26
|
import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
|
|
27
27
|
import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
|
|
28
28
|
import type { DependencySection, Module } from "./ref.ts";
|
|
29
|
-
import {
|
|
29
|
+
import { UNANSWERED } from "./query.ts";
|
|
30
30
|
import type { Handle, ProjectView } from "./session.ts";
|
|
31
31
|
|
|
32
32
|
const MANIFEST = "package.json";
|
|
@@ -70,6 +70,11 @@ export interface ProjectNeeds {
|
|
|
70
70
|
* compiler configuration names, which no deletion removes.
|
|
71
71
|
*/
|
|
72
72
|
readonly uses: ReadonlyMap<string, ReadonlySet<string>>;
|
|
73
|
+
/**
|
|
74
|
+
* Whether the checker left a module specifier unanswered, so the project may use any
|
|
75
|
+
* dependency the manifest declares.
|
|
76
|
+
*/
|
|
77
|
+
readonly unanswered: boolean;
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
/** The run's dependency evidence. */
|
|
@@ -299,10 +304,11 @@ export function projectNeeds<Brand>(
|
|
|
299
304
|
};
|
|
300
305
|
use([PROJECT_OWNER], packages);
|
|
301
306
|
|
|
307
|
+
let unanswered = false;
|
|
302
308
|
for (const file of project.ownSourceFiles()) {
|
|
303
|
-
fileNeeds(project, held, file, programPackages, use);
|
|
309
|
+
unanswered = fileNeeds(project, held, file, programPackages, use) || unanswered;
|
|
304
310
|
}
|
|
305
|
-
return { packages, uses };
|
|
311
|
+
return { packages, uses, unanswered };
|
|
306
312
|
}
|
|
307
313
|
|
|
308
314
|
/** The declarations of the inventory a use written at one node belongs to. */
|
|
@@ -329,13 +335,14 @@ function ownersOf(file: SourceFile, held: Inventory, node: Node): readonly strin
|
|
|
329
335
|
return [PROJECT_OWNER];
|
|
330
336
|
}
|
|
331
337
|
|
|
338
|
+
/** Records the needs of one file, and answers whether a specifier of it went unanswered. */
|
|
332
339
|
function fileNeeds<Brand>(
|
|
333
340
|
project: ProjectView<Brand>,
|
|
334
341
|
held: Inventory,
|
|
335
342
|
file: SourceFile,
|
|
336
343
|
programPackages: ReadonlySet<string>,
|
|
337
344
|
use: (owners: readonly string[], named: Iterable<string>) => void,
|
|
338
|
-
):
|
|
345
|
+
): boolean {
|
|
339
346
|
const specifiers = [...file.imports, ...file.moduleAugmentations].filter(isLiteralText);
|
|
340
347
|
const bindings: { readonly node: Texted; readonly specifier: Texted }[] = [];
|
|
341
348
|
const bound = new Set<Node>();
|
|
@@ -351,7 +358,7 @@ function fileNeeds<Brand>(
|
|
|
351
358
|
}
|
|
352
359
|
}
|
|
353
360
|
const names = new Set(bindings.map(({ node }) => node.text));
|
|
354
|
-
const candidates:
|
|
361
|
+
const candidates: Identifier[] = [];
|
|
355
362
|
const visit = (node: Node): void => {
|
|
356
363
|
if (isIdentifier(node) && names.has(node.text) && !bound.has(node)) {
|
|
357
364
|
candidates.push(node);
|
|
@@ -363,13 +370,8 @@ function fileNeeds<Brand>(
|
|
|
363
370
|
}
|
|
364
371
|
|
|
365
372
|
const asked = [...specifiers, ...bindings.map(({ node }) => node), ...candidates];
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
const handles: Handle<Brand>[] = asked
|
|
369
|
-
.slice(at, at + DEFAULT_BATCH_CAP)
|
|
370
|
-
.map((node) => project.handle(node));
|
|
371
|
-
resolved.push(...project.symbolsAt(handles));
|
|
372
|
-
}
|
|
373
|
+
const handles: Handle<Brand>[] = asked.map((node) => project.handle(node));
|
|
374
|
+
const resolved = project.symbolsAt(handles);
|
|
373
375
|
|
|
374
376
|
const packagesOf = new Map<Node, readonly string[]>();
|
|
375
377
|
specifiers.forEach((specifier, index) => {
|
|
@@ -378,7 +380,8 @@ function fileNeeds<Brand>(
|
|
|
378
380
|
if (byName !== undefined) {
|
|
379
381
|
named.add(byName);
|
|
380
382
|
}
|
|
381
|
-
|
|
383
|
+
const symbol = resolved[index];
|
|
384
|
+
for (const declaration of typeof symbol === "object" ? symbol.declarations : []) {
|
|
382
385
|
const byPath = packageOfPath(declaration.path);
|
|
383
386
|
if (byPath !== undefined) {
|
|
384
387
|
named.add(byPath);
|
|
@@ -387,11 +390,19 @@ function fileNeeds<Brand>(
|
|
|
387
390
|
packagesOf.set(specifier, [...named]);
|
|
388
391
|
});
|
|
389
392
|
|
|
393
|
+
// A name the checker could not resolve counts as a use of every package a binding of
|
|
394
|
+
// that name brings in, so a gap never makes a dependency read as unused.
|
|
390
395
|
const bySymbol = new Map<number, readonly string[]>();
|
|
391
|
-
|
|
396
|
+
const byName = new Map<string, string[]>();
|
|
397
|
+
const unresolvedNames = new Set<string>();
|
|
398
|
+
bindings.forEach(({ node, specifier }, index) => {
|
|
399
|
+
const packages = packagesOf.get(specifier) ?? [];
|
|
400
|
+
byName.set(node.text, [...(byName.get(node.text) ?? []), ...packages]);
|
|
392
401
|
const symbol = resolved[specifiers.length + index];
|
|
393
|
-
if (symbol
|
|
394
|
-
|
|
402
|
+
if (symbol === UNANSWERED) {
|
|
403
|
+
unresolvedNames.add(node.text);
|
|
404
|
+
} else if (symbol !== undefined) {
|
|
405
|
+
bySymbol.set(symbol.id, packages);
|
|
395
406
|
}
|
|
396
407
|
});
|
|
397
408
|
|
|
@@ -409,7 +420,13 @@ function fileNeeds<Brand>(
|
|
|
409
420
|
if (parent !== undefined && isShorthandPropertyAssignment(parent) && parent.name === node) {
|
|
410
421
|
symbol = project.shorthandValueAt(project.handle(node));
|
|
411
422
|
}
|
|
412
|
-
const named =
|
|
423
|
+
const named =
|
|
424
|
+
symbol === UNANSWERED || (unresolvedNames.has(node.text) && symbol === undefined)
|
|
425
|
+
? byName.get(node.text)
|
|
426
|
+
: symbol === undefined
|
|
427
|
+
? undefined
|
|
428
|
+
: (bySymbol.get(symbol.id) ??
|
|
429
|
+
(unresolvedNames.has(node.text) ? byName.get(node.text) : undefined));
|
|
413
430
|
if (named !== undefined) {
|
|
414
431
|
use(ownersOf(file, held, node), named);
|
|
415
432
|
}
|
|
@@ -418,6 +435,7 @@ function fileNeeds<Brand>(
|
|
|
418
435
|
for (const reference of file.typeReferenceDirectives) {
|
|
419
436
|
use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
|
|
420
437
|
}
|
|
438
|
+
return resolved.slice(0, specifiers.length).includes(UNANSWERED);
|
|
421
439
|
}
|
|
422
440
|
|
|
423
441
|
/** The offset just past the JSON value that starts at `at` in text the parser has accepted. */
|
|
@@ -591,12 +609,16 @@ export function dependenciesOf(
|
|
|
591
609
|
const needed = new Set<string>();
|
|
592
610
|
const uses = new Map<string, Set<string>>();
|
|
593
611
|
for (const project of perProject) {
|
|
594
|
-
|
|
595
|
-
|
|
612
|
+
// A specifier the checker left unanswered may resolve to any declared dependency,
|
|
613
|
+
// and what the configuration needs is never deleted, so it is needed there.
|
|
614
|
+
const unknown = project.unanswered ? declared.map((dependency) => dependency.name) : [];
|
|
615
|
+
const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)] as const];
|
|
616
|
+
for (const [id, used] of [...project.uses, ...held]) {
|
|
596
617
|
const merged = uses.get(id) ?? new Set<string>();
|
|
597
618
|
used.forEach((one) => merged.add(one));
|
|
598
619
|
uses.set(id, merged);
|
|
599
620
|
}
|
|
621
|
+
[...project.packages, ...unknown].forEach((one) => needed.add(one));
|
|
600
622
|
}
|
|
601
623
|
const users = new Map<string, number>();
|
|
602
624
|
for (const used of uses.values()) {
|
package/src/deprecation.ts
CHANGED
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { isVariableStatement, SyntaxKind, type Node } from "@typescript/native/unstable/ast";
|
|
11
|
-
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
12
11
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
13
|
-
import {
|
|
12
|
+
import { isAnswered, UNANSWERED } from "./query.ts";
|
|
14
13
|
import type { Handle, ProjectView } from "./session.ts";
|
|
15
14
|
|
|
16
15
|
/** The tag the TypeScript tools recognize as marking a declaration deprecated. */
|
|
@@ -31,19 +30,31 @@ function nameNode(node: Node): Node {
|
|
|
31
30
|
return (node as { readonly name?: Node }).name ?? node;
|
|
32
31
|
}
|
|
33
32
|
|
|
33
|
+
/** Which declarations of one project carry the deprecation marker. */
|
|
34
|
+
export interface Deprecation {
|
|
35
|
+
/** Every declaration that carries it, in the inventory's order. */
|
|
36
|
+
readonly deprecated: readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* Every declaration whose symbol or tags the checker left unanswered where the
|
|
39
|
+
* project's files hold the tag, in the inventory's order: whether it carries the
|
|
40
|
+
* marker is unknown.
|
|
41
|
+
*/
|
|
42
|
+
readonly unanswered: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
/**
|
|
35
|
-
* The
|
|
36
|
-
* marker, in the inventory's order.
|
|
46
|
+
* The declarations of one project that carry the deprecation marker.
|
|
37
47
|
*
|
|
38
48
|
* The checker is asked only for a symbol with a declaration in a file whose tree holds
|
|
39
49
|
* the tag, so a project whose files hold none costs no round trip, and one that does
|
|
40
|
-
* costs one batch per file and one
|
|
50
|
+
* costs one batch per file and one for the tags of the symbols that could carry it.
|
|
41
51
|
*/
|
|
42
52
|
export function deprecatedDeclarations<Brand>(
|
|
43
53
|
project: ProjectView<Brand>,
|
|
44
54
|
held: Inventory,
|
|
45
|
-
):
|
|
55
|
+
): Deprecation {
|
|
46
56
|
const marked = new Set<string>();
|
|
57
|
+
const unknown = new Set<string>();
|
|
47
58
|
const markedPaths = new Set<string>();
|
|
48
59
|
const declared: { readonly id: string; readonly handle: Handle<Brand> }[][] = [];
|
|
49
60
|
|
|
@@ -72,30 +83,45 @@ export function deprecatedDeclarations<Brand>(
|
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
if (markedPaths.size > 0) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
const pairs = declared.flatMap((named) => {
|
|
87
|
+
const symbols = project.symbolsAt(named.map((one) => one.handle));
|
|
88
|
+
return named.flatMap((one, at) => {
|
|
89
|
+
const symbol = symbols[at];
|
|
90
|
+
if (symbol === UNANSWERED) {
|
|
91
|
+
unknown.add(one.id);
|
|
92
|
+
}
|
|
93
|
+
return typeof symbol === "object" &&
|
|
94
|
+
symbol.declarations.some((declaration) => markedPaths.has(declaration.path))
|
|
95
|
+
? [{ id: one.id, symbol }]
|
|
96
|
+
: [];
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
const distinct = [...new Map(pairs.map((one) => [one.symbol.id, one.symbol])).values()];
|
|
100
|
+
const tags = project.queries.jsDocTags(distinct);
|
|
101
|
+
const unread = new Set(
|
|
102
|
+
distinct.filter((_, at) => tags[at] === UNANSWERED).map((one) => one.id),
|
|
103
|
+
);
|
|
104
|
+
const carries = new Set(
|
|
105
|
+
distinct.flatMap((symbol, at) => {
|
|
106
|
+
const read = tags[at];
|
|
107
|
+
return read !== undefined &&
|
|
108
|
+
isAnswered(read) &&
|
|
109
|
+
read.some((tag) => tag.name === DEPRECATED_TAG)
|
|
110
|
+
? [symbol.id]
|
|
111
|
+
: [];
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
for (const one of pairs) {
|
|
115
|
+
if (carries.has(one.symbol.id)) {
|
|
116
|
+
marked.add(one.id);
|
|
80
117
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
symbol.getJsDocTags(project.checker).some((tag) => tag.name === DEPRECATED_TAG);
|
|
84
|
-
asked.set(symbol.id, carries);
|
|
85
|
-
return carries;
|
|
86
|
-
};
|
|
87
|
-
for (const named of declared) {
|
|
88
|
-
for (let start = 0; start < named.length; start += DEFAULT_BATCH_CAP) {
|
|
89
|
-
const batch = named.slice(start, start + DEFAULT_BATCH_CAP);
|
|
90
|
-
project.symbolsAt(batch.map((one) => one.handle)).forEach((symbol, at) => {
|
|
91
|
-
const one = batch[at];
|
|
92
|
-
if (symbol !== undefined && one !== undefined && carriesTag(symbol)) {
|
|
93
|
-
marked.add(one.id);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
118
|
+
if (unread.has(one.symbol.id)) {
|
|
119
|
+
unknown.add(one.id);
|
|
96
120
|
}
|
|
97
121
|
}
|
|
98
122
|
}
|
|
99
123
|
|
|
100
|
-
|
|
124
|
+
const inOrder = (ids: ReadonlySet<string>): readonly string[] =>
|
|
125
|
+
held.symbols.filter((symbol) => ids.has(symbol.id)).map((symbol) => symbol.id);
|
|
126
|
+
return { deprecated: inOrder(marked), unanswered: inOrder(unknown) };
|
|
101
127
|
}
|
package/src/destructuring.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type Node,
|
|
26
26
|
} from "@typescript/native/unstable/ast";
|
|
27
27
|
import { TypeFlags, type Symbol as TSSymbol, type Type } from "@typescript/native/unstable/sync";
|
|
28
|
+
import { isAnswered, UNANSWERED, type Answer } from "./query.ts";
|
|
28
29
|
import type { ProjectView } from "./session.ts";
|
|
29
30
|
|
|
30
31
|
/** One property one object pattern reads. */
|
|
@@ -212,97 +213,102 @@ interface DestructuringCost {
|
|
|
212
213
|
interface Destructuring {
|
|
213
214
|
/**
|
|
214
215
|
* The property symbol each read names, in the order given, `undefined` where the
|
|
215
|
-
* destructured value's type declares no property of that name or is not known
|
|
216
|
+
* destructured value's type declares no property of that name or is not known, and
|
|
217
|
+
* {@link UNANSWERED} where a question about the value's type went unanswered.
|
|
216
218
|
*/
|
|
217
|
-
resolve(reads: readonly PropertyRead[]): readonly
|
|
219
|
+
resolve(reads: readonly PropertyRead[]): readonly Answer<TSSymbol | undefined>[];
|
|
218
220
|
readonly cost: DestructuringCost;
|
|
219
221
|
}
|
|
220
222
|
|
|
223
|
+
/** A type along a destructuring, or the mark that a question about it went unanswered. */
|
|
224
|
+
type Found = Answer<Type | undefined>;
|
|
225
|
+
|
|
221
226
|
/**
|
|
222
227
|
* The resolution over one project, its batches capped at `cap`. Property tables are
|
|
223
228
|
* read once per type for the whole project, since one type is destructured in many
|
|
224
229
|
* places and a table answers every name of it.
|
|
225
230
|
*/
|
|
226
231
|
export function destructuring<Brand>(project: ProjectView<Brand>, cap: number): Destructuring {
|
|
227
|
-
const
|
|
228
|
-
const tables = new Map<number, ReadonlyMap<string, TSSymbol
|
|
232
|
+
const queries = project.queries;
|
|
233
|
+
const tables = new Map<number, Answer<ReadonlyMap<string, TSSymbol>>>();
|
|
229
234
|
let patternBatches = 0;
|
|
230
235
|
let patternLookups = 0;
|
|
231
236
|
|
|
232
|
-
const
|
|
233
|
-
const types: (Type | undefined)[] = [];
|
|
234
|
-
for (let from = 0; from < nodes.length; from += cap) {
|
|
235
|
-
types.push(...checker.getTypeAtLocation(nodes.slice(from, from + cap)));
|
|
236
|
-
patternBatches += 1;
|
|
237
|
-
}
|
|
238
|
-
return types;
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
const tableOf = (type: Type): ReadonlyMap<string, TSSymbol> => {
|
|
237
|
+
const tableOf = (type: Type): Answer<ReadonlyMap<string, TSSymbol>> => {
|
|
242
238
|
const known = tables.get(type.id);
|
|
243
239
|
if (known !== undefined) {
|
|
244
240
|
return known;
|
|
245
241
|
}
|
|
246
|
-
|
|
242
|
+
let table: Answer<ReadonlyMap<string, TSSymbol>> = new Map<string, TSSymbol>();
|
|
247
243
|
if ((type.flags & TypeFlags.AnyOrUnknown) === 0) {
|
|
248
244
|
patternLookups += 1;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
245
|
+
const properties = queries.propertiesOf(type);
|
|
246
|
+
table = isAnswered(properties)
|
|
247
|
+
? new Map(properties.map((property) => [property.name, property]))
|
|
248
|
+
: UNANSWERED;
|
|
252
249
|
}
|
|
253
250
|
tables.set(type.id, table);
|
|
254
251
|
return table;
|
|
255
252
|
};
|
|
256
253
|
|
|
257
|
-
const definedOf = (type: Type):
|
|
254
|
+
const definedOf = (type: Type): Found => {
|
|
258
255
|
if ((type.flags & TypeFlags.Union) === 0) {
|
|
259
256
|
return type;
|
|
260
257
|
}
|
|
261
258
|
patternLookups += 1;
|
|
262
|
-
return
|
|
259
|
+
return queries.nonNullable(type);
|
|
263
260
|
};
|
|
264
261
|
|
|
265
262
|
/**
|
|
266
263
|
* The type one element of an array or tuple value has: a tuple's element at `index`,
|
|
267
264
|
* an array's element type at any index, and `undefined` for a tuple iterated whole.
|
|
268
265
|
*/
|
|
269
|
-
const elementOf = (type: Type, index: number | undefined):
|
|
266
|
+
const elementOf = (type: Type, index: number | undefined): Found => {
|
|
270
267
|
if (!type.isTypeReference()) {
|
|
271
268
|
return undefined;
|
|
272
269
|
}
|
|
273
|
-
|
|
274
|
-
if (checker.isTupleType(type)) {
|
|
270
|
+
if (type.isTupleType()) {
|
|
275
271
|
if (index === undefined) {
|
|
276
272
|
return undefined;
|
|
277
273
|
}
|
|
278
274
|
patternLookups += 1;
|
|
279
|
-
|
|
275
|
+
const elements = queries.typeArguments(type);
|
|
276
|
+
return isAnswered(elements) ? elements[index] : UNANSWERED;
|
|
280
277
|
}
|
|
281
278
|
patternLookups += 1;
|
|
282
|
-
|
|
279
|
+
const array = queries.isArray(type);
|
|
280
|
+
if (!isAnswered(array)) {
|
|
281
|
+
return UNANSWERED;
|
|
282
|
+
}
|
|
283
|
+
if (!array) {
|
|
283
284
|
return undefined;
|
|
284
285
|
}
|
|
285
286
|
patternLookups += 1;
|
|
286
|
-
|
|
287
|
+
const elements = queries.typeArguments(type);
|
|
288
|
+
return isAnswered(elements) ? elements[0] : UNANSWERED;
|
|
287
289
|
};
|
|
288
290
|
|
|
289
|
-
const step = (type: Type, next: Step):
|
|
291
|
+
const step = (type: Type, next: Step): Found => {
|
|
290
292
|
const defined = definedOf(type);
|
|
291
|
-
if (defined === undefined) {
|
|
292
|
-
return
|
|
293
|
+
if (defined === undefined || defined === UNANSWERED) {
|
|
294
|
+
return defined;
|
|
293
295
|
}
|
|
294
296
|
if (next.kind === "property") {
|
|
295
|
-
const
|
|
297
|
+
const table = tableOf(defined);
|
|
298
|
+
if (!isAnswered(table)) {
|
|
299
|
+
return UNANSWERED;
|
|
300
|
+
}
|
|
301
|
+
const property = table.get(next.name);
|
|
296
302
|
if (property === undefined) {
|
|
297
303
|
return undefined;
|
|
298
304
|
}
|
|
299
305
|
patternLookups += 1;
|
|
300
|
-
return
|
|
306
|
+
return queries.typeOfSymbol(property);
|
|
301
307
|
}
|
|
302
308
|
return elementOf(defined, next.kind === "element" ? next.index : undefined);
|
|
303
309
|
};
|
|
304
310
|
|
|
305
|
-
const resolve = (reads: readonly PropertyRead[]): readonly
|
|
311
|
+
const resolve = (reads: readonly PropertyRead[]): readonly Answer<TSSymbol | undefined>[] => {
|
|
306
312
|
const patterns = [...new Set(reads.map((read) => read.pattern))];
|
|
307
313
|
const sources = new Map<Node, Source | undefined>();
|
|
308
314
|
const asking = new Set<Node>();
|
|
@@ -317,15 +323,16 @@ export function destructuring<Brand>(project: ProjectView<Brand>, cap: number):
|
|
|
317
323
|
asking.add(source.expression);
|
|
318
324
|
}
|
|
319
325
|
}
|
|
320
|
-
const answered = new Map<Node, Type>();
|
|
321
326
|
const asked = [...asking];
|
|
322
|
-
|
|
327
|
+
patternBatches += Math.ceil(asked.length / cap);
|
|
328
|
+
const answered = new Map<Node, Found>();
|
|
329
|
+
queries.typesAt(asked, cap).forEach((type, index) => {
|
|
323
330
|
const node = asked[index];
|
|
324
|
-
if (
|
|
331
|
+
if (node !== undefined) {
|
|
325
332
|
answered.set(node, type);
|
|
326
333
|
}
|
|
327
334
|
});
|
|
328
|
-
const patternTypes = new Map<Node,
|
|
335
|
+
const patternTypes = new Map<Node, Found>();
|
|
329
336
|
for (const pattern of patterns) {
|
|
330
337
|
const source = sources.get(pattern);
|
|
331
338
|
if (source === undefined) {
|
|
@@ -334,13 +341,17 @@ export function destructuring<Brand>(project: ProjectView<Brand>, cap: number):
|
|
|
334
341
|
}
|
|
335
342
|
let type = answered.get(source.expression);
|
|
336
343
|
for (const next of source.steps) {
|
|
337
|
-
type = type === undefined ?
|
|
344
|
+
type = type === undefined || type === UNANSWERED ? type : step(type, next);
|
|
338
345
|
}
|
|
339
|
-
patternTypes.set(pattern, type === undefined ?
|
|
346
|
+
patternTypes.set(pattern, type === undefined || type === UNANSWERED ? type : definedOf(type));
|
|
340
347
|
}
|
|
341
348
|
return reads.map((read) => {
|
|
342
349
|
const type = patternTypes.get(read.pattern);
|
|
343
|
-
|
|
350
|
+
if (type === undefined || type === UNANSWERED) {
|
|
351
|
+
return type;
|
|
352
|
+
}
|
|
353
|
+
const table = tableOf(type);
|
|
354
|
+
return isAnswered(table) ? table.get(read.name) : UNANSWERED;
|
|
344
355
|
});
|
|
345
356
|
};
|
|
346
357
|
|