@cplieger/deadset-ts 3.12.1 → 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 +4 -4
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +217 -48
- package/dist/src/calls.js +23 -22
- package/dist/src/class-members.js +10 -11
- package/dist/src/components.js +4 -2
- package/dist/src/configured-declarations.js +56 -7
- package/dist/src/conformance-record.js +2 -2
- 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/edges.js +4 -1
- 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/completion.js +37 -8
- package/dist/src/findings/emitters.js +20 -6
- package/dist/src/findings/interfaces.js +7 -1
- package/dist/src/findings/intra-function.js +91 -2
- package/dist/src/findings/self-check.js +30 -7
- package/dist/src/findings/unused-declarations.js +15 -3
- package/dist/src/findings-pass.js +18 -11
- 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 +660 -0
- package/dist/src/inventory.js +85 -4
- package/dist/src/json-chunks.js +76 -0
- package/dist/src/kinds.js +337 -35
- 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 +46 -4
- package/dist/src/report.js +14 -3
- package/dist/src/reporters/annotations.js +9 -5
- package/dist/src/reporters/json.js +3 -2
- package/dist/src/reporters/sarif.js +22 -16
- package/dist/src/reporters/template.js +112 -12
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- package/dist/src/serialization-contract.js +53 -32
- package/dist/src/session.js +79 -39
- package/dist/src/signatures.js +32 -0
- package/dist/src/suppress-file.js +29 -7
- package/dist/src/suppress-inline.js +8 -1
- package/dist/src/suppress.js +27 -0
- package/dist/src/sweep.js +4 -1
- package/dist/src/value-reach.js +44 -25
- package/dist/src/verbs/analyze.js +39 -14
- package/dist/src/verbs/describe.js +12 -8
- 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/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +75 -11
- package/src/analysis.ts +385 -104
- package/src/calls.ts +10 -9
- package/src/class-members.ts +10 -11
- package/src/components.ts +4 -2
- package/src/configured-declarations.ts +80 -8
- package/src/conformance-record.ts +2 -2
- 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/edges.ts +4 -1
- package/src/entry-points.ts +7 -0
- package/src/enum-group.ts +56 -42
- package/src/exempt.ts +50 -2
- package/src/finding.ts +5 -1
- package/src/findings/completion.ts +46 -10
- package/src/findings/emitter.ts +3 -0
- package/src/findings/emitters.ts +27 -6
- package/src/findings/interfaces.ts +7 -1
- package/src/findings/intra-function.ts +106 -3
- package/src/findings/self-check.ts +48 -7
- package/src/findings/unused-declarations.ts +18 -4
- package/src/findings-pass.ts +26 -11
- 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 +889 -0
- package/src/inventory.ts +97 -4
- package/src/json-chunks.ts +87 -0
- package/src/kinds.ts +355 -41
- package/src/partial.ts +40 -0
- package/src/query.ts +522 -0
- package/src/references.ts +119 -38
- package/src/reflective-lookup.ts +56 -6
- package/src/report.ts +32 -4
- 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 +27 -18
- package/src/reporters/template.ts +119 -14
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- package/src/serialization-contract.ts +59 -40
- package/src/session.ts +146 -58
- package/src/signatures.ts +55 -0
- package/src/suppress-file.ts +40 -8
- package/src/suppress-inline.ts +8 -1
- package/src/suppress.ts +31 -0
- package/src/sweep.ts +9 -4
- package/src/value-reach.ts +66 -28
- package/src/verbs/analyze.ts +51 -14
- package/src/verbs/describe.ts +13 -9
- 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/version.ts +1 -1
- package/dist/src/reporters/rules.js +0 -178
- package/src/reporters/rules.ts +0 -195
package/src/calls.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
27
27
|
import type { AliasChains } from "./alias-chain.ts";
|
|
28
28
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
29
|
-
import {
|
|
29
|
+
import { must } from "./query.ts";
|
|
30
30
|
import type { ProjectView } from "./session.ts";
|
|
31
31
|
|
|
32
32
|
/** One value written where a call passes it, and the node its declaration is resolved from. */
|
|
@@ -77,7 +77,8 @@ export function decoratorsOf(node: Node): readonly Decorator[] {
|
|
|
77
77
|
/**
|
|
78
78
|
* What each of a batch of nodes resolves to, every alias followed to the declaration
|
|
79
79
|
* it stands for. A node the checker resolves to nothing, and an alias that resolves
|
|
80
|
-
* to nothing, are absent from the answer. Each alias is followed once.
|
|
80
|
+
* to nothing, are absent from the answer. Each alias is followed once. A question the
|
|
81
|
+
* checker does not answer stops the pass, as {@link must} stops.
|
|
81
82
|
*/
|
|
82
83
|
export function resolvedTargets<Brand>(
|
|
83
84
|
project: ProjectView<Brand>,
|
|
@@ -85,17 +86,18 @@ export function resolvedTargets<Brand>(
|
|
|
85
86
|
): ReadonlyMap<Node, TSSymbol> {
|
|
86
87
|
const followed = new Map<number, TSSymbol | undefined>();
|
|
87
88
|
const targets = new Map<Node, TSSymbol>();
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
project
|
|
90
|
+
.symbolsAt(nodes.map((node) => project.handle(node)))
|
|
91
|
+
.map(must)
|
|
92
|
+
.forEach((symbol, index) => {
|
|
93
|
+
const node = nodes[index];
|
|
92
94
|
if (symbol === undefined || node === undefined) {
|
|
93
95
|
return;
|
|
94
96
|
}
|
|
95
97
|
let target: TSSymbol | undefined = symbol;
|
|
96
98
|
if ((symbol.flags & SymbolFlags.Alias) !== 0) {
|
|
97
99
|
if (!followed.has(symbol.id)) {
|
|
98
|
-
const aliased = project.
|
|
100
|
+
const aliased = must(project.queries.aliased(symbol));
|
|
99
101
|
followed.set(symbol.id, aliased.declarations.length === 0 ? undefined : aliased);
|
|
100
102
|
}
|
|
101
103
|
target = followed.get(symbol.id);
|
|
@@ -104,7 +106,6 @@ export function resolvedTargets<Brand>(
|
|
|
104
106
|
targets.set(node, target);
|
|
105
107
|
}
|
|
106
108
|
});
|
|
107
|
-
}
|
|
108
109
|
return targets;
|
|
109
110
|
}
|
|
110
111
|
|
|
@@ -166,7 +167,7 @@ export function classesPassed<Brand>(
|
|
|
166
167
|
);
|
|
167
168
|
const classesOf = (value: PassedValue): readonly string[] => {
|
|
168
169
|
const symbol = value.shorthand
|
|
169
|
-
? project.shorthandValueAt(project.handle(value.name))
|
|
170
|
+
? must(project.shorthandValueAt(project.handle(value.name)))
|
|
170
171
|
: targets.get(value.name);
|
|
171
172
|
return symbol === undefined
|
|
172
173
|
? []
|
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;
|
package/src/components.ts
CHANGED
|
@@ -132,7 +132,8 @@ function linesSpanned(symbols: readonly (InventorySymbol | undefined)[]): number
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
|
-
* The dead declarations by their place in the subgraph,
|
|
135
|
+
* The dead declarations by their place in the subgraph, a test file's declaration only
|
|
136
|
+
* where it is a test of dead code, and the edges between them: each
|
|
136
137
|
* reference one makes to another, an edge each way between a dead member and its dead
|
|
137
138
|
* container (one direction alone leaves the member a component the container reaches),
|
|
138
139
|
* and an edge back from each production target of an admitted test, which puts the test in
|
|
@@ -145,7 +146,8 @@ function deadSubgraph(
|
|
|
145
146
|
): { readonly at: readonly number[]; readonly adjacent: readonly (readonly number[])[] } {
|
|
146
147
|
const at: number[] = [];
|
|
147
148
|
const position = graph.symbols.map((_symbol, index) => {
|
|
148
|
-
|
|
149
|
+
// A test file's declaration belongs to a component only as a test of dead code.
|
|
150
|
+
if (dead[index] !== true || (graph.test[index] === true && testOfDeadCode[index] !== true)) {
|
|
149
151
|
return UNVISITED;
|
|
150
152
|
}
|
|
151
153
|
at.push(index);
|
|
@@ -23,8 +23,9 @@ import {
|
|
|
23
23
|
} from "@typescript/native/unstable/sync";
|
|
24
24
|
import type { AliasChains } from "./alias-chain.ts";
|
|
25
25
|
import { resolvedTargets } from "./calls.ts";
|
|
26
|
-
import type { DeclarationEntry } from "./config.ts";
|
|
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);
|
|
@@ -306,3 +311,70 @@ export function spelledEntry(entry: DeclarationEntry, held: Inventory): string {
|
|
|
306
311
|
return entry.global;
|
|
307
312
|
}
|
|
308
313
|
}
|
|
314
|
+
|
|
315
|
+
/** One entry of a configuration key that names a declaration, and the key's dotted path. */
|
|
316
|
+
interface ConfiguredDeclaration {
|
|
317
|
+
readonly key: string;
|
|
318
|
+
readonly entry: DeclarationEntry;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The key under which a lifecycle contract's declarations are configured. */
|
|
322
|
+
const LIFECYCLE_KEY = "ts.lifecycle_contracts";
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Every entry of the three keys that name declarations, in the configuration's order:
|
|
326
|
+
* the injection registrations, each lifecycle contract's components and then its bases,
|
|
327
|
+
* and the serializers.
|
|
328
|
+
*/
|
|
329
|
+
export function configuredDeclarations(config: Config): readonly ConfiguredDeclaration[] {
|
|
330
|
+
return [
|
|
331
|
+
...config.ts.injectionRegistrations.map((entry) => ({
|
|
332
|
+
key: "ts.injection_registrations",
|
|
333
|
+
entry,
|
|
334
|
+
})),
|
|
335
|
+
...config.ts.lifecycleContracts.flatMap((contract) =>
|
|
336
|
+
[...contract.components, ...contract.bases].map((entry) => ({ key: LIFECYCLE_KEY, entry })),
|
|
337
|
+
),
|
|
338
|
+
...config.ts.serializers.map((entry) => ({ key: "ts.serializers", entry })),
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
|
|
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
|
+
*/
|
|
347
|
+
export function declarationsNamed<Brand>(
|
|
348
|
+
project: ProjectView<Brand>,
|
|
349
|
+
held: Inventory,
|
|
350
|
+
configured: readonly ConfiguredDeclaration[],
|
|
351
|
+
): readonly boolean[] {
|
|
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
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The reference a finding about a configured entry names it by, the entry as the
|
|
368
|
+
* configuration spells it: a symbol entry's reference, a module entry's module, a number
|
|
369
|
+
* sign and its name, and a global entry's path after a number sign.
|
|
370
|
+
*/
|
|
371
|
+
export function configuredRef(entry: DeclarationEntry): string {
|
|
372
|
+
switch (entry.shape) {
|
|
373
|
+
case "symbol":
|
|
374
|
+
return entry.symbol;
|
|
375
|
+
case "module":
|
|
376
|
+
return `${entry.module}#${entry.name}`;
|
|
377
|
+
case "global":
|
|
378
|
+
return `#${entry.global}`;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
|
|
3
3
|
export const RECORD = {
|
|
4
4
|
conformance: {
|
|
5
|
-
corpusVersion: "
|
|
5
|
+
corpusVersion: "2.0.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:1274315595d7b944d22eb0126081e24cf293c9fe53a236f4d263c714644f32ee",
|
|
8
8
|
},
|
|
9
9
|
gaps: [
|
|
10
10
|
{
|
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
|
}
|