@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/README.md CHANGED
@@ -90,7 +90,7 @@ A TypeScript caller that imports the source sets `allowImportingTsExtensions`, b
90
90
 
91
91
  ## Related projects
92
92
 
93
- deadset-ts implements the [deadset Contract](https://github.com/cplieger/deadset-spec), which fixes the issue codes, the report schema and the exit codes. It passes every fixture of the Contract's conformance corpus except one reflective-lookup fixture, the gap [`conformance.json`](conformance.json) declares.
93
+ deadset-ts implements the [deadset Contract](https://github.com/cplieger/deadset-spec), which fixes the issue codes, the report schema and the exit codes. It passes every fixture of the Contract's conformance corpus.
94
94
 
95
95
  - [deadset-go](https://github.com/cplieger/deadset-go) is the same analysis for Go modules.
96
96
  - [deadset](https://github.com/cplieger/deadset) runs both analyzers as one command and merges their reports, resolving the edges between Go and TypeScript code.
@@ -11,16 +11,17 @@ import { lineOf, readComponent } from "./component-files.js";
11
11
  import { consumerLoads, consumerReferences, moduleIdentity, } from "./consumers.js";
12
12
  import { configuredDeclarations, configuredRef, declarationsNamed, } from "./configured-declarations.js";
13
13
  import { decorator } from "./decorator.js";
14
- import { dependenciesOf, projectNeeds } from "./dependencies.js";
14
+ import { configurationNeeds, dependenciesOf, projectNeeds } from "./dependencies.js";
15
15
  import { deprecatedDeclarations } from "./deprecation.js";
16
16
  import { discoverProjects, DiscoveryError, isGuess, projectErrors, renderDiagnostic, targetRelative, } from "./discover.js";
17
17
  import { conventionFailuresFor, generatedFiles, readConventions, } from "./conventions.js";
18
18
  import { readEdgeSides } from "./edges.js";
19
19
  import { enumGroup } from "./enum-group.js";
20
- import { generatedFile } from "./generated-file.js";
20
+ import { generatedFile, generatedFilesOf } from "./generated-file.js";
21
21
  import { sourceFilesOf } from "./source-files.js";
22
- import { detectExemptions, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, } from "./exempt.js";
23
- import { heldByInclusion, sourceTree } from "./file-facts.js";
22
+ import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, } from "./exempt.js";
23
+ import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.js";
24
+ import { globExpression } from "./glob.js";
24
25
  import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
25
26
  import { NO_SELF_CHECK, unmatchedRootFindings, } from "./findings/self-check.js";
26
27
  import { frameworkLifecycle } from "./framework-lifecycle.js";
@@ -29,9 +30,9 @@ import { injectionContainer } from "./injection-container.js";
29
30
  import { interfaceSatisfaction } from "./interface-satisfaction.js";
30
31
  import { intraFunctionFacts, projectParts } from "./intra-function-parts.js";
31
32
  import { inventory } from "./inventory.js";
32
- import { readManifest } from "./manifest.js";
33
+ import { packageEntries, readManifest } from "./manifest.js";
33
34
  import { liveFilesAt, matrixOf, referenceKey, sweepMatrix, } from "./matrix.js";
34
- import { joinPath, relativePath, resolvePath } from "./paths.js";
35
+ import { joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
35
36
  import { byPosition, isComponentFile } from "./position.js";
36
37
  import { SetupError, setupLine } from "./setup-failure.js";
37
38
  import { pathsOf, readErrors, skippedRoots, skippedUnits, } from "./type-errors.js";
@@ -39,6 +40,7 @@ import { reflectiveLookup } from "./reflective-lookup.js";
39
40
  import { references, testFileRulesOf } from "./references.js";
40
41
  import { roots, unmatchedEverywhere } from "./roots.js";
41
42
  import { serializationContract } from "./serialization-contract.js";
43
+ import { outsideTestReferences, testFilesAt } from "./outside-tests.js";
42
44
  import { supportReferences, testSupportFiles } from "./test-support.js";
43
45
  import { diagnosticsOf, ownedAs, runSession } from "./session.js";
44
46
  import { accessorsOf, storesOf } from "./stores.js";
@@ -107,7 +109,10 @@ function readProjects(engine, host, scope, config, stage) {
107
109
  const options = {
108
110
  host,
109
111
  manifest: workspace?.manifest ?? readManifest(host, targetRoot),
110
- workspaceEntries: workspace?.entries,
112
+ otherPackages: [
113
+ ...(workspace?.entries ?? []),
114
+ ...packageEntries(host, normalizePath(absoluteRoot), discovered.configFiles, new Set(workspace?.memberDirs ?? [])),
115
+ ],
111
116
  patterns: config.rootPatterns,
112
117
  entryFiles: config.ts.entryFiles,
113
118
  conventions,
@@ -127,6 +132,8 @@ function readProjects(engine, host, scope, config, stage) {
127
132
  const unbuildable = [];
128
133
  const setupDropped = [];
129
134
  const consumed = new Map();
135
+ const heldFiles = new Set();
136
+ let outsideTests = [];
130
137
  const setupMet = (configFile, met) => {
131
138
  const [first] = met;
132
139
  if (first !== undefined && isGuess(discovered, configFile)) {
@@ -217,6 +224,7 @@ function readProjects(engine, host, scope, config, stage) {
217
224
  ...kept.map((id) => ({ id, kind: "type-error", source: "" })),
218
225
  ],
219
226
  };
227
+ const own = sourceFilesOf(project, targetRoot);
220
228
  return {
221
229
  configuration,
222
230
  configFile: project.configFile,
@@ -228,11 +236,17 @@ function readProjects(engine, host, scope, config, stage) {
228
236
  .ownSourceFiles()
229
237
  .filter((file) => referenceOnly(file.fileName))
230
238
  .flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
231
- generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
239
+ generated: generatedFilesOf(own, generatedFiles(conventions, own)),
232
240
  };
233
241
  };
242
+ for (const file of project.ownSourceFiles()) {
243
+ heldFiles.add(relativePath(absoluteRoot, file.fileName) ?? file.fileName);
244
+ }
234
245
  return stage(project, { configuration, read });
235
- }, discovered.derived ? "record" : "refuse", workspace?.programs);
246
+ }, discovered.derived ? "record" : "refuse", workspace?.programs, () => {
247
+ const patterns = config.ts.testFiles.map(globExpression);
248
+ outsideTests = testFilesAt(host, targetRoot, sourcePaths(host, targetRoot).filter((path) => !heldFiles.has(path) && patterns.some((pattern) => pattern.test(path))), options.parse);
249
+ });
236
250
  for (const configFile of session.unopened) {
237
251
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
238
252
  throw new DiscoveryError(`${configFile} opened no project`, []);
@@ -263,6 +277,8 @@ function readProjects(engine, host, scope, config, stage) {
263
277
  unanswered: session.unanswered,
264
278
  componentsUnread: workspace?.componentsUnread ?? [],
265
279
  conventionsApplied: conventions.applied,
280
+ conventionUses: conventions.uses,
281
+ outsideTests,
266
282
  };
267
283
  }
268
284
  /**
@@ -351,8 +367,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
351
367
  const resolved = references(project, projectRead.held, targetRoot, {
352
368
  testFiles: config.ts.testFiles,
353
369
  });
354
- const support = testSupportFiles(projectRead.held.symbols, resolved.references, projectRead.rooted.liveUnderReachability, resolved.testFilePaths);
355
- const detected = detectExemptions({
370
+ const evidence = evidenceRecords({
356
371
  project,
357
372
  held: projectRead.held,
358
373
  targetRoot,
@@ -362,52 +377,78 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
362
377
  ...(config.analysis.generatedFiles === "include"
363
378
  ? {}
364
379
  : { generated: projectRead.generated }),
365
- }, detectors, {
380
+ }, detectors, disabled);
381
+ const components = project.ownSourceFiles().filter(isComponentFile);
382
+ const pathOf = (file) => relativePath(targetRoot, file.fileName) ?? file.fileName;
383
+ return {
384
+ projectRead,
385
+ references: resolved.references,
386
+ testFiles: resolved.testFilePaths,
387
+ evidence,
388
+ componentFiles: components.map(pathOf),
389
+ componentWarnings: components.flatMap((file) => readComponent(file.originalText).warnings.map((warning) => ({
390
+ path: pathOf(file),
391
+ line: lineOf(file.originalText, warning.offset),
392
+ reason: warning.reason,
393
+ }))),
394
+ extra: extra(project, projectRead, resolved.references),
395
+ };
396
+ });
397
+ const outsidePaths = read.outsideTests.map((one) => one.path);
398
+ const withOutside = read.answers.map((one) => ({
399
+ ...one,
400
+ references: [
401
+ ...one.references,
402
+ ...outsideTestReferences(read.outsideTests, one.projectRead.held, targetRoot),
403
+ ],
404
+ testFiles: [...one.testFiles, ...outsidePaths],
405
+ }));
406
+ // Test-support code is code only test code imports in any configuration of the run: a
407
+ // configuration that holds no test file says nothing about what the tests import.
408
+ const support = testSupportFiles(withOutside.flatMap((one) => one.projectRead.held.symbols), withOutside.flatMap((one) => one.references), withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability), withOutside.flatMap((one) => one.testFiles));
409
+ const answers = withOutside.map((one) => {
410
+ const { projectRead } = one;
411
+ const detected = heldRecords(one.evidence, {
366
412
  disabled,
367
413
  mode,
368
- testFiles: new Set([...resolved.testFilePaths, ...support]),
414
+ testFiles: new Set([...one.testFiles, ...support]),
369
415
  });
370
416
  const exempt = detected.records;
371
- const made = supportReferences(resolved.references, support);
417
+ const made = supportReferences(one.references, support);
418
+ const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
419
+ const supportFiles = [...support].filter((path) => held.has(path)).sort();
372
420
  const configured = {
373
421
  configuration: projectRead.configuration,
374
422
  symbols: projectRead.held.symbols,
375
423
  references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
376
424
  roots: projectRead.rooted.liveUnderReachability,
377
- testFiles: resolved.testFilePaths,
378
- ...(support.size === 0 ? {} : { supportFiles: [...support].sort() }),
425
+ testFiles: one.testFiles,
426
+ ...(supportFiles.length === 0 ? {} : { supportFiles }),
379
427
  ...(projectRead.referenceOnly.length === 0
380
428
  ? {}
381
429
  : { referenceOnly: projectRead.referenceOnly }),
382
430
  };
383
- const unanswered = [
384
- ...projectRead.held.unanswered,
385
- ...projectRead.rooted.unanswered,
386
- ...made
387
- .filter((reference) => reference.resolution === "by-name")
388
- .map((reference) => reference.to),
389
- ...exempt.filter(isUnansweredRecord).map((record) => record.id),
390
- ];
391
- const components = project.ownSourceFiles().filter(isComponentFile);
392
- const pathOf = (file) => relativePath(targetRoot, file.fileName) ?? file.fileName;
393
431
  return {
394
432
  configFile: projectRead.configFile,
395
433
  configured,
396
434
  exempt,
397
- unanswered,
435
+ unanswered: [
436
+ ...projectRead.held.unanswered,
437
+ ...projectRead.rooted.unanswered,
438
+ ...made
439
+ .filter((reference) => reference.resolution === "by-name")
440
+ .map((reference) => reference.to),
441
+ ...exempt.filter(isUnansweredRecord).map((record) => record.id),
442
+ ],
398
443
  typeErrorSkips: projectRead.typeErrorSkips,
399
444
  skipped: projectRead.skipped,
400
- componentFiles: components.map(pathOf),
401
- componentWarnings: components.flatMap((file) => readComponent(file.originalText).warnings.map((warning) => ({
402
- path: pathOf(file),
403
- line: lineOf(file.originalText, warning.offset),
404
- reason: warning.reason,
405
- }))),
406
- extra: extra(project, projectRead, made),
445
+ componentFiles: one.componentFiles,
446
+ componentWarnings: one.componentWarnings,
447
+ extra: one.extra,
407
448
  };
408
449
  });
409
450
  const consumed = read.consumers.references;
410
- const projects = read.answers.map((one) => consumed.length === 0
451
+ const projects = answers.map((one) => consumed.length === 0
411
452
  ? one
412
453
  : {
413
454
  ...one,
@@ -421,7 +462,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
421
462
  exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
422
463
  unansweredHeld: [
423
464
  ...new Set([
424
- ...read.answers.flatMap((one) => one.unanswered),
465
+ ...answers.flatMap((one) => one.unanswered),
425
466
  ...consumed.filter((one) => one.resolution === "by-name").map((one) => one.to),
426
467
  ]),
427
468
  ],
@@ -431,6 +472,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
431
472
  unanswered: read.unanswered,
432
473
  componentsUnread: read.componentsUnread,
433
474
  conventionsApplied: read.conventionsApplied,
475
+ conventionUses: read.conventionUses,
434
476
  };
435
477
  }
436
478
  /**
@@ -607,7 +649,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
607
649
  references: one.configured.references,
608
650
  accessors: one.extra.accessors,
609
651
  })), swept.exempt, input.mode),
610
- dependencies: dependenciesOf(host, targetRoot, projects.map((one) => one.extra.needs), swept.sweep.candidates.map((candidate) => candidate.id)),
652
+ dependencies: dependenciesOf(host, targetRoot, [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)], swept.sweep.candidates.map((candidate) => candidate.id)),
611
653
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
612
654
  files: {
613
655
  tree: sourceTree(host, targetRoot),
@@ -49,9 +49,12 @@ export function defaultConfig() {
49
49
  ts: {
50
50
  testFiles: [
51
51
  "**/*.test.{ts,tsx,mts,cts}",
52
+ "**/*.test-d.{ts,tsx,mts,cts}",
53
+ "**/*.spec-d.{ts,tsx,mts,cts}",
52
54
  "**/*.spec.*",
53
55
  "**/__tests__/**",
54
56
  "**/__mocks__/**",
57
+ "**/test-d/**",
55
58
  ],
56
59
  entryFiles: [],
57
60
  componentExtensions: [".vue", ".svelte", ".astro"],
@@ -39,7 +39,7 @@ const COUNTERPARTS = [
39
39
  /** The extensions appended to a relative specifier from a configuration file, in order. */
40
40
  const APPENDED = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
41
41
  /** The extensions the program's module resolution appends to a specifier written without one. */
42
- const RESOLVED_EXTENSIONS = [
42
+ export const RESOLVED_EXTENSIONS = [
43
43
  ".ts",
44
44
  ".tsx",
45
45
  ".d.ts",
@@ -164,33 +164,31 @@ function relativeSpecifiers(file) {
164
164
  });
165
165
  }
166
166
  /**
167
- * The module one relative specifier from a configuration file reaches: the path it names,
168
- * that path with its JavaScript extension replaced by the TypeScript counterparts, that
169
- * path with an extension appended, and a directory's `index` with one appended.
167
+ * The paths one relative specifier from a configuration file may reach, in the order they
168
+ * are tried: the path it names where that ends with a module extension, that path with its
169
+ * JavaScript extension replaced by the TypeScript counterparts, that path with an
170
+ * extension appended, and a directory's `index` with one appended. A data file such as
171
+ * `./package.json` is no module, so it is no candidate.
170
172
  */
171
- function reachedBy(host, dir, specifier) {
173
+ export function specifierCandidates(dir, specifier) {
172
174
  const path = resolvePath(dir, specifier);
173
- const isFile = (candidate) => host.kindOf(candidate) === "file";
174
- // A data file such as `./package.json` is no module, so its strings use no dependency.
175
- if (isFile(path) && APPENDED.some((extension) => path.endsWith(extension))) {
176
- return path;
175
+ const candidates = [];
176
+ if (APPENDED.some((extension) => path.endsWith(extension))) {
177
+ candidates.push(path);
177
178
  }
178
179
  for (const [extension, counterparts] of COUNTERPARTS) {
179
180
  if (path.endsWith(extension)) {
180
181
  const stem = path.slice(0, -extension.length);
181
- const found = counterparts.map((one) => stem + one).find(isFile);
182
- if (found !== undefined) {
183
- return found;
184
- }
182
+ candidates.push(...counterparts.map((one) => stem + one));
185
183
  }
186
184
  }
187
- const appended = APPENDED.map((extension) => path + extension).find(isFile);
188
- if (appended !== undefined) {
189
- return appended;
190
- }
191
- return host.kindOf(path) === "directory"
192
- ? APPENDED.map((extension) => joinPath(path, `index${extension}`)).find(isFile)
193
- : undefined;
185
+ candidates.push(...APPENDED.map((extension) => path + extension));
186
+ candidates.push(...APPENDED.map((extension) => joinPath(path, `index${extension}`)));
187
+ return candidates;
188
+ }
189
+ /** The file on disk one relative specifier from a configuration file reaches, or undefined. */
190
+ export function resolveRelativeSpecifier(host, dir, specifier) {
191
+ return specifierCandidates(dir, specifier).find((path) => host.kindOf(path) === "file");
194
192
  }
195
193
  /**
196
194
  * The configuration modules of the directories that hold a manifest which the program
@@ -233,7 +231,7 @@ function outsideModules(host, dirs, files, holdsManifest, parse) {
233
231
  const file = parse(next.path, text);
234
232
  found.push({ file, manifestDir: next.manifestDir });
235
233
  for (const specifier of relativeSpecifiers(file)) {
236
- const reached = reachedBy(host, dirnamePath(next.path), specifier);
234
+ const reached = resolveRelativeSpecifier(host, dirnamePath(next.path), specifier);
237
235
  if (reached !== undefined) {
238
236
  pending.push({ path: reached, manifestDir: next.manifestDir });
239
237
  }
@@ -267,12 +265,13 @@ export function dependenciesNamed(text, declared) {
267
265
  return declared.filter((name) => text === name || text.startsWith(`${name}/`));
268
266
  }
269
267
  /**
270
- * The own files one string names read against a directory: none unless it starts with
271
- * `./` or `../`, else the file it names directly or through the emit mapping, or the
272
- * file it names with an extension the module resolution appends.
268
+ * The own files one string names read against a directory: the file it names directly or
269
+ * through the emit mapping, or the file it names with an extension the module resolution
270
+ * appends. An empty string, one starting with `/` and one holding a glob character names
271
+ * none.
273
272
  */
274
273
  export function filesNamedBy(files, dir, text) {
275
- if (!RELATIVE_PREFIXES.some((prefix) => text.startsWith(prefix)) || text.includes("*")) {
274
+ if (text === "" || text.startsWith("/") || /[*?[{]/u.test(text)) {
276
275
  return [];
277
276
  }
278
277
  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
  };
@@ -32,8 +32,6 @@ function propertyName(property) {
32
32
  }
33
33
  return undefined;
34
34
  }
35
- /** How many element and signature steps the structural walk takes below a value's own type. */
36
- const MAX_DEPTH = 3;
37
35
  /**
38
36
  * The contextual uses of one project's literals and values, asked in batches: one
39
37
  * contextual type per literal and per value whose type carries a member of the target,
@@ -41,6 +39,7 @@ const MAX_DEPTH = 3;
41
39
  */
42
40
  export function contextualUses(project, held, chains, literals, flows, cap, properties) {
43
41
  const queries = project.queries;
42
+ const ownPaths = project.ownPaths();
44
43
  const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
45
44
  let batches = 0;
46
45
  let lookups = 0;
@@ -80,6 +79,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
80
79
  .declarationsOf(property)
81
80
  .filter((id) => MEMBER_KINDS.has(kinds.get(id) ?? "file")),
82
81
  handles: new Set(property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`)),
82
+ own: property.declarations.some((handle) => ownPaths.has(handle.path)),
83
83
  });
84
84
  }
85
85
  tables.set(type.id, table);
@@ -111,6 +111,11 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
111
111
  /** The members a position may read structurally: those of object types, not of classes. */
112
112
  const readable = (member) => member.ids.filter((id) => kinds.get(id) === "type-member");
113
113
  const carries = (part) => [...membersOf(part).values()].some((member) => readable(member).length > 0);
114
+ /** Whether an object type has members and declares none of them in the target. */
115
+ const outside = (part) => {
116
+ const members = [...membersOf(part).values()];
117
+ return members.length > 0 && members.every((member) => !member.own);
118
+ };
114
119
  const elements = new Map();
115
120
  /** The element type of an array type, read once per type. */
116
121
  const elementOf = (type) => {
@@ -154,14 +159,15 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
154
159
  * Each object type a value of `source` is checked as against the object types of
155
160
  * `target`: the value's own, and through array elements and through a function's
156
161
  * parameters, checked the other way, and its result. A source that is one of the
157
- * target's own object types matches it as it stands.
162
+ * target's own object types matches it as it stands. Each pair is compared once, which
163
+ * ends the walk over a recursive type.
158
164
  */
159
- const compared = (source, target, depth, into, seen) => {
165
+ const compared = (source, target, into, seen) => {
160
166
  const key = `${String(source?.id)}>${String(target?.id)}`;
161
167
  if (source === undefined || target === undefined || source.id === target.id) {
162
168
  return;
163
169
  }
164
- if (depth > MAX_DEPTH || seen.has(key)) {
170
+ if (seen.has(key)) {
165
171
  return;
166
172
  }
167
173
  seen.add(key);
@@ -169,13 +175,16 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
169
175
  const sources = partsOf(source).filter((one) => !targets.some((part) => part.id === one.id));
170
176
  for (const one of sources) {
171
177
  if (carries(one)) {
172
- into.push({ source: one, targets });
178
+ into.push({ source: one, targets, outside: false });
179
+ }
180
+ else if (outside(one)) {
181
+ into.push({ source: one, targets, outside: true });
173
182
  }
174
183
  const element = elementOf(one);
175
184
  const called = element === undefined ? callOf(one) : undefined;
176
185
  for (const part of targets) {
177
186
  if (element !== undefined) {
178
- compared(element, elementOf(part), depth + 1, into, seen);
187
+ compared(element, elementOf(part), into, seen);
179
188
  continue;
180
189
  }
181
190
  const other = called === undefined ? undefined : callOf(part);
@@ -183,27 +192,35 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
183
192
  continue;
184
193
  }
185
194
  other.parameters.forEach((parameter, index) => {
186
- compared(parameter, called.parameters[index], depth + 1, into, seen);
195
+ compared(parameter, called.parameters[index], into, seen);
187
196
  });
188
- compared(called.result, other.result, depth + 1, into, seen);
197
+ compared(called.result, other.result, into, seen);
189
198
  }
190
199
  }
191
200
  };
192
- /** Whether a value of one type may carry a member a position reads, at any depth the walk takes. */
193
- const mayCarry = (type, depth) => depth <= MAX_DEPTH &&
194
- partsOf(type).some((part) => {
195
- if (carries(part)) {
201
+ /**
202
+ * Whether a value of one type may carry a member a position reads, at any depth the
203
+ * walk takes. A type met again on the way adds nothing its first visit did not.
204
+ */
205
+ const mayCarry = (type, seen = new Set()) => {
206
+ if (seen.has(type.id)) {
207
+ return false;
208
+ }
209
+ seen.add(type.id);
210
+ return partsOf(type).some((part) => {
211
+ if (carries(part) || outside(part)) {
196
212
  return true;
197
213
  }
198
214
  const element = elementOf(part);
199
215
  if (element !== undefined) {
200
- return mayCarry(element, depth + 1);
216
+ return mayCarry(element, seen);
201
217
  }
202
218
  return callOf(part) !== undefined;
203
219
  });
220
+ };
204
221
  const sourced = carried.flatMap((flow, index) => {
205
222
  const type = types[index];
206
- return type === undefined || !isAnswered(type) || !mayCarry(type, 0) ? [] : [{ flow, type }];
223
+ return type === undefined || !isAnswered(type) || !mayCarry(type) ? [] : [{ flow, type }];
207
224
  });
208
225
  batches += batchesOf(sourced.length);
209
226
  const positions = queries.contextualTypes(sourced.map((one) => one.flow.value), cap);
@@ -214,11 +231,22 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
214
231
  return;
215
232
  }
216
233
  const checked = [];
217
- compared(type, position, 0, checked, new Set());
234
+ compared(type, position, checked, new Set());
218
235
  const ids = new Set();
219
- for (const { source, targets } of checked) {
236
+ for (const { source, targets, outside: held } of checked) {
220
237
  const tables = targets.map(membersOf);
221
- for (const [name, member] of membersOf(source)) {
238
+ const sourceMembers = membersOf(source);
239
+ if (held) {
240
+ for (const table of tables) {
241
+ for (const [name, member] of table) {
242
+ if (sourceMembers.has(name)) {
243
+ member.ids.forEach((id) => ids.add(id));
244
+ }
245
+ }
246
+ }
247
+ continue;
248
+ }
249
+ for (const [name, member] of sourceMembers) {
222
250
  const read = tables.some((table) => {
223
251
  const declared = table.get(name);
224
252
  return (declared !== undefined &&
@@ -385,5 +385,12 @@ export const CONVENTION_ROWS = [
385
385
  `**/{vitest,vite}.*.config.${ANY_MODULE}`,
386
386
  ],
387
387
  moves: [],
388
+ shortNames: [
389
+ {
390
+ property: "test.coverage.provider",
391
+ files: [`{vitest,vite}.config.${ANY_MODULE}`],
392
+ package: "@vitest/coverage-{}",
393
+ },
394
+ ],
388
395
  },
389
396
  ];