@cplieger/deadset-ts 5.0.0 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +145 -39
  3. package/dist/src/configuration-files.js +108 -4
  4. package/dist/src/conformance-record.js +3 -9
  5. package/dist/src/contextual-uses.js +247 -0
  6. package/dist/src/convention-rows.js +6 -0
  7. package/dist/src/conventions.js +123 -14
  8. package/dist/src/dependencies.js +8 -2
  9. package/dist/src/destructuring.js +6 -6
  10. package/dist/src/entry-points.js +8 -5
  11. package/dist/src/exempt-classes.js +6 -1
  12. package/dist/src/exempt.js +35 -16
  13. package/dist/src/file-facts.js +23 -20
  14. package/dist/src/findings/completion.js +27 -3
  15. package/dist/src/findings/interfaces.js +6 -2
  16. package/dist/src/findings/reads-and-writes.js +53 -9
  17. package/dist/src/findings/unused-declarations.js +40 -1
  18. package/dist/src/generated-file.js +24 -0
  19. package/dist/src/graph.js +1 -1
  20. package/dist/src/interface-satisfaction.js +2 -68
  21. package/dist/src/inventory.js +17 -4
  22. package/dist/src/kinds.js +4 -3
  23. package/dist/src/manifest.js +29 -4
  24. package/dist/src/outside-tests.js +139 -0
  25. package/dist/src/overrides.js +104 -0
  26. package/dist/src/paths.js +17 -0
  27. package/dist/src/query.js +15 -0
  28. package/dist/src/references.js +229 -11
  29. package/dist/src/resolve.js +12 -11
  30. package/dist/src/roots.js +121 -37
  31. package/dist/src/session.js +4 -3
  32. package/dist/src/stores.js +3 -0
  33. package/dist/src/template-field.js +60 -3
  34. package/dist/src/value-flow.js +74 -0
  35. package/dist/src/version.js +1 -1
  36. package/jsr.json +1 -1
  37. package/package.json +2 -2
  38. package/src/analysis.ts +194 -49
  39. package/src/configuration-files.ts +137 -2
  40. package/src/conformance-record.ts +3 -10
  41. package/src/contextual-uses.ts +358 -0
  42. package/src/convention-rows.ts +8 -0
  43. package/src/conventions.ts +161 -13
  44. package/src/dependencies.ts +14 -1
  45. package/src/destructuring.ts +16 -6
  46. package/src/entry-points.ts +16 -3
  47. package/src/exempt-classes.ts +6 -1
  48. package/src/exempt.ts +67 -16
  49. package/src/file-facts.ts +26 -20
  50. package/src/findings/completion.ts +31 -3
  51. package/src/findings/interfaces.ts +9 -2
  52. package/src/findings/reads-and-writes.ts +70 -9
  53. package/src/findings/unused-declarations.ts +51 -1
  54. package/src/generated-file.ts +27 -0
  55. package/src/graph.ts +4 -4
  56. package/src/interface-satisfaction.ts +1 -92
  57. package/src/inventory.ts +19 -4
  58. package/src/kinds.ts +5 -3
  59. package/src/manifest.ts +40 -4
  60. package/src/outside-tests.ts +191 -0
  61. package/src/overrides.ts +136 -0
  62. package/src/paths.ts +18 -0
  63. package/src/query.ts +26 -0
  64. package/src/references.ts +294 -22
  65. package/src/resolve.ts +14 -13
  66. package/src/roots.ts +152 -44
  67. package/src/session.ts +4 -2
  68. package/src/stores.ts +9 -0
  69. package/src/template-field.ts +65 -3
  70. package/src/value-flow.ts +104 -0
  71. 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.
@@ -14,11 +14,14 @@ import { decorator } from "./decorator.js";
14
14
  import { 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
- import { conventionFailuresFor, readConventions, } from "./conventions.js";
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 { computeExemptions, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, } from "./exempt.js";
21
- import { heldByInclusion, sourceTree } from "./file-facts.js";
20
+ import { generatedFile } from "./generated-file.js";
21
+ import { sourceFilesOf } from "./source-files.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";
22
25
  import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
23
26
  import { NO_SELF_CHECK, unmatchedRootFindings, } from "./findings/self-check.js";
24
27
  import { frameworkLifecycle } from "./framework-lifecycle.js";
@@ -27,9 +30,9 @@ import { injectionContainer } from "./injection-container.js";
27
30
  import { interfaceSatisfaction } from "./interface-satisfaction.js";
28
31
  import { intraFunctionFacts, projectParts } from "./intra-function-parts.js";
29
32
  import { inventory } from "./inventory.js";
30
- import { readManifest } from "./manifest.js";
33
+ import { packageEntries, readManifest } from "./manifest.js";
31
34
  import { liveFilesAt, matrixOf, referenceKey, sweepMatrix, } from "./matrix.js";
32
- import { joinPath, relativePath, resolvePath } from "./paths.js";
35
+ import { joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
33
36
  import { byPosition, isComponentFile } from "./position.js";
34
37
  import { SetupError, setupLine } from "./setup-failure.js";
35
38
  import { pathsOf, readErrors, skippedRoots, skippedUnits, } from "./type-errors.js";
@@ -37,6 +40,7 @@ import { reflectiveLookup } from "./reflective-lookup.js";
37
40
  import { references, testFileRulesOf } from "./references.js";
38
41
  import { roots, unmatchedEverywhere } from "./roots.js";
39
42
  import { serializationContract } from "./serialization-contract.js";
43
+ import { outsideTestReferences, testFilesAt } from "./outside-tests.js";
40
44
  import { supportReferences, testSupportFiles } from "./test-support.js";
41
45
  import { diagnosticsOf, ownedAs, runSession } from "./session.js";
42
46
  import { accessorsOf, storesOf } from "./stores.js";
@@ -105,11 +109,15 @@ function readProjects(engine, host, scope, config, stage) {
105
109
  const options = {
106
110
  host,
107
111
  manifest: workspace?.manifest ?? readManifest(host, targetRoot),
108
- workspaceEntries: workspace?.entries,
112
+ otherPackages: [
113
+ ...(workspace?.entries ?? []),
114
+ ...packageEntries(host, normalizePath(absoluteRoot), discovered.configFiles, new Set(workspace?.memberDirs ?? [])),
115
+ ],
109
116
  patterns: config.rootPatterns,
110
117
  entryFiles: config.ts.entryFiles,
111
118
  conventions,
112
119
  testFiles: config.ts.testFiles,
120
+ parse: (fileName, text) => engine.parseSourceFile(fileName, text),
113
121
  publishedAPI: config.targetKind === "library",
114
122
  };
115
123
  const notBuilt = [...discovered.notBuilt];
@@ -124,6 +132,8 @@ function readProjects(engine, host, scope, config, stage) {
124
132
  const unbuildable = [];
125
133
  const setupDropped = [];
126
134
  const consumed = new Map();
135
+ const heldFiles = new Set();
136
+ let outsideTests = [];
127
137
  const setupMet = (configFile, met) => {
128
138
  const [first] = met;
129
139
  if (first !== undefined && isGuess(discovered, configFile)) {
@@ -225,10 +235,17 @@ function readProjects(engine, host, scope, config, stage) {
225
235
  .ownSourceFiles()
226
236
  .filter((file) => referenceOnly(file.fileName))
227
237
  .flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
238
+ generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
228
239
  };
229
240
  };
241
+ for (const file of project.ownSourceFiles()) {
242
+ heldFiles.add(relativePath(absoluteRoot, file.fileName) ?? file.fileName);
243
+ }
230
244
  return stage(project, { configuration, read });
231
- }, discovered.derived ? "record" : "refuse", workspace?.programs);
245
+ }, discovered.derived ? "record" : "refuse", workspace?.programs, () => {
246
+ const patterns = config.ts.testFiles.map(globExpression);
247
+ outsideTests = testFilesAt(host, targetRoot, sourcePaths(host, targetRoot).filter((path) => !heldFiles.has(path) && patterns.some((pattern) => pattern.test(path))), options.parse);
248
+ });
232
249
  for (const configFile of session.unopened) {
233
250
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
234
251
  throw new DiscoveryError(`${configFile} opened no project`, []);
@@ -259,6 +276,7 @@ function readProjects(engine, host, scope, config, stage) {
259
276
  unanswered: session.unanswered,
260
277
  componentsUnread: workspace?.componentsUnread ?? [],
261
278
  conventionsApplied: conventions.applied,
279
+ outsideTests,
262
280
  };
263
281
  }
264
282
  /**
@@ -321,6 +339,7 @@ export function runRoots(engine, host, scope, config, provenance) {
321
339
  const DETECTORS = new Map([
322
340
  ["interface-satisfaction", interfaceSatisfaction],
323
341
  ["enum-group", enumGroup],
342
+ ["generated-file", generatedFile],
324
343
  ["template-field", templateField],
325
344
  ["reflective-lookup", reflectiveLookup],
326
345
  ["decorator", decorator],
@@ -346,52 +365,88 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
346
365
  const resolved = references(project, projectRead.held, targetRoot, {
347
366
  testFiles: config.ts.testFiles,
348
367
  });
349
- const support = testSupportFiles(projectRead.held.symbols, resolved.references, projectRead.rooted.liveUnderReachability, resolved.testFilePaths);
350
- const made = supportReferences(resolved.references, support);
368
+ const evidence = evidenceRecords({
369
+ project,
370
+ held: projectRead.held,
371
+ targetRoot,
372
+ templates,
373
+ ts: config.ts,
374
+ consumers,
375
+ ...(config.analysis.generatedFiles === "include"
376
+ ? {}
377
+ : { generated: projectRead.generated }),
378
+ }, detectors, disabled);
379
+ const components = project.ownSourceFiles().filter(isComponentFile);
380
+ const pathOf = (file) => relativePath(targetRoot, file.fileName) ?? file.fileName;
381
+ return {
382
+ projectRead,
383
+ references: resolved.references,
384
+ testFiles: resolved.testFilePaths,
385
+ evidence,
386
+ componentFiles: components.map(pathOf),
387
+ componentWarnings: components.flatMap((file) => readComponent(file.originalText).warnings.map((warning) => ({
388
+ path: pathOf(file),
389
+ line: lineOf(file.originalText, warning.offset),
390
+ reason: warning.reason,
391
+ }))),
392
+ extra: extra(project, projectRead, resolved.references),
393
+ };
394
+ });
395
+ const outsidePaths = read.outsideTests.map((one) => one.path);
396
+ const withOutside = read.answers.map((one) => ({
397
+ ...one,
398
+ references: [
399
+ ...one.references,
400
+ ...outsideTestReferences(read.outsideTests, one.projectRead.held, targetRoot),
401
+ ],
402
+ testFiles: [...one.testFiles, ...outsidePaths],
403
+ }));
404
+ // Test-support code is code only test code imports in any configuration of the run: a
405
+ // configuration that holds no test file says nothing about what the tests import.
406
+ 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));
407
+ const answers = withOutside.map((one) => {
408
+ const { projectRead } = one;
409
+ const detected = heldRecords(one.evidence, {
410
+ disabled,
411
+ mode,
412
+ testFiles: new Set([...one.testFiles, ...support]),
413
+ });
414
+ const exempt = detected.records;
415
+ const made = supportReferences(one.references, support);
416
+ const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
417
+ const supportFiles = [...support].filter((path) => held.has(path)).sort();
351
418
  const configured = {
352
419
  configuration: projectRead.configuration,
353
420
  symbols: projectRead.held.symbols,
354
- references: made,
421
+ references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
355
422
  roots: projectRead.rooted.liveUnderReachability,
356
- testFiles: resolved.testFilePaths,
357
- ...(support.size === 0 ? {} : { supportFiles: [...support].sort() }),
423
+ testFiles: one.testFiles,
424
+ ...(supportFiles.length === 0 ? {} : { supportFiles }),
358
425
  ...(projectRead.referenceOnly.length === 0
359
426
  ? {}
360
427
  : { referenceOnly: projectRead.referenceOnly }),
361
428
  };
362
- const exempt = computeExemptions({ project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers }, detectors, {
363
- disabled,
364
- mode,
365
- testFiles: new Set([...resolved.testFilePaths, ...support]),
366
- });
367
- const unanswered = [
368
- ...projectRead.held.unanswered,
369
- ...projectRead.rooted.unanswered,
370
- ...made
371
- .filter((reference) => reference.resolution === "by-name")
372
- .map((reference) => reference.to),
373
- ...exempt.filter(isUnansweredRecord).map((record) => record.id),
374
- ];
375
- const components = project.ownSourceFiles().filter(isComponentFile);
376
- const pathOf = (file) => relativePath(targetRoot, file.fileName) ?? file.fileName;
377
429
  return {
378
430
  configFile: projectRead.configFile,
379
431
  configured,
380
432
  exempt,
381
- unanswered,
433
+ unanswered: [
434
+ ...projectRead.held.unanswered,
435
+ ...projectRead.rooted.unanswered,
436
+ ...made
437
+ .filter((reference) => reference.resolution === "by-name")
438
+ .map((reference) => reference.to),
439
+ ...exempt.filter(isUnansweredRecord).map((record) => record.id),
440
+ ],
382
441
  typeErrorSkips: projectRead.typeErrorSkips,
383
442
  skipped: projectRead.skipped,
384
- componentFiles: components.map(pathOf),
385
- componentWarnings: components.flatMap((file) => readComponent(file.originalText).warnings.map((warning) => ({
386
- path: pathOf(file),
387
- line: lineOf(file.originalText, warning.offset),
388
- reason: warning.reason,
389
- }))),
390
- extra: extra(project, projectRead, made),
443
+ componentFiles: one.componentFiles,
444
+ componentWarnings: one.componentWarnings,
445
+ extra: one.extra,
391
446
  };
392
447
  });
393
448
  const consumed = read.consumers.references;
394
- const projects = read.answers.map((one) => consumed.length === 0
449
+ const projects = answers.map((one) => consumed.length === 0
395
450
  ? one
396
451
  : {
397
452
  ...one,
@@ -405,7 +460,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
405
460
  exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
406
461
  unansweredHeld: [
407
462
  ...new Set([
408
- ...read.answers.flatMap((one) => one.unanswered),
463
+ ...answers.flatMap((one) => one.unanswered),
409
464
  ...consumed.filter((one) => one.resolution === "by-name").map((one) => one.to),
410
465
  ]),
411
466
  ],
@@ -440,6 +495,55 @@ function holdingSweep(read, input) {
440
495
  live = next;
441
496
  }
442
497
  }
498
+ /**
499
+ * The test references exemption evidence written in a test file makes: each is a read of
500
+ * the declaration it would retain, made by the test file at the evidence's site.
501
+ */
502
+ function evidenceReferences(symbols, inTestFiles) {
503
+ if (inTestFiles.length === 0) {
504
+ return [];
505
+ }
506
+ const files = new Map(symbols
507
+ .filter((symbol) => symbol.kind === "file")
508
+ .map((symbol) => [symbol.position.path, symbol.id]));
509
+ return inTestFiles.flatMap((record) => {
510
+ const from = files.get(record.site.path);
511
+ return from === undefined
512
+ ? []
513
+ : [
514
+ {
515
+ from,
516
+ to: record.id,
517
+ position: record.site,
518
+ use: "read",
519
+ resolution: "syntax",
520
+ test: true,
521
+ },
522
+ ];
523
+ });
524
+ }
525
+ /**
526
+ * The declarations every reference the mode counts stores into, a loaded consumer's
527
+ * reference counting as a read: what the write-only kind would report but for a record.
528
+ */
529
+ function writtenOnly(read, mode) {
530
+ const written = new Set();
531
+ const readSome = new Set();
532
+ for (const { configured } of read.projects) {
533
+ for (const reference of configured.references) {
534
+ if (mode.production && reference.test) {
535
+ continue;
536
+ }
537
+ if (reference.use === "write" && reference.consumer === undefined) {
538
+ written.add(reference.to);
539
+ }
540
+ else {
541
+ readSome.add(reference.to);
542
+ }
543
+ }
544
+ }
545
+ return new Set([...written].filter((id) => !readSome.has(id)));
546
+ }
443
547
  /**
444
548
  * The sweep of a run read once, under one set of marks. Where a question went unanswered,
445
549
  * the run is swept once more without what that question could have kept live and without
@@ -483,7 +587,7 @@ function sweptOf(read, input) {
483
587
  matrix: read.matrix,
484
588
  sweep: result,
485
589
  exempt,
486
- retained: retainedIn(read.matrix, swept),
590
+ retained: retainedIn(read.matrix, swept, writtenOnly(read, input.mode)),
487
591
  notBuilt: read.notBuilt,
488
592
  unanswered: read.unanswered,
489
593
  heldByUnanswered: [...held],
@@ -510,9 +614,10 @@ function readEmitterRun(engine, host, scope, config, mode, detectors) {
510
614
  const run = readRun(engine, host, scope, config, mode, detectors, (project, read, references) => ({
511
615
  deprecation: deprecatedDeclarations(project, read.held),
512
616
  accessors: accessorsOf(project, read.held),
513
- needs: projectNeeds(project, read.held, host, targetRoot),
617
+ needs: projectNeeds(project, read.held, host, targetRoot, (fileName, text) => engine.parseSourceFile(fileName, text)),
514
618
  implementations: implementations(project, read.held),
515
619
  heldByInclusion: heldByInclusion(project, targetRoot),
620
+ generated: [...read.generated.keys()],
516
621
  rooted: read.rooted,
517
622
  directives: inlineDirectives(project, read.held, targetRoot).filter((directive) => !read.referenceOnly.includes(directive.site.path)),
518
623
  declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
@@ -546,6 +651,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
546
651
  files: {
547
652
  tree: sourceTree(host, targetRoot),
548
653
  heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
654
+ generated: new Set(projects.flatMap((one) => one.extra.generated)),
549
655
  },
550
656
  boundary: {
551
657
  consumers: {
@@ -6,7 +6,7 @@
6
6
  * every string either holds as it likes, so each one that spells a declared dependency
7
7
  * uses it and each relative one that names a source file of the program roots the file.
8
8
  */
9
- import { isNoSubstitutionTemplateLiteral, isStringLiteral, } from "@typescript/native/unstable/ast";
9
+ import { isExportDeclaration, isImportDeclaration, isNoSubstitutionTemplateLiteral, isStringLiteral, } from "@typescript/native/unstable/ast";
10
10
  import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
11
11
  const MANIFEST = "package.json";
12
12
  /** A configuration module's name, each form with the spelling a root reports it by. */
@@ -29,8 +29,17 @@ const DEPENDENCY_MEMBERS = [
29
29
  ];
30
30
  /** The prefixes a string names a file with, relative to the file that writes it. */
31
31
  const RELATIVE_PREFIXES = ["./", "../"];
32
+ /** The JavaScript extensions a relative specifier from a configuration file stands for TypeScript under. */
33
+ const COUNTERPARTS = [
34
+ [".js", [".ts", ".tsx"]],
35
+ [".jsx", [".tsx", ".ts"]],
36
+ [".mjs", [".mts"]],
37
+ [".cjs", [".cts"]],
38
+ ];
39
+ /** The extensions appended to a relative specifier from a configuration file, in order. */
40
+ const APPENDED = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
32
41
  /** The extensions the program's module resolution appends to a specifier written without one. */
33
- const RESOLVED_EXTENSIONS = [
42
+ export const RESOLVED_EXTENSIONS = [
34
43
  ".ts",
35
44
  ".tsx",
36
45
  ".d.ts",
@@ -85,7 +94,7 @@ export function moduleStrings(file) {
85
94
  * in a directory that holds a manifest. A document that cannot be read or parsed as
86
95
  * JSON holds no string.
87
96
  */
88
- export function configurationFiles(host, targetRoot, files) {
97
+ export function configurationFiles(host, targetRoot, files, parse) {
89
98
  const root = resolvePath(host.workingDirectory(), targetRoot);
90
99
  const manifests = new Map();
91
100
  const holdsManifest = (dir) => {
@@ -135,7 +144,102 @@ export function configurationFiles(host, targetRoot, files) {
135
144
  documents.push({ path, strings: documentStrings(value) });
136
145
  }
137
146
  }
138
- return { modules, documents };
147
+ return {
148
+ modules,
149
+ documents,
150
+ outside: parse === undefined ? [] : outsideModules(host, [...dirs], files, holdsManifest, parse),
151
+ };
152
+ }
153
+ /** The relative specifiers one file's imports and re-exports name. */
154
+ function relativeSpecifiers(file) {
155
+ return file.statements.flatMap((statement) => {
156
+ const specifier = isImportDeclaration(statement) || isExportDeclaration(statement)
157
+ ? statement.moduleSpecifier
158
+ : undefined;
159
+ return specifier !== undefined &&
160
+ isStringLiteral(specifier) &&
161
+ RELATIVE_PREFIXES.some((prefix) => specifier.text.startsWith(prefix))
162
+ ? [specifier.text]
163
+ : [];
164
+ });
165
+ }
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.
170
+ */
171
+ function reachedBy(host, dir, specifier) {
172
+ 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;
177
+ }
178
+ for (const [extension, counterparts] of COUNTERPARTS) {
179
+ if (path.endsWith(extension)) {
180
+ 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
+ }
185
+ }
186
+ }
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;
194
+ }
195
+ /**
196
+ * The configuration modules of the directories that hold a manifest which the program
197
+ * does not hold, and the files they import by a relative specifier, until no further
198
+ * file joins. A file that cannot be read is skipped.
199
+ */
200
+ function outsideModules(host, dirs, files, holdsManifest, parse) {
201
+ const pending = [];
202
+ for (const dir of [...dirs].sort()) {
203
+ if (!holdsManifest(dir)) {
204
+ continue;
205
+ }
206
+ let names;
207
+ try {
208
+ names = host
209
+ .readDirectory(dir)
210
+ .filter((entry) => !entry.directory && configurationModuleForm(entry.name) !== undefined)
211
+ .map((entry) => entry.name)
212
+ .sort();
213
+ }
214
+ catch {
215
+ continue;
216
+ }
217
+ pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
218
+ }
219
+ const found = [];
220
+ const seen = new Set();
221
+ for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
222
+ if (seen.has(next.path) || files.byName.has(next.path)) {
223
+ continue;
224
+ }
225
+ seen.add(next.path);
226
+ let text;
227
+ try {
228
+ text = host.readFile(next.path);
229
+ }
230
+ catch {
231
+ continue;
232
+ }
233
+ const file = parse(next.path, text);
234
+ found.push({ file, manifestDir: next.manifestDir });
235
+ for (const specifier of relativeSpecifiers(file)) {
236
+ const reached = reachedBy(host, dirnamePath(next.path), specifier);
237
+ if (reached !== undefined) {
238
+ pending.push({ path: reached, manifestDir: next.manifestDir });
239
+ }
240
+ }
241
+ }
242
+ return found;
139
243
  }
140
244
  /** Every dependency the manifest in one directory declares, or none where it holds none to read. */
141
245
  export function declaredIn(host, dir) {
@@ -2,15 +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.1.1",
5
+ corpusVersion: "2.2.0",
6
6
  result: "pass",
7
- digest: "sha256:e5e456bf3a706f1fa9f906ae71e10ea98a78da52d7d99a5ed59dca8a72bbe927",
7
+ digest: "sha256:ce7c9087ed7b2baf3de9fa2df436b500706f4f9e94fd9e0fed633054305f06a4",
8
8
  },
9
- gaps: [
10
- {
11
- fixture: "private-member-unread",
12
- capability: "reflective-lookup",
13
- reason: "The reference pass resolves a string-literal element access to the member it names, so the member is live by that reference and the retained listing names no class for it.",
14
- },
15
- ],
9
+ gaps: [],
16
10
  };