@cplieger/deadset-ts 5.1.1 → 5.2.1

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 +7 -4
  2. package/dist/src/config.js +3 -0
  3. package/dist/src/configuration-files.js +23 -24
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/contextual-uses.js +25 -4
  6. package/dist/src/convention-rows.js +7 -0
  7. package/dist/src/conventions.js +97 -33
  8. package/dist/src/dependencies.js +8 -0
  9. package/dist/src/entry-point-gaps.js +10 -26
  10. package/dist/src/entry-points.js +4 -0
  11. package/dist/src/findings/completion.js +27 -11
  12. package/dist/src/findings/interfaces.js +2 -6
  13. package/dist/src/findings/intra-function.js +15 -3
  14. package/dist/src/findings/unused-declarations.js +5 -6
  15. package/dist/src/findings/visibility-narrowing.js +6 -1
  16. package/dist/src/generated-file.js +72 -12
  17. package/dist/src/graph.js +5 -4
  18. package/dist/src/html-entries.js +137 -0
  19. package/dist/src/kinds.js +2 -2
  20. package/dist/src/manifest.js +5 -1
  21. package/dist/src/matrix.js +9 -3
  22. package/dist/src/namespace-objects.js +158 -0
  23. package/dist/src/partial.js +1 -1
  24. package/dist/src/references.js +120 -3
  25. package/dist/src/roots.js +52 -3
  26. package/dist/src/sweep.js +17 -0
  27. package/dist/src/test-support.js +35 -23
  28. package/dist/src/version.js +1 -1
  29. package/jsr.json +1 -1
  30. package/package.json +2 -2
  31. package/src/analysis.ts +12 -5
  32. package/src/config.ts +3 -0
  33. package/src/configuration-files.ts +28 -24
  34. package/src/conformance-record.ts +2 -2
  35. package/src/contextual-uses.ts +31 -4
  36. package/src/convention-rows.ts +15 -0
  37. package/src/conventions.ts +119 -30
  38. package/src/dependencies.ts +9 -0
  39. package/src/entry-point-gaps.ts +11 -28
  40. package/src/entry-points.ts +7 -1
  41. package/src/exempt.ts +1 -4
  42. package/src/file-facts.ts +1 -1
  43. package/src/findings/completion.ts +31 -11
  44. package/src/findings/interfaces.ts +2 -9
  45. package/src/findings/intra-function.ts +17 -3
  46. package/src/findings/unused-declarations.ts +5 -8
  47. package/src/findings/visibility-narrowing.ts +7 -1
  48. package/src/generated-file.ts +80 -12
  49. package/src/graph.ts +5 -4
  50. package/src/html-entries.ts +170 -0
  51. package/src/kinds.ts +2 -2
  52. package/src/manifest.ts +9 -3
  53. package/src/matrix.ts +21 -3
  54. package/src/namespace-objects.ts +216 -0
  55. package/src/partial.ts +1 -1
  56. package/src/references.ts +161 -3
  57. package/src/roots.ts +69 -10
  58. package/src/sweep.ts +19 -0
  59. package/src/test-support.ts +34 -22
  60. package/src/version.ts +1 -1
@@ -97,12 +97,19 @@ interface Called {
97
97
  interface Checked {
98
98
  readonly source: Type;
99
99
  readonly targets: readonly Type[];
100
+ /**
101
+ * Whether the source is declared outside the target, so code the analysis does not
102
+ * hold reads the members of the target's types it declares.
103
+ */
104
+ readonly outside: boolean;
100
105
  }
101
106
 
102
107
  /** One member of an object type, by the inventory's declarations and the checker's declaration handles. */
103
108
  interface Member {
104
109
  readonly ids: readonly string[];
105
110
  readonly handles: ReadonlySet<string>;
111
+ /** Whether a declaration of it is in one of the target's own files. */
112
+ readonly own: boolean;
106
113
  }
107
114
 
108
115
  /**
@@ -120,6 +127,7 @@ export function contextualUses<Brand>(
120
127
  properties: PropertyTables,
121
128
  ): ContextualUses {
122
129
  const queries = project.queries;
130
+ const ownPaths = project.ownPaths();
123
131
  const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
124
132
  let batches = 0;
125
133
  let lookups = 0;
@@ -162,6 +170,7 @@ export function contextualUses<Brand>(
162
170
  handles: new Set(
163
171
  property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`),
164
172
  ),
173
+ own: property.declarations.some((handle) => ownPaths.has(handle.path)),
165
174
  });
166
175
  }
167
176
  tables.set(type.id, table);
@@ -203,6 +212,11 @@ export function contextualUses<Brand>(
203
212
  member.ids.filter((id) => kinds.get(id) === "type-member");
204
213
  const carries = (part: Type): boolean =>
205
214
  [...membersOf(part).values()].some((member) => readable(member).length > 0);
215
+ /** Whether an object type has members and declares none of them in the target. */
216
+ const outside = (part: Type): boolean => {
217
+ const members = [...membersOf(part).values()];
218
+ return members.length > 0 && members.every((member) => !member.own);
219
+ };
206
220
 
207
221
  const elements = new Map<number, Type | undefined>();
208
222
  /** The element type of an array type, read once per type. */
@@ -272,7 +286,9 @@ export function contextualUses<Brand>(
272
286
  const sources = partsOf(source).filter((one) => !targets.some((part) => part.id === one.id));
273
287
  for (const one of sources) {
274
288
  if (carries(one)) {
275
- into.push({ source: one, targets });
289
+ into.push({ source: one, targets, outside: false });
290
+ } else if (outside(one)) {
291
+ into.push({ source: one, targets, outside: true });
276
292
  }
277
293
  const element = elementOf(one);
278
294
  const called = element === undefined ? callOf(one) : undefined;
@@ -303,7 +319,7 @@ export function contextualUses<Brand>(
303
319
  }
304
320
  seen.add(type.id);
305
321
  return partsOf(type).some((part) => {
306
- if (carries(part)) {
322
+ if (carries(part) || outside(part)) {
307
323
  return true;
308
324
  }
309
325
  const element = elementOf(part);
@@ -332,9 +348,20 @@ export function contextualUses<Brand>(
332
348
  const checked: Checked[] = [];
333
349
  compared(type, position, checked, new Set());
334
350
  const ids = new Set<string>();
335
- for (const { source, targets } of checked) {
351
+ for (const { source, targets, outside: held } of checked) {
336
352
  const tables = targets.map(membersOf);
337
- for (const [name, member] of membersOf(source)) {
353
+ const sourceMembers = membersOf(source);
354
+ if (held) {
355
+ for (const table of tables) {
356
+ for (const [name, member] of table) {
357
+ if (sourceMembers.has(name)) {
358
+ member.ids.forEach((id) => ids.add(id));
359
+ }
360
+ }
361
+ }
362
+ continue;
363
+ }
364
+ for (const [name, member] of sourceMembers) {
338
365
  const read = tables.some((table) => {
339
366
  const declared = table.get(name);
340
367
  return (
@@ -25,6 +25,12 @@ interface MoveReading {
25
25
  readonly call?: OptionsCall;
26
26
  }
27
27
 
28
+ /** One configuration property whose value names a package by a short name. */
29
+ export interface ShortNameReading extends MoveReading {
30
+ /** The package name a value stands for, written with `{}` where the value goes. */
31
+ readonly package: string;
32
+ }
33
+
28
34
  /** One directory a row's globs name that a configuration property moves. */
29
35
  export interface DirectoryMove {
30
36
  /** The placeholder the row's globs and later moves write as `<id>`. */
@@ -56,6 +62,8 @@ export interface ConventionRow {
56
62
  readonly generated?: readonly string[];
57
63
  /** The directories configuration moves, in the order their placeholders depend on each other. */
58
64
  readonly moves: readonly DirectoryMove[];
65
+ /** The properties that name a package the configuration uses by a short name. */
66
+ readonly shortNames?: readonly ShortNameReading[];
59
67
  }
60
68
 
61
69
  const SCRIPT = "{js,jsx,ts,tsx}";
@@ -444,5 +452,12 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
444
452
  `**/{vitest,vite}.*.config.${ANY_MODULE}`,
445
453
  ],
446
454
  moves: [],
455
+ shortNames: [
456
+ {
457
+ property: "test.coverage.provider",
458
+ files: [`{vitest,vite}.config.${ANY_MODULE}`],
459
+ package: "@vitest/coverage-{}",
460
+ },
461
+ ],
447
462
  },
448
463
  ];
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import {
10
+ isArrayLiteralExpression,
10
11
  isAsExpression,
11
12
  isBinaryExpression,
12
13
  isCallExpression,
@@ -50,6 +51,7 @@ import {
50
51
  type ConventionRow,
51
52
  type DirectoryMove,
52
53
  type OptionsCall,
54
+ type ShortNameReading,
53
55
  } from "./convention-rows.ts";
54
56
  import { globExpression } from "./glob.ts";
55
57
  import type { Host } from "./host.ts";
@@ -106,6 +108,8 @@ export interface Conventions {
106
108
  readonly globs: readonly ConventionGlob[];
107
109
  readonly generated: readonly GeneratedDirectory[];
108
110
  readonly failures: readonly ConventionFailure[];
111
+ /** The dependencies the applied rows' short names use, by name, each once. */
112
+ readonly uses: readonly string[];
109
113
  }
110
114
 
111
115
  /** One file a row roots. */
@@ -445,6 +449,28 @@ function objectOf(
445
449
  return at;
446
450
  }
447
451
 
452
+ /** The text of a string literal or a template literal with no substitution, or undefined. */
453
+ function literalText(node: Node): string | undefined {
454
+ const at = unwrapped(node);
455
+ return isStringLiteral(at) || isNoSubstitutionTemplateLiteral(at) ? at.text : undefined;
456
+ }
457
+
458
+ /**
459
+ * The literal texts one value at a path's end writes: the literal itself, or, where
460
+ * `elements`, each element of an array literal of literals. Undefined for any other value.
461
+ */
462
+ function literalTexts(held: Node, elements: boolean): readonly string[] | undefined {
463
+ const one = literalText(held);
464
+ if (one !== undefined) {
465
+ return [one];
466
+ }
467
+ if (!elements || !isArrayLiteralExpression(held)) {
468
+ return undefined;
469
+ }
470
+ const texts = held.elements.map(literalText);
471
+ return texts.every((text) => text !== undefined) ? texts : undefined;
472
+ }
473
+
448
474
  /**
449
475
  * Reads the values at one path of keys below one object literal into `values`, and
450
476
  * answers the first member that does not write its value out: a spread, a value that is
@@ -454,6 +480,7 @@ function readPath(
454
480
  object: ObjectLiteralExpression,
455
481
  keys: readonly string[],
456
482
  values: string[],
483
+ elements: boolean,
457
484
  ): Node | undefined {
458
485
  const [key, ...rest] = keys;
459
486
  for (const member of object.properties) {
@@ -465,15 +492,16 @@ function readPath(
465
492
  }
466
493
  const held = isPropertyAssignment(member) ? unwrapped(member.initializer) : undefined;
467
494
  if (rest.length === 0) {
468
- if (held === undefined || !(isStringLiteral(held) || isNoSubstitutionTemplateLiteral(held))) {
495
+ const texts = held === undefined ? undefined : literalTexts(held, elements);
496
+ if (texts === undefined) {
469
497
  return member;
470
498
  }
471
- values.push(held.text);
499
+ values.push(...texts);
472
500
  } else {
473
501
  if (held === undefined || !isObjectLiteralExpression(held)) {
474
502
  return member;
475
503
  }
476
- const failed = readPath(held, rest, values);
504
+ const failed = readPath(held, rest, values, elements);
477
505
  if (failed !== undefined) {
478
506
  return failed;
479
507
  }
@@ -493,14 +521,16 @@ interface PropertyReading {
493
521
  /**
494
522
  * Every value one file writes at one property's path inside the object it is read from:
495
523
  * the options argument of every call `call` names, or the file's default export. A string
496
- * literal or a template literal with no substitution is the value. A read object, or an
497
- * object along the path, that is not written out as an object literal, or that spreads
498
- * another object into itself, is not a literal, and neither is any other value at the path.
524
+ * literal or a template literal with no substitution is the value, and so is each element
525
+ * of an array literal of them where `elements`. A read object, or an object along the
526
+ * path, that is not written out as an object literal, or that spreads another object into
527
+ * itself, is not a literal, and neither is any other value at the path.
499
528
  */
500
529
  export function readProperty(
501
530
  file: SourceFile,
502
531
  property: string,
503
532
  call?: OptionsCall,
533
+ elements = false,
504
534
  ): PropertyReading {
505
535
  const declared = declaredValues(file);
506
536
  const objects =
@@ -515,7 +545,7 @@ export function readProperty(
515
545
  continue;
516
546
  }
517
547
  const notLiteral = isObjectLiteralExpression(object)
518
- ? readPath(object, property.split("."), values)
548
+ ? readPath(object, property.split("."), values, elements)
519
549
  : object;
520
550
  if (notLiteral !== undefined) {
521
551
  return { values, notLiteral };
@@ -585,29 +615,19 @@ function resolveMove(
585
615
  ): MoveResult {
586
616
  const set: string[] = [];
587
617
  for (const reading of move.readings) {
588
- for (const glob of reading.files) {
589
- for (const name of configurationFiles(host, dir, glob)) {
590
- const path = joinPath(dir, name);
591
- let text: string;
592
- try {
593
- text = host.readFile(path);
594
- } catch {
595
- continue;
596
- }
597
- const file = parse(path, text);
598
- const read = readProperty(file, reading.property, reading.call);
599
- if (read.notLiteral !== undefined) {
600
- const line = lineOf(text, read.notLiteral.getStart());
601
- return {
602
- failure:
603
- `${name}:${String(line)} sets ${reading.property}, which moves a directory ` +
604
- `the convention row ${row.name} reads, to a value that is not a string literal: ` +
605
- `write it as a literal, or name ${row.name} in ts.disabled_conventions and its ` +
606
- `files in ts.entry_files`,
607
- };
608
- }
609
- set.push(...read.values);
618
+ for (const { name, text, file } of readingFiles(host, parse, dir, reading.files)) {
619
+ const read = readProperty(file, reading.property, reading.call);
620
+ if (read.notLiteral !== undefined) {
621
+ const line = lineOf(text, read.notLiteral.getStart());
622
+ return {
623
+ failure:
624
+ `${name}:${String(line)} sets ${reading.property}, which moves a directory ` +
625
+ `the convention row ${row.name} reads, to a value that is not a string literal: ` +
626
+ `write it as a literal, or name ${row.name} in ts.disabled_conventions and its ` +
627
+ `files in ts.entry_files`,
628
+ };
610
629
  }
630
+ set.push(...read.values);
611
631
  }
612
632
  }
613
633
  const dirs: (string | undefined)[] = [];
@@ -621,6 +641,71 @@ function resolveMove(
621
641
  return { dirs: [...new Set(dirs)] };
622
642
  }
623
643
 
644
+ /** The texts each configuration file a reading names holds, parsed, in name order. */
645
+ function readingFiles(
646
+ host: Host,
647
+ parse: ParseSource,
648
+ dir: string,
649
+ files: readonly string[],
650
+ ): readonly { readonly name: string; readonly text: string; readonly file: SourceFile }[] {
651
+ const found = [];
652
+ for (const glob of files) {
653
+ for (const name of configurationFiles(host, dir, glob)) {
654
+ const path = joinPath(dir, name);
655
+ let text: string;
656
+ try {
657
+ text = host.readFile(path);
658
+ } catch {
659
+ continue;
660
+ }
661
+ found.push({ name, text, file: parse(path, text) });
662
+ }
663
+ }
664
+ return found;
665
+ }
666
+
667
+ /**
668
+ * The dependencies one manifest declares that an applied row's short names use: the
669
+ * package each literal value stands for, and, for a value that is not a literal, every
670
+ * declared dependency the row's package name stands for with some value.
671
+ */
672
+ function shortNameUses(
673
+ host: Host,
674
+ parse: ParseSource,
675
+ dir: string,
676
+ manifest: unknown,
677
+ readings: readonly ShortNameReading[],
678
+ ): readonly string[] {
679
+ const declared = new Set(
680
+ DEPENDENCY_SECTIONS.flatMap((section) => {
681
+ const held = isRecord(manifest) ? manifest[section] : undefined;
682
+ return isRecord(held) ? Object.keys(held) : [];
683
+ }),
684
+ );
685
+ const used = new Set<string>();
686
+ for (const reading of readings) {
687
+ const [before = "", after = ""] = reading.package.split("{}");
688
+ for (const { file } of readingFiles(host, parse, dir, reading.files)) {
689
+ const read = readProperty(file, reading.property, reading.call, true);
690
+ for (const value of read.values) {
691
+ used.add(`${before}${value}${after}`);
692
+ }
693
+ if (read.notLiteral !== undefined) {
694
+ for (const name of declared) {
695
+ if (
696
+ name.length >= before.length + after.length &&
697
+ name.startsWith(before) &&
698
+ name.endsWith(after)
699
+ ) {
700
+ used.add(name);
701
+ }
702
+ }
703
+ }
704
+ }
705
+ }
706
+ return [...used].filter((name) => declared.has(name)).sort(compare);
707
+ }
708
+
624
709
  /** Two strings ordered bytewise. */
625
710
  function compare(a: string, b: string): number {
626
711
  if (a === b) {
@@ -644,6 +729,7 @@ export function readConventions(
644
729
  const globs: ConventionGlob[] = [];
645
730
  const generated: GeneratedDirectory[] = [];
646
731
  const failures: ConventionFailure[] = [];
732
+ const uses = new Set<string>();
647
733
  for (const manifest of manifests) {
648
734
  const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
649
735
  const versions = new Map<string, string | undefined>();
@@ -702,6 +788,9 @@ export function readConventions(
702
788
  version,
703
789
  manifest: manifest.path,
704
790
  });
791
+ for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
792
+ uses.add(name);
793
+ }
705
794
  const excludes = [
706
795
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
707
796
  ].map(globExpression);
@@ -725,7 +814,7 @@ export function readConventions(
725
814
  compare(a.manifest, b.manifest) ||
726
815
  compare(JSON.stringify(a), JSON.stringify(b)),
727
816
  );
728
- return { applied: unique, globs, generated, failures };
817
+ return { applied: unique, globs, generated, failures, uses: [...uses].sort(compare) };
729
818
  }
730
819
 
731
820
  /**
@@ -61,6 +61,15 @@ export interface DeclaredDependency {
61
61
  readonly position: FindingPosition;
62
62
  }
63
63
 
64
+ /** What a configuration's own reading needs: packages no deletion of a declaration removes. */
65
+ export function configurationNeeds(packages: readonly string[]): ProjectNeeds {
66
+ return {
67
+ packages: new Set(packages),
68
+ uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
69
+ unanswered: false,
70
+ };
71
+ }
72
+
64
73
  /** What the installed copy of one declared dependency states about itself. */
65
74
  export interface InstalledDependency {
66
75
  /** Whether its manifest declares a command. */
@@ -26,10 +26,10 @@ const UNREAD =
26
26
  "file only it loads is reported as never imported unless ts.entry_files or roots.patterns " +
27
27
  "names it.";
28
28
 
29
- /** Why a configuration's file names that are not relative literals are declined. */
30
- const NOT_RELATIVE =
31
- "A string of a configuration file names a file only where it is a literal starting with ./ " +
32
- "or ../, so a file named otherwise is reported as never imported.";
29
+ /** Why a configuration's file names that are not literals are declined. */
30
+ const NOT_LITERAL =
31
+ "A string of a configuration file names a file only where it is a literal, so a file a " +
32
+ "computed value names is reported as never imported.";
33
33
 
34
34
  /** The tools whose own conventions are not read. */
35
35
  const UNREAD_TOOLS: readonly string[] = [
@@ -230,11 +230,6 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
230
230
  "A testMatch that is not a string literal leaves the test-file set unknown, and an " +
231
231
  "unknown set enters nothing.",
232
232
  },
233
- {
234
- tool: "playwright",
235
- convention: "a globalSetup or globalTeardown file named without a leading ./ or ../",
236
- reason: NOT_RELATIVE,
237
- },
238
233
  {
239
234
  tool: "playwright",
240
235
  convention: "the testDir and testMatch of an entry in projects",
@@ -300,22 +295,15 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
300
295
  },
301
296
  {
302
297
  tool: "vite",
303
- convention: "an entry point its configuration names without a leading ./ or ../, or computes",
304
- reason: NOT_RELATIVE,
305
- },
306
- {
307
- tool: "vite",
308
- convention: "the module scripts an index.html loads",
309
- reason:
310
- "No rule reads an HTML file, so a script only index.html loads is reported as never " +
311
- "imported unless ts.entry_files names it.",
298
+ convention: "an entry point its configuration computes",
299
+ reason: NOT_LITERAL,
312
300
  },
313
301
  {
314
302
  tool: "vitest",
315
- convention: "the benchmark and type-test files",
303
+ convention: "the benchmark files",
316
304
  reason:
317
- "A test file is one ts.test_files classifies, so a *.bench.* or *.test-d.* file is " +
318
- "entered only where a configured pattern names it.",
305
+ "A test file is one ts.test_files classifies, so a *.bench.* file is entered only " +
306
+ "where a configured pattern names it.",
319
307
  },
320
308
  {
321
309
  tool: "vitest",
@@ -324,11 +312,6 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
324
312
  "A string is read against the configuration file's directory, so a setup file named " +
325
313
  "against a root the configuration moves is not entered.",
326
314
  },
327
- {
328
- tool: "vitest",
329
- convention: "a setup file named without a leading ./ or ../",
330
- reason: NOT_RELATIVE,
331
- },
332
315
  {
333
316
  tool: "vitest",
334
317
  convention: "the test files a configuration's include member names",
@@ -338,8 +321,8 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
338
321
  },
339
322
  {
340
323
  tool: "webpack",
341
- convention: "an entry point its configuration names without a leading ./ or ../, or computes",
342
- reason: NOT_RELATIVE,
324
+ convention: "an entry point its configuration computes",
325
+ reason: NOT_LITERAL,
343
326
  },
344
327
  ];
345
328
 
@@ -38,6 +38,7 @@ import {
38
38
  } from "./configuration-files.ts";
39
39
  import { globExpression } from "./glob.ts";
40
40
  import type { Host } from "./host.ts";
41
+ import { htmlEntries } from "./html-entries.ts";
41
42
  import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
42
43
  import { UNANSWERED } from "./query.ts";
43
44
  import type { ProjectView } from "./session.ts";
@@ -56,7 +57,9 @@ export type EntryRule =
56
57
  /** The test files below the browser-test runner's test directory. */
57
58
  | "browser-tests"
58
59
  /** A worker or service worker a call addresses by a string literal. */
59
- | "worker";
60
+ | "worker"
61
+ /** A file a module script of an HTML file below the target root loads. */
62
+ | "html-entry";
60
63
 
61
64
  /** One file a rule enters. */
62
65
  export interface EntryPoint {
@@ -542,5 +545,8 @@ export function entryPoints<Brand>(
542
545
  }
543
546
  found.push(...workersOf(project, file, files));
544
547
  }
548
+ for (const { file, source } of htmlEntries(host, targetRoot, files, parse)) {
549
+ found.push({ file, rule: "html-entry", source, exports: true });
550
+ }
545
551
  return found;
546
552
  }
package/src/exempt.ts CHANGED
@@ -48,10 +48,7 @@ export interface DetectorInput<Brand> {
48
48
  readonly ts: TSSection;
49
49
  /** The absolute path of each declared consumer, whose modules are part of the analysis. */
50
50
  readonly consumers: readonly string[];
51
- /**
52
- * The project's generated files, by path below the target root, each with the
53
- * convention row that names its directory as generated.
54
- */
51
+ /** The project's generated files, by path below the target root, each with the detail its retentions name. */
55
52
  readonly generated?: ReadonlyMap<string, string>;
56
53
  }
57
54
 
package/src/file-facts.ts CHANGED
@@ -47,7 +47,7 @@ export interface FileFacts {
47
47
  * reference names is included by the file that names it.
48
48
  */
49
49
  readonly heldByInclusion: ReadonlySet<string>;
50
- /** The paths below the target root of the files below a directory an applied row names as generated. */
50
+ /** The paths below the target root of the generated files. */
51
51
  readonly generated?: ReadonlySet<string>;
52
52
  }
53
53
 
@@ -55,16 +55,19 @@ function publishedIds(union: Graph): ReadonlySet<string> {
55
55
 
56
56
  /**
57
57
  * The reachability class of one declaration of the run. A declaration of a library's
58
- * published API, a public member of a published type included, has callers outside the
59
- * target: it is `certain` where the run loaded every consumer the scope declared and
60
- * declared at least one, `probable` where it declared consumers and did not load them
61
- * all, and `possible` where it declared none. Whether the configuration declares the
62
- * consumer set complete decides nothing here. Every other declaration has every reference
63
- * in the loaded program, a private member and a `#private` name among them, and is
64
- * `certain`.
58
+ * published API, a public member of a published type included, is `certain` where the
59
+ * run loaded every consumer the scope declared and declared one, `probable` where one did
60
+ * not load, and `possible` where it declared none, whatever the configuration says of the
61
+ * consumer set. A declaration of test-support code that test code references, outside the
62
+ * test-of-dead-code rule, is `possible`. Every other declaration is `certain`.
65
63
  */
66
64
  function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
67
65
  const published = publishedIds(input.swept.matrix.union);
66
+ const usedByTests = new Set(
67
+ input.swept.sweep.candidates
68
+ .filter((candidate) => candidate.heldByTests === candidate.id)
69
+ .map((candidate) => candidate.id),
70
+ );
68
71
  const { declared } = input.boundary.consumers;
69
72
  const outside: Confidence =
70
73
  declared.length === 0
@@ -72,7 +75,12 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
72
75
  : everyDeclaredLoaded(input.boundary)
73
76
  ? "certain"
74
77
  : "probable";
75
- return (id) => (published.has(id) ? outside : "certain");
78
+ return (id) => {
79
+ if (usedByTests.has(id)) {
80
+ return "possible";
81
+ }
82
+ return published.has(id) ? outside : "certain";
83
+ };
76
84
  }
77
85
 
78
86
  /**
@@ -147,6 +155,12 @@ export function completed(
147
155
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
148
156
  let minted = swept.sweep.components.length;
149
157
  const dead = new Set(swept.sweep.components.map((component) => component.id));
158
+ const fallingWith = new Map<string, number>();
159
+ for (const { heldByTests } of swept.sweep.candidates) {
160
+ if (heldByTests !== undefined) {
161
+ fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ?? 0) + 1);
162
+ }
163
+ }
150
164
 
151
165
  const done = findings.map((finding): CompletedFinding => {
152
166
  const row = kinds.get(finding.code);
@@ -165,8 +179,14 @@ export function completed(
165
179
  const judged = !part && union.subject[at] === true ? symbol : undefined;
166
180
  const candidate = judged === undefined ? undefined : candidates.get(judged.id);
167
181
  const found = symbol === undefined || part ? undefined : componentOf(symbol.id);
168
- // A test file's declaration is a component member only as a test of dead code.
169
- if (candidate !== undefined && found === undefined && union.test[at] !== true) {
182
+ // A test file's declaration is a component member only as a test of dead code, and
183
+ // test code keeps the test-support code it references out of every component.
184
+ if (
185
+ candidate !== undefined &&
186
+ found === undefined &&
187
+ union.test[at] !== true &&
188
+ candidate.heldByTests === undefined
189
+ ) {
170
190
  throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
171
191
  }
172
192
  let component = found;
@@ -175,7 +195,7 @@ export function completed(
175
195
  component = {
176
196
  id: componentId(minted),
177
197
  root: true,
178
- symbolCount: 1,
198
+ symbolCount: symbol?.kind === "interface" ? (fallingWith.get(symbol.id) ?? 1) : 1,
179
199
  deletableLines: 0,
180
200
  };
181
201
  }
@@ -31,7 +31,6 @@ function compare(a: string, b: string): number {
31
31
  function indexed(input: EmitterInput): {
32
32
  readonly symbolOf: (id: string) => InventorySymbol | undefined;
33
33
  readonly inTest: (id: string) => boolean;
34
- readonly inSupport: (id: string) => boolean;
35
34
  readonly candidateOf: (id: string) => Candidate | undefined;
36
35
  readonly implementationsOf: (id: string) => readonly PositionedSymbol[];
37
36
  } {
@@ -44,7 +43,6 @@ function indexed(input: EmitterInput): {
44
43
  return {
45
44
  symbolOf,
46
45
  inTest: (id) => union.test[union.at(id)] === true,
47
- inSupport: (id) => union.support[union.at(id)] === true,
48
46
  candidateOf: (id) => candidates.get(id),
49
47
  implementationsOf: (id) =>
50
48
  [...(input.implementations.classes.get(id) ?? [])]
@@ -82,19 +80,14 @@ function findingOf(
82
80
  * interface, whose members fall with it unreported; and `DS1203`, a method of a live
83
81
  * interface no reference names, unless it is a marker, a method whose every
84
82
  * implementation has an empty body. A test file's declaration is neither kind's
85
- * subject, because a production sweep counts none of the references it can have, and
86
- * neither is a test-support declaration test code references, which that code keeps live.
83
+ * subject, because a production sweep counts none of the references it can have.
87
84
  */
88
85
  export const interfaces: Emitter = (input) => {
89
86
  const run = indexed(input);
90
87
  const found: Finding[] = [];
91
88
  for (const candidate of input.swept.sweep.candidates) {
92
89
  const symbol = run.symbolOf(candidate.id);
93
- if (
94
- symbol === undefined ||
95
- run.inTest(candidate.id) ||
96
- (run.inSupport(candidate.id) && candidate.testRefs > 0)
97
- ) {
90
+ if (symbol === undefined || run.inTest(candidate.id)) {
98
91
  continue;
99
92
  }
100
93
  if (symbol.kind === "interface") {