@cplieger/deadset-ts 5.1.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.
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.
@@ -19,8 +19,9 @@ import { readEdgeSides } from "./edges.js";
19
19
  import { enumGroup } from "./enum-group.js";
20
20
  import { generatedFile } 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)) {
@@ -231,8 +238,14 @@ function readProjects(engine, host, scope, config, stage) {
231
238
  generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
232
239
  };
233
240
  };
241
+ for (const file of project.ownSourceFiles()) {
242
+ heldFiles.add(relativePath(absoluteRoot, file.fileName) ?? file.fileName);
243
+ }
234
244
  return stage(project, { configuration, read });
235
- }, 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
+ });
236
249
  for (const configFile of session.unopened) {
237
250
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
238
251
  throw new DiscoveryError(`${configFile} opened no project`, []);
@@ -263,6 +276,7 @@ function readProjects(engine, host, scope, config, stage) {
263
276
  unanswered: session.unanswered,
264
277
  componentsUnread: workspace?.componentsUnread ?? [],
265
278
  conventionsApplied: conventions.applied,
279
+ outsideTests,
266
280
  };
267
281
  }
268
282
  /**
@@ -351,8 +365,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
351
365
  const resolved = references(project, projectRead.held, targetRoot, {
352
366
  testFiles: config.ts.testFiles,
353
367
  });
354
- const support = testSupportFiles(projectRead.held.symbols, resolved.references, projectRead.rooted.liveUnderReachability, resolved.testFilePaths);
355
- const detected = detectExemptions({
368
+ const evidence = evidenceRecords({
356
369
  project,
357
370
  held: projectRead.held,
358
371
  targetRoot,
@@ -362,52 +375,78 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
362
375
  ...(config.analysis.generatedFiles === "include"
363
376
  ? {}
364
377
  : { generated: projectRead.generated }),
365
- }, detectors, {
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, {
366
410
  disabled,
367
411
  mode,
368
- testFiles: new Set([...resolved.testFilePaths, ...support]),
412
+ testFiles: new Set([...one.testFiles, ...support]),
369
413
  });
370
414
  const exempt = detected.records;
371
- const made = supportReferences(resolved.references, support);
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();
372
418
  const configured = {
373
419
  configuration: projectRead.configuration,
374
420
  symbols: projectRead.held.symbols,
375
421
  references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
376
422
  roots: projectRead.rooted.liveUnderReachability,
377
- testFiles: resolved.testFilePaths,
378
- ...(support.size === 0 ? {} : { supportFiles: [...support].sort() }),
423
+ testFiles: one.testFiles,
424
+ ...(supportFiles.length === 0 ? {} : { supportFiles }),
379
425
  ...(projectRead.referenceOnly.length === 0
380
426
  ? {}
381
427
  : { referenceOnly: projectRead.referenceOnly }),
382
428
  };
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
429
  return {
394
430
  configFile: projectRead.configFile,
395
431
  configured,
396
432
  exempt,
397
- 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
+ ],
398
441
  typeErrorSkips: projectRead.typeErrorSkips,
399
442
  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),
443
+ componentFiles: one.componentFiles,
444
+ componentWarnings: one.componentWarnings,
445
+ extra: one.extra,
407
446
  };
408
447
  });
409
448
  const consumed = read.consumers.references;
410
- const projects = read.answers.map((one) => consumed.length === 0
449
+ const projects = answers.map((one) => consumed.length === 0
411
450
  ? one
412
451
  : {
413
452
  ...one,
@@ -421,7 +460,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
421
460
  exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
422
461
  unansweredHeld: [
423
462
  ...new Set([
424
- ...read.answers.flatMap((one) => one.unanswered),
463
+ ...answers.flatMap((one) => one.unanswered),
425
464
  ...consumed.filter((one) => one.resolution === "by-name").map((one) => one.to),
426
465
  ]),
427
466
  ],
@@ -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",
@@ -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,
@@ -154,14 +152,15 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
154
152
  * Each object type a value of `source` is checked as against the object types of
155
153
  * `target`: the value's own, and through array elements and through a function's
156
154
  * 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.
155
+ * target's own object types matches it as it stands. Each pair is compared once, which
156
+ * ends the walk over a recursive type.
158
157
  */
159
- const compared = (source, target, depth, into, seen) => {
158
+ const compared = (source, target, into, seen) => {
160
159
  const key = `${String(source?.id)}>${String(target?.id)}`;
161
160
  if (source === undefined || target === undefined || source.id === target.id) {
162
161
  return;
163
162
  }
164
- if (depth > MAX_DEPTH || seen.has(key)) {
163
+ if (seen.has(key)) {
165
164
  return;
166
165
  }
167
166
  seen.add(key);
@@ -175,7 +174,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
175
174
  const called = element === undefined ? callOf(one) : undefined;
176
175
  for (const part of targets) {
177
176
  if (element !== undefined) {
178
- compared(element, elementOf(part), depth + 1, into, seen);
177
+ compared(element, elementOf(part), into, seen);
179
178
  continue;
180
179
  }
181
180
  const other = called === undefined ? undefined : callOf(part);
@@ -183,27 +182,35 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
183
182
  continue;
184
183
  }
185
184
  other.parameters.forEach((parameter, index) => {
186
- compared(parameter, called.parameters[index], depth + 1, into, seen);
185
+ compared(parameter, called.parameters[index], into, seen);
187
186
  });
188
- compared(called.result, other.result, depth + 1, into, seen);
187
+ compared(called.result, other.result, into, seen);
189
188
  }
190
189
  }
191
190
  };
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) => {
191
+ /**
192
+ * Whether a value of one type may carry a member a position reads, at any depth the
193
+ * walk takes. A type met again on the way adds nothing its first visit did not.
194
+ */
195
+ const mayCarry = (type, seen = new Set()) => {
196
+ if (seen.has(type.id)) {
197
+ return false;
198
+ }
199
+ seen.add(type.id);
200
+ return partsOf(type).some((part) => {
195
201
  if (carries(part)) {
196
202
  return true;
197
203
  }
198
204
  const element = elementOf(part);
199
205
  if (element !== undefined) {
200
- return mayCarry(element, depth + 1);
206
+ return mayCarry(element, seen);
201
207
  }
202
208
  return callOf(part) !== undefined;
203
209
  });
210
+ };
204
211
  const sourced = carried.flatMap((flow, index) => {
205
212
  const type = types[index];
206
- return type === undefined || !isAnswered(type) || !mayCarry(type, 0) ? [] : [{ flow, type }];
213
+ return type === undefined || !isAnswered(type) || !mayCarry(type) ? [] : [{ flow, type }];
207
214
  });
208
215
  batches += batchesOf(sourced.length);
209
216
  const positions = queries.contextualTypes(sourced.map((one) => one.flow.value), cap);
@@ -214,7 +221,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
214
221
  return;
215
222
  }
216
223
  const checked = [];
217
- compared(type, position, 0, checked, new Set());
224
+ compared(type, position, checked, new Set());
218
225
  const ids = new Set();
219
226
  for (const { source, targets } of checked) {
220
227
  const tables = targets.map(membersOf);
@@ -92,12 +92,18 @@ export function computeExemptions(input, detectors, holding) {
92
92
  }
93
93
  /** The records of {@link computeExemptions}, beside the test-file evidence it set aside. */
94
94
  export function detectExemptions(input, detectors, holding) {
95
+ return heldRecords(evidenceRecords(input, detectors, holding.disabled), holding);
96
+ }
97
+ /**
98
+ * Every record one project's detectors found, one per piece of evidence, before the
99
+ * mode decides which of them hold: what {@link heldRecords} reads.
100
+ */
101
+ export function evidenceRecords(input, detectors, disabled) {
95
102
  const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
96
103
  const found = [];
97
- const inTestFiles = [];
98
104
  for (const exemptionClass of TS_EXEMPTION_CLASSES) {
99
105
  const detect = detectors.get(exemptionClass);
100
- if (detect === undefined || holding.disabled.has(exemptionClass)) {
106
+ if (detect === undefined || disabled.has(exemptionClass)) {
101
107
  continue;
102
108
  }
103
109
  const allowed = typescriptVisibilityOf(exemptionClass);
@@ -105,17 +111,26 @@ export function detectExemptions(input, detectors, holding) {
105
111
  if (!retainable(allowed, visibility.get(evidence.id))) {
106
112
  continue;
107
113
  }
108
- const record = {
114
+ found.push({
109
115
  id: evidence.id,
110
116
  class: exemptionClass,
111
117
  detail: evidence.detail,
112
118
  site: evidence.site,
113
119
  ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
114
- };
115
- if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
116
- inTestFiles.push(record);
117
- continue;
118
- }
120
+ });
121
+ }
122
+ }
123
+ return found;
124
+ }
125
+ /** The records that hold under one mode, beside the evidence a production mode found in a test file. */
126
+ export function heldRecords(records, holding) {
127
+ const found = [];
128
+ const inTestFiles = [];
129
+ for (const record of records) {
130
+ if (holding.mode.production && holding.testFiles.has(record.site.path)) {
131
+ inTestFiles.push(record);
132
+ }
133
+ else {
119
134
  found.push(record);
120
135
  }
121
136
  }
@@ -26,12 +26,10 @@ function lastLine(text) {
26
26
  return line;
27
27
  }
28
28
  /**
29
- * Every source file below the target root, in path order. A directory that cannot be
30
- * read is skipped, as discovery skips it, and a file that cannot be read is skipped
31
- * with it: a tree the analysis may not enter holds no file it can report.
29
+ * The path below the target root of every source file there, in path order. A directory
30
+ * that cannot be read is skipped, as discovery skips it.
32
31
  */
33
- export function sourceTree(host, targetRoot) {
34
- const scopeOf = packageScope(host, targetRoot);
32
+ export function sourcePaths(host, targetRoot) {
35
33
  const found = [];
36
34
  const pending = [""];
37
35
  for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
@@ -50,24 +48,29 @@ export function sourceTree(host, targetRoot) {
50
48
  }
51
49
  continue;
52
50
  }
53
- if (!SOURCE_FILE.test(entry.name) || DECLARATION_FILE.test(entry.name)) {
54
- continue;
55
- }
56
- let text;
57
- try {
58
- text = host.readFile(joinPath(targetRoot, path));
51
+ if (SOURCE_FILE.test(entry.name) && !DECLARATION_FILE.test(entry.name)) {
52
+ found.push(path);
59
53
  }
60
- catch {
61
- continue;
62
- }
63
- found.push({
64
- path,
65
- ref: renderRef(scopeOf(path), { of: "module" }),
66
- endLine: lastLine(text),
67
- });
68
54
  }
69
55
  }
70
- return found.sort((a, b) => (a.path < b.path ? -1 : 1));
56
+ return found.sort((a, b) => (a < b ? -1 : 1));
57
+ }
58
+ /**
59
+ * Every source file below the target root, in path order. A file that cannot be read is
60
+ * skipped: a tree the analysis may not enter holds no file it can report.
61
+ */
62
+ export function sourceTree(host, targetRoot) {
63
+ const scopeOf = packageScope(host, targetRoot);
64
+ return sourcePaths(host, targetRoot).flatMap((path) => {
65
+ let text;
66
+ try {
67
+ text = host.readFile(joinPath(targetRoot, path));
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ return [{ path, ref: renderRef(scopeOf(path), { of: "module" }), endLine: lastLine(text) }];
73
+ });
71
74
  }
72
75
  /** Whether one file changes what the program sees by being included in it. */
73
76
  function actsByInclusion(file) {
package/dist/src/graph.js CHANGED
@@ -94,7 +94,7 @@ export function graphOf(symbols, references, roots, testFiles, supportFiles = []
94
94
  }
95
95
  }
96
96
  const edge = { to, test: reference.test, evaluation: reference.use === "evaluation" };
97
- if (reference.consumer !== undefined) {
97
+ if (reference.consumer !== undefined || reference.unheld === true) {
98
98
  consumed.push(edge);
99
99
  continue;
100
100
  }
@@ -213,7 +213,18 @@ function nameNodeOf(node) {
213
213
  * needs the declaration this enumeration made for it.
214
214
  */
215
215
  export function nodeKey(file, node) {
216
- return `${file.fileName}:${String(node.pos)}:${String(node.end)}`;
216
+ return `${file.fileName}:${String(node.pos)}:${String(node.end)}:${String(node.kind)}`;
217
+ }
218
+ /** `export default`, up to the keyword that stands where an unnamed default export's name would. */
219
+ const DEFAULT_EXPORT = /^export\s+(?=default\b)/u;
220
+ /**
221
+ * Where an unnamed declaration is placed: at `default` when it is the default export, so it
222
+ * never shares the file's own position, else at its first token.
223
+ */
224
+ function unnamedStart(node) {
225
+ const start = node.getStart();
226
+ const prefix = DEFAULT_EXPORT.exec(node.getText());
227
+ return prefix === null ? start : start + prefix[0].length;
217
228
  }
218
229
  /** The kind a class element takes: a method, or a property or accessor. */
219
230
  function classMemberKind(node) {
@@ -287,12 +298,11 @@ export function inventory(project, host, targetRoot) {
287
298
  }
288
299
  }
289
300
  function declare(node, owner, kind, name, named) {
290
- const at = name ?? node;
291
301
  const record = keep({
292
302
  file: owner.file,
293
303
  node,
294
304
  module: owner.module,
295
- position: renderPosition(owner.file, targetRoot, at.getStart()),
305
+ position: renderPosition(owner.file, targetRoot, name?.getStart() ?? unnamedStart(node)),
296
306
  endLine: renderPosition(owner.file, targetRoot, Math.max(node.end - 1, 0)).line,
297
307
  kind,
298
308
  parentChain: owner.kind === "file" ? [] : chainOf(owner),
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * The entry points a package manifest names, which are roots because something
3
3
  * outside the analyzed program reaches them: a consumer importing the package, or a
4
- * shell running its command or one of its scripts.
5
- *
6
- * One call reads one manifest, the one in the directory it is given.
4
+ * shell running its command or one of its scripts. {@link readManifest} reads one
5
+ * directory's manifest, {@link packageEntries} those of the packages below the target root.
7
6
  */
8
- import { isAbsolutePath, joinPath, resolvePath } from "./paths.js";
7
+ import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
9
8
  /** The manifest's own name. */
10
9
  const MANIFEST = "package.json";
11
10
  /** The three members that name an entry point beside `exports` and `bin`. */
@@ -144,3 +143,29 @@ export function readManifest(host, targetRoot) {
144
143
  declaresExports: manifest["exports"] !== undefined,
145
144
  };
146
145
  }
146
+ /**
147
+ * The entry points every package manifest between the target root and each compiler
148
+ * configuration names: each directory holding a `package.json` that a configuration sits
149
+ * in or below, the target root and `known` excluded, whose manifests the run reads
150
+ * otherwise. Each entry is spelled under its manifest's path below the target root, and
151
+ * is published by its own manifest's rule, as the target's own entries are.
152
+ */
153
+ export function packageEntries(host, targetRoot, configFiles, known) {
154
+ const dirs = new Set();
155
+ for (const configFile of configFiles) {
156
+ for (let dir = dirnamePath(resolvePath(targetRoot, configFile)); dir !== targetRoot && relativePath(targetRoot, dir) !== undefined; dir = dirnamePath(dir)) {
157
+ if (!known.has(dir) && host.kindOf(joinPath(dir, MANIFEST)) === "file") {
158
+ dirs.add(dir);
159
+ }
160
+ }
161
+ }
162
+ return [...dirs].sort().flatMap((dir) => {
163
+ const manifest = readManifest(host, dir);
164
+ const where = `${relativePath(targetRoot, dir) ?? dir}/${MANIFEST}`;
165
+ return manifest.entries.map((entry) => ({
166
+ ...entry,
167
+ member: `${where} ${entry.member}`,
168
+ published: entry.published && (!manifest.declaresExports || entry.member.startsWith("exports")),
169
+ }));
170
+ });
171
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The test files no compiler configuration of a run holds. The runner executes them all
3
+ * the same, so each module one names is a test reference: the own file a relative
4
+ * specifier names, and the declarations that file exports under the names read from it.
5
+ * Such a file is read without a program, so what its body references beyond the modules
6
+ * it names is not known.
7
+ */
8
+ import { isCallExpression, isExportDeclaration, isImportDeclaration, isNamespaceExport, isNamespaceImport, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
9
+ import { RESOLVED_EXTENSIONS } from "./configuration-files.js";
10
+ import { emittedFrom } from "./emit-map.js";
11
+ import { dirnamePath, joinPath, normalizePath, queriedModulePath } from "./paths.js";
12
+ import { renderPosition } from "./position.js";
13
+ import { nameComponent } from "./ref.js";
14
+ /** The paths below the target root one relative specifier may name, written path first. */
15
+ function candidates(dir, specifier) {
16
+ if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
17
+ return [];
18
+ }
19
+ const path = normalizePath(dir === "" ? specifier : `${dir}/${specifier}`);
20
+ return [
21
+ path,
22
+ ...emittedFrom(path, { sourceDirs: [] }),
23
+ ...RESOLVED_EXTENSIONS.map((extension) => path + extension),
24
+ ...RESOLVED_EXTENSIONS.map((extension) => `${path}/index${extension}`),
25
+ ];
26
+ }
27
+ /** The text of a specifier written as literal text, or undefined for any other expression. */
28
+ function literalText(node) {
29
+ return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node))
30
+ ? node.text
31
+ : undefined;
32
+ }
33
+ /**
34
+ * Every module one file names: an import declaration, a re-export, and an `import()` call
35
+ * of literal text anywhere in the file. A namespace form, a star re-export, a bare import
36
+ * and an `import()` evaluate the module.
37
+ */
38
+ function modulesNamed(file) {
39
+ const found = [];
40
+ const name = (specifier, names, evaluates) => {
41
+ const text = literalText(specifier);
42
+ if (text !== undefined) {
43
+ found.push({ specifier, text, names, evaluates });
44
+ }
45
+ };
46
+ for (const statement of file.statements) {
47
+ if (isImportDeclaration(statement)) {
48
+ const clause = statement.importClause;
49
+ const bindings = clause?.namedBindings;
50
+ name(statement.moduleSpecifier, [
51
+ ...(clause?.name === undefined ? [] : ["default"]),
52
+ ...(bindings === undefined || isNamespaceImport(bindings)
53
+ ? []
54
+ : bindings.elements.map((one) => (one.propertyName ?? one.name).text)),
55
+ ], bindings === undefined ? clause?.name === undefined : isNamespaceImport(bindings));
56
+ }
57
+ else if (isExportDeclaration(statement) && statement.moduleSpecifier !== undefined) {
58
+ const clause = statement.exportClause;
59
+ const star = clause === undefined || isNamespaceExport(clause);
60
+ name(statement.moduleSpecifier, star ? [] : clause.elements.map((one) => (one.propertyName ?? one.name).text), star);
61
+ }
62
+ }
63
+ const visit = (node) => {
64
+ if (isCallExpression(node) && node.expression.kind === SyntaxKind.ImportKeyword) {
65
+ const [argument] = node.arguments;
66
+ if (argument !== undefined) {
67
+ name(argument, [], true);
68
+ }
69
+ }
70
+ node.forEachChild(visit);
71
+ };
72
+ file.forEachChild(visit);
73
+ return found;
74
+ }
75
+ /** Each of the given paths below the target root, parsed; one that cannot be read is skipped. */
76
+ export function testFilesAt(host, targetRoot, paths, parse) {
77
+ return paths.flatMap((path) => {
78
+ const fileName = joinPath(targetRoot, path);
79
+ let text;
80
+ try {
81
+ text = host.readFile(fileName);
82
+ }
83
+ catch {
84
+ return [];
85
+ }
86
+ return [{ path, file: parse(fileName, text) }];
87
+ });
88
+ }
89
+ /**
90
+ * The references the given test files make to one project's own files through the
91
+ * modules they name ({@link modulesNamed}). A name read from a module references each
92
+ * top-level declaration the file exports under it; an evaluating form references the
93
+ * file. A query after the path names the file as {@link queriedModulePath} reads it.
94
+ */
95
+ export function outsideTestReferences(tests, held, targetRoot) {
96
+ const fileAt = new Map(held.symbols
97
+ .filter((symbol) => symbol.kind === "file")
98
+ .map((symbol) => [symbol.position.path, symbol.id]));
99
+ // Keyed on the reference's fragment, which spells the export name where the display
100
+ // name of a default export is its declared name.
101
+ const exported = new Map();
102
+ for (const symbol of held.symbols) {
103
+ if (symbol.exported) {
104
+ const fragment = symbol.ref.slice(symbol.ref.lastIndexOf("#") + 1).replace(/:alias$/u, "");
105
+ const key = `${symbol.parent}\u0000${fragment}`;
106
+ exported.set(key, [...(exported.get(key) ?? []), symbol.id]);
107
+ }
108
+ }
109
+ const found = [];
110
+ for (const { path, file } of tests) {
111
+ const from = `${path}#`;
112
+ const dir = dirnamePath(path) === "." ? "" : dirnamePath(path);
113
+ for (const named of modulesNamed(file)) {
114
+ const written = named.text.includes("?") ? queriedModulePath(named.text) : named.text;
115
+ if (written === undefined) {
116
+ continue;
117
+ }
118
+ const fileId = candidates(dir, written)
119
+ .map((one) => fileAt.get(one))
120
+ .find((one) => one !== undefined);
121
+ if (fileId === undefined) {
122
+ continue;
123
+ }
124
+ const position = renderPosition(file, targetRoot, named.specifier.getStart());
125
+ const read = (to, use) => {
126
+ found.push({ from, to, position, use, resolution: "syntax", test: true, unheld: true });
127
+ };
128
+ if (named.evaluates) {
129
+ read(fileId, "evaluation");
130
+ }
131
+ for (const name of named.names) {
132
+ for (const id of exported.get(`${fileId}\u0000${nameComponent(name)}`) ?? []) {
133
+ read(id, "read");
134
+ }
135
+ }
136
+ }
137
+ }
138
+ return found;
139
+ }