@cplieger/deadset-ts 5.1.0 → 5.2.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.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +79 -37
  3. package/dist/src/config.js +3 -0
  4. package/dist/src/configuration-files.js +24 -25
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/contextual-uses.js +46 -18
  7. package/dist/src/convention-rows.js +7 -0
  8. package/dist/src/conventions.js +97 -33
  9. package/dist/src/dependencies.js +8 -0
  10. package/dist/src/entry-point-gaps.js +10 -26
  11. package/dist/src/entry-points.js +4 -0
  12. package/dist/src/exempt.js +23 -8
  13. package/dist/src/file-facts.js +23 -20
  14. package/dist/src/findings/completion.js +27 -11
  15. package/dist/src/findings/interfaces.js +2 -6
  16. package/dist/src/findings/intra-function.js +15 -3
  17. package/dist/src/findings/unused-declarations.js +5 -6
  18. package/dist/src/findings/visibility-narrowing.js +6 -1
  19. package/dist/src/generated-file.js +72 -12
  20. package/dist/src/graph.js +6 -5
  21. package/dist/src/html-entries.js +137 -0
  22. package/dist/src/inventory.js +13 -3
  23. package/dist/src/kinds.js +2 -2
  24. package/dist/src/manifest.js +34 -5
  25. package/dist/src/matrix.js +9 -3
  26. package/dist/src/namespace-objects.js +158 -0
  27. package/dist/src/outside-tests.js +139 -0
  28. package/dist/src/overrides.js +104 -0
  29. package/dist/src/paths.js +17 -0
  30. package/dist/src/references.js +150 -4
  31. package/dist/src/roots.js +172 -39
  32. package/dist/src/session.js +4 -3
  33. package/dist/src/sweep.js +17 -0
  34. package/dist/src/template-field.js +60 -3
  35. package/dist/src/test-support.js +35 -23
  36. package/dist/src/version.js +1 -1
  37. package/jsr.json +1 -1
  38. package/package.json +1 -1
  39. package/src/analysis.ts +122 -49
  40. package/src/config.ts +3 -0
  41. package/src/configuration-files.ts +29 -25
  42. package/src/conformance-record.ts +2 -2
  43. package/src/contextual-uses.ts +51 -20
  44. package/src/convention-rows.ts +15 -0
  45. package/src/conventions.ts +119 -30
  46. package/src/dependencies.ts +9 -0
  47. package/src/entry-point-gaps.ts +11 -28
  48. package/src/entry-points.ts +7 -1
  49. package/src/exempt.ts +29 -12
  50. package/src/file-facts.ts +25 -21
  51. package/src/findings/completion.ts +31 -11
  52. package/src/findings/interfaces.ts +2 -9
  53. package/src/findings/intra-function.ts +17 -3
  54. package/src/findings/unused-declarations.ts +5 -8
  55. package/src/findings/visibility-narrowing.ts +7 -1
  56. package/src/generated-file.ts +80 -12
  57. package/src/graph.ts +9 -8
  58. package/src/html-entries.ts +170 -0
  59. package/src/inventory.ts +15 -3
  60. package/src/kinds.ts +2 -2
  61. package/src/manifest.ts +49 -7
  62. package/src/matrix.ts +21 -3
  63. package/src/namespace-objects.ts +216 -0
  64. package/src/outside-tests.ts +191 -0
  65. package/src/overrides.ts +136 -0
  66. package/src/paths.ts +18 -0
  67. package/src/references.ts +206 -9
  68. package/src/roots.ts +210 -53
  69. package/src/session.ts +4 -2
  70. package/src/sweep.ts +19 -0
  71. package/src/template-field.ts +65 -3
  72. package/src/test-support.ts +34 -22
  73. package/src/version.ts +1 -1
@@ -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
 
@@ -187,12 +184,23 @@ export function detectExemptions<Brand>(
187
184
  detectors: Detectors,
188
185
  holding: Holding,
189
186
  ): Detected {
187
+ return heldRecords(evidenceRecords(input, detectors, holding.disabled), holding);
188
+ }
189
+
190
+ /**
191
+ * Every record one project's detectors found, one per piece of evidence, before the
192
+ * mode decides which of them hold: what {@link heldRecords} reads.
193
+ */
194
+ export function evidenceRecords<Brand>(
195
+ input: DetectorInput<Brand>,
196
+ detectors: Detectors,
197
+ disabled: ReadonlySet<ExemptionClass>,
198
+ ): readonly Exemption[] {
190
199
  const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
191
200
  const found: Exemption[] = [];
192
- const inTestFiles: Exemption[] = [];
193
201
  for (const exemptionClass of TS_EXEMPTION_CLASSES) {
194
202
  const detect = detectors.get(exemptionClass);
195
- if (detect === undefined || holding.disabled.has(exemptionClass)) {
203
+ if (detect === undefined || disabled.has(exemptionClass)) {
196
204
  continue;
197
205
  }
198
206
  const allowed = typescriptVisibilityOf(exemptionClass);
@@ -200,17 +208,26 @@ export function detectExemptions<Brand>(
200
208
  if (!retainable(allowed, visibility.get(evidence.id))) {
201
209
  continue;
202
210
  }
203
- const record = {
211
+ found.push({
204
212
  id: evidence.id,
205
213
  class: exemptionClass,
206
214
  detail: evidence.detail,
207
215
  site: evidence.site,
208
216
  ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
209
- };
210
- if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
211
- inTestFiles.push(record);
212
- continue;
213
- }
217
+ });
218
+ }
219
+ }
220
+ return found;
221
+ }
222
+
223
+ /** The records that hold under one mode, beside the evidence a production mode found in a test file. */
224
+ export function heldRecords(records: readonly Exemption[], holding: Holding): Detected {
225
+ const found: Exemption[] = [];
226
+ const inTestFiles: Exemption[] = [];
227
+ for (const record of records) {
228
+ if (holding.mode.production && holding.testFiles.has(record.site.path)) {
229
+ inTestFiles.push(record);
230
+ } else {
214
231
  found.push(record);
215
232
  }
216
233
  }
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
 
@@ -65,13 +65,11 @@ function lastLine(text: string): number {
65
65
  }
66
66
 
67
67
  /**
68
- * Every source file below the target root, in path order. A directory that cannot be
69
- * read is skipped, as discovery skips it, and a file that cannot be read is skipped
70
- * with it: a tree the analysis may not enter holds no file it can report.
68
+ * The path below the target root of every source file there, in path order. A directory
69
+ * that cannot be read is skipped, as discovery skips it.
71
70
  */
72
- export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[] {
73
- const scopeOf = packageScope(host, targetRoot);
74
- const found: TreeFile[] = [];
71
+ export function sourcePaths(host: Host, targetRoot: string): readonly string[] {
72
+ const found: string[] = [];
75
73
  const pending = [""];
76
74
  for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
77
75
  let entries: readonly { readonly name: string; readonly directory: boolean }[];
@@ -88,23 +86,29 @@ export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[]
88
86
  }
89
87
  continue;
90
88
  }
91
- if (!SOURCE_FILE.test(entry.name) || DECLARATION_FILE.test(entry.name)) {
92
- continue;
89
+ if (SOURCE_FILE.test(entry.name) && !DECLARATION_FILE.test(entry.name)) {
90
+ found.push(path);
93
91
  }
94
- let text: string;
95
- try {
96
- text = host.readFile(joinPath(targetRoot, path));
97
- } catch {
98
- continue;
99
- }
100
- found.push({
101
- path,
102
- ref: renderRef(scopeOf(path), { of: "module" }),
103
- endLine: lastLine(text),
104
- });
105
92
  }
106
93
  }
107
- return found.sort((a, b) => (a.path < b.path ? -1 : 1));
94
+ return found.sort((a, b) => (a < b ? -1 : 1));
95
+ }
96
+
97
+ /**
98
+ * Every source file below the target root, in path order. A file that cannot be read is
99
+ * skipped: a tree the analysis may not enter holds no file it can report.
100
+ */
101
+ export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[] {
102
+ const scopeOf = packageScope(host, targetRoot);
103
+ return sourcePaths(host, targetRoot).flatMap((path) => {
104
+ let text: string;
105
+ try {
106
+ text = host.readFile(joinPath(targetRoot, path));
107
+ } catch {
108
+ return [];
109
+ }
110
+ return [{ path, ref: renderRef(scopeOf(path), { of: "module" }), endLine: lastLine(text) }];
111
+ });
108
112
  }
109
113
 
110
114
  /** Whether one file changes what the program sees by being included in it. */
@@ -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") {
@@ -1,6 +1,7 @@
1
1
  import type { Finding } from "../finding.ts";
2
2
  import { KINDS } from "../kinds.ts";
3
3
  import { UNUSED_PARAMETER, UNUSED_RESULT, type PartFact } from "../intra-function-parts.ts";
4
+ import { reachedWithTheTests } from "../sweep.ts";
4
5
  import type { Emitter, EmitterInput } from "./emitter.ts";
5
6
 
6
7
  /** A list of rules as a sentence names them. */
@@ -51,12 +52,25 @@ function reported(
51
52
  /**
52
53
  * The findings of the intra-function family, `DS1800` to `DS1899`, each about a part of
53
54
  * the declaration its reference names, at the part's own position, whatever the sweep
54
- * decided about that declaration. Each message names the other rules that report the
55
- * kind, as the vocabulary lists them, and so does the finding's details.
55
+ * decided about a production declaration. A part of test code is reported only where the
56
+ * run that counts test references reaches its declaration and that is no test of dead
57
+ * code. Each message and the finding's details name the other rules that report the kind.
56
58
  */
57
59
  export const intraFunction: Emitter = (input) => {
58
60
  const union = input.swept.matrix.union;
59
61
  const byId = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
62
+ const reached = reachedWithTheTests(union, [
63
+ ...input.swept.exempt.map((record) => record.id),
64
+ ...input.swept.heldByUnanswered,
65
+ ]);
66
+ const testsOfDeadCode = new Set(
67
+ input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id),
68
+ );
69
+ const judged = (id: string): boolean => {
70
+ const at = union.at(id);
71
+ const testCode = union.test[at] === true || union.support[at] === true;
72
+ return !testCode || (reached[at] === true && !testsOfDeadCode.has(id));
73
+ };
60
74
  const unknown = new Set(
61
75
  union.rooted.flatMap((root) => {
62
76
  const symbol = union.symbols[root.at];
@@ -84,7 +98,7 @@ export const intraFunction: Emitter = (input) => {
84
98
  for (const part of input.intraFunction.parts) {
85
99
  const declaration = byId.get(part.declaration);
86
100
  const overlap = KINDS.get(part.code)?.overlap ?? [];
87
- if (declaration === undefined || !reported(input, part, unknown)) {
101
+ if (declaration === undefined || !judged(declaration.id) || !reported(input, part, unknown)) {
88
102
  continue;
89
103
  }
90
104
  found.push({