@cplieger/deadset-ts 3.12.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/node-host.js +10 -7
  3. package/dist/src/alias-chain.js +59 -7
  4. package/dist/src/analysis.js +217 -48
  5. package/dist/src/calls.js +23 -22
  6. package/dist/src/class-members.js +10 -11
  7. package/dist/src/components.js +4 -2
  8. package/dist/src/configured-declarations.js +56 -7
  9. package/dist/src/conformance-record.js +2 -2
  10. package/dist/src/dependencies.js +34 -17
  11. package/dist/src/deprecation.js +35 -24
  12. package/dist/src/destructuring.js +37 -29
  13. package/dist/src/discover.js +105 -28
  14. package/dist/src/edges.js +4 -1
  15. package/dist/src/entry-points.js +7 -0
  16. package/dist/src/enum-group.js +46 -40
  17. package/dist/src/exempt.js +42 -1
  18. package/dist/src/findings/completion.js +37 -8
  19. package/dist/src/findings/emitters.js +20 -6
  20. package/dist/src/findings/interfaces.js +7 -1
  21. package/dist/src/findings/intra-function.js +91 -2
  22. package/dist/src/findings/self-check.js +30 -7
  23. package/dist/src/findings/unused-declarations.js +15 -3
  24. package/dist/src/findings-pass.js +18 -11
  25. package/dist/src/framework-lifecycle.js +8 -6
  26. package/dist/src/implementations.js +45 -23
  27. package/dist/src/interface-satisfaction.js +30 -27
  28. package/dist/src/intra-function-parts.js +660 -0
  29. package/dist/src/inventory.js +85 -4
  30. package/dist/src/json-chunks.js +76 -0
  31. package/dist/src/kinds.js +337 -35
  32. package/dist/src/partial.js +23 -0
  33. package/dist/src/query.js +207 -0
  34. package/dist/src/references.js +92 -20
  35. package/dist/src/reflective-lookup.js +46 -4
  36. package/dist/src/report.js +14 -3
  37. package/dist/src/reporters/annotations.js +9 -5
  38. package/dist/src/reporters/json.js +3 -2
  39. package/dist/src/reporters/sarif.js +22 -16
  40. package/dist/src/reporters/template.js +112 -12
  41. package/dist/src/reporters/text.js +8 -6
  42. package/dist/src/roots.js +27 -11
  43. package/dist/src/serialization-contract.js +53 -32
  44. package/dist/src/session.js +79 -39
  45. package/dist/src/signatures.js +32 -0
  46. package/dist/src/suppress-file.js +29 -7
  47. package/dist/src/suppress-inline.js +8 -1
  48. package/dist/src/suppress.js +27 -0
  49. package/dist/src/sweep.js +4 -1
  50. package/dist/src/value-reach.js +44 -25
  51. package/dist/src/verbs/analyze.js +39 -14
  52. package/dist/src/verbs/describe.js +12 -8
  53. package/dist/src/verbs/explain.js +22 -9
  54. package/dist/src/verbs/print-projects.js +40 -7
  55. package/dist/src/verbs/print-retained.js +6 -2
  56. package/dist/src/verbs/print-roots.js +5 -1
  57. package/dist/src/version.js +1 -1
  58. package/jsr.json +1 -1
  59. package/package.json +3 -3
  60. package/src/alias-chain.ts +75 -11
  61. package/src/analysis.ts +385 -104
  62. package/src/calls.ts +10 -9
  63. package/src/class-members.ts +10 -11
  64. package/src/components.ts +4 -2
  65. package/src/configured-declarations.ts +80 -8
  66. package/src/conformance-record.ts +2 -2
  67. package/src/dependencies.ts +42 -20
  68. package/src/deprecation.ts +53 -27
  69. package/src/destructuring.ts +50 -39
  70. package/src/discover.ts +177 -33
  71. package/src/edges.ts +4 -1
  72. package/src/entry-points.ts +7 -0
  73. package/src/enum-group.ts +56 -42
  74. package/src/exempt.ts +50 -2
  75. package/src/finding.ts +5 -1
  76. package/src/findings/completion.ts +46 -10
  77. package/src/findings/emitter.ts +3 -0
  78. package/src/findings/emitters.ts +27 -6
  79. package/src/findings/interfaces.ts +7 -1
  80. package/src/findings/intra-function.ts +106 -3
  81. package/src/findings/self-check.ts +48 -7
  82. package/src/findings/unused-declarations.ts +18 -4
  83. package/src/findings-pass.ts +26 -11
  84. package/src/framework-lifecycle.ts +10 -6
  85. package/src/host.ts +5 -3
  86. package/src/implementations.ts +59 -23
  87. package/src/interface-satisfaction.ts +30 -27
  88. package/src/intra-function-parts.ts +889 -0
  89. package/src/inventory.ts +97 -4
  90. package/src/json-chunks.ts +87 -0
  91. package/src/kinds.ts +355 -41
  92. package/src/partial.ts +40 -0
  93. package/src/query.ts +522 -0
  94. package/src/references.ts +119 -38
  95. package/src/reflective-lookup.ts +56 -6
  96. package/src/report.ts +32 -4
  97. package/src/reporters/annotations.ts +23 -21
  98. package/src/reporters/json.ts +3 -2
  99. package/src/reporters/reporter.ts +2 -2
  100. package/src/reporters/sarif.ts +27 -18
  101. package/src/reporters/template.ts +119 -14
  102. package/src/reporters/text.ts +12 -12
  103. package/src/roots.ts +34 -12
  104. package/src/serialization-contract.ts +59 -40
  105. package/src/session.ts +146 -58
  106. package/src/signatures.ts +55 -0
  107. package/src/suppress-file.ts +40 -8
  108. package/src/suppress-inline.ts +8 -1
  109. package/src/suppress.ts +31 -0
  110. package/src/sweep.ts +9 -4
  111. package/src/value-reach.ts +66 -28
  112. package/src/verbs/analyze.ts +51 -14
  113. package/src/verbs/describe.ts +13 -9
  114. package/src/verbs/explain.ts +20 -9
  115. package/src/verbs/print-projects.ts +53 -7
  116. package/src/verbs/print-retained.ts +6 -2
  117. package/src/verbs/print-roots.ts +5 -1
  118. package/src/version.ts +1 -1
  119. package/dist/src/reporters/rules.js +0 -178
  120. package/src/reporters/rules.ts +0 -195
package/src/calls.ts CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
27
27
  import type { AliasChains } from "./alias-chain.ts";
28
28
  import { nodeKey, type Inventory } from "./inventory.ts";
29
- import { DEFAULT_BATCH_CAP } from "./references.ts";
29
+ import { must } from "./query.ts";
30
30
  import type { ProjectView } from "./session.ts";
31
31
 
32
32
  /** One value written where a call passes it, and the node its declaration is resolved from. */
@@ -77,7 +77,8 @@ export function decoratorsOf(node: Node): readonly Decorator[] {
77
77
  /**
78
78
  * What each of a batch of nodes resolves to, every alias followed to the declaration
79
79
  * it stands for. A node the checker resolves to nothing, and an alias that resolves
80
- * to nothing, are absent from the answer. Each alias is followed once.
80
+ * to nothing, are absent from the answer. Each alias is followed once. A question the
81
+ * checker does not answer stops the pass, as {@link must} stops.
81
82
  */
82
83
  export function resolvedTargets<Brand>(
83
84
  project: ProjectView<Brand>,
@@ -85,17 +86,18 @@ export function resolvedTargets<Brand>(
85
86
  ): ReadonlyMap<Node, TSSymbol> {
86
87
  const followed = new Map<number, TSSymbol | undefined>();
87
88
  const targets = new Map<Node, TSSymbol>();
88
- for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
89
- const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
90
- project.symbolsAt(run.map((node) => project.handle(node))).forEach((symbol, index) => {
91
- const node = run[index];
89
+ project
90
+ .symbolsAt(nodes.map((node) => project.handle(node)))
91
+ .map(must)
92
+ .forEach((symbol, index) => {
93
+ const node = nodes[index];
92
94
  if (symbol === undefined || node === undefined) {
93
95
  return;
94
96
  }
95
97
  let target: TSSymbol | undefined = symbol;
96
98
  if ((symbol.flags & SymbolFlags.Alias) !== 0) {
97
99
  if (!followed.has(symbol.id)) {
98
- const aliased = project.checker.getAliasedSymbol(symbol);
100
+ const aliased = must(project.queries.aliased(symbol));
99
101
  followed.set(symbol.id, aliased.declarations.length === 0 ? undefined : aliased);
100
102
  }
101
103
  target = followed.get(symbol.id);
@@ -104,7 +106,6 @@ export function resolvedTargets<Brand>(
104
106
  targets.set(node, target);
105
107
  }
106
108
  });
107
- }
108
109
  return targets;
109
110
  }
110
111
 
@@ -166,7 +167,7 @@ export function classesPassed<Brand>(
166
167
  );
167
168
  const classesOf = (value: PassedValue): readonly string[] => {
168
169
  const symbol = value.shorthand
169
- ? project.shorthandValueAt(project.handle(value.name))
170
+ ? must(project.shorthandValueAt(project.handle(value.name)))
170
171
  : targets.get(value.name);
171
172
  return symbol === undefined
172
173
  ? []
@@ -7,7 +7,7 @@
7
7
  import { isClassDeclaration, isClassExpression, type Node } from "@typescript/native/unstable/ast";
8
8
  import type { Symbol as TSSymbol, SymbolFlags, Type } from "@typescript/native/unstable/sync";
9
9
  import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
10
- import { DEFAULT_BATCH_CAP } from "./references.ts";
10
+ import { must } from "./query.ts";
11
11
  import type { ProjectView } from "./session.ts";
12
12
 
13
13
  /** One member of a class's value that the target declares. */
@@ -44,16 +44,15 @@ export function classNames<Brand>(
44
44
  return names;
45
45
  }
46
46
 
47
- /** The types of a batch of nodes, asked in capped batches. */
47
+ /**
48
+ * The types of a batch of nodes, for a pass that cannot go on past a type the checker
49
+ * did not answer: it stops there, as {@link must} stops.
50
+ */
48
51
  export function typesAt<Brand>(
49
52
  project: ProjectView<Brand>,
50
53
  nodes: readonly Node[],
51
54
  ): (Type | undefined)[] {
52
- const types: (Type | undefined)[] = [];
53
- for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
54
- types.push(...project.checker.getTypeAtLocation(nodes.slice(from, from + DEFAULT_BATCH_CAP)));
55
- }
56
- return types;
55
+ return project.queries.typesAt(nodes).map(must);
57
56
  }
58
57
 
59
58
  /**
@@ -68,9 +67,9 @@ function sideTypes<Brand>(
68
67
  if (side === "instance") {
69
68
  return typesAt(project, nodes);
70
69
  }
71
- const symbols = project.symbolsAt(nodes.map((node) => project.handle(node)));
70
+ const symbols = project.symbolsAt(nodes.map((node) => project.handle(node))).map(must);
72
71
  const present = symbols.filter((symbol): symbol is TSSymbol => symbol !== undefined);
73
- const types = present.length === 0 ? [] : project.checker.getTypeOfSymbol(present);
72
+ const types = project.queries.typesOfSymbols(present).map(must);
74
73
  let next = 0;
75
74
  return symbols.map((symbol) => (symbol === undefined ? undefined : types[next++]));
76
75
  }
@@ -87,7 +86,7 @@ export function classMembers<Brand>(
87
86
  side: ClassSide,
88
87
  ): ReadonlyMap<string, readonly ClassMember[]> {
89
88
  const named = [...classNames(project, held)].filter(([id]) => classIds.has(id));
90
- const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
89
+ const own = project.ownPaths();
91
90
  const types = sideTypes(
92
91
  project,
93
92
  named.map(([, node]) => node),
@@ -100,7 +99,7 @@ export function classMembers<Brand>(
100
99
  return;
101
100
  }
102
101
  const found: ClassMember[] = [];
103
- for (const property of project.checker.getPropertiesOfType(type)) {
102
+ for (const property of must(project.queries.propertiesOf(type))) {
104
103
  for (const handle of property.declarations) {
105
104
  if (!own.has(handle.path)) {
106
105
  continue;
package/src/components.ts CHANGED
@@ -132,7 +132,8 @@ function linesSpanned(symbols: readonly (InventorySymbol | undefined)[]): number
132
132
  }
133
133
 
134
134
  /**
135
- * The dead declarations by their place in the subgraph, and the edges between them: each
135
+ * The dead declarations by their place in the subgraph, a test file's declaration only
136
+ * where it is a test of dead code, and the edges between them: each
136
137
  * reference one makes to another, an edge each way between a dead member and its dead
137
138
  * container (one direction alone leaves the member a component the container reaches),
138
139
  * and an edge back from each production target of an admitted test, which puts the test in
@@ -145,7 +146,8 @@ function deadSubgraph(
145
146
  ): { readonly at: readonly number[]; readonly adjacent: readonly (readonly number[])[] } {
146
147
  const at: number[] = [];
147
148
  const position = graph.symbols.map((_symbol, index) => {
148
- if (dead[index] !== true) {
149
+ // A test file's declaration belongs to a component only as a test of dead code.
150
+ if (dead[index] !== true || (graph.test[index] === true && testOfDeadCode[index] !== true)) {
149
151
  return UNVISITED;
150
152
  }
151
153
  at.push(index);
@@ -23,8 +23,9 @@ import {
23
23
  } from "@typescript/native/unstable/sync";
24
24
  import type { AliasChains } from "./alias-chain.ts";
25
25
  import { resolvedTargets } from "./calls.ts";
26
- import type { DeclarationEntry } from "./config.ts";
26
+ import type { Config, DeclarationEntry } from "./config.ts";
27
27
  import type { Inventory } from "./inventory.ts";
28
+ import { must, Unanswerable } from "./query.ts";
28
29
  import { nameComponent } from "./ref.ts";
29
30
  import type { ProjectView } from "./session.ts";
30
31
 
@@ -90,7 +91,7 @@ function unaliased<Brand>(project: ProjectView<Brand>, symbol: TSSymbol): TSSymb
90
91
  if ((symbol.flags & SymbolFlags.Alias) === 0) {
91
92
  return symbol;
92
93
  }
93
- const target = project.checker.getAliasedSymbol(symbol);
94
+ const target = must(project.queries.aliased(symbol));
94
95
  return target.declarations.length === 0 ? undefined : target;
95
96
  }
96
97
 
@@ -105,14 +106,18 @@ function isClassMember(symbol: TSSymbol): boolean {
105
106
  * with the static suffix names one of the first and a component without it names an
106
107
  * instance member or one of the second.
107
108
  */
108
- function candidatesOf(container: TSSymbol, isStatic: boolean): readonly TSSymbol[] {
109
- const exported = [...container.getExports().values()];
109
+ function candidatesOf<Brand>(
110
+ project: ProjectView<Brand>,
111
+ container: TSSymbol,
112
+ isStatic: boolean,
113
+ ): readonly TSSymbol[] {
114
+ const exported = [...must(project.queries.exportsOf(container)).values()];
110
115
  if (isStatic) {
111
116
  return exported.filter(isClassMember);
112
117
  }
113
118
  const isClass = (container.flags & SymbolFlags.Class) !== 0;
114
119
  return [
115
- ...container.getMembers().values(),
120
+ ...must(project.queries.membersOf(container)).values(),
116
121
  ...exported.filter((member) => !isClass || !isClassMember(member)),
117
122
  ];
118
123
  }
@@ -124,7 +129,7 @@ function memberOf<Brand>(
124
129
  component: string,
125
130
  isStatic: boolean,
126
131
  ): TSSymbol | undefined {
127
- const member = candidatesOf(container, isStatic).find(
132
+ const member = candidatesOf(project, container, isStatic).find(
128
133
  (candidate) => nameComponent(candidate.name) === component,
129
134
  );
130
135
  return member === undefined ? undefined : unaliased(project, member);
@@ -190,7 +195,7 @@ function modulesNamed<Brand>(
190
195
  file.forEachChild(visit);
191
196
  }
192
197
  const specified = [...written];
193
- const symbols = project.symbolsAt(specified.map(([, node]) => project.handle(node)));
198
+ const symbols = project.symbolsAt(specified.map(([, node]) => project.handle(node))).map(must);
194
199
  const modules = new Map<string, TSSymbol>();
195
200
  specified.forEach(([name], index) => {
196
201
  const symbol = symbols[index];
@@ -238,7 +243,7 @@ export function resolveEntries<Brand>(
238
243
  const module = modules.get(entry.module);
239
244
  start = module === undefined ? undefined : memberOf(project, module, first, false);
240
245
  } else if (nameComponent(first) === first) {
241
- const global = project.checker.resolveName(first, ANY_MEANING, undefined, false);
246
+ const global = must(project.queries.resolveName(first, ANY_MEANING, undefined, false));
242
247
  start = global === undefined ? undefined : unaliased(project, global);
243
248
  }
244
249
  const declared = walkPath(project, start, rest, isStatic);
@@ -306,3 +311,70 @@ export function spelledEntry(entry: DeclarationEntry, held: Inventory): string {
306
311
  return entry.global;
307
312
  }
308
313
  }
314
+
315
+ /** One entry of a configuration key that names a declaration, and the key's dotted path. */
316
+ interface ConfiguredDeclaration {
317
+ readonly key: string;
318
+ readonly entry: DeclarationEntry;
319
+ }
320
+
321
+ /** The key under which a lifecycle contract's declarations are configured. */
322
+ const LIFECYCLE_KEY = "ts.lifecycle_contracts";
323
+
324
+ /**
325
+ * Every entry of the three keys that name declarations, in the configuration's order:
326
+ * the injection registrations, each lifecycle contract's components and then its bases,
327
+ * and the serializers.
328
+ */
329
+ export function configuredDeclarations(config: Config): readonly ConfiguredDeclaration[] {
330
+ return [
331
+ ...config.ts.injectionRegistrations.map((entry) => ({
332
+ key: "ts.injection_registrations",
333
+ entry,
334
+ })),
335
+ ...config.ts.lifecycleContracts.flatMap((contract) =>
336
+ [...contract.components, ...contract.bases].map((entry) => ({ key: LIFECYCLE_KEY, entry })),
337
+ ),
338
+ ...config.ts.serializers.map((entry) => ({ key: "ts.serializers", entry })),
339
+ ];
340
+ }
341
+
342
+ /**
343
+ * Whether each configured entry names a declaration in one project, in the entries'
344
+ * order. Where the checker leaves a question of the resolution unanswered, every entry
345
+ * counts as naming one, so no entry is reported as naming nothing on a gap.
346
+ */
347
+ export function declarationsNamed<Brand>(
348
+ project: ProjectView<Brand>,
349
+ held: Inventory,
350
+ configured: readonly ConfiguredDeclaration[],
351
+ ): readonly boolean[] {
352
+ try {
353
+ return resolveEntries(
354
+ project,
355
+ held,
356
+ configured.map((one) => one.entry),
357
+ ).map(namesADeclaration);
358
+ } catch (error: unknown) {
359
+ if (!(error instanceof Unanswerable)) {
360
+ throw error;
361
+ }
362
+ return configured.map(() => true);
363
+ }
364
+ }
365
+
366
+ /**
367
+ * The reference a finding about a configured entry names it by, the entry as the
368
+ * configuration spells it: a symbol entry's reference, a module entry's module, a number
369
+ * sign and its name, and a global entry's path after a number sign.
370
+ */
371
+ export function configuredRef(entry: DeclarationEntry): string {
372
+ switch (entry.shape) {
373
+ case "symbol":
374
+ return entry.symbol;
375
+ case "module":
376
+ return `${entry.module}#${entry.name}`;
377
+ case "global":
378
+ return `#${entry.global}`;
379
+ }
380
+ }
@@ -2,9 +2,9 @@
2
2
  // Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
3
3
  export const RECORD = {
4
4
  conformance: {
5
- corpusVersion: "1.9.0",
5
+ corpusVersion: "2.0.0",
6
6
  result: "pass",
7
- digest: "sha256:3e9e40876551faa2c196311bf06d47d220ab5127a9fb8921969aae552c42e824",
7
+ digest: "sha256:1274315595d7b944d22eb0126081e24cf293c9fe53a236f4d263c714644f32ee",
8
8
  },
9
9
  gaps: [
10
10
  {
@@ -17,16 +17,16 @@ import {
17
17
  isStringLiteral,
18
18
  isNoSubstitutionTemplateLiteral,
19
19
  SyntaxKind,
20
+ type Identifier,
20
21
  type Node,
21
22
  type SourceFile,
22
23
  } from "@typescript/native/unstable/ast";
23
- import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
24
24
  import type { FindingPosition } from "./finding.ts";
25
25
  import type { Host } from "./host.ts";
26
26
  import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
27
27
  import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
28
28
  import type { DependencySection, Module } from "./ref.ts";
29
- import { DEFAULT_BATCH_CAP } from "./references.ts";
29
+ import { UNANSWERED } from "./query.ts";
30
30
  import type { Handle, ProjectView } from "./session.ts";
31
31
 
32
32
  const MANIFEST = "package.json";
@@ -70,6 +70,11 @@ export interface ProjectNeeds {
70
70
  * compiler configuration names, which no deletion removes.
71
71
  */
72
72
  readonly uses: ReadonlyMap<string, ReadonlySet<string>>;
73
+ /**
74
+ * Whether the checker left a module specifier unanswered, so the project may use any
75
+ * dependency the manifest declares.
76
+ */
77
+ readonly unanswered: boolean;
73
78
  }
74
79
 
75
80
  /** The run's dependency evidence. */
@@ -299,10 +304,11 @@ export function projectNeeds<Brand>(
299
304
  };
300
305
  use([PROJECT_OWNER], packages);
301
306
 
307
+ let unanswered = false;
302
308
  for (const file of project.ownSourceFiles()) {
303
- fileNeeds(project, held, file, programPackages, use);
309
+ unanswered = fileNeeds(project, held, file, programPackages, use) || unanswered;
304
310
  }
305
- return { packages, uses };
311
+ return { packages, uses, unanswered };
306
312
  }
307
313
 
308
314
  /** The declarations of the inventory a use written at one node belongs to. */
@@ -329,13 +335,14 @@ function ownersOf(file: SourceFile, held: Inventory, node: Node): readonly strin
329
335
  return [PROJECT_OWNER];
330
336
  }
331
337
 
338
+ /** Records the needs of one file, and answers whether a specifier of it went unanswered. */
332
339
  function fileNeeds<Brand>(
333
340
  project: ProjectView<Brand>,
334
341
  held: Inventory,
335
342
  file: SourceFile,
336
343
  programPackages: ReadonlySet<string>,
337
344
  use: (owners: readonly string[], named: Iterable<string>) => void,
338
- ): void {
345
+ ): boolean {
339
346
  const specifiers = [...file.imports, ...file.moduleAugmentations].filter(isLiteralText);
340
347
  const bindings: { readonly node: Texted; readonly specifier: Texted }[] = [];
341
348
  const bound = new Set<Node>();
@@ -351,7 +358,7 @@ function fileNeeds<Brand>(
351
358
  }
352
359
  }
353
360
  const names = new Set(bindings.map(({ node }) => node.text));
354
- const candidates: Node[] = [];
361
+ const candidates: Identifier[] = [];
355
362
  const visit = (node: Node): void => {
356
363
  if (isIdentifier(node) && names.has(node.text) && !bound.has(node)) {
357
364
  candidates.push(node);
@@ -363,13 +370,8 @@ function fileNeeds<Brand>(
363
370
  }
364
371
 
365
372
  const asked = [...specifiers, ...bindings.map(({ node }) => node), ...candidates];
366
- const resolved: (TSSymbol | undefined)[] = [];
367
- for (let at = 0; at < asked.length; at += DEFAULT_BATCH_CAP) {
368
- const handles: Handle<Brand>[] = asked
369
- .slice(at, at + DEFAULT_BATCH_CAP)
370
- .map((node) => project.handle(node));
371
- resolved.push(...project.symbolsAt(handles));
372
- }
373
+ const handles: Handle<Brand>[] = asked.map((node) => project.handle(node));
374
+ const resolved = project.symbolsAt(handles);
373
375
 
374
376
  const packagesOf = new Map<Node, readonly string[]>();
375
377
  specifiers.forEach((specifier, index) => {
@@ -378,7 +380,8 @@ function fileNeeds<Brand>(
378
380
  if (byName !== undefined) {
379
381
  named.add(byName);
380
382
  }
381
- for (const declaration of resolved[index]?.declarations ?? []) {
383
+ const symbol = resolved[index];
384
+ for (const declaration of typeof symbol === "object" ? symbol.declarations : []) {
382
385
  const byPath = packageOfPath(declaration.path);
383
386
  if (byPath !== undefined) {
384
387
  named.add(byPath);
@@ -387,11 +390,19 @@ function fileNeeds<Brand>(
387
390
  packagesOf.set(specifier, [...named]);
388
391
  });
389
392
 
393
+ // A name the checker could not resolve counts as a use of every package a binding of
394
+ // that name brings in, so a gap never makes a dependency read as unused.
390
395
  const bySymbol = new Map<number, readonly string[]>();
391
- bindings.forEach(({ specifier }, index) => {
396
+ const byName = new Map<string, string[]>();
397
+ const unresolvedNames = new Set<string>();
398
+ bindings.forEach(({ node, specifier }, index) => {
399
+ const packages = packagesOf.get(specifier) ?? [];
400
+ byName.set(node.text, [...(byName.get(node.text) ?? []), ...packages]);
392
401
  const symbol = resolved[specifiers.length + index];
393
- if (symbol !== undefined) {
394
- bySymbol.set(symbol.id, packagesOf.get(specifier) ?? []);
402
+ if (symbol === UNANSWERED) {
403
+ unresolvedNames.add(node.text);
404
+ } else if (symbol !== undefined) {
405
+ bySymbol.set(symbol.id, packages);
395
406
  }
396
407
  });
397
408
 
@@ -409,7 +420,13 @@ function fileNeeds<Brand>(
409
420
  if (parent !== undefined && isShorthandPropertyAssignment(parent) && parent.name === node) {
410
421
  symbol = project.shorthandValueAt(project.handle(node));
411
422
  }
412
- const named = symbol === undefined ? undefined : bySymbol.get(symbol.id);
423
+ const named =
424
+ symbol === UNANSWERED || (unresolvedNames.has(node.text) && symbol === undefined)
425
+ ? byName.get(node.text)
426
+ : symbol === undefined
427
+ ? undefined
428
+ : (bySymbol.get(symbol.id) ??
429
+ (unresolvedNames.has(node.text) ? byName.get(node.text) : undefined));
413
430
  if (named !== undefined) {
414
431
  use(ownersOf(file, held, node), named);
415
432
  }
@@ -418,6 +435,7 @@ function fileNeeds<Brand>(
418
435
  for (const reference of file.typeReferenceDirectives) {
419
436
  use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
420
437
  }
438
+ return resolved.slice(0, specifiers.length).includes(UNANSWERED);
421
439
  }
422
440
 
423
441
  /** The offset just past the JSON value that starts at `at` in text the parser has accepted. */
@@ -591,12 +609,16 @@ export function dependenciesOf(
591
609
  const needed = new Set<string>();
592
610
  const uses = new Map<string, Set<string>>();
593
611
  for (const project of perProject) {
594
- project.packages.forEach((one) => needed.add(one));
595
- for (const [id, used] of project.uses) {
612
+ // A specifier the checker left unanswered may resolve to any declared dependency,
613
+ // and what the configuration needs is never deleted, so it is needed there.
614
+ const unknown = project.unanswered ? declared.map((dependency) => dependency.name) : [];
615
+ const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)] as const];
616
+ for (const [id, used] of [...project.uses, ...held]) {
596
617
  const merged = uses.get(id) ?? new Set<string>();
597
618
  used.forEach((one) => merged.add(one));
598
619
  uses.set(id, merged);
599
620
  }
621
+ [...project.packages, ...unknown].forEach((one) => needed.add(one));
600
622
  }
601
623
  const users = new Map<string, number>();
602
624
  for (const used of uses.values()) {
@@ -8,9 +8,8 @@
8
8
  */
9
9
 
10
10
  import { isVariableStatement, SyntaxKind, type Node } from "@typescript/native/unstable/ast";
11
- import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
12
11
  import { nodeKey, type Inventory } from "./inventory.ts";
13
- import { DEFAULT_BATCH_CAP } from "./references.ts";
12
+ import { isAnswered, UNANSWERED } from "./query.ts";
14
13
  import type { Handle, ProjectView } from "./session.ts";
15
14
 
16
15
  /** The tag the TypeScript tools recognize as marking a declaration deprecated. */
@@ -31,19 +30,31 @@ function nameNode(node: Node): Node {
31
30
  return (node as { readonly name?: Node }).name ?? node;
32
31
  }
33
32
 
33
+ /** Which declarations of one project carry the deprecation marker. */
34
+ export interface Deprecation {
35
+ /** Every declaration that carries it, in the inventory's order. */
36
+ readonly deprecated: readonly string[];
37
+ /**
38
+ * Every declaration whose symbol or tags the checker left unanswered where the
39
+ * project's files hold the tag, in the inventory's order: whether it carries the
40
+ * marker is unknown.
41
+ */
42
+ readonly unanswered: readonly string[];
43
+ }
44
+
34
45
  /**
35
- * The identifiers of every declaration of one project that carries the deprecation
36
- * marker, in the inventory's order.
46
+ * The declarations of one project that carry the deprecation marker.
37
47
  *
38
48
  * The checker is asked only for a symbol with a declaration in a file whose tree holds
39
49
  * the tag, so a project whose files hold none costs no round trip, and one that does
40
- * costs one batch per file and one request per symbol that could carry the tag.
50
+ * costs one batch per file and one for the tags of the symbols that could carry it.
41
51
  */
42
52
  export function deprecatedDeclarations<Brand>(
43
53
  project: ProjectView<Brand>,
44
54
  held: Inventory,
45
- ): readonly string[] {
55
+ ): Deprecation {
46
56
  const marked = new Set<string>();
57
+ const unknown = new Set<string>();
47
58
  const markedPaths = new Set<string>();
48
59
  const declared: { readonly id: string; readonly handle: Handle<Brand> }[][] = [];
49
60
 
@@ -72,30 +83,45 @@ export function deprecatedDeclarations<Brand>(
72
83
  }
73
84
 
74
85
  if (markedPaths.size > 0) {
75
- const asked = new Map<number, boolean>();
76
- const carriesTag = (symbol: TSSymbol): boolean => {
77
- const known = asked.get(symbol.id);
78
- if (known !== undefined) {
79
- return known;
86
+ const pairs = declared.flatMap((named) => {
87
+ const symbols = project.symbolsAt(named.map((one) => one.handle));
88
+ return named.flatMap((one, at) => {
89
+ const symbol = symbols[at];
90
+ if (symbol === UNANSWERED) {
91
+ unknown.add(one.id);
92
+ }
93
+ return typeof symbol === "object" &&
94
+ symbol.declarations.some((declaration) => markedPaths.has(declaration.path))
95
+ ? [{ id: one.id, symbol }]
96
+ : [];
97
+ });
98
+ });
99
+ const distinct = [...new Map(pairs.map((one) => [one.symbol.id, one.symbol])).values()];
100
+ const tags = project.queries.jsDocTags(distinct);
101
+ const unread = new Set(
102
+ distinct.filter((_, at) => tags[at] === UNANSWERED).map((one) => one.id),
103
+ );
104
+ const carries = new Set(
105
+ distinct.flatMap((symbol, at) => {
106
+ const read = tags[at];
107
+ return read !== undefined &&
108
+ isAnswered(read) &&
109
+ read.some((tag) => tag.name === DEPRECATED_TAG)
110
+ ? [symbol.id]
111
+ : [];
112
+ }),
113
+ );
114
+ for (const one of pairs) {
115
+ if (carries.has(one.symbol.id)) {
116
+ marked.add(one.id);
80
117
  }
81
- const carries =
82
- symbol.declarations.some((declaration) => markedPaths.has(declaration.path)) &&
83
- symbol.getJsDocTags(project.checker).some((tag) => tag.name === DEPRECATED_TAG);
84
- asked.set(symbol.id, carries);
85
- return carries;
86
- };
87
- for (const named of declared) {
88
- for (let start = 0; start < named.length; start += DEFAULT_BATCH_CAP) {
89
- const batch = named.slice(start, start + DEFAULT_BATCH_CAP);
90
- project.symbolsAt(batch.map((one) => one.handle)).forEach((symbol, at) => {
91
- const one = batch[at];
92
- if (symbol !== undefined && one !== undefined && carriesTag(symbol)) {
93
- marked.add(one.id);
94
- }
95
- });
118
+ if (unread.has(one.symbol.id)) {
119
+ unknown.add(one.id);
96
120
  }
97
121
  }
98
122
  }
99
123
 
100
- return held.symbols.filter((symbol) => marked.has(symbol.id)).map((symbol) => symbol.id);
124
+ const inOrder = (ids: ReadonlySet<string>): readonly string[] =>
125
+ held.symbols.filter((symbol) => ids.has(symbol.id)).map((symbol) => symbol.id);
126
+ return { deprecated: inOrder(marked), unanswered: inOrder(unknown) };
101
127
  }