@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.18
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/dist/src/analysis.js +26 -71
- package/dist/src/augmentations.js +1 -1
- package/dist/src/calls.js +267 -48
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +2 -2
- package/dist/src/exempt.js +0 -26
- package/dist/src/exposure.js +400 -0
- package/dist/src/findings/completion.js +4 -17
- package/dist/src/findings/emitters.js +22 -33
- package/dist/src/findings/intra-function.js +3 -3
- package/dist/src/findings/reads-and-writes.js +71 -15
- package/dist/src/findings/unused-declarations.js +2 -22
- package/dist/src/findings/visibility-narrowing.js +14 -20
- package/dist/src/inventory.js +18 -0
- package/dist/src/{components.js → liveness/components.js} +1 -1
- package/dist/src/{graph.js → liveness/graph.js} +38 -21
- package/dist/src/liveness/matrix.js +99 -0
- package/dist/src/liveness/sweep.js +558 -0
- package/dist/src/query.js +46 -0
- package/dist/src/references/sink.js +87 -1
- package/dist/src/references.js +1 -1
- package/dist/src/roots/published-reach.js +4 -4
- package/dist/src/verbs/explain.js +73 -152
- package/dist/src/verbs/print-retained.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +41 -107
- package/src/augmentations.ts +1 -1
- package/src/calls.ts +326 -52
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +2 -2
- package/src/exempt.ts +31 -46
- package/src/exposure.ts +492 -0
- package/src/finding.ts +1 -1
- package/src/findings/boundary.ts +7 -0
- package/src/findings/completion.ts +5 -19
- package/src/findings/emitters.ts +23 -41
- package/src/findings/interfaces.ts +3 -3
- package/src/findings/intra-function.ts +3 -3
- package/src/findings/reads-and-writes.ts +99 -26
- package/src/findings/unused-declarations.ts +4 -28
- package/src/findings/visibility-narrowing.ts +25 -26
- package/src/interface-satisfaction.ts +1 -1
- package/src/inventory.ts +19 -0
- package/src/{components.ts → liveness/components.ts} +3 -3
- package/src/{graph.ts → liveness/graph.ts} +74 -26
- package/src/liveness/matrix.ts +162 -0
- package/src/liveness/sweep.ts +855 -0
- package/src/query.ts +81 -0
- package/src/references/sink.ts +101 -0
- package/src/references.ts +8 -1
- package/src/roots/published-reach.ts +9 -4
- package/src/stores.ts +2 -1
- package/src/tests/file-class.ts +19 -2
- package/src/verbs/explain.ts +81 -199
- package/src/verbs/print-retained.ts +1 -1
- package/dist/src/matrix.js +0 -275
- package/dist/src/sweep.js +0 -234
- package/src/matrix.ts +0 -466
- package/src/sweep.ts +0 -384
package/src/query.ts
CHANGED
|
@@ -207,8 +207,22 @@ export interface Queries {
|
|
|
207
207
|
constraintOf(type: Type): Answer<Type | undefined>;
|
|
208
208
|
/** The object and the index of an indexed-access type; none for another type. */
|
|
209
209
|
indexedAccessOf(type: Type): Answer<{ readonly object: Type; readonly index: Type } | undefined>;
|
|
210
|
+
/**
|
|
211
|
+
* The types one type is spelled from besides its constituents, type arguments and members:
|
|
212
|
+
* a type parameter's constraint and default, a conditional's check, extends, true
|
|
213
|
+
* and false types, a mapped type's constraint, name and template, an indexed access's
|
|
214
|
+
* object and index, the operand of `keyof` or of a string mapping, a template literal's
|
|
215
|
+
* spans and a substitution's base and constraint. None for another type.
|
|
216
|
+
*/
|
|
217
|
+
partsOf(type: Type): Answer<readonly Type[]>;
|
|
218
|
+
/** The type parameters of a signature, its `this` parameter's type and its predicate's type. */
|
|
219
|
+
signaturePartsOf(signature: Signature): Answer<readonly Type[]>;
|
|
210
220
|
/** The symbol a type is declared by. */
|
|
211
221
|
symbolOfType(type: Type): Answer<TSSymbol | undefined>;
|
|
222
|
+
/** The type alias a type was instantiated from, where it was. */
|
|
223
|
+
aliasSymbolOf(type: Type): Answer<TSSymbol | undefined>;
|
|
224
|
+
/** The type arguments the alias a type was instantiated from was given. */
|
|
225
|
+
aliasArgumentsOf(type: Type): Answer<readonly Type[]>;
|
|
212
226
|
signaturesOf(type: Type, kind: SignatureKind): Answer<readonly Signature[]>;
|
|
213
227
|
resolvedSignature(node: Node): Answer<Signature | undefined>;
|
|
214
228
|
/** The signature each call-like node resolves to. */
|
|
@@ -223,6 +237,37 @@ export interface Queries {
|
|
|
223
237
|
typeToString(type: Type): Answer<string>;
|
|
224
238
|
}
|
|
225
239
|
|
|
240
|
+
/** How to read the types one type is spelled from, as {@link Queries.partsOf} names them. */
|
|
241
|
+
function partQuestion(type: Type): (() => readonly (Type | undefined)[]) | undefined {
|
|
242
|
+
if (type.isTypeParameter()) {
|
|
243
|
+
return () => [type.getConstraint(), type.getDefault()];
|
|
244
|
+
}
|
|
245
|
+
if (type.isConditionalType()) {
|
|
246
|
+
return () => [
|
|
247
|
+
type.getCheckType(),
|
|
248
|
+
type.getExtendsType(),
|
|
249
|
+
type.getTrueType(),
|
|
250
|
+
type.getFalseType(),
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
if (type.isMappedType()) {
|
|
254
|
+
return () => [type.getConstraintType(), type.getNameType(), type.getTemplateType()];
|
|
255
|
+
}
|
|
256
|
+
if (type.isIndexedAccessType()) {
|
|
257
|
+
return () => [type.getObjectType(), type.getIndexType()];
|
|
258
|
+
}
|
|
259
|
+
if (type.isIndexType() || type.isStringMappingType()) {
|
|
260
|
+
return () => [type.getTarget()];
|
|
261
|
+
}
|
|
262
|
+
if (type.isTemplateLiteralType()) {
|
|
263
|
+
return () => type.getTypes();
|
|
264
|
+
}
|
|
265
|
+
if (type.isSubstitutionType()) {
|
|
266
|
+
return () => [type.getBaseType(), type.getConstraint()];
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
226
271
|
/**
|
|
227
272
|
* The guarded questions over one project's checker. `configFile` names the project
|
|
228
273
|
* in the log, `batch` sends many questions in one round trip, `log` is the run's,
|
|
@@ -528,12 +573,48 @@ export function queriesOf(
|
|
|
528
573
|
() => ({ object: type.getObjectType(), index: type.getIndexType() }),
|
|
529
574
|
)
|
|
530
575
|
: undefined,
|
|
576
|
+
partsOf: (type) => {
|
|
577
|
+
const parts = partQuestion(type);
|
|
578
|
+
return parts === undefined
|
|
579
|
+
? []
|
|
580
|
+
: one(
|
|
581
|
+
"getParts",
|
|
582
|
+
() => typeLocation(type),
|
|
583
|
+
() => parts().filter((part) => part !== undefined),
|
|
584
|
+
);
|
|
585
|
+
},
|
|
586
|
+
signaturePartsOf: (signature) =>
|
|
587
|
+
one(
|
|
588
|
+
"getSignatureParts",
|
|
589
|
+
() => `signature ${String(signature.id)}`,
|
|
590
|
+
() => {
|
|
591
|
+
const self =
|
|
592
|
+
signature.thisParameter === undefined ? undefined : signature.getThisParameter();
|
|
593
|
+
return [
|
|
594
|
+
...((signature.typeParameters ?? []).length === 0 ? [] : signature.getTypeParameters()),
|
|
595
|
+
self === undefined ? undefined : checker.getTypeOfSymbol(self),
|
|
596
|
+
checker.getTypePredicateOfSignature(signature)?.type,
|
|
597
|
+
].filter((part) => part !== undefined);
|
|
598
|
+
},
|
|
599
|
+
),
|
|
531
600
|
symbolOfType: (type) =>
|
|
532
601
|
one(
|
|
533
602
|
"getSymbol",
|
|
534
603
|
() => typeLocation(type),
|
|
535
604
|
() => type.getSymbol(),
|
|
536
605
|
),
|
|
606
|
+
aliasSymbolOf: (type) =>
|
|
607
|
+
one(
|
|
608
|
+
"getAliasSymbol",
|
|
609
|
+
() => typeLocation(type),
|
|
610
|
+
() => type.getAliasSymbol(),
|
|
611
|
+
),
|
|
612
|
+
aliasArgumentsOf: (type) =>
|
|
613
|
+
one(
|
|
614
|
+
"getAliasTypeArguments",
|
|
615
|
+
() => typeLocation(type),
|
|
616
|
+
() => type.getAliasTypeArguments(),
|
|
617
|
+
),
|
|
537
618
|
signaturesOf: (type, kind) =>
|
|
538
619
|
one(
|
|
539
620
|
"getSignaturesOfType",
|
package/src/references/sink.ts
CHANGED
|
@@ -25,6 +25,13 @@ interface Valued {
|
|
|
25
25
|
readonly value: Node;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** One record by its file, with the node that names its target and whether `from`'s own annotation holds it. */
|
|
29
|
+
interface Named {
|
|
30
|
+
readonly index: number;
|
|
31
|
+
readonly at: Node;
|
|
32
|
+
readonly annotated: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
/**
|
|
29
36
|
* Where every reference of one pass is made: it renders the position against the side's
|
|
30
37
|
* root, takes the writing file's test class from the file classes, marks a use only a
|
|
@@ -36,17 +43,21 @@ export class Sink {
|
|
|
36
43
|
readonly #testFile: (file: SourceFile) => boolean;
|
|
37
44
|
readonly #consumer: ConsumerSide | undefined;
|
|
38
45
|
readonly #found: Reference[] = [];
|
|
46
|
+
readonly #declaredAt: (node: Node) => string | undefined;
|
|
39
47
|
readonly #valued = new Map<SourceFile, Valued[]>();
|
|
48
|
+
readonly #named = new Map<SourceFile, Named[]>();
|
|
40
49
|
readonly #held = new Set<string>();
|
|
41
50
|
|
|
42
51
|
constructor(
|
|
43
52
|
root: string,
|
|
44
53
|
testFile: (file: SourceFile) => boolean,
|
|
45
54
|
consumer: ConsumerSide | undefined,
|
|
55
|
+
declaredAt: (node: Node) => string | undefined,
|
|
46
56
|
) {
|
|
47
57
|
this.#root = root;
|
|
48
58
|
this.#testFile = testFile;
|
|
49
59
|
this.#consumer = consumer;
|
|
60
|
+
this.#declaredAt = declaredAt;
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
/** Whether a test file wrote one walked file's uses. */
|
|
@@ -87,6 +98,11 @@ export class Sink {
|
|
|
87
98
|
test,
|
|
88
99
|
...(target.guessed === true ? { guessed: true as const } : {}),
|
|
89
100
|
});
|
|
101
|
+
if (this.#consumer === undefined && !("column" in at)) {
|
|
102
|
+
const named = this.#named.get(file) ?? [];
|
|
103
|
+
named.push({ index: this.#found.length - 1, at, annotated: this.#annotates(at, from) });
|
|
104
|
+
this.#named.set(file, named);
|
|
105
|
+
}
|
|
90
106
|
if (use === "write" && value !== undefined) {
|
|
91
107
|
const valued = this.#valued.get(file) ?? [];
|
|
92
108
|
valued.push({ index: this.#found.length - 1, value });
|
|
@@ -95,6 +111,27 @@ export class Sink {
|
|
|
95
111
|
}
|
|
96
112
|
}
|
|
97
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Whether a name sits in the annotation of `from`'s own declaration. An annotation holds
|
|
116
|
+
* no block, so the walk stops at the first declaration or block above the name.
|
|
117
|
+
*/
|
|
118
|
+
#annotates(name: Node, from: string): boolean {
|
|
119
|
+
if (name.kind === SyntaxKind.SourceFile) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
let child = name;
|
|
123
|
+
let parent = name.parent;
|
|
124
|
+
while (parent.kind !== SyntaxKind.Block && parent.kind !== SyntaxKind.SourceFile) {
|
|
125
|
+
const id = this.#declaredAt(parent);
|
|
126
|
+
if (id !== undefined) {
|
|
127
|
+
return id === from && (parent as { readonly type?: Node }).type === child;
|
|
128
|
+
}
|
|
129
|
+
child = parent;
|
|
130
|
+
parent = parent.parent;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
98
135
|
/** Holds declarations an unanswered question could have made live, and the one whose read asked it. */
|
|
99
136
|
hold(ids: Iterable<string>): void {
|
|
100
137
|
for (const id of ids) {
|
|
@@ -130,6 +167,14 @@ export class Sink {
|
|
|
130
167
|
}
|
|
131
168
|
}
|
|
132
169
|
}
|
|
170
|
+
for (const [file, named] of this.#named) {
|
|
171
|
+
for (const { index, carriers } of carriersIn(named, this.#valued.get(file) ?? [], found)) {
|
|
172
|
+
const reference = found[index];
|
|
173
|
+
if (reference !== undefined) {
|
|
174
|
+
found[index] = { ...reference, carriers };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
133
178
|
found.sort(
|
|
134
179
|
(a, b) =>
|
|
135
180
|
byPosition(a.position, b.position) ||
|
|
@@ -211,3 +256,59 @@ function aloneIn(
|
|
|
211
256
|
}
|
|
212
257
|
return result;
|
|
213
258
|
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The carriers of each named record of one file: `from` where its own annotation holds the
|
|
262
|
+
* name, then the target of each write whose stored value holds it and that names another
|
|
263
|
+
* declaration, the innermost value first. Values nest or are disjoint.
|
|
264
|
+
*/
|
|
265
|
+
function carriersIn(
|
|
266
|
+
named: readonly Named[],
|
|
267
|
+
writes: readonly Valued[],
|
|
268
|
+
found: readonly Reference[],
|
|
269
|
+
): readonly { index: number; carriers: readonly string[] }[] {
|
|
270
|
+
const byStart = [...named].sort((a, b) => a.at.pos - b.at.pos);
|
|
271
|
+
const holding = new Map<number, Valued[]>();
|
|
272
|
+
for (const write of writes) {
|
|
273
|
+
let low = 0;
|
|
274
|
+
let high = byStart.length;
|
|
275
|
+
while (low < high) {
|
|
276
|
+
const middle = (low + high) >>> 1;
|
|
277
|
+
if ((byStart[middle]?.at.pos ?? 0) < write.value.pos) {
|
|
278
|
+
low = middle + 1;
|
|
279
|
+
} else {
|
|
280
|
+
high = middle;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (let at = low; at < byStart.length; at += 1) {
|
|
284
|
+
const one = byStart[at];
|
|
285
|
+
if (one === undefined || one.at.pos >= write.value.end) {
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
if (one.at.end <= write.value.end) {
|
|
289
|
+
holding.set(one.index, [...(holding.get(one.index) ?? []), write]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const result: { index: number; carriers: readonly string[] }[] = [];
|
|
294
|
+
for (const one of named) {
|
|
295
|
+
const reference = found[one.index];
|
|
296
|
+
if (reference === undefined) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const carriers = one.annotated ? [reference.from] : [];
|
|
300
|
+
const values = [...(holding.get(one.index) ?? [])].sort(
|
|
301
|
+
(a, b) => a.value.end - a.value.pos - (b.value.end - b.value.pos),
|
|
302
|
+
);
|
|
303
|
+
for (const write of values) {
|
|
304
|
+
const to = found[write.index]?.to;
|
|
305
|
+
if (to !== undefined && to !== reference.to && !carriers.includes(to)) {
|
|
306
|
+
carriers.push(to);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (carriers.length > 0) {
|
|
310
|
+
result.push({ index: one.index, carriers });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
314
|
+
}
|
package/src/references.ts
CHANGED
|
@@ -94,6 +94,13 @@ export interface Reference {
|
|
|
94
94
|
* declaration with its writes deletes too.
|
|
95
95
|
*/
|
|
96
96
|
readonly alone?: readonly string[];
|
|
97
|
+
/**
|
|
98
|
+
* The declarations whose deletion with their writes deletes the referencing name,
|
|
99
|
+
* innermost first: `from` where the name is in its own annotation, then each declaration
|
|
100
|
+
* a write of the same file stores into whose stored value holds the name. Absent where
|
|
101
|
+
* there is none, and on a consumer's reference.
|
|
102
|
+
*/
|
|
103
|
+
readonly carriers?: readonly string[];
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
/** The consumer one reference pass reads the files of, beside the target it names. */
|
|
@@ -185,13 +192,13 @@ export function references<Brand>(
|
|
|
185
192
|
relativePath(consumer.root, file.fileName) !== undefined &&
|
|
186
193
|
relativePath(targetRoot, file.fileName) === undefined,
|
|
187
194
|
);
|
|
188
|
-
const sink = new Sink(consumer?.root ?? targetRoot, options.testFile, consumer);
|
|
189
195
|
const ownPaths = project.ownPaths();
|
|
190
196
|
const fileIds = new Set(
|
|
191
197
|
held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id),
|
|
192
198
|
);
|
|
193
199
|
const declaredAt = (node: Node): string | undefined =>
|
|
194
200
|
declarations.get(nodeKey(node.getSourceFile(), node));
|
|
201
|
+
const sink = new Sink(consumer?.root ?? targetRoot, options.testFile, consumer, declaredAt);
|
|
195
202
|
|
|
196
203
|
let moduleBodies: ReadonlyMap<string, readonly InventorySymbol[]> | undefined;
|
|
197
204
|
/**
|
|
@@ -235,7 +235,9 @@ function declarationNodes(
|
|
|
235
235
|
return {
|
|
236
236
|
typeNamesOf: (symbol) => {
|
|
237
237
|
const { byId, nodes } = nodesIn(symbol.position.path);
|
|
238
|
-
return (byId.get(symbol.id) ?? []).flatMap((declaration) =>
|
|
238
|
+
return (byId.get(symbol.id) ?? []).flatMap((declaration) =>
|
|
239
|
+
typeNamesOf(declaration, (nested) => nodes.has(nested)),
|
|
240
|
+
);
|
|
239
241
|
},
|
|
240
242
|
nodesOf: (symbol) => nodesIn(symbol.position.path).byId.get(symbol.id) ?? [],
|
|
241
243
|
};
|
|
@@ -438,12 +440,15 @@ function inferredTypeSymbols<Brand>(
|
|
|
438
440
|
/**
|
|
439
441
|
* The names of types one declaration's own syntax writes: in a type reference, a
|
|
440
442
|
* heritage clause or a type query. A function body is not read, and neither is a
|
|
441
|
-
* declaration nested in this one
|
|
443
|
+
* declaration nested in this one that `readAlone` says is read on its own.
|
|
442
444
|
*/
|
|
443
|
-
function typeNamesOf(
|
|
445
|
+
export function typeNamesOf(
|
|
446
|
+
declaration: Node,
|
|
447
|
+
readAlone: (nested: Node) => boolean,
|
|
448
|
+
): readonly Node[] {
|
|
444
449
|
const found: Node[] = [];
|
|
445
450
|
const visit = (node: Node): void => {
|
|
446
|
-
if (node !== declaration &&
|
|
451
|
+
if (node !== declaration && readAlone(node)) {
|
|
447
452
|
return;
|
|
448
453
|
}
|
|
449
454
|
if (isTypeReferenceNode(node)) {
|
package/src/stores.ts
CHANGED
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
13
13
|
import type { Reference } from "./references.ts";
|
|
14
14
|
import type { ProjectView } from "./session.ts";
|
|
15
|
-
import type { Exemption
|
|
15
|
+
import type { Exemption } from "./exempt.ts";
|
|
16
|
+
import type { Mode } from "./tests/file-class.ts";
|
|
16
17
|
import { classedAsTest, madeByTestCode, withheldBy } from "./tests/file-class.ts";
|
|
17
18
|
|
|
18
19
|
/** One run's stores and reads, over every configuration. */
|
package/src/tests/file-class.ts
CHANGED
|
@@ -8,14 +8,31 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { SourceFile } from "@typescript/native/ast";
|
|
11
|
-
import type { Edge } from "../graph.ts";
|
|
11
|
+
import type { Edge } from "../liveness/graph.ts";
|
|
12
12
|
import { renderPosition } from "../position.ts";
|
|
13
13
|
import type { Reference } from "../references.ts";
|
|
14
14
|
import type { ProjectView } from "../session.ts";
|
|
15
|
-
import type { Mode } from "../sweep.ts";
|
|
16
15
|
import { bytewise } from "../text/compare.ts";
|
|
17
16
|
import { globExpression } from "../text/glob.ts";
|
|
18
17
|
|
|
18
|
+
/**
|
|
19
|
+
* The run's reference mode, decided once by the composition and read by every stage
|
|
20
|
+
* that counts references.
|
|
21
|
+
*/
|
|
22
|
+
export interface Mode {
|
|
23
|
+
/**
|
|
24
|
+
* Counts no reference a test file made, under either relation. Every reference a root
|
|
25
|
+
* in a test file makes is such a reference, so nothing only a test reaches is live.
|
|
26
|
+
*/
|
|
27
|
+
readonly production: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Classes a reference a loaded consumer's test file made as a production one rather than a
|
|
30
|
+
* test one, `analysis.consumer_tests` at `production`: a classification, not a filter, so it
|
|
31
|
+
* decides the split a candidate reports as well as what a production sweep counts.
|
|
32
|
+
*/
|
|
33
|
+
readonly consumerTestsProduction?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
/** One rule that classified files as test files, and how many files it matched. */
|
|
20
37
|
export interface TestFileRule {
|
|
21
38
|
/** The rule's name, as a report's own rule list spells one. */
|