@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/src/analysis.ts CHANGED
@@ -46,7 +46,8 @@ import { enumGroup } from "./enum-group.ts";
46
46
  import { generatedFile } 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";
@@ -219,6 +222,8 @@ 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 test files below the target root no project the run reads holds, parsed. */
226
+ readonly outsideTests: readonly OutsideTest[];
222
227
  }
223
228
 
224
229
  /**
@@ -284,7 +289,15 @@ function readProjects<Answer>(
284
289
  const options = {
285
290
  host,
286
291
  manifest: workspace?.manifest ?? readManifest(host, targetRoot),
287
- workspaceEntries: workspace?.entries,
292
+ otherPackages: [
293
+ ...(workspace?.entries ?? []),
294
+ ...packageEntries(
295
+ host,
296
+ normalizePath(absoluteRoot),
297
+ discovered.configFiles,
298
+ new Set(workspace?.memberDirs ?? []),
299
+ ),
300
+ ],
288
301
  patterns: config.rootPatterns,
289
302
  entryFiles: config.ts.entryFiles,
290
303
  conventions,
@@ -304,6 +317,8 @@ function readProjects<Answer>(
304
317
  const unbuildable: SetupFailure[] = [];
305
318
  const setupDropped: SetupFailure[] = [];
306
319
  const consumed = new Map<string, Reference>();
320
+ const heldFiles = new Set<string>();
321
+ let outsideTests: readonly OutsideTest[] = [];
307
322
  const setupMet = (configFile: string, met: readonly SetupFailure[]): void => {
308
323
  const [first] = met;
309
324
  if (first !== undefined && isGuess(discovered, configFile)) {
@@ -434,10 +449,24 @@ function readProjects<Answer>(
434
449
  generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
435
450
  };
436
451
  };
452
+ for (const file of project.ownSourceFiles()) {
453
+ heldFiles.add(relativePath(absoluteRoot, file.fileName) ?? file.fileName);
454
+ }
437
455
  return stage(project, { configuration, read });
438
456
  },
439
457
  discovered.derived ? "record" : "refuse",
440
458
  workspace?.programs,
459
+ () => {
460
+ const patterns = config.ts.testFiles.map(globExpression);
461
+ outsideTests = testFilesAt(
462
+ host,
463
+ targetRoot,
464
+ sourcePaths(host, targetRoot).filter(
465
+ (path) => !heldFiles.has(path) && patterns.some((pattern) => pattern.test(path)),
466
+ ),
467
+ options.parse,
468
+ );
469
+ },
441
470
  );
442
471
  for (const configFile of session.unopened) {
443
472
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
@@ -475,6 +504,7 @@ function readProjects<Answer>(
475
504
  unanswered: session.unanswered,
476
505
  componentsUnread: workspace?.componentsUnread ?? [],
477
506
  conventionsApplied: conventions.applied,
507
+ outsideTests,
478
508
  };
479
509
  }
480
510
 
@@ -630,6 +660,17 @@ interface SweptProject<Extra> {
630
660
  readonly extra: Extra;
631
661
  }
632
662
 
663
+ /** What one project's stage reads while its view is open, before the run's test-support code is known. */
664
+ interface ReadProject<Extra> {
665
+ readonly projectRead: ProjectRead;
666
+ readonly references: readonly Reference[];
667
+ readonly testFiles: readonly string[];
668
+ readonly evidence: readonly Exemption[];
669
+ readonly componentFiles: readonly string[];
670
+ readonly componentWarnings: readonly ComponentWarning[];
671
+ readonly extra: Extra;
672
+ }
673
+
633
674
  /** What a run read of its projects before any sweep: the matrix, its exemptions, and each project's answers. */
634
675
  interface ReadRun<Extra> {
635
676
  readonly matrix: Matrix;
@@ -674,18 +715,12 @@ function readRun<Extra>(
674
715
  ? { delimiters: config.analysis.templateDelimiters, files: [] }
675
716
  : readTemplates(host, targetRoot, config.analysis);
676
717
  const consumers = scope.consumers.map((consumer) => consumer.path);
677
- const read = readProjects<SweptProject<Extra>>(engine, host, scope, config, (project, opened) => {
718
+ const read = readProjects<ReadProject<Extra>>(engine, host, scope, config, (project, opened) => {
678
719
  const projectRead = opened.read();
679
720
  const resolved = references(project, projectRead.held, targetRoot, {
680
721
  testFiles: config.ts.testFiles,
681
722
  });
682
- const support = testSupportFiles(
683
- projectRead.held.symbols,
684
- resolved.references,
685
- projectRead.rooted.liveUnderReachability,
686
- resolved.testFilePaths,
687
- );
688
- const detected = detectExemptions(
723
+ const evidence = evidenceRecords(
689
724
  {
690
725
  project,
691
726
  held: projectRead.held,
@@ -698,56 +733,87 @@ function readRun<Extra>(
698
733
  : { generated: projectRead.generated }),
699
734
  },
700
735
  detectors,
701
- {
702
- disabled,
703
- mode,
704
- testFiles: new Set([...resolved.testFilePaths, ...support]),
705
- },
736
+ disabled,
706
737
  );
738
+ const components = project.ownSourceFiles().filter(isComponentFile);
739
+ const pathOf = (file: (typeof components)[number]): string =>
740
+ relativePath(targetRoot, file.fileName) ?? file.fileName;
741
+ return {
742
+ projectRead,
743
+ references: resolved.references,
744
+ testFiles: resolved.testFilePaths,
745
+ evidence,
746
+ componentFiles: components.map(pathOf),
747
+ componentWarnings: components.flatMap((file) =>
748
+ readComponent(file.originalText).warnings.map((warning) => ({
749
+ path: pathOf(file),
750
+ line: lineOf(file.originalText, warning.offset),
751
+ reason: warning.reason,
752
+ })),
753
+ ),
754
+ extra: extra(project, projectRead, resolved.references),
755
+ };
756
+ });
757
+ const outsidePaths = read.outsideTests.map((one) => one.path);
758
+ const withOutside = read.answers.map((one) => ({
759
+ ...one,
760
+ references: [
761
+ ...one.references,
762
+ ...outsideTestReferences(read.outsideTests, one.projectRead.held, targetRoot),
763
+ ],
764
+ testFiles: [...one.testFiles, ...outsidePaths],
765
+ }));
766
+ // Test-support code is code only test code imports in any configuration of the run: a
767
+ // configuration that holds no test file says nothing about what the tests import.
768
+ const support = testSupportFiles(
769
+ withOutside.flatMap((one) => one.projectRead.held.symbols),
770
+ withOutside.flatMap((one) => one.references),
771
+ withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability),
772
+ withOutside.flatMap((one) => one.testFiles),
773
+ );
774
+ const answers = withOutside.map((one): SweptProject<Extra> => {
775
+ const { projectRead } = one;
776
+ const detected = heldRecords(one.evidence, {
777
+ disabled,
778
+ mode,
779
+ testFiles: new Set([...one.testFiles, ...support]),
780
+ });
707
781
  const exempt = detected.records;
708
- const made = supportReferences(resolved.references, support);
782
+ const made = supportReferences(one.references, support);
783
+ const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
784
+ const supportFiles = [...support].filter((path) => held.has(path)).sort();
709
785
  const configured: Configured = {
710
786
  configuration: projectRead.configuration,
711
787
  symbols: projectRead.held.symbols,
712
788
  references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
713
789
  roots: projectRead.rooted.liveUnderReachability,
714
- testFiles: resolved.testFilePaths,
715
- ...(support.size === 0 ? {} : { supportFiles: [...support].sort() }),
790
+ testFiles: one.testFiles,
791
+ ...(supportFiles.length === 0 ? {} : { supportFiles }),
716
792
  ...(projectRead.referenceOnly.length === 0
717
793
  ? {}
718
794
  : { referenceOnly: projectRead.referenceOnly }),
719
795
  };
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
796
  return {
732
797
  configFile: projectRead.configFile,
733
798
  configured,
734
799
  exempt,
735
- unanswered,
800
+ unanswered: [
801
+ ...projectRead.held.unanswered,
802
+ ...projectRead.rooted.unanswered,
803
+ ...made
804
+ .filter((reference) => reference.resolution === "by-name")
805
+ .map((reference) => reference.to),
806
+ ...exempt.filter(isUnansweredRecord).map((record) => record.id),
807
+ ],
736
808
  typeErrorSkips: projectRead.typeErrorSkips,
737
809
  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),
810
+ componentFiles: one.componentFiles,
811
+ componentWarnings: one.componentWarnings,
812
+ extra: one.extra,
747
813
  };
748
814
  });
749
815
  const consumed = read.consumers.references;
750
- const projects = read.answers.map((one) =>
816
+ const projects = answers.map((one) =>
751
817
  consumed.length === 0
752
818
  ? one
753
819
  : {
@@ -763,7 +829,7 @@ function readRun<Extra>(
763
829
  exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
764
830
  unansweredHeld: [
765
831
  ...new Set([
766
- ...read.answers.flatMap((one) => one.unanswered),
832
+ ...answers.flatMap((one) => one.unanswered),
767
833
  ...consumed.filter((one) => one.resolution === "by-name").map((one) => one.to),
768
834
  ]),
769
835
  ],
@@ -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",
@@ -99,9 +99,6 @@ interface Checked {
99
99
  readonly targets: readonly Type[];
100
100
  }
101
101
 
102
- /** How many element and signature steps the structural walk takes below a value's own type. */
103
- const MAX_DEPTH = 3;
104
-
105
102
  /** One member of an object type, by the inventory's declarations and the checker's declaration handles. */
106
103
  interface Member {
107
104
  readonly ids: readonly string[];
@@ -254,12 +251,12 @@ export function contextualUses<Brand>(
254
251
  * Each object type a value of `source` is checked as against the object types of
255
252
  * `target`: the value's own, and through array elements and through a function's
256
253
  * 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.
254
+ * target's own object types matches it as it stands. Each pair is compared once, which
255
+ * ends the walk over a recursive type.
258
256
  */
259
257
  const compared = (
260
258
  source: Type | undefined,
261
259
  target: Type | undefined,
262
- depth: number,
263
260
  into: Checked[],
264
261
  seen: Set<string>,
265
262
  ): void => {
@@ -267,7 +264,7 @@ export function contextualUses<Brand>(
267
264
  if (source === undefined || target === undefined || source.id === target.id) {
268
265
  return;
269
266
  }
270
- if (depth > MAX_DEPTH || seen.has(key)) {
267
+ if (seen.has(key)) {
271
268
  return;
272
269
  }
273
270
  seen.add(key);
@@ -281,7 +278,7 @@ export function contextualUses<Brand>(
281
278
  const called = element === undefined ? callOf(one) : undefined;
282
279
  for (const part of targets) {
283
280
  if (element !== undefined) {
284
- compared(element, elementOf(part), depth + 1, into, seen);
281
+ compared(element, elementOf(part), into, seen);
285
282
  continue;
286
283
  }
287
284
  const other = called === undefined ? undefined : callOf(part);
@@ -289,30 +286,37 @@ export function contextualUses<Brand>(
289
286
  continue;
290
287
  }
291
288
  other.parameters.forEach((parameter, index) => {
292
- compared(parameter, called.parameters[index], depth + 1, into, seen);
289
+ compared(parameter, called.parameters[index], into, seen);
293
290
  });
294
- compared(called.result, other.result, depth + 1, into, seen);
291
+ compared(called.result, other.result, into, seen);
295
292
  }
296
293
  }
297
294
  };
298
295
 
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) => {
296
+ /**
297
+ * Whether a value of one type may carry a member a position reads, at any depth the
298
+ * walk takes. A type met again on the way adds nothing its first visit did not.
299
+ */
300
+ const mayCarry = (type: Type, seen: Set<number> = new Set()): boolean => {
301
+ if (seen.has(type.id)) {
302
+ return false;
303
+ }
304
+ seen.add(type.id);
305
+ return partsOf(type).some((part) => {
303
306
  if (carries(part)) {
304
307
  return true;
305
308
  }
306
309
  const element = elementOf(part);
307
310
  if (element !== undefined) {
308
- return mayCarry(element, depth + 1);
311
+ return mayCarry(element, seen);
309
312
  }
310
313
  return callOf(part) !== undefined;
311
314
  });
315
+ };
312
316
 
313
317
  const sourced = carried.flatMap((flow, index) => {
314
318
  const type = types[index];
315
- return type === undefined || !isAnswered(type) || !mayCarry(type, 0) ? [] : [{ flow, type }];
319
+ return type === undefined || !isAnswered(type) || !mayCarry(type) ? [] : [{ flow, type }];
316
320
  });
317
321
  batches += batchesOf(sourced.length);
318
322
  const positions = queries.contextualTypes(
@@ -326,7 +330,7 @@ export function contextualUses<Brand>(
326
330
  return;
327
331
  }
328
332
  const checked: Checked[] = [];
329
- compared(type, position, 0, checked, new Set());
333
+ compared(type, position, checked, new Set());
330
334
  const ids = new Set<string>();
331
335
  for (const { source, targets } of checked) {
332
336
  const tables = targets.map(membersOf);
package/src/exempt.ts CHANGED
@@ -187,12 +187,23 @@ export function detectExemptions<Brand>(
187
187
  detectors: Detectors,
188
188
  holding: Holding,
189
189
  ): Detected {
190
+ return heldRecords(evidenceRecords(input, detectors, holding.disabled), holding);
191
+ }
192
+
193
+ /**
194
+ * Every record one project's detectors found, one per piece of evidence, before the
195
+ * mode decides which of them hold: what {@link heldRecords} reads.
196
+ */
197
+ export function evidenceRecords<Brand>(
198
+ input: DetectorInput<Brand>,
199
+ detectors: Detectors,
200
+ disabled: ReadonlySet<ExemptionClass>,
201
+ ): readonly Exemption[] {
190
202
  const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
191
203
  const found: Exemption[] = [];
192
- const inTestFiles: Exemption[] = [];
193
204
  for (const exemptionClass of TS_EXEMPTION_CLASSES) {
194
205
  const detect = detectors.get(exemptionClass);
195
- if (detect === undefined || holding.disabled.has(exemptionClass)) {
206
+ if (detect === undefined || disabled.has(exemptionClass)) {
196
207
  continue;
197
208
  }
198
209
  const allowed = typescriptVisibilityOf(exemptionClass);
@@ -200,17 +211,26 @@ export function detectExemptions<Brand>(
200
211
  if (!retainable(allowed, visibility.get(evidence.id))) {
201
212
  continue;
202
213
  }
203
- const record = {
214
+ found.push({
204
215
  id: evidence.id,
205
216
  class: exemptionClass,
206
217
  detail: evidence.detail,
207
218
  site: evidence.site,
208
219
  ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
209
- };
210
- if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
211
- inTestFiles.push(record);
212
- continue;
213
- }
220
+ });
221
+ }
222
+ }
223
+ return found;
224
+ }
225
+
226
+ /** The records that hold under one mode, beside the evidence a production mode found in a test file. */
227
+ export function heldRecords(records: readonly Exemption[], holding: Holding): Detected {
228
+ const found: Exemption[] = [];
229
+ const inTestFiles: Exemption[] = [];
230
+ for (const record of records) {
231
+ if (holding.mode.production && holding.testFiles.has(record.site.path)) {
232
+ inTestFiles.push(record);
233
+ } else {
214
234
  found.push(record);
215
235
  }
216
236
  }
package/src/file-facts.ts CHANGED
@@ -65,13 +65,11 @@ function lastLine(text: string): number {
65
65
  }
66
66
 
67
67
  /**
68
- * Every source file below the target root, in path order. A directory that cannot be
69
- * read is skipped, as discovery skips it, and a file that cannot be read is skipped
70
- * with it: a tree the analysis may not enter holds no file it can report.
68
+ * The path below the target root of every source file there, in path order. A directory
69
+ * that cannot be read is skipped, as discovery skips it.
71
70
  */
72
- export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[] {
73
- const scopeOf = packageScope(host, targetRoot);
74
- const found: TreeFile[] = [];
71
+ export function sourcePaths(host: Host, targetRoot: string): readonly string[] {
72
+ const found: string[] = [];
75
73
  const pending = [""];
76
74
  for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
77
75
  let entries: readonly { readonly name: string; readonly directory: boolean }[];
@@ -88,23 +86,29 @@ export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[]
88
86
  }
89
87
  continue;
90
88
  }
91
- if (!SOURCE_FILE.test(entry.name) || DECLARATION_FILE.test(entry.name)) {
92
- continue;
89
+ if (SOURCE_FILE.test(entry.name) && !DECLARATION_FILE.test(entry.name)) {
90
+ found.push(path);
93
91
  }
94
- let text: string;
95
- try {
96
- text = host.readFile(joinPath(targetRoot, path));
97
- } catch {
98
- continue;
99
- }
100
- found.push({
101
- path,
102
- ref: renderRef(scopeOf(path), { of: "module" }),
103
- endLine: lastLine(text),
104
- });
105
92
  }
106
93
  }
107
- return found.sort((a, b) => (a.path < b.path ? -1 : 1));
94
+ return found.sort((a, b) => (a < b ? -1 : 1));
95
+ }
96
+
97
+ /**
98
+ * Every source file below the target root, in path order. A file that cannot be read is
99
+ * skipped: a tree the analysis may not enter holds no file it can report.
100
+ */
101
+ export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[] {
102
+ const scopeOf = packageScope(host, targetRoot);
103
+ return sourcePaths(host, targetRoot).flatMap((path) => {
104
+ let text: string;
105
+ try {
106
+ text = host.readFile(joinPath(targetRoot, path));
107
+ } catch {
108
+ return [];
109
+ }
110
+ return [{ path, ref: renderRef(scopeOf(path), { of: "module" }), endLine: lastLine(text) }];
111
+ });
108
112
  }
109
113
 
110
114
  /** Whether one file changes what the program sees by being included in it. */
package/src/graph.ts CHANGED
@@ -122,9 +122,9 @@ export interface Graph {
122
122
  /** Every root naming a declaration the inventory holds, in the order the root set gives them. */
123
123
  readonly rooted: readonly Rooted[];
124
124
  /**
125
- * Every reference a loaded consumer makes to a declaration the inventory holds, in the
126
- * order given. A consumer is a caller the analysis read, so the sweep holds what it
127
- * names live under both relations and reaches on from there.
125
+ * Every reference a loaded consumer or an unheld file makes to a declaration the
126
+ * inventory holds, in the order given. Such a file is a caller the analysis read, so the
127
+ * sweep holds what it names live under both relations and reaches on from there.
128
128
  */
129
129
  readonly consumed: readonly Edge[];
130
130
  /** The position of the declaration one identifier names, or {@link OUTSIDE}. */
@@ -167,7 +167,7 @@ export function graphOf(
167
167
  }
168
168
  }
169
169
  const edge = { to, test: reference.test, evaluation: reference.use === "evaluation" };
170
- if (reference.consumer !== undefined) {
170
+ if (reference.consumer !== undefined || reference.unheld === true) {
171
171
  consumed.push(edge);
172
172
  continue;
173
173
  }
package/src/inventory.ts CHANGED
@@ -470,7 +470,20 @@ function nameNodeOf(node: Node): Node | undefined {
470
470
  * needs the declaration this enumeration made for it.
471
471
  */
472
472
  export function nodeKey(file: SourceFile, node: Node): string {
473
- return `${file.fileName}:${String(node.pos)}:${String(node.end)}`;
473
+ return `${file.fileName}:${String(node.pos)}:${String(node.end)}:${String(node.kind)}`;
474
+ }
475
+
476
+ /** `export default`, up to the keyword that stands where an unnamed default export's name would. */
477
+ const DEFAULT_EXPORT = /^export\s+(?=default\b)/u;
478
+
479
+ /**
480
+ * Where an unnamed declaration is placed: at `default` when it is the default export, so it
481
+ * never shares the file's own position, else at its first token.
482
+ */
483
+ function unnamedStart(node: Node): number {
484
+ const start = node.getStart();
485
+ const prefix = DEFAULT_EXPORT.exec(node.getText());
486
+ return prefix === null ? start : start + prefix[0].length;
474
487
  }
475
488
 
476
489
  /** The kind a class element takes: a method, or a property or accessor. */
@@ -565,12 +578,11 @@ export function inventory<Brand>(
565
578
  name: Node | undefined,
566
579
  named: { readonly component: Component; readonly localName: string | undefined },
567
580
  ): Building {
568
- const at = name ?? node;
569
581
  const record = keep({
570
582
  file: owner.file,
571
583
  node,
572
584
  module: owner.module,
573
- position: renderPosition(owner.file, targetRoot, at.getStart()),
585
+ position: renderPosition(owner.file, targetRoot, name?.getStart() ?? unnamedStart(node)),
574
586
  endLine: renderPosition(owner.file, targetRoot, Math.max(node.end - 1, 0)).line,
575
587
  kind,
576
588
  parentChain: owner.kind === "file" ? [] : chainOf(owner),
package/src/manifest.ts CHANGED
@@ -1,13 +1,12 @@
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
7
 
9
8
  import type { Host } from "./host.ts";
10
- import { isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
9
+ import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
11
10
 
12
11
  /** The manifest's own name. */
13
12
  const MANIFEST = "package.json";
@@ -205,3 +204,40 @@ export function readManifest(host: Host, targetRoot: string): Manifest {
205
204
  declaresExports: manifest["exports"] !== undefined,
206
205
  };
207
206
  }
207
+
208
+ /**
209
+ * The entry points every package manifest between the target root and each compiler
210
+ * configuration names: each directory holding a `package.json` that a configuration sits
211
+ * in or below, the target root and `known` excluded, whose manifests the run reads
212
+ * otherwise. Each entry is spelled under its manifest's path below the target root, and
213
+ * is published by its own manifest's rule, as the target's own entries are.
214
+ */
215
+ export function packageEntries(
216
+ host: Host,
217
+ targetRoot: string,
218
+ configFiles: readonly string[],
219
+ known: ReadonlySet<string>,
220
+ ): readonly ManifestEntry[] {
221
+ const dirs = new Set<string>();
222
+ for (const configFile of configFiles) {
223
+ for (
224
+ let dir = dirnamePath(resolvePath(targetRoot, configFile));
225
+ dir !== targetRoot && relativePath(targetRoot, dir) !== undefined;
226
+ dir = dirnamePath(dir)
227
+ ) {
228
+ if (!known.has(dir) && host.kindOf(joinPath(dir, MANIFEST)) === "file") {
229
+ dirs.add(dir);
230
+ }
231
+ }
232
+ }
233
+ return [...dirs].sort().flatMap((dir) => {
234
+ const manifest = readManifest(host, dir);
235
+ const where = `${relativePath(targetRoot, dir) ?? dir}/${MANIFEST}`;
236
+ return manifest.entries.map((entry) => ({
237
+ ...entry,
238
+ member: `${where} ${entry.member}`,
239
+ published:
240
+ entry.published && (!manifest.declaresExports || entry.member.startsWith("exports")),
241
+ }));
242
+ });
243
+ }