@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.
Files changed (60) hide show
  1. package/dist/src/analysis.js +26 -71
  2. package/dist/src/augmentations.js +1 -1
  3. package/dist/src/calls.js +267 -48
  4. package/dist/src/conformance-record.js +1 -1
  5. package/dist/src/contract/contract.generated.js +2 -2
  6. package/dist/src/exempt.js +0 -26
  7. package/dist/src/exposure.js +400 -0
  8. package/dist/src/findings/completion.js +4 -17
  9. package/dist/src/findings/emitters.js +22 -33
  10. package/dist/src/findings/intra-function.js +3 -3
  11. package/dist/src/findings/reads-and-writes.js +71 -15
  12. package/dist/src/findings/unused-declarations.js +2 -22
  13. package/dist/src/findings/visibility-narrowing.js +14 -20
  14. package/dist/src/inventory.js +18 -0
  15. package/dist/src/{components.js → liveness/components.js} +1 -1
  16. package/dist/src/{graph.js → liveness/graph.js} +38 -21
  17. package/dist/src/liveness/matrix.js +99 -0
  18. package/dist/src/liveness/sweep.js +558 -0
  19. package/dist/src/query.js +46 -0
  20. package/dist/src/references/sink.js +87 -1
  21. package/dist/src/references.js +1 -1
  22. package/dist/src/roots/published-reach.js +4 -4
  23. package/dist/src/verbs/explain.js +73 -152
  24. package/dist/src/verbs/print-retained.js +1 -1
  25. package/jsr.json +1 -1
  26. package/package.json +1 -1
  27. package/src/analysis.ts +41 -107
  28. package/src/augmentations.ts +1 -1
  29. package/src/calls.ts +326 -52
  30. package/src/conformance-record.ts +1 -1
  31. package/src/contract/contract.generated.ts +2 -2
  32. package/src/exempt.ts +31 -46
  33. package/src/exposure.ts +492 -0
  34. package/src/finding.ts +1 -1
  35. package/src/findings/boundary.ts +7 -0
  36. package/src/findings/completion.ts +5 -19
  37. package/src/findings/emitters.ts +23 -41
  38. package/src/findings/interfaces.ts +3 -3
  39. package/src/findings/intra-function.ts +3 -3
  40. package/src/findings/reads-and-writes.ts +99 -26
  41. package/src/findings/unused-declarations.ts +4 -28
  42. package/src/findings/visibility-narrowing.ts +25 -26
  43. package/src/interface-satisfaction.ts +1 -1
  44. package/src/inventory.ts +19 -0
  45. package/src/{components.ts → liveness/components.ts} +3 -3
  46. package/src/{graph.ts → liveness/graph.ts} +74 -26
  47. package/src/liveness/matrix.ts +162 -0
  48. package/src/liveness/sweep.ts +855 -0
  49. package/src/query.ts +81 -0
  50. package/src/references/sink.ts +101 -0
  51. package/src/references.ts +8 -1
  52. package/src/roots/published-reach.ts +9 -4
  53. package/src/stores.ts +2 -1
  54. package/src/tests/file-class.ts +19 -2
  55. package/src/verbs/explain.ts +81 -199
  56. package/src/verbs/print-retained.ts +1 -1
  57. package/dist/src/matrix.js +0 -275
  58. package/dist/src/sweep.js +0 -234
  59. package/src/matrix.ts +0 -466
  60. 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",
@@ -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) => typeNamesOf(declaration, nodes));
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, which `nested` holds and which is read on its own.
443
+ * declaration nested in this one that `readAlone` says is read on its own.
442
444
  */
443
- function typeNamesOf(declaration: Node, nested: ReadonlySet<Node>): readonly Node[] {
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 && nested.has(node)) {
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, Mode } from "./sweep.ts";
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. */
@@ -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. */