@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/roots.ts CHANGED
@@ -8,7 +8,16 @@
8
8
  * under reachability and still a candidate under reference counting.
9
9
  */
10
10
 
11
- import type { SourceFile } from "@typescript/native/unstable/ast";
11
+ import {
12
+ isExpressionWithTypeArguments,
13
+ isFunctionLikeDeclaration,
14
+ isPropertyAccessExpression,
15
+ isQualifiedName,
16
+ isTypeQueryNode,
17
+ isTypeReferenceNode,
18
+ type Node,
19
+ type SourceFile,
20
+ } from "@typescript/native/unstable/ast";
12
21
  import { aliasChains } from "./alias-chain.ts";
13
22
  import type { ParseFile } from "./configuration-files.ts";
14
23
  import { conventionEntries, type Conventions } from "./conventions.ts";
@@ -88,8 +97,8 @@ export interface Roots {
88
97
  readonly unmatched: readonly string[];
89
98
  /**
90
99
  * Every declaration of the project, where the checker left the module or export table
91
- * of an entry or a published file unanswered, and none otherwise: which of them that
92
- * file exports is unknown.
100
+ * of an entry or a published file unanswered, since which of them that file exports is
101
+ * unknown; otherwise each one an unanswered published type name may name.
93
102
  */
94
103
  readonly unanswered: readonly string[];
95
104
  }
@@ -101,10 +110,10 @@ export interface RootOptions {
101
110
  /** The target's own manifest. */
102
111
  readonly manifest: Manifest;
103
112
  /**
104
- * The entry points the other packages of the target's workspace name, each already
105
- * marked published or not by its own manifest's rule.
113
+ * The entry points the target's other packages name, its workspace's members and the
114
+ * packages below it, each already marked published or not by its own manifest's rule.
106
115
  */
107
- readonly workspaceEntries?: readonly ManifestEntry[] | undefined;
116
+ readonly otherPackages?: readonly ManifestEntry[] | undefined;
108
117
  /** `roots.patterns` from the resolved configuration. */
109
118
  readonly patterns: readonly string[];
110
119
  /** `ts.entry_files` from the resolved configuration. */
@@ -244,7 +253,7 @@ export function roots<Brand>(
244
253
  entry.published &&
245
254
  (options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
246
255
  })),
247
- ...(options.workspaceEntries ?? []).map((entry) => ({ entry, publishes: entry.published })),
256
+ ...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
248
257
  ];
249
258
  for (const { entry, publishes } of entries) {
250
259
  if (options.publishedAPI && publishes) {
@@ -310,10 +319,11 @@ export function roots<Brand>(
310
319
  }
311
320
  }
312
321
 
313
- if (options.publishedAPI) {
314
- for (const id of publishedAPI(published, exportsOf, held)) {
315
- add(id, "published-api", "");
316
- }
322
+ const api = options.publishedAPI
323
+ ? publishedAPI(project, published, exportsOf, held, files.byPath)
324
+ : { ids: [], guessed: [] };
325
+ for (const id of api.ids) {
326
+ add(id, "published-api", "");
317
327
  }
318
328
 
319
329
  const unmatched = configured(held, options.patterns, add);
@@ -330,7 +340,7 @@ export function roots<Brand>(
330
340
  configFile: project.configFile,
331
341
  liveUnderReachability: ordered,
332
342
  unmatched,
333
- unanswered: answered ? [] : held.symbols.map((symbol) => symbol.id),
343
+ unanswered: answered ? api.guessed : held.symbols.map((symbol) => symbol.id),
334
344
  };
335
345
  }
336
346
 
@@ -388,55 +398,143 @@ function exportTables<Brand>(
388
398
  const EXPORTING_CONTAINERS: ReadonlySet<string> = new Set(["file", "namespace"]);
389
399
 
390
400
  /**
391
- * Every declaration a consumer of the library can name: what the published files
392
- * export, and below each, the members a consumer reaches through it.
393
- *
394
- * The published files are the ones `exports` reaches where the manifest declares
395
- * one, because a package that declares `exports` has said exactly what a consumer may
396
- * import; where it declares none, they are the ones the members that preceded it
397
- * name. Below a published module or namespace the walk follows what its export table
398
- * names; below any other published declaration it follows every member a consumer
399
- * can write: a public one, and a protected one, which a consumer's subclass names. A
400
- * private member is not published, nor is anything below one, and a type parameter
401
- * is not published either, because a consumer supplies a type argument by position.
402
- *
403
- * A published file is not itself published. A consumer imports a module and names
404
- * what it exports; the file is a root because the manifest names it, which is a
405
- * different reason.
401
+ * Every declaration a consumer of the library can name: what the published files export
402
+ * and, below each, the members a consumer writes: a namespace's exported ones, any other
403
+ * declaration's public and protected ones, never a private one or a type parameter. A
404
+ * declaration a published one's type names is published too, exported or not. A type name
405
+ * the checker leaves unanswered publishes every declaration spelled as it is, and those are
406
+ * `guessed`. A published file is not itself published.
406
407
  */
407
- function publishedAPI(
408
+ function publishedAPI<Brand>(
409
+ project: ProjectView<Brand>,
408
410
  published: readonly SourceFile[],
409
411
  exportsOf: ReadonlyMap<string, readonly string[]>,
410
412
  held: Inventory,
411
- ): readonly string[] {
413
+ byPath: ReadonlyMap<string, SourceFile>,
414
+ ): { readonly ids: readonly string[]; readonly guessed: readonly string[] } {
412
415
  const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
413
416
  const children = childrenOf(held);
417
+ const nodes = declarationNodes(held, byPath);
418
+ const chains = aliasChains(project, held);
414
419
  const reached = new Set<string>();
420
+ const guessed = new Set<string>();
415
421
  const pending: string[] = [];
422
+ const reach = (id: string): void => {
423
+ if (!reached.has(id) && byId.get(id)?.kind !== "file") {
424
+ reached.add(id);
425
+ pending.push(id);
426
+ }
427
+ };
416
428
  for (const file of published) {
417
429
  for (const id of exportsOf.get(file.fileName) ?? []) {
418
430
  if (byId.get(id)?.kind === "file") {
419
431
  pending.push(id);
420
- } else if (!reached.has(id)) {
421
- reached.add(id);
422
- pending.push(id);
432
+ } else {
433
+ reach(id);
423
434
  }
424
435
  }
425
436
  }
426
- for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
427
- const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
428
- for (const member of children.get(next) ?? []) {
429
- const reachable = exporting
430
- ? member.exported
431
- : member.visibility !== "private" && member.visibility !== "private-name";
432
- if (member.kind === "type-parameter" || reached.has(member.id) || !reachable) {
433
- continue;
437
+ while (pending.length > 0) {
438
+ const round: string[] = [];
439
+ for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
440
+ round.push(next);
441
+ const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
442
+ for (const member of children.get(next) ?? []) {
443
+ const reachable = exporting
444
+ ? member.exported
445
+ : member.visibility !== "private" && member.visibility !== "private-name";
446
+ if (member.kind !== "type-parameter" && reachable) {
447
+ reach(member.id);
448
+ }
434
449
  }
435
- reached.add(member.id);
436
- pending.push(member.id);
437
450
  }
451
+ const named = round.flatMap((id) => {
452
+ const symbol = byId.get(id);
453
+ return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
454
+ });
455
+ project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
456
+ if (symbol === UNANSWERED) {
457
+ for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
458
+ guessed.add(id);
459
+ reach(id);
460
+ }
461
+ } else if (symbol !== undefined) {
462
+ for (const target of chains.chainOf(symbol)) {
463
+ reach(target.id);
464
+ }
465
+ }
466
+ });
438
467
  }
439
- return [...reached];
468
+ return { ids: [...reached], guessed: [...guessed] };
469
+ }
470
+
471
+ /** The type names declarations of the inventory write, each file walked once, when first asked. */
472
+ function declarationNodes(
473
+ held: Inventory,
474
+ byPath: ReadonlyMap<string, SourceFile>,
475
+ ): { typeNamesOf(symbol: InventorySymbol): readonly Node[] } {
476
+ const walked = new Map<string, { byId: Map<string, Node>; nodes: Set<Node> }>();
477
+ const nodesIn = (path: string): { byId: Map<string, Node>; nodes: Set<Node> } => {
478
+ const known = walked.get(path);
479
+ if (known !== undefined) {
480
+ return known;
481
+ }
482
+ const found = { byId: new Map<string, Node>(), nodes: new Set<Node>() };
483
+ const file = byPath.get(path);
484
+ if (file !== undefined) {
485
+ const visit = (node: Node): void => {
486
+ const id = held.declarations.get(nodeKey(file, node));
487
+ if (id !== undefined) {
488
+ found.nodes.add(node);
489
+ if (!found.byId.has(id)) {
490
+ found.byId.set(id, node);
491
+ }
492
+ }
493
+ node.forEachChild(visit);
494
+ };
495
+ visit(file);
496
+ }
497
+ walked.set(path, found);
498
+ return found;
499
+ };
500
+ return {
501
+ typeNamesOf: (symbol) => {
502
+ const { byId, nodes } = nodesIn(symbol.position.path);
503
+ const declaration = byId.get(symbol.id);
504
+ return declaration === undefined ? [] : typeNamesOf(declaration, nodes);
505
+ },
506
+ };
507
+ }
508
+
509
+ /**
510
+ * The names of types one declaration's own syntax writes: in a type reference, a
511
+ * heritage clause or a type query. A function body is not read, and neither is a
512
+ * declaration nested in this one, which `nested` holds and which is read on its own.
513
+ */
514
+ function typeNamesOf(declaration: Node, nested: ReadonlySet<Node>): readonly Node[] {
515
+ const found: Node[] = [];
516
+ const visit = (node: Node): void => {
517
+ if (node !== declaration && nested.has(node)) {
518
+ return;
519
+ }
520
+ if (isTypeReferenceNode(node)) {
521
+ found.push(isQualifiedName(node.typeName) ? node.typeName.right : node.typeName);
522
+ } else if (isExpressionWithTypeArguments(node)) {
523
+ found.push(
524
+ isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression,
525
+ );
526
+ } else if (isTypeQueryNode(node)) {
527
+ found.push(isQualifiedName(node.exprName) ? node.exprName.right : node.exprName);
528
+ }
529
+ const body = isFunctionLikeDeclaration(node) ? node.body : undefined;
530
+ node.forEachChild((child) => {
531
+ if (child !== body) {
532
+ visit(child);
533
+ }
534
+ });
535
+ };
536
+ visit(declaration);
537
+ return found;
440
538
  }
441
539
 
442
540
  /**
package/src/session.ts CHANGED
@@ -485,8 +485,8 @@ function projectsOf(
485
485
  * snapshot of one client, and answers in their order. A configuration `programs` describes is
486
486
  * built as that program, or opened from the configuration it writes, instead of as a project.
487
487
  * One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
488
- * under `"record"`. The snapshot is disposed after the last visit and the client released
489
- * after it, whether the visit completed or threw.
488
+ * under `"record"`. `last` runs after the last visit while the client still answers; the
489
+ * snapshot is disposed and the client released after it, whether the visit completed or threw.
490
490
  */
491
491
  export function runSession<Result>(
492
492
  engine: Engine,
@@ -494,6 +494,7 @@ export function runSession<Result>(
494
494
  visit: ProjectVisitor<Result>,
495
495
  unopened: Unopened = "refuse",
496
496
  programs: ReadonlyMap<string, WorkspaceProgram> = new Map(),
497
+ last: () => void = () => undefined,
497
498
  ): SessionResult<Result> {
498
499
  try {
499
500
  const built: string[] = [];
@@ -531,6 +532,7 @@ export function runSession<Result>(
531
532
  }
532
533
  projects.push(visit(viewOf(project, configFile, engine, log)));
533
534
  }
535
+ last();
534
536
  return {
535
537
  projects,
536
538
  unopened: missing,
@@ -125,12 +125,74 @@ function stringEnd(text: string, from: number): number {
125
125
  return at + 1;
126
126
  }
127
127
 
128
- /** The identifiers one action's inner text writes outside a quoted string, with their offsets in it. */
129
- function identifiersOf(inner: string): readonly { name: string; offset: number }[] {
128
+ /** The end of the `${` substitution whose expression starts at `from`, past its closing brace. */
129
+ function substitutionEnd(text: string, from: number): number {
130
+ let depth = 1;
131
+ let at = from;
132
+ while (at < text.length) {
133
+ const char = text[at];
134
+ if (char === '"' || char === "'") {
135
+ at = stringEnd(text, at);
136
+ continue;
137
+ }
138
+ if (char === "`") {
139
+ at = templateEnd(text, at, () => undefined);
140
+ continue;
141
+ }
142
+ if (char === "{") {
143
+ depth += 1;
144
+ } else if (char === "}") {
145
+ depth -= 1;
146
+ if (depth === 0) {
147
+ return at + 1;
148
+ }
149
+ }
150
+ at += 1;
151
+ }
152
+ return at;
153
+ }
154
+
155
+ /** The end of the template literal at `from`; each `${…}` expression goes to `substitution`. */
156
+ function templateEnd(
157
+ text: string,
158
+ from: number,
159
+ substitution: (expression: string, offset: number) => void,
160
+ ): number {
161
+ let at = from + 1;
162
+ while (at < text.length && text[at] !== "`") {
163
+ if (text[at] === "\\") {
164
+ at += 2;
165
+ continue;
166
+ }
167
+ if (text[at] === "$" && text[at + 1] === "{") {
168
+ const start = at + 2;
169
+ const end = substitutionEnd(text, start);
170
+ substitution(text.slice(start, end - 1), start);
171
+ at = end;
172
+ continue;
173
+ }
174
+ at += 1;
175
+ }
176
+ return at + 1;
177
+ }
178
+
179
+ /**
180
+ * The identifiers one action's inner text writes outside a quoted string, with their offsets
181
+ * in it. In `script` text, a component's markup, a template literal's substitutions are code.
182
+ */
183
+ function identifiersOf(inner: string, script = false): readonly { name: string; offset: number }[] {
130
184
  const found: { name: string; offset: number }[] = [];
131
185
  let at = 0;
132
186
  while (at < inner.length) {
133
187
  const char = inner[at] ?? "";
188
+ if (char === "`" && script) {
189
+ at = templateEnd(inner, at, (expression, offset) => {
190
+ for (const one of identifiersOf(expression, true)) {
191
+ found.push({ name: one.name, offset: offset + one.offset });
192
+ }
193
+ });
194
+ continue;
195
+ }
134
196
  if (char === '"' || char === "'" || char === "`") {
135
197
  at = stringEnd(inner, at);
136
198
  continue;
@@ -183,7 +245,7 @@ function namesOf(text: string, delimiters: TemplateDelimiters): readonly Named[]
183
245
  /** Every identifier the markup of one component file writes in an action. */
184
246
  function markupNamesOf(text: string): readonly Named[] {
185
247
  return readComponent(text).markup.flatMap((one) =>
186
- identifiersOf(one.text).map(({ name, offset }) => ({
248
+ identifiersOf(one.text, true).map(({ name, offset }) => ({
187
249
  name,
188
250
  offset: one.start + offset,
189
251
  action: one.action,