@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
package/src/analysis.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  declarationsNamed,
23
23
  } from "./configured-declarations.ts";
24
24
  import { decorator } from "./decorator.ts";
25
- import { dependenciesOf, projectNeeds } from "./dependencies.ts";
25
+ import { configurationNeeds, dependenciesOf, projectNeeds } from "./dependencies.ts";
26
26
  import { deprecatedDeclarations, type Deprecation } from "./deprecation.ts";
27
27
  import {
28
28
  discoverProjects,
@@ -43,10 +43,11 @@ import {
43
43
  } from "./conventions.ts";
44
44
  import { readEdgeSides } from "./edges.ts";
45
45
  import { enumGroup } from "./enum-group.ts";
46
- import { generatedFile } from "./generated-file.ts";
46
+ import { generatedFile, generatedFilesOf } from "./generated-file.ts";
47
47
  import { sourceFilesOf } from "./source-files.ts";
48
48
  import {
49
- detectExemptions,
49
+ evidenceRecords,
50
+ heldRecords,
50
51
  disabledClasses,
51
52
  exemptionsOf,
52
53
  holdingWhile,
@@ -56,7 +57,8 @@ import {
56
57
  type Detectors,
57
58
  } from "./exempt.ts";
58
59
  import type { TSExemptionClass } from "./exempt-classes.ts";
59
- import { heldByInclusion, sourceTree } from "./file-facts.ts";
60
+ import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.ts";
61
+ import { globExpression } from "./glob.ts";
60
62
  import type { Finding } from "./finding.ts";
61
63
  import { findingsPass, recordedFindings, rowKey, type PassResult } from "./findings-pass.ts";
62
64
  import type { EmitterInput } from "./findings/emitter.ts";
@@ -72,7 +74,7 @@ import { injectionContainer } from "./injection-container.ts";
72
74
  import { interfaceSatisfaction } from "./interface-satisfaction.ts";
73
75
  import { intraFunctionFacts, projectParts, type ProjectParts } from "./intra-function-parts.ts";
74
76
  import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
75
- import { readManifest } from "./manifest.ts";
77
+ import { packageEntries, readManifest } from "./manifest.ts";
76
78
  import {
77
79
  liveFilesAt,
78
80
  matrixOf,
@@ -82,7 +84,7 @@ import {
82
84
  type Matrix,
83
85
  type SweepResult,
84
86
  } from "./matrix.ts";
85
- import { joinPath, relativePath, resolvePath } from "./paths.ts";
87
+ import { joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
86
88
  import type { UnansweredQuestion } from "./query.ts";
87
89
  import { byPosition, isComponentFile, type Position } from "./position.ts";
88
90
  import type { TypeErrorSkip, UnansweredCount } from "./report.ts";
@@ -99,6 +101,7 @@ import { references, testFileRulesOf, type Reference, type TestFileRule } from "
99
101
  import { roots, unmatchedEverywhere, type RootKind, type Roots } from "./roots.ts";
100
102
  import type { Scope } from "./scope.ts";
101
103
  import { serializationContract } from "./serialization-contract.ts";
104
+ import { outsideTestReferences, testFilesAt, type OutsideTest } from "./outside-tests.ts";
102
105
  import { supportReferences, testSupportFiles } from "./test-support.ts";
103
106
  import { diagnosticsOf, ownedAs, runSession, type Engine, type ProjectView } from "./session.ts";
104
107
  import { accessorsOf, storesOf } from "./stores.ts";
@@ -188,7 +191,7 @@ interface ProjectRead {
188
191
  readonly typeErrorSkips: readonly TypeErrorSkip[];
189
192
  /** The lines each of those skips withholds every finding on. */
190
193
  readonly skipped: readonly SkippedUnit[];
191
- /** The project's generated files, by path below the target root, each with the row naming it. */
194
+ /** The project's generated files, by path below the target root, each with the detail its retentions name. */
192
195
  readonly generated: ReadonlyMap<string, string>;
193
196
  }
194
197
 
@@ -219,6 +222,10 @@ interface ReadProjects<Answer> {
219
222
  readonly componentsUnread: readonly ComponentsUnread[];
220
223
  /** The convention rows the run applied. */
221
224
  readonly conventionsApplied: readonly AppliedConvention[];
225
+ /** The dependencies the applied rows' short names use. */
226
+ readonly conventionUses: readonly string[];
227
+ /** The test files below the target root no project the run reads holds, parsed. */
228
+ readonly outsideTests: readonly OutsideTest[];
222
229
  }
223
230
 
224
231
  /**
@@ -284,7 +291,15 @@ function readProjects<Answer>(
284
291
  const options = {
285
292
  host,
286
293
  manifest: workspace?.manifest ?? readManifest(host, targetRoot),
287
- workspaceEntries: workspace?.entries,
294
+ otherPackages: [
295
+ ...(workspace?.entries ?? []),
296
+ ...packageEntries(
297
+ host,
298
+ normalizePath(absoluteRoot),
299
+ discovered.configFiles,
300
+ new Set(workspace?.memberDirs ?? []),
301
+ ),
302
+ ],
288
303
  patterns: config.rootPatterns,
289
304
  entryFiles: config.ts.entryFiles,
290
305
  conventions,
@@ -304,6 +319,8 @@ function readProjects<Answer>(
304
319
  const unbuildable: SetupFailure[] = [];
305
320
  const setupDropped: SetupFailure[] = [];
306
321
  const consumed = new Map<string, Reference>();
322
+ const heldFiles = new Set<string>();
323
+ let outsideTests: readonly OutsideTest[] = [];
307
324
  const setupMet = (configFile: string, met: readonly SetupFailure[]): void => {
308
325
  const [first] = met;
309
326
  if (first !== undefined && isGuess(discovered, configFile)) {
@@ -420,6 +437,7 @@ function readProjects<Answer>(
420
437
  ...kept.map((id) => ({ id, kind: "type-error" as const, source: "" })),
421
438
  ],
422
439
  };
440
+ const own = sourceFilesOf(project, targetRoot);
423
441
  return {
424
442
  configuration,
425
443
  configFile: project.configFile,
@@ -431,13 +449,27 @@ function readProjects<Answer>(
431
449
  .ownSourceFiles()
432
450
  .filter((file) => referenceOnly(file.fileName))
433
451
  .flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
434
- generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
452
+ generated: generatedFilesOf(own, generatedFiles(conventions, own)),
435
453
  };
436
454
  };
455
+ for (const file of project.ownSourceFiles()) {
456
+ heldFiles.add(relativePath(absoluteRoot, file.fileName) ?? file.fileName);
457
+ }
437
458
  return stage(project, { configuration, read });
438
459
  },
439
460
  discovered.derived ? "record" : "refuse",
440
461
  workspace?.programs,
462
+ () => {
463
+ const patterns = config.ts.testFiles.map(globExpression);
464
+ outsideTests = testFilesAt(
465
+ host,
466
+ targetRoot,
467
+ sourcePaths(host, targetRoot).filter(
468
+ (path) => !heldFiles.has(path) && patterns.some((pattern) => pattern.test(path)),
469
+ ),
470
+ options.parse,
471
+ );
472
+ },
441
473
  );
442
474
  for (const configFile of session.unopened) {
443
475
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
@@ -475,6 +507,8 @@ function readProjects<Answer>(
475
507
  unanswered: session.unanswered,
476
508
  componentsUnread: workspace?.componentsUnread ?? [],
477
509
  conventionsApplied: conventions.applied,
510
+ conventionUses: conventions.uses,
511
+ outsideTests,
478
512
  };
479
513
  }
480
514
 
@@ -630,6 +664,17 @@ interface SweptProject<Extra> {
630
664
  readonly extra: Extra;
631
665
  }
632
666
 
667
+ /** What one project's stage reads while its view is open, before the run's test-support code is known. */
668
+ interface ReadProject<Extra> {
669
+ readonly projectRead: ProjectRead;
670
+ readonly references: readonly Reference[];
671
+ readonly testFiles: readonly string[];
672
+ readonly evidence: readonly Exemption[];
673
+ readonly componentFiles: readonly string[];
674
+ readonly componentWarnings: readonly ComponentWarning[];
675
+ readonly extra: Extra;
676
+ }
677
+
633
678
  /** What a run read of its projects before any sweep: the matrix, its exemptions, and each project's answers. */
634
679
  interface ReadRun<Extra> {
635
680
  readonly matrix: Matrix;
@@ -647,6 +692,8 @@ interface ReadRun<Extra> {
647
692
  readonly componentsUnread: readonly ComponentsUnread[];
648
693
  /** The convention rows the run applied. */
649
694
  readonly conventionsApplied: readonly AppliedConvention[];
695
+ /** The dependencies the applied rows' short names use. */
696
+ readonly conventionUses: readonly string[];
650
697
  }
651
698
 
652
699
  /**
@@ -674,18 +721,12 @@ function readRun<Extra>(
674
721
  ? { delimiters: config.analysis.templateDelimiters, files: [] }
675
722
  : readTemplates(host, targetRoot, config.analysis);
676
723
  const consumers = scope.consumers.map((consumer) => consumer.path);
677
- const read = readProjects<SweptProject<Extra>>(engine, host, scope, config, (project, opened) => {
724
+ const read = readProjects<ReadProject<Extra>>(engine, host, scope, config, (project, opened) => {
678
725
  const projectRead = opened.read();
679
726
  const resolved = references(project, projectRead.held, targetRoot, {
680
727
  testFiles: config.ts.testFiles,
681
728
  });
682
- const support = testSupportFiles(
683
- projectRead.held.symbols,
684
- resolved.references,
685
- projectRead.rooted.liveUnderReachability,
686
- resolved.testFilePaths,
687
- );
688
- const detected = detectExemptions(
729
+ const evidence = evidenceRecords(
689
730
  {
690
731
  project,
691
732
  held: projectRead.held,
@@ -698,56 +739,87 @@ function readRun<Extra>(
698
739
  : { generated: projectRead.generated }),
699
740
  },
700
741
  detectors,
701
- {
702
- disabled,
703
- mode,
704
- testFiles: new Set([...resolved.testFilePaths, ...support]),
705
- },
742
+ disabled,
706
743
  );
744
+ const components = project.ownSourceFiles().filter(isComponentFile);
745
+ const pathOf = (file: (typeof components)[number]): string =>
746
+ relativePath(targetRoot, file.fileName) ?? file.fileName;
747
+ return {
748
+ projectRead,
749
+ references: resolved.references,
750
+ testFiles: resolved.testFilePaths,
751
+ evidence,
752
+ componentFiles: components.map(pathOf),
753
+ componentWarnings: components.flatMap((file) =>
754
+ readComponent(file.originalText).warnings.map((warning) => ({
755
+ path: pathOf(file),
756
+ line: lineOf(file.originalText, warning.offset),
757
+ reason: warning.reason,
758
+ })),
759
+ ),
760
+ extra: extra(project, projectRead, resolved.references),
761
+ };
762
+ });
763
+ const outsidePaths = read.outsideTests.map((one) => one.path);
764
+ const withOutside = read.answers.map((one) => ({
765
+ ...one,
766
+ references: [
767
+ ...one.references,
768
+ ...outsideTestReferences(read.outsideTests, one.projectRead.held, targetRoot),
769
+ ],
770
+ testFiles: [...one.testFiles, ...outsidePaths],
771
+ }));
772
+ // Test-support code is code only test code imports in any configuration of the run: a
773
+ // configuration that holds no test file says nothing about what the tests import.
774
+ const support = testSupportFiles(
775
+ withOutside.flatMap((one) => one.projectRead.held.symbols),
776
+ withOutside.flatMap((one) => one.references),
777
+ withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability),
778
+ withOutside.flatMap((one) => one.testFiles),
779
+ );
780
+ const answers = withOutside.map((one): SweptProject<Extra> => {
781
+ const { projectRead } = one;
782
+ const detected = heldRecords(one.evidence, {
783
+ disabled,
784
+ mode,
785
+ testFiles: new Set([...one.testFiles, ...support]),
786
+ });
707
787
  const exempt = detected.records;
708
- const made = supportReferences(resolved.references, support);
788
+ const made = supportReferences(one.references, support);
789
+ const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
790
+ const supportFiles = [...support].filter((path) => held.has(path)).sort();
709
791
  const configured: Configured = {
710
792
  configuration: projectRead.configuration,
711
793
  symbols: projectRead.held.symbols,
712
794
  references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
713
795
  roots: projectRead.rooted.liveUnderReachability,
714
- testFiles: resolved.testFilePaths,
715
- ...(support.size === 0 ? {} : { supportFiles: [...support].sort() }),
796
+ testFiles: one.testFiles,
797
+ ...(supportFiles.length === 0 ? {} : { supportFiles }),
716
798
  ...(projectRead.referenceOnly.length === 0
717
799
  ? {}
718
800
  : { referenceOnly: projectRead.referenceOnly }),
719
801
  };
720
- const unanswered = [
721
- ...projectRead.held.unanswered,
722
- ...projectRead.rooted.unanswered,
723
- ...made
724
- .filter((reference) => reference.resolution === "by-name")
725
- .map((reference) => reference.to),
726
- ...exempt.filter(isUnansweredRecord).map((record) => record.id),
727
- ];
728
- const components = project.ownSourceFiles().filter(isComponentFile);
729
- const pathOf = (file: (typeof components)[number]): string =>
730
- relativePath(targetRoot, file.fileName) ?? file.fileName;
731
802
  return {
732
803
  configFile: projectRead.configFile,
733
804
  configured,
734
805
  exempt,
735
- unanswered,
806
+ unanswered: [
807
+ ...projectRead.held.unanswered,
808
+ ...projectRead.rooted.unanswered,
809
+ ...made
810
+ .filter((reference) => reference.resolution === "by-name")
811
+ .map((reference) => reference.to),
812
+ ...exempt.filter(isUnansweredRecord).map((record) => record.id),
813
+ ],
736
814
  typeErrorSkips: projectRead.typeErrorSkips,
737
815
  skipped: projectRead.skipped,
738
- componentFiles: components.map(pathOf),
739
- componentWarnings: components.flatMap((file) =>
740
- readComponent(file.originalText).warnings.map((warning) => ({
741
- path: pathOf(file),
742
- line: lineOf(file.originalText, warning.offset),
743
- reason: warning.reason,
744
- })),
745
- ),
746
- extra: extra(project, projectRead, made),
816
+ componentFiles: one.componentFiles,
817
+ componentWarnings: one.componentWarnings,
818
+ extra: one.extra,
747
819
  };
748
820
  });
749
821
  const consumed = read.consumers.references;
750
- const projects = read.answers.map((one) =>
822
+ const projects = answers.map((one) =>
751
823
  consumed.length === 0
752
824
  ? one
753
825
  : {
@@ -763,7 +835,7 @@ function readRun<Extra>(
763
835
  exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
764
836
  unansweredHeld: [
765
837
  ...new Set([
766
- ...read.answers.flatMap((one) => one.unanswered),
838
+ ...answers.flatMap((one) => one.unanswered),
767
839
  ...consumed.filter((one) => one.resolution === "by-name").map((one) => one.to),
768
840
  ]),
769
841
  ],
@@ -773,6 +845,7 @@ function readRun<Extra>(
773
845
  unanswered: read.unanswered,
774
846
  componentsUnread: read.componentsUnread,
775
847
  conventionsApplied: read.conventionsApplied,
848
+ conventionUses: read.conventionUses,
776
849
  };
777
850
  }
778
851
 
@@ -1027,7 +1100,7 @@ function emitterInputOver(
1027
1100
  dependencies: dependenciesOf(
1028
1101
  host,
1029
1102
  targetRoot,
1030
- projects.map((one) => one.extra.needs),
1103
+ [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)],
1031
1104
  swept.sweep.candidates.map((candidate) => candidate.id),
1032
1105
  ),
1033
1106
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
package/src/config.ts CHANGED
@@ -232,9 +232,12 @@ export function defaultConfig(): Config {
232
232
  ts: {
233
233
  testFiles: [
234
234
  "**/*.test.{ts,tsx,mts,cts}",
235
+ "**/*.test-d.{ts,tsx,mts,cts}",
236
+ "**/*.spec-d.{ts,tsx,mts,cts}",
235
237
  "**/*.spec.*",
236
238
  "**/__tests__/**",
237
239
  "**/__mocks__/**",
240
+ "**/test-d/**",
238
241
  ],
239
242
  entryFiles: [],
240
243
  componentExtensions: [".vue", ".svelte", ".astro"],
@@ -57,7 +57,7 @@ const COUNTERPARTS: readonly (readonly [string, readonly string[]])[] = [
57
57
  const APPENDED = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"] as const;
58
58
 
59
59
  /** The extensions the program's module resolution appends to a specifier written without one. */
60
- const RESOLVED_EXTENSIONS = [
60
+ export const RESOLVED_EXTENSIONS = [
61
61
  ".ts",
62
62
  ".tsx",
63
63
  ".d.ts",
@@ -237,33 +237,36 @@ function relativeSpecifiers(file: SourceFile): readonly string[] {
237
237
  }
238
238
 
239
239
  /**
240
- * The module one relative specifier from a configuration file reaches: the path it names,
241
- * that path with its JavaScript extension replaced by the TypeScript counterparts, that
242
- * path with an extension appended, and a directory's `index` with one appended.
240
+ * The paths one relative specifier from a configuration file may reach, in the order they
241
+ * are tried: the path it names where that ends with a module extension, that path with its
242
+ * JavaScript extension replaced by the TypeScript counterparts, that path with an
243
+ * extension appended, and a directory's `index` with one appended. A data file such as
244
+ * `./package.json` is no module, so it is no candidate.
243
245
  */
244
- function reachedBy(host: Host, dir: string, specifier: string): string | undefined {
246
+ export function specifierCandidates(dir: string, specifier: string): readonly string[] {
245
247
  const path = resolvePath(dir, specifier);
246
- const isFile = (candidate: string): boolean => host.kindOf(candidate) === "file";
247
- // A data file such as `./package.json` is no module, so its strings use no dependency.
248
- if (isFile(path) && APPENDED.some((extension) => path.endsWith(extension))) {
249
- return path;
248
+ const candidates: string[] = [];
249
+ if (APPENDED.some((extension) => path.endsWith(extension))) {
250
+ candidates.push(path);
250
251
  }
251
252
  for (const [extension, counterparts] of COUNTERPARTS) {
252
253
  if (path.endsWith(extension)) {
253
254
  const stem = path.slice(0, -extension.length);
254
- const found = counterparts.map((one) => stem + one).find(isFile);
255
- if (found !== undefined) {
256
- return found;
257
- }
255
+ candidates.push(...counterparts.map((one) => stem + one));
258
256
  }
259
257
  }
260
- const appended = APPENDED.map((extension) => path + extension).find(isFile);
261
- if (appended !== undefined) {
262
- return appended;
263
- }
264
- return host.kindOf(path) === "directory"
265
- ? APPENDED.map((extension) => joinPath(path, `index${extension}`)).find(isFile)
266
- : undefined;
258
+ candidates.push(...APPENDED.map((extension) => path + extension));
259
+ candidates.push(...APPENDED.map((extension) => joinPath(path, `index${extension}`)));
260
+ return candidates;
261
+ }
262
+
263
+ /** The file on disk one relative specifier from a configuration file reaches, or undefined. */
264
+ export function resolveRelativeSpecifier(
265
+ host: Host,
266
+ dir: string,
267
+ specifier: string,
268
+ ): string | undefined {
269
+ return specifierCandidates(dir, specifier).find((path) => host.kindOf(path) === "file");
267
270
  }
268
271
 
269
272
  /**
@@ -311,7 +314,7 @@ function outsideModules(
311
314
  const file = parse(next.path, text);
312
315
  found.push({ file, manifestDir: next.manifestDir });
313
316
  for (const specifier of relativeSpecifiers(file)) {
314
- const reached = reachedBy(host, dirnamePath(next.path), specifier);
317
+ const reached = resolveRelativeSpecifier(host, dirnamePath(next.path), specifier);
315
318
  if (reached !== undefined) {
316
319
  pending.push({ path: reached, manifestDir: next.manifestDir });
317
320
  }
@@ -347,12 +350,13 @@ export function dependenciesNamed(text: string, declared: readonly string[]): re
347
350
  }
348
351
 
349
352
  /**
350
- * The own files one string names read against a directory: none unless it starts with
351
- * `./` or `../`, else the file it names directly or through the emit mapping, or the
352
- * file it names with an extension the module resolution appends.
353
+ * The own files one string names read against a directory: the file it names directly or
354
+ * through the emit mapping, or the file it names with an extension the module resolution
355
+ * appends. An empty string, one starting with `/` and one holding a glob character names
356
+ * none.
353
357
  */
354
358
  export function filesNamedBy(files: SourceFiles, dir: string, text: string): readonly SourceFile[] {
355
- if (!RELATIVE_PREFIXES.some((prefix) => text.startsWith(prefix)) || text.includes("*")) {
359
+ if (text === "" || text.startsWith("/") || /[*?[{]/u.test(text)) {
356
360
  return [];
357
361
  }
358
362
  const path = resolvePath(dir, text);
@@ -2,9 +2,9 @@
2
2
  // Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
3
3
  export const RECORD = {
4
4
  conformance: {
5
- corpusVersion: "2.2.0",
5
+ corpusVersion: "3.0.0",
6
6
  result: "pass",
7
- digest: "sha256:ce7c9087ed7b2baf3de9fa2df436b500706f4f9e94fd9e0fed633054305f06a4",
7
+ digest: "sha256:db52e3164347cab70f45f438b4ac9ce89573be9303503daeb237a3c40361b001",
8
8
  },
9
9
  gaps: [],
10
10
  } as const;
@@ -97,15 +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
- /** How many element and signature steps the structural walk takes below a value's own type. */
103
- const MAX_DEPTH = 3;
104
-
105
107
  /** One member of an object type, by the inventory's declarations and the checker's declaration handles. */
106
108
  interface Member {
107
109
  readonly ids: readonly string[];
108
110
  readonly handles: ReadonlySet<string>;
111
+ /** Whether a declaration of it is in one of the target's own files. */
112
+ readonly own: boolean;
109
113
  }
110
114
 
111
115
  /**
@@ -123,6 +127,7 @@ export function contextualUses<Brand>(
123
127
  properties: PropertyTables,
124
128
  ): ContextualUses {
125
129
  const queries = project.queries;
130
+ const ownPaths = project.ownPaths();
126
131
  const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
127
132
  let batches = 0;
128
133
  let lookups = 0;
@@ -165,6 +170,7 @@ export function contextualUses<Brand>(
165
170
  handles: new Set(
166
171
  property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`),
167
172
  ),
173
+ own: property.declarations.some((handle) => ownPaths.has(handle.path)),
168
174
  });
169
175
  }
170
176
  tables.set(type.id, table);
@@ -206,6 +212,11 @@ export function contextualUses<Brand>(
206
212
  member.ids.filter((id) => kinds.get(id) === "type-member");
207
213
  const carries = (part: Type): boolean =>
208
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
+ };
209
220
 
210
221
  const elements = new Map<number, Type | undefined>();
211
222
  /** The element type of an array type, read once per type. */
@@ -254,12 +265,12 @@ export function contextualUses<Brand>(
254
265
  * Each object type a value of `source` is checked as against the object types of
255
266
  * `target`: the value's own, and through array elements and through a function's
256
267
  * parameters, checked the other way, and its result. A source that is one of the
257
- * target's own object types matches it as it stands.
268
+ * target's own object types matches it as it stands. Each pair is compared once, which
269
+ * ends the walk over a recursive type.
258
270
  */
259
271
  const compared = (
260
272
  source: Type | undefined,
261
273
  target: Type | undefined,
262
- depth: number,
263
274
  into: Checked[],
264
275
  seen: Set<string>,
265
276
  ): void => {
@@ -267,7 +278,7 @@ export function contextualUses<Brand>(
267
278
  if (source === undefined || target === undefined || source.id === target.id) {
268
279
  return;
269
280
  }
270
- if (depth > MAX_DEPTH || seen.has(key)) {
281
+ if (seen.has(key)) {
271
282
  return;
272
283
  }
273
284
  seen.add(key);
@@ -275,13 +286,15 @@ export function contextualUses<Brand>(
275
286
  const sources = partsOf(source).filter((one) => !targets.some((part) => part.id === one.id));
276
287
  for (const one of sources) {
277
288
  if (carries(one)) {
278
- 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 });
279
292
  }
280
293
  const element = elementOf(one);
281
294
  const called = element === undefined ? callOf(one) : undefined;
282
295
  for (const part of targets) {
283
296
  if (element !== undefined) {
284
- compared(element, elementOf(part), depth + 1, into, seen);
297
+ compared(element, elementOf(part), into, seen);
285
298
  continue;
286
299
  }
287
300
  const other = called === undefined ? undefined : callOf(part);
@@ -289,30 +302,37 @@ export function contextualUses<Brand>(
289
302
  continue;
290
303
  }
291
304
  other.parameters.forEach((parameter, index) => {
292
- compared(parameter, called.parameters[index], depth + 1, into, seen);
305
+ compared(parameter, called.parameters[index], into, seen);
293
306
  });
294
- compared(called.result, other.result, depth + 1, into, seen);
307
+ compared(called.result, other.result, into, seen);
295
308
  }
296
309
  }
297
310
  };
298
311
 
299
- /** Whether a value of one type may carry a member a position reads, at any depth the walk takes. */
300
- const mayCarry = (type: Type, depth: number): boolean =>
301
- depth <= MAX_DEPTH &&
302
- partsOf(type).some((part) => {
303
- if (carries(part)) {
312
+ /**
313
+ * Whether a value of one type may carry a member a position reads, at any depth the
314
+ * walk takes. A type met again on the way adds nothing its first visit did not.
315
+ */
316
+ const mayCarry = (type: Type, seen: Set<number> = new Set()): boolean => {
317
+ if (seen.has(type.id)) {
318
+ return false;
319
+ }
320
+ seen.add(type.id);
321
+ return partsOf(type).some((part) => {
322
+ if (carries(part) || outside(part)) {
304
323
  return true;
305
324
  }
306
325
  const element = elementOf(part);
307
326
  if (element !== undefined) {
308
- return mayCarry(element, depth + 1);
327
+ return mayCarry(element, seen);
309
328
  }
310
329
  return callOf(part) !== undefined;
311
330
  });
331
+ };
312
332
 
313
333
  const sourced = carried.flatMap((flow, index) => {
314
334
  const type = types[index];
315
- return type === undefined || !isAnswered(type) || !mayCarry(type, 0) ? [] : [{ flow, type }];
335
+ return type === undefined || !isAnswered(type) || !mayCarry(type) ? [] : [{ flow, type }];
316
336
  });
317
337
  batches += batchesOf(sourced.length);
318
338
  const positions = queries.contextualTypes(
@@ -326,11 +346,22 @@ export function contextualUses<Brand>(
326
346
  return;
327
347
  }
328
348
  const checked: Checked[] = [];
329
- compared(type, position, 0, checked, new Set());
349
+ compared(type, position, checked, new Set());
330
350
  const ids = new Set<string>();
331
- for (const { source, targets } of checked) {
351
+ for (const { source, targets, outside: held } of checked) {
332
352
  const tables = targets.map(membersOf);
333
- 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) {
334
365
  const read = tables.some((table) => {
335
366
  const declared = table.get(name);
336
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
  ];