@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/implementations.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { SymbolFlags, type Symbol as TSSymbol, type Type } from "@typescript/native/unstable/sync";
|
|
18
18
|
import { aliasChains } from "./alias-chain.ts";
|
|
19
19
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
20
|
-
import {
|
|
20
|
+
import { must, Unanswerable } from "./query.ts";
|
|
21
21
|
import type { ProjectView } from "./session.ts";
|
|
22
22
|
|
|
23
23
|
/** What one run knows about how its interfaces are implemented. */
|
|
@@ -29,6 +29,13 @@ export interface Implementations {
|
|
|
29
29
|
* class's implementation is a method whose body holds no statement.
|
|
30
30
|
*/
|
|
31
31
|
readonly bodies: ReadonlyMap<string, ReadonlyMap<string, boolean>>;
|
|
32
|
+
/**
|
|
33
|
+
* The interface methods whose implementations a project could not read in full,
|
|
34
|
+
* because the checker left a question about them unanswered. Each is treated as a
|
|
35
|
+
* marker, whose every implementation is empty, so none is reported for want of an
|
|
36
|
+
* answer.
|
|
37
|
+
*/
|
|
38
|
+
readonly unknown: ReadonlySet<string>;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
/** One interface or class the inventory holds, by its declaration. */
|
|
@@ -97,24 +104,22 @@ function walk<Brand>(
|
|
|
97
104
|
return { interfaces, classes, clauses };
|
|
98
105
|
}
|
|
99
106
|
|
|
100
|
-
/** The type at each node
|
|
107
|
+
/** The type at each node. */
|
|
101
108
|
function typesAt<Brand>(project: ProjectView<Brand>, nodes: readonly Node[]): Map<Node, Type> {
|
|
102
109
|
const typeAt = new Map<Node, Type>();
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
});
|
|
111
|
-
}
|
|
110
|
+
project.queries.typesAt(nodes).forEach((answer, index) => {
|
|
111
|
+
const type = must(answer);
|
|
112
|
+
const node = nodes[index];
|
|
113
|
+
if (type !== undefined && node !== undefined) {
|
|
114
|
+
typeAt.set(node, type);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
112
117
|
return typeAt;
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
/** Whether one class member is a method whose every written body holds no statement. */
|
|
116
121
|
function emptyImplementation<Brand>(project: ProjectView<Brand>, member: TSSymbol): boolean {
|
|
117
|
-
const own =
|
|
122
|
+
const own = project.ownPaths();
|
|
118
123
|
let bodies = 0;
|
|
119
124
|
for (const handle of member.declarations) {
|
|
120
125
|
// A body outside the target is one this analysis cannot read, so it is not empty.
|
|
@@ -135,19 +140,41 @@ function emptyImplementation<Brand>(project: ProjectView<Brand>, member: TSSymbo
|
|
|
135
140
|
return bodies > 0;
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
/**
|
|
143
|
+
/**
|
|
144
|
+
* How one project's interfaces are implemented by its classes. Where the checker leaves
|
|
145
|
+
* a question unanswered, what was read stands and every interface method of the project
|
|
146
|
+
* is named in {@link Implementations.unknown}.
|
|
147
|
+
*/
|
|
139
148
|
export function implementations<Brand>(
|
|
140
149
|
project: ProjectView<Brand>,
|
|
141
150
|
held: Inventory,
|
|
142
151
|
): Implementations {
|
|
143
152
|
const classes = new Map<string, Set<string>>();
|
|
144
153
|
const bodies = new Map<string, Map<string, boolean>>();
|
|
154
|
+
try {
|
|
155
|
+
implementedIn(project, held, classes, bodies);
|
|
156
|
+
return { classes, bodies, unknown: new Set() };
|
|
157
|
+
} catch (error: unknown) {
|
|
158
|
+
if (!(error instanceof Unanswerable)) {
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
const methods = held.symbols.filter((symbol) => symbol.kind === "interface-method");
|
|
162
|
+
return { classes, bodies, unknown: new Set(methods.map((symbol) => symbol.id)) };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function implementedIn<Brand>(
|
|
167
|
+
project: ProjectView<Brand>,
|
|
168
|
+
held: Inventory,
|
|
169
|
+
classes: Map<string, Set<string>>,
|
|
170
|
+
bodies: Map<string, Map<string, boolean>>,
|
|
171
|
+
): void {
|
|
145
172
|
const walked = walk(project, held);
|
|
146
173
|
if (walked.interfaces.length === 0 || walked.classes.length === 0) {
|
|
147
|
-
return
|
|
174
|
+
return;
|
|
148
175
|
}
|
|
149
176
|
|
|
150
|
-
const
|
|
177
|
+
const queries = project.queries;
|
|
151
178
|
const typeAt = typesAt(project, [
|
|
152
179
|
...walked.interfaces.map((one) => one.nameNode),
|
|
153
180
|
...walked.classes.map((one) => one.nameNode),
|
|
@@ -178,10 +205,10 @@ export function implementations<Brand>(
|
|
|
178
205
|
}
|
|
179
206
|
|
|
180
207
|
const membersOf = (at: InterfaceAt): readonly TSSymbol[] =>
|
|
181
|
-
(at.members ??=
|
|
208
|
+
(at.members ??= must(queries.propertiesOf(at.type)));
|
|
182
209
|
const classMembers = (at: ClassAt): ReadonlyMap<string, TSSymbol> =>
|
|
183
210
|
(at.members ??= new Map(
|
|
184
|
-
|
|
211
|
+
must(queries.propertiesOf(at.type)).map((member) => [member.escapedName, member]),
|
|
185
212
|
));
|
|
186
213
|
|
|
187
214
|
const methodIds = new Set(
|
|
@@ -233,12 +260,14 @@ export function implementations<Brand>(
|
|
|
233
260
|
if (entry === undefined || implementer === undefined) {
|
|
234
261
|
continue;
|
|
235
262
|
}
|
|
236
|
-
const at = byType.get(
|
|
263
|
+
const at = byType.get(must(queries.targetOf(entry)).id);
|
|
237
264
|
if (at !== undefined) {
|
|
238
265
|
pair(at, implementer);
|
|
239
266
|
}
|
|
240
267
|
}
|
|
241
268
|
|
|
269
|
+
// Every pair whose names line up is asked about at once.
|
|
270
|
+
const candidates: { readonly at: InterfaceAt; readonly implementer: ClassAt }[] = [];
|
|
242
271
|
for (const at of byType.values()) {
|
|
243
272
|
const members = at.generic ? [] : membersOf(at);
|
|
244
273
|
if (members.length === 0) {
|
|
@@ -252,18 +281,25 @@ export function implementations<Brand>(
|
|
|
252
281
|
const written = classMembers(implementer);
|
|
253
282
|
if (
|
|
254
283
|
required.every((member) => written.has(member.escapedName)) &&
|
|
255
|
-
members.some((member) => written.has(member.escapedName))
|
|
256
|
-
checker.isTypeAssignableTo(implementer.type, at.type)
|
|
284
|
+
members.some((member) => written.has(member.escapedName))
|
|
257
285
|
) {
|
|
258
|
-
|
|
286
|
+
candidates.push({ at, implementer });
|
|
259
287
|
}
|
|
260
288
|
}
|
|
261
289
|
}
|
|
262
|
-
|
|
290
|
+
const assignable = queries.assignableEach(
|
|
291
|
+
candidates.map((one) => ({ source: one.implementer.type, target: one.at.type })),
|
|
292
|
+
);
|
|
293
|
+
candidates.forEach((one, index) => {
|
|
294
|
+
if (must(assignable[index] ?? false)) {
|
|
295
|
+
pair(one.at, one.implementer);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
263
298
|
}
|
|
264
299
|
|
|
265
300
|
/** Every project's answers as one run's, each implementation once. */
|
|
266
301
|
export function mergeImplementations(per: readonly Implementations[]): Implementations {
|
|
302
|
+
const unknown = new Set(per.flatMap((one) => [...one.unknown]));
|
|
267
303
|
const classes = new Map<string, Set<string>>();
|
|
268
304
|
const bodies = new Map<string, Map<string, boolean>>();
|
|
269
305
|
for (const one of per) {
|
|
@@ -282,5 +318,5 @@ export function mergeImplementations(per: readonly Implementations[]): Implement
|
|
|
282
318
|
}
|
|
283
319
|
}
|
|
284
320
|
}
|
|
285
|
-
return { classes, bodies };
|
|
321
|
+
return { classes, bodies, unknown };
|
|
286
322
|
}
|
|
@@ -52,7 +52,7 @@ import { aliasChains } from "./alias-chain.ts";
|
|
|
52
52
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
53
53
|
import { nodeKey } from "./inventory.ts";
|
|
54
54
|
import { renderPosition } from "./position.ts";
|
|
55
|
-
import {
|
|
55
|
+
import { must } from "./query.ts";
|
|
56
56
|
|
|
57
57
|
/** One class the inventory holds, by its declaration. */
|
|
58
58
|
interface ClassAt {
|
|
@@ -246,7 +246,7 @@ function walk(files: readonly SourceFile[], declarations: ReadonlyMap<string, st
|
|
|
246
246
|
*/
|
|
247
247
|
export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
|
|
248
248
|
const { project, held, targetRoot } = input;
|
|
249
|
-
const
|
|
249
|
+
const queries = project.queries;
|
|
250
250
|
const files = project.ownSourceFiles();
|
|
251
251
|
const { classes, clauses, flows } = walk(files, held.declarations);
|
|
252
252
|
if (classes.length === 0) {
|
|
@@ -261,15 +261,13 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
|
|
|
261
261
|
...flows.filter((flow) => flow.self === undefined).map((flow) => flow.value),
|
|
262
262
|
];
|
|
263
263
|
const typeAt = new Map<Node, Type>();
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
272
|
-
}
|
|
264
|
+
queries.typesAt(asked).forEach((answer, index) => {
|
|
265
|
+
const type = must(answer);
|
|
266
|
+
const node = asked[index];
|
|
267
|
+
if (type !== undefined && node !== undefined) {
|
|
268
|
+
typeAt.set(node, type);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
273
271
|
|
|
274
272
|
const classOfType = new Map<number, ClassAt>();
|
|
275
273
|
const instanceOf = new Map<string, Type>();
|
|
@@ -289,7 +287,7 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
|
|
|
289
287
|
}
|
|
290
288
|
let parts = constituents.get(type.id);
|
|
291
289
|
if (parts === undefined) {
|
|
292
|
-
parts =
|
|
290
|
+
parts = must(queries.constituents(type));
|
|
293
291
|
constituents.set(type.id, parts);
|
|
294
292
|
}
|
|
295
293
|
return parts;
|
|
@@ -307,7 +305,7 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
|
|
|
307
305
|
return [];
|
|
308
306
|
}
|
|
309
307
|
if (!targets.has(part.id)) {
|
|
310
|
-
targets.set(part.id, classOfType.get(
|
|
308
|
+
targets.set(part.id, classOfType.get(must(queries.targetOf(part)).id));
|
|
311
309
|
}
|
|
312
310
|
const generic = targets.get(part.id);
|
|
313
311
|
return generic === undefined ? [] : [{ owner: generic, type: part }];
|
|
@@ -318,7 +316,7 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
|
|
|
318
316
|
const interfacesOf = (type: Type): readonly { type: Type; name: string }[] =>
|
|
319
317
|
partsOf(type).flatMap((part) => {
|
|
320
318
|
if (!interfaceNames.has(part.id)) {
|
|
321
|
-
const symbol = part.isObjectType() ?
|
|
319
|
+
const symbol = part.isObjectType() ? must(queries.symbolOfType(part)) : undefined;
|
|
322
320
|
interfaceNames.set(
|
|
323
321
|
part.id,
|
|
324
322
|
symbol !== undefined && (symbol.flags & SymbolFlags.Interface) !== 0
|
|
@@ -352,7 +350,8 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
|
|
|
352
350
|
}
|
|
353
351
|
}
|
|
354
352
|
|
|
355
|
-
|
|
353
|
+
// The positions of every flow carrying a class are asked for at once.
|
|
354
|
+
const carrying = flows.flatMap((flow) => {
|
|
356
355
|
const self = flow.self === undefined ? undefined : instanceOf.get(flow.self.id);
|
|
357
356
|
const valueType = typeAt.get(flow.value);
|
|
358
357
|
const sources =
|
|
@@ -361,32 +360,35 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
|
|
|
361
360
|
: valueType === undefined
|
|
362
361
|
? []
|
|
363
362
|
: classesOf(valueType);
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
363
|
+
return sources.length === 0 ? [] : [{ flow, sources }];
|
|
364
|
+
});
|
|
365
|
+
const positions = queries.contextualTypes(carrying.map((one) => one.flow.value));
|
|
366
|
+
carrying.forEach(({ flow, sources }, index) => {
|
|
367
|
+
const position = must(positions[index]);
|
|
368
368
|
if (position === undefined) {
|
|
369
|
-
|
|
369
|
+
return;
|
|
370
370
|
}
|
|
371
371
|
for (const reached of interfacesOf(position)) {
|
|
372
372
|
for (const source of sources) {
|
|
373
373
|
reach(source.owner, source.type, reached.type, reached.name, flow.value);
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
|
-
}
|
|
376
|
+
});
|
|
377
377
|
|
|
378
378
|
const chains = aliasChains(project, held);
|
|
379
379
|
const required = new Map<number, ReadonlySet<string>>();
|
|
380
380
|
const members = new Map<string, ReadonlyMap<string, readonly string[]>>();
|
|
381
381
|
const found: Evidence[] = [];
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
const asking = [...pairs.values()];
|
|
383
|
+
const assignable = queries.assignableEach(asking);
|
|
384
|
+
for (const [index, pair] of asking.entries()) {
|
|
385
|
+
if (!must(assignable[index] ?? false)) {
|
|
384
386
|
continue;
|
|
385
387
|
}
|
|
386
388
|
let names = required.get(pair.target.id);
|
|
387
389
|
if (names === undefined) {
|
|
388
390
|
names = new Set(
|
|
389
|
-
|
|
391
|
+
must(queries.propertiesOf(pair.target)).map((property) => property.escapedName),
|
|
390
392
|
);
|
|
391
393
|
required.set(pair.target.id, names);
|
|
392
394
|
}
|
|
@@ -394,9 +396,10 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
|
|
|
394
396
|
if (answering === undefined) {
|
|
395
397
|
const instance = instanceOf.get(pair.owner.id) ?? pair.source;
|
|
396
398
|
answering = new Map(
|
|
397
|
-
|
|
398
|
-
.
|
|
399
|
-
|
|
399
|
+
must(queries.propertiesOf(instance)).map((property) => [
|
|
400
|
+
property.escapedName,
|
|
401
|
+
chains.declarationsOf(property),
|
|
402
|
+
]),
|
|
400
403
|
);
|
|
401
404
|
members.set(pair.owner.id, answering);
|
|
402
405
|
}
|
|
@@ -38,12 +38,14 @@ import {
|
|
|
38
38
|
isParenthesizedExpression,
|
|
39
39
|
isPrefixUnaryExpression,
|
|
40
40
|
isPropertyAccessExpression,
|
|
41
|
+
isQualifiedName,
|
|
41
42
|
isReturnStatement,
|
|
42
43
|
isShorthandPropertyAssignment,
|
|
43
44
|
isStringLiteral,
|
|
44
45
|
isSwitchStatement,
|
|
45
46
|
isThrowStatement,
|
|
46
47
|
isTypeAliasDeclaration,
|
|
48
|
+
isTypeReferenceNode,
|
|
47
49
|
isVariableStatement,
|
|
48
50
|
isVoidExpression,
|
|
49
51
|
NodeFlags,
|
|
@@ -58,7 +60,8 @@ import {
|
|
|
58
60
|
import type { FindingPosition } from "./finding.ts";
|
|
59
61
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
60
62
|
import { positionKey, renderPosition } from "./position.ts";
|
|
61
|
-
import {
|
|
63
|
+
import { isAnswered, UNANSWERED, type Answer } from "./query.ts";
|
|
64
|
+
import type { Reference } from "./references.ts";
|
|
62
65
|
import type { ProjectView } from "./session.ts";
|
|
63
66
|
import { signatureOf, type SignatureNode } from "./signatures.ts";
|
|
64
67
|
|
|
@@ -318,8 +321,14 @@ function extendedNames(files: readonly SourceFile[]): ReadonlySet<string> {
|
|
|
318
321
|
const visit = (node: Node): void => {
|
|
319
322
|
if (isHeritageClause(node) && node.token === SyntaxKind.ExtendsKeyword) {
|
|
320
323
|
for (const type of node.types) {
|
|
321
|
-
|
|
322
|
-
|
|
324
|
+
// A class's clause holds expressions; an interface's holds type references.
|
|
325
|
+
let named: Node;
|
|
326
|
+
if (isTypeReferenceNode(type)) {
|
|
327
|
+
named = isQualifiedName(type.typeName) ? type.typeName.right : type.typeName;
|
|
328
|
+
} else {
|
|
329
|
+
const expression = unwrapped(type.expression);
|
|
330
|
+
named = isPropertyAccessExpression(expression) ? expression.name : expression;
|
|
331
|
+
}
|
|
323
332
|
if (isIdentifier(named)) {
|
|
324
333
|
names.add(named.text);
|
|
325
334
|
}
|
|
@@ -364,39 +373,41 @@ interface Declared {
|
|
|
364
373
|
readonly signature: SignatureNode;
|
|
365
374
|
}
|
|
366
375
|
|
|
367
|
-
/**
|
|
376
|
+
/**
|
|
377
|
+
* The symbol identifier each of a batch of identifiers resolves to, where the batch
|
|
378
|
+
* resolves it, and {@link UNANSWERED} where the checker did not answer.
|
|
379
|
+
*/
|
|
368
380
|
function resolvedIds<Brand>(
|
|
369
381
|
project: ProjectView<Brand>,
|
|
370
382
|
nodes: readonly Identifier[],
|
|
371
|
-
): ReadonlyMap<Node, number
|
|
372
|
-
const ids = new Map<Node, number
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
}
|
|
383
|
+
): ReadonlyMap<Node, Answer<number>> {
|
|
384
|
+
const ids = new Map<Node, Answer<number>>();
|
|
385
|
+
project.symbolsAt(nodes.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
386
|
+
const node = nodes[index];
|
|
387
|
+
if (node !== undefined && symbol !== undefined) {
|
|
388
|
+
ids.set(node, symbol === UNANSWERED ? UNANSWERED : symbol.id);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
382
391
|
return ids;
|
|
383
392
|
}
|
|
384
393
|
|
|
385
394
|
/**
|
|
386
395
|
* Whether one identifier reads the binding a name node declares: it resolves to the
|
|
387
|
-
* same symbol, or the checker resolves it to nothing
|
|
388
|
-
* risk a finding; a shorthand property reads
|
|
396
|
+
* same symbol, or the checker resolves it to nothing or does not answer for either
|
|
397
|
+
* name, which is read as a use rather than risk a finding; a shorthand property reads
|
|
398
|
+
* the binding of its own name.
|
|
389
399
|
*/
|
|
390
400
|
function readsBinding(
|
|
391
401
|
use: Identifier,
|
|
392
402
|
declared: Identifier,
|
|
393
|
-
ids: ReadonlyMap<Node, number
|
|
403
|
+
ids: ReadonlyMap<Node, Answer<number>>,
|
|
394
404
|
): boolean {
|
|
395
405
|
if (isShorthandPropertyAssignment(use.parent)) {
|
|
396
406
|
return true;
|
|
397
407
|
}
|
|
398
408
|
const at = ids.get(use);
|
|
399
|
-
|
|
409
|
+
const own = ids.get(declared);
|
|
410
|
+
return at === undefined || at === UNANSWERED || own === UNANSWERED || at === own;
|
|
400
411
|
}
|
|
401
412
|
|
|
402
413
|
/** What one file's walk collects before the batch that resolves its names. */
|
|
@@ -587,7 +598,7 @@ function parameterParts(
|
|
|
587
598
|
root: string,
|
|
588
599
|
one: Declared,
|
|
589
600
|
uses: readonly Identifier[],
|
|
590
|
-
ids: ReadonlyMap<Node, number
|
|
601
|
+
ids: ReadonlyMap<Node, Answer<number>>,
|
|
591
602
|
): PartFact[] {
|
|
592
603
|
const signature = one.signature;
|
|
593
604
|
// A function that reads `arguments` reads every parameter by position.
|
|
@@ -676,7 +687,7 @@ function deadStores(
|
|
|
676
687
|
block: Block,
|
|
677
688
|
holder: string,
|
|
678
689
|
locals: readonly Local[],
|
|
679
|
-
ids: ReadonlyMap<Node, number
|
|
690
|
+
ids: ReadonlyMap<Node, Answer<number>>,
|
|
680
691
|
): PartFact[] {
|
|
681
692
|
const found: PartFact[] = [];
|
|
682
693
|
for (const local of locals) {
|
|
@@ -779,10 +790,17 @@ function resultParts<Brand>(
|
|
|
779
790
|
if (!discardedAt.has(id) || generator || !returnsAValue(signature)) {
|
|
780
791
|
continue;
|
|
781
792
|
}
|
|
782
|
-
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
793
|
+
// A question the checker leaves unanswered withholds the finding.
|
|
794
|
+
const typed = project.queries.signatureOf(signature);
|
|
795
|
+
if (typed === undefined || !isAnswered(typed)) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const result = project.queries.returnTypeOf(typed);
|
|
799
|
+
if (result === undefined || !isAnswered(result)) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const spelled = project.queries.typeToString(result);
|
|
803
|
+
if (!isAnswered(spelled) || NO_VALUE.has(spelled)) {
|
|
786
804
|
continue;
|
|
787
805
|
}
|
|
788
806
|
const at = signature.type ?? (node as { readonly name?: Node }).name ?? node;
|
package/src/inventory.ts
CHANGED
|
@@ -33,6 +33,7 @@ import type { Host } from "./host.ts";
|
|
|
33
33
|
import { dirnamePath, joinPath, relativePath } from "./paths.ts";
|
|
34
34
|
import { byPosition, positionKey, renderPosition, type Position } from "./position.ts";
|
|
35
35
|
import { computedComponent, nameComponent, renderRef, type Component, type Module } from "./ref.ts";
|
|
36
|
+
import { isAnswered, UNANSWERED } from "./query.ts";
|
|
36
37
|
import type { Handle, ProjectView } from "./session.ts";
|
|
37
38
|
|
|
38
39
|
/**
|
|
@@ -160,6 +161,17 @@ export interface Inventory {
|
|
|
160
161
|
* that the omission is a number rather than a silence.
|
|
161
162
|
*/
|
|
162
163
|
readonly outsideOwnFiles: number;
|
|
164
|
+
/**
|
|
165
|
+
* The declarations whose module, namespace or container table the checker did not
|
|
166
|
+
* answer, in position order: whether each is exported, and which members a table
|
|
167
|
+
* would have added, are unknown.
|
|
168
|
+
*/
|
|
169
|
+
readonly unanswered: readonly string[];
|
|
170
|
+
/**
|
|
171
|
+
* Every declaration other than a file by each name a use may spell it with: its
|
|
172
|
+
* component, its declared name where that differs, and a type parameter's own name.
|
|
173
|
+
*/
|
|
174
|
+
readonly byName: ReadonlyMap<string, readonly string[]>;
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
/** A declaration under construction, before its reference is rendered. */
|
|
@@ -758,12 +770,28 @@ export function inventory<Brand>(
|
|
|
758
770
|
const handles: Handle<Brand>[] = batched.map((node) => project.handle(node));
|
|
759
771
|
const resolved = project.symbolsAt(handles);
|
|
760
772
|
const symbolOf = new Map<Node, TSSymbol>();
|
|
773
|
+
// A scope or a container whose symbol or table went unanswered keeps what its syntax
|
|
774
|
+
// declares, and everything under it is recorded unanswered.
|
|
775
|
+
const unansweredOwners = new Set<Building>();
|
|
761
776
|
batched.forEach((node, index) => {
|
|
762
777
|
const symbol = resolved[index];
|
|
763
|
-
if (symbol
|
|
778
|
+
if (typeof symbol === "object") {
|
|
764
779
|
symbolOf.set(node, symbol);
|
|
765
780
|
}
|
|
766
781
|
});
|
|
782
|
+
resolved.slice(0, exportScopes.length).forEach((symbol, index) => {
|
|
783
|
+
const scope = exportScopes[index];
|
|
784
|
+
if (symbol === UNANSWERED && scope !== undefined) {
|
|
785
|
+
unansweredOwners.add(scope.owner);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
containers
|
|
789
|
+
.filter((container) => container.nameNode !== undefined)
|
|
790
|
+
.forEach((container, index) => {
|
|
791
|
+
if (resolved[exportScopes.length + index] === UNANSWERED) {
|
|
792
|
+
unansweredOwners.add(container.owner);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
767
795
|
|
|
768
796
|
let exportTables = 0;
|
|
769
797
|
for (const scope of exportScopes) {
|
|
@@ -772,7 +800,11 @@ export function inventory<Brand>(
|
|
|
772
800
|
continue;
|
|
773
801
|
}
|
|
774
802
|
exportTables += 1;
|
|
775
|
-
|
|
803
|
+
const table = project.queries.exportsOf(symbol);
|
|
804
|
+
if (!isAnswered(table)) {
|
|
805
|
+
unansweredOwners.add(scope.owner);
|
|
806
|
+
}
|
|
807
|
+
for (const [, exported] of isAnswered(table) ? table : []) {
|
|
776
808
|
for (const declaration of exported.declarations) {
|
|
777
809
|
const held = project.declarationAt(declaration);
|
|
778
810
|
if (held === undefined) {
|
|
@@ -794,11 +826,21 @@ export function inventory<Brand>(
|
|
|
794
826
|
if (symbol !== undefined) {
|
|
795
827
|
if (!container.membersAreExports) {
|
|
796
828
|
memberTables += 1;
|
|
797
|
-
|
|
829
|
+
const table = project.queries.membersOf(symbol);
|
|
830
|
+
if (isAnswered(table)) {
|
|
831
|
+
outsideOwnFiles += readTable(container, table, seen, false);
|
|
832
|
+
} else {
|
|
833
|
+
unansweredOwners.add(container.owner);
|
|
834
|
+
}
|
|
798
835
|
}
|
|
799
836
|
if (container.membersAreExports || container.isClass) {
|
|
800
837
|
memberTables += 1;
|
|
801
|
-
|
|
838
|
+
const table = project.queries.exportsOf(symbol);
|
|
839
|
+
if (isAnswered(table)) {
|
|
840
|
+
outsideOwnFiles += readTable(container, table, seen, container.isClass);
|
|
841
|
+
} else {
|
|
842
|
+
unansweredOwners.add(container.owner);
|
|
843
|
+
}
|
|
802
844
|
}
|
|
803
845
|
}
|
|
804
846
|
completeFromTree(container, seen);
|
|
@@ -919,6 +961,7 @@ export function inventory<Brand>(
|
|
|
919
961
|
return undefined;
|
|
920
962
|
}
|
|
921
963
|
|
|
964
|
+
const byId = new Map(building.map((record) => [positionKey(record.position), record]));
|
|
922
965
|
const symbols = building
|
|
923
966
|
.map(render)
|
|
924
967
|
.sort((a, b) => byPosition(a.position, b.position) || (a.ref < b.ref ? -1 : 1));
|
|
@@ -932,9 +975,50 @@ export function inventory<Brand>(
|
|
|
932
975
|
cost: { batches: 1, exportTables, memberTables },
|
|
933
976
|
starReExports,
|
|
934
977
|
outsideOwnFiles,
|
|
978
|
+
unanswered: symbols
|
|
979
|
+
.filter((symbol) => underUnanswered(byId.get(symbol.id), unansweredOwners))
|
|
980
|
+
.map((symbol) => symbol.id),
|
|
981
|
+
byName: namesOf(building),
|
|
935
982
|
};
|
|
936
983
|
}
|
|
937
984
|
|
|
985
|
+
/** Whether one declaration is, or is declared under, a scope or container left unanswered. */
|
|
986
|
+
function underUnanswered(record: Building | undefined, owners: ReadonlySet<Building>): boolean {
|
|
987
|
+
for (let at = record; at !== undefined; at = at.parent) {
|
|
988
|
+
if (owners.has(at)) {
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** Every declaration but a file, by each name a use may spell it with. */
|
|
996
|
+
function namesOf(building: readonly Building[]): ReadonlyMap<string, readonly string[]> {
|
|
997
|
+
const byName = new Map<string, string[]>();
|
|
998
|
+
for (const record of building) {
|
|
999
|
+
if (record.kind === "file") {
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
const id = positionKey(record.position);
|
|
1003
|
+
const names =
|
|
1004
|
+
record.typeParameter === undefined
|
|
1005
|
+
? [record.component.text, record.localName]
|
|
1006
|
+
: [record.typeParameter];
|
|
1007
|
+
for (const name of new Set(names)) {
|
|
1008
|
+
if (name === undefined) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
const ids = byName.get(name);
|
|
1012
|
+
if (ids === undefined) {
|
|
1013
|
+
byName.set(name, [id]);
|
|
1014
|
+
} else if (!ids.includes(id)) {
|
|
1015
|
+
ids.push(id);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return byName;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
938
1022
|
/** The chain of components that names one declaration, read from the module inward. */
|
|
939
1023
|
function chainOf(record: Building): readonly Component[] {
|
|
940
1024
|
return [...record.parentChain, record.component];
|
|
@@ -984,3 +1068,12 @@ function refOf(record: Building, chain: readonly Component[]): string {
|
|
|
984
1068
|
...(record.typeParameter === undefined ? {} : { typeParameter: record.typeParameter }),
|
|
985
1069
|
});
|
|
986
1070
|
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Every declaration of an inventory other than a file, by each name a use may spell it
|
|
1074
|
+
* with. It is how a reference the checker could not resolve is still counted, as a
|
|
1075
|
+
* reference to every declaration that bears the name it spells.
|
|
1076
|
+
*/
|
|
1077
|
+
export function declarationsByName(held: Inventory): ReadonlyMap<string, readonly string[]> {
|
|
1078
|
+
return held.byName;
|
|
1079
|
+
}
|