@cplieger/deadset-ts 3.13.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +67 -49
  2. package/dist/bin/node-host.js +10 -7
  3. package/dist/src/alias-chain.js +59 -7
  4. package/dist/src/analysis.js +120 -24
  5. package/dist/src/calls.js +23 -22
  6. package/dist/src/class-members.js +10 -11
  7. package/dist/src/configured-declarations.js +22 -9
  8. package/dist/src/dependencies.js +34 -17
  9. package/dist/src/deprecation.js +35 -24
  10. package/dist/src/destructuring.js +37 -29
  11. package/dist/src/discover.js +106 -29
  12. package/dist/src/entry-point-gaps.js +2 -2
  13. package/dist/src/entry-points.js +7 -0
  14. package/dist/src/enum-group.js +46 -40
  15. package/dist/src/exempt.js +42 -1
  16. package/dist/src/findings/emitters.js +15 -1
  17. package/dist/src/findings/interfaces.js +7 -1
  18. package/dist/src/framework-lifecycle.js +8 -6
  19. package/dist/src/implementations.js +45 -23
  20. package/dist/src/interface-satisfaction.js +30 -27
  21. package/dist/src/intra-function-parts.js +37 -20
  22. package/dist/src/inventory.js +85 -4
  23. package/dist/src/json-chunks.js +76 -0
  24. package/dist/src/partial.js +23 -0
  25. package/dist/src/query.js +207 -0
  26. package/dist/src/references.js +92 -20
  27. package/dist/src/reflective-lookup.js +18 -9
  28. package/dist/src/report.js +3 -1
  29. package/dist/src/reporters/annotations.js +9 -5
  30. package/dist/src/reporters/json.js +3 -2
  31. package/dist/src/reporters/sarif.js +2 -1
  32. package/dist/src/reporters/template.js +51 -10
  33. package/dist/src/reporters/text.js +8 -6
  34. package/dist/src/roots.js +27 -11
  35. package/dist/src/schema.js +2 -2
  36. package/dist/src/serialization-contract.js +12 -10
  37. package/dist/src/session.js +79 -39
  38. package/dist/src/suppress-file.js +2 -1
  39. package/dist/src/sweep.js +4 -1
  40. package/dist/src/value-reach.js +14 -19
  41. package/dist/src/verbs/analyze.js +20 -3
  42. package/dist/src/verbs/explain.js +22 -9
  43. package/dist/src/verbs/print-projects.js +41 -8
  44. package/dist/src/verbs/print-retained.js +6 -2
  45. package/dist/src/verbs/print-roots.js +5 -1
  46. package/jsr.json +1 -1
  47. package/package.json +3 -3
  48. package/src/alias-chain.ts +75 -11
  49. package/src/analysis.ts +224 -62
  50. package/src/calls.ts +10 -9
  51. package/src/class-members.ts +10 -11
  52. package/src/configured-declarations.ts +29 -13
  53. package/src/dependencies.ts +42 -20
  54. package/src/deprecation.ts +53 -27
  55. package/src/destructuring.ts +50 -39
  56. package/src/discover.ts +178 -34
  57. package/src/entry-point-gaps.ts +2 -2
  58. package/src/entry-points.ts +7 -0
  59. package/src/enum-group.ts +56 -42
  60. package/src/exempt.ts +50 -2
  61. package/src/findings/emitters.ts +18 -1
  62. package/src/findings/interfaces.ts +7 -1
  63. package/src/framework-lifecycle.ts +10 -6
  64. package/src/host.ts +5 -3
  65. package/src/implementations.ts +59 -23
  66. package/src/interface-satisfaction.ts +30 -27
  67. package/src/intra-function-parts.ts +43 -25
  68. package/src/inventory.ts +97 -4
  69. package/src/json-chunks.ts +87 -0
  70. package/src/partial.ts +40 -0
  71. package/src/query.ts +522 -0
  72. package/src/references.ts +119 -38
  73. package/src/reflective-lookup.ts +21 -11
  74. package/src/report.ts +14 -2
  75. package/src/reporters/annotations.ts +23 -21
  76. package/src/reporters/json.ts +3 -2
  77. package/src/reporters/reporter.ts +2 -2
  78. package/src/reporters/sarif.ts +3 -2
  79. package/src/reporters/template.ts +53 -11
  80. package/src/reporters/text.ts +12 -12
  81. package/src/roots.ts +34 -12
  82. package/src/schema.ts +2 -2
  83. package/src/serialization-contract.ts +12 -10
  84. package/src/session.ts +146 -58
  85. package/src/suppress-file.ts +6 -2
  86. package/src/sweep.ts +9 -4
  87. package/src/value-reach.ts +20 -23
  88. package/src/verbs/analyze.ts +25 -3
  89. package/src/verbs/explain.ts +20 -9
  90. package/src/verbs/print-projects.ts +54 -8
  91. package/src/verbs/print-retained.ts +6 -2
  92. package/src/verbs/print-roots.ts +5 -1
@@ -9,7 +9,7 @@ import { isClassDeclaration, isHeritageClause, isInterfaceDeclaration, isMethodD
9
9
  import { SymbolFlags } from "@typescript/native/unstable/sync";
10
10
  import { aliasChains } from "./alias-chain.js";
11
11
  import { nodeKey } from "./inventory.js";
12
- import { DEFAULT_BATCH_CAP } from "./references.js";
12
+ import { must, Unanswerable } from "./query.js";
13
13
  /** The interfaces, the classes and the `implements` entries of one project's own files. */
14
14
  function walk(project, held) {
15
15
  const interfaces = [];
@@ -41,23 +41,21 @@ function walk(project, held) {
41
41
  }
42
42
  return { interfaces, classes, clauses };
43
43
  }
44
- /** The type at each node, asked in capped batches. */
44
+ /** The type at each node. */
45
45
  function typesAt(project, nodes) {
46
46
  const typeAt = new Map();
47
- for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
48
- const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
49
- project.checker.getTypeAtLocation(run).forEach((type, index) => {
50
- const node = run[index];
51
- if (type !== undefined && node !== undefined) {
52
- typeAt.set(node, type);
53
- }
54
- });
55
- }
47
+ project.queries.typesAt(nodes).forEach((answer, index) => {
48
+ const type = must(answer);
49
+ const node = nodes[index];
50
+ if (type !== undefined && node !== undefined) {
51
+ typeAt.set(node, type);
52
+ }
53
+ });
56
54
  return typeAt;
57
55
  }
58
56
  /** Whether one class member is a method whose every written body holds no statement. */
59
57
  function emptyImplementation(project, member) {
60
- const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
58
+ const own = project.ownPaths();
61
59
  let bodies = 0;
62
60
  for (const handle of member.declarations) {
63
61
  // A body outside the target is one this analysis cannot read, so it is not empty.
@@ -77,15 +75,32 @@ function emptyImplementation(project, member) {
77
75
  }
78
76
  return bodies > 0;
79
77
  }
80
- /** How one project's interfaces are implemented by its classes. */
78
+ /**
79
+ * How one project's interfaces are implemented by its classes. Where the checker leaves
80
+ * a question unanswered, what was read stands and every interface method of the project
81
+ * is named in {@link Implementations.unknown}.
82
+ */
81
83
  export function implementations(project, held) {
82
84
  const classes = new Map();
83
85
  const bodies = new Map();
86
+ try {
87
+ implementedIn(project, held, classes, bodies);
88
+ return { classes, bodies, unknown: new Set() };
89
+ }
90
+ catch (error) {
91
+ if (!(error instanceof Unanswerable)) {
92
+ throw error;
93
+ }
94
+ const methods = held.symbols.filter((symbol) => symbol.kind === "interface-method");
95
+ return { classes, bodies, unknown: new Set(methods.map((symbol) => symbol.id)) };
96
+ }
97
+ }
98
+ function implementedIn(project, held, classes, bodies) {
84
99
  const walked = walk(project, held);
85
100
  if (walked.interfaces.length === 0 || walked.classes.length === 0) {
86
- return { classes, bodies };
101
+ return;
87
102
  }
88
- const checker = project.checker;
103
+ const queries = project.queries;
89
104
  const typeAt = typesAt(project, [
90
105
  ...walked.interfaces.map((one) => one.nameNode),
91
106
  ...walked.classes.map((one) => one.nameNode),
@@ -114,8 +129,8 @@ export function implementations(project, held) {
114
129
  classAt.set(owner.id, { owner, type });
115
130
  }
116
131
  }
117
- const membersOf = (at) => (at.members ??= checker.getPropertiesOfType(at.type));
118
- const classMembers = (at) => (at.members ??= new Map(checker.getPropertiesOfType(at.type).map((member) => [member.escapedName, member])));
132
+ const membersOf = (at) => (at.members ??= must(queries.propertiesOf(at.type)));
133
+ const classMembers = (at) => (at.members ??= new Map(must(queries.propertiesOf(at.type)).map((member) => [member.escapedName, member])));
119
134
  const methodIds = new Set(held.symbols.filter((symbol) => symbol.kind === "interface-method").map((symbol) => symbol.id));
120
135
  const chains = aliasChains(project, held);
121
136
  const record = (at, implementer) => {
@@ -161,11 +176,13 @@ export function implementations(project, held) {
161
176
  if (entry === undefined || implementer === undefined) {
162
177
  continue;
163
178
  }
164
- const at = byType.get(entry.isTypeReference() ? entry.getTarget().id : entry.id);
179
+ const at = byType.get(must(queries.targetOf(entry)).id);
165
180
  if (at !== undefined) {
166
181
  pair(at, implementer);
167
182
  }
168
183
  }
184
+ // Every pair whose names line up is asked about at once.
185
+ const candidates = [];
169
186
  for (const at of byType.values()) {
170
187
  const members = at.generic ? [] : membersOf(at);
171
188
  if (members.length === 0) {
@@ -178,16 +195,21 @@ export function implementations(project, held) {
178
195
  }
179
196
  const written = classMembers(implementer);
180
197
  if (required.every((member) => written.has(member.escapedName)) &&
181
- members.some((member) => written.has(member.escapedName)) &&
182
- checker.isTypeAssignableTo(implementer.type, at.type)) {
183
- pair(at, implementer);
198
+ members.some((member) => written.has(member.escapedName))) {
199
+ candidates.push({ at, implementer });
184
200
  }
185
201
  }
186
202
  }
187
- return { classes, bodies };
203
+ const assignable = queries.assignableEach(candidates.map((one) => ({ source: one.implementer.type, target: one.at.type })));
204
+ candidates.forEach((one, index) => {
205
+ if (must(assignable[index] ?? false)) {
206
+ pair(one.at, one.implementer);
207
+ }
208
+ });
188
209
  }
189
210
  /** Every project's answers as one run's, each implementation once. */
190
211
  export function mergeImplementations(per) {
212
+ const unknown = new Set(per.flatMap((one) => [...one.unknown]));
191
213
  const classes = new Map();
192
214
  const bodies = new Map();
193
215
  for (const one of per) {
@@ -206,5 +228,5 @@ export function mergeImplementations(per) {
206
228
  }
207
229
  }
208
230
  }
209
- return { classes, bodies };
231
+ return { classes, bodies, unknown };
210
232
  }
@@ -17,7 +17,7 @@ import { SymbolFlags } from "@typescript/native/unstable/sync";
17
17
  import { aliasChains } from "./alias-chain.js";
18
18
  import { nodeKey } from "./inventory.js";
19
19
  import { renderPosition } from "./position.js";
20
- import { DEFAULT_BATCH_CAP } from "./references.js";
20
+ import { must } from "./query.js";
21
21
  /** The operators of an assignment whose right side is stored into the left side's type. */
22
22
  const STORES = new Set([
23
23
  SyntaxKind.EqualsToken,
@@ -157,7 +157,7 @@ function walk(files, declarations) {
157
157
  */
158
158
  export function interfaceSatisfaction(input) {
159
159
  const { project, held, targetRoot } = input;
160
- const checker = project.checker;
160
+ const queries = project.queries;
161
161
  const files = project.ownSourceFiles();
162
162
  const { classes, clauses, flows } = walk(files, held.declarations);
163
163
  if (classes.length === 0) {
@@ -171,15 +171,13 @@ export function interfaceSatisfaction(input) {
171
171
  ...flows.filter((flow) => flow.self === undefined).map((flow) => flow.value),
172
172
  ];
173
173
  const typeAt = new Map();
174
- for (let from = 0; from < asked.length; from += DEFAULT_BATCH_CAP) {
175
- const run = asked.slice(from, from + DEFAULT_BATCH_CAP);
176
- checker.getTypeAtLocation(run).forEach((type, index) => {
177
- const node = run[index];
178
- if (type !== undefined && node !== undefined) {
179
- typeAt.set(node, type);
180
- }
181
- });
182
- }
174
+ queries.typesAt(asked).forEach((answer, index) => {
175
+ const type = must(answer);
176
+ const node = asked[index];
177
+ if (type !== undefined && node !== undefined) {
178
+ typeAt.set(node, type);
179
+ }
180
+ });
183
181
  const classOfType = new Map();
184
182
  const instanceOf = new Map();
185
183
  for (const owner of classes) {
@@ -197,7 +195,7 @@ export function interfaceSatisfaction(input) {
197
195
  }
198
196
  let parts = constituents.get(type.id);
199
197
  if (parts === undefined) {
200
- parts = type.getTypes();
198
+ parts = must(queries.constituents(type));
201
199
  constituents.set(type.id, parts);
202
200
  }
203
201
  return parts;
@@ -213,7 +211,7 @@ export function interfaceSatisfaction(input) {
213
211
  return [];
214
212
  }
215
213
  if (!targets.has(part.id)) {
216
- targets.set(part.id, classOfType.get(part.getTarget().id));
214
+ targets.set(part.id, classOfType.get(must(queries.targetOf(part)).id));
217
215
  }
218
216
  const generic = targets.get(part.id);
219
217
  return generic === undefined ? [] : [{ owner: generic, type: part }];
@@ -222,7 +220,7 @@ export function interfaceSatisfaction(input) {
222
220
  /** The interface types one position's type is or holds, each with the interface's name. */
223
221
  const interfacesOf = (type) => partsOf(type).flatMap((part) => {
224
222
  if (!interfaceNames.has(part.id)) {
225
- const symbol = part.isObjectType() ? part.getSymbol() : undefined;
223
+ const symbol = part.isObjectType() ? must(queries.symbolOfType(part)) : undefined;
226
224
  interfaceNames.set(part.id, symbol !== undefined && (symbol.flags & SymbolFlags.Interface) !== 0
227
225
  ? symbol.name
228
226
  : undefined);
@@ -251,7 +249,8 @@ export function interfaceSatisfaction(input) {
251
249
  reach(clause.owner, source, reached.type, reached.name, clause.entry);
252
250
  }
253
251
  }
254
- for (const flow of flows) {
252
+ // The positions of every flow carrying a class are asked for at once.
253
+ const carrying = flows.flatMap((flow) => {
255
254
  const self = flow.self === undefined ? undefined : instanceOf.get(flow.self.id);
256
255
  const valueType = typeAt.get(flow.value);
257
256
  const sources = flow.self !== undefined && self !== undefined
@@ -259,38 +258,42 @@ export function interfaceSatisfaction(input) {
259
258
  : valueType === undefined
260
259
  ? []
261
260
  : classesOf(valueType);
262
- if (sources.length === 0) {
263
- continue;
264
- }
265
- const position = checker.getContextualType(flow.value);
261
+ return sources.length === 0 ? [] : [{ flow, sources }];
262
+ });
263
+ const positions = queries.contextualTypes(carrying.map((one) => one.flow.value));
264
+ carrying.forEach(({ flow, sources }, index) => {
265
+ const position = must(positions[index]);
266
266
  if (position === undefined) {
267
- continue;
267
+ return;
268
268
  }
269
269
  for (const reached of interfacesOf(position)) {
270
270
  for (const source of sources) {
271
271
  reach(source.owner, source.type, reached.type, reached.name, flow.value);
272
272
  }
273
273
  }
274
- }
274
+ });
275
275
  const chains = aliasChains(project, held);
276
276
  const required = new Map();
277
277
  const members = new Map();
278
278
  const found = [];
279
- for (const pair of pairs.values()) {
280
- if (!checker.isTypeAssignableTo(pair.source, pair.target)) {
279
+ const asking = [...pairs.values()];
280
+ const assignable = queries.assignableEach(asking);
281
+ for (const [index, pair] of asking.entries()) {
282
+ if (!must(assignable[index] ?? false)) {
281
283
  continue;
282
284
  }
283
285
  let names = required.get(pair.target.id);
284
286
  if (names === undefined) {
285
- names = new Set(checker.getPropertiesOfType(pair.target).map((property) => property.escapedName));
287
+ names = new Set(must(queries.propertiesOf(pair.target)).map((property) => property.escapedName));
286
288
  required.set(pair.target.id, names);
287
289
  }
288
290
  let answering = members.get(pair.owner.id);
289
291
  if (answering === undefined) {
290
292
  const instance = instanceOf.get(pair.owner.id) ?? pair.source;
291
- answering = new Map(checker
292
- .getPropertiesOfType(instance)
293
- .map((property) => [property.escapedName, chains.declarationsOf(property)]));
293
+ answering = new Map(must(queries.propertiesOf(instance)).map((property) => [
294
+ property.escapedName,
295
+ chains.declarationsOf(property),
296
+ ]));
294
297
  members.set(pair.owner.id, answering);
295
298
  }
296
299
  const detail = `satisfies ${pair.name}`;
@@ -4,10 +4,10 @@
4
4
  * call sites that use or discard it, each statement control flow cannot reach, each
5
5
  * write to a local that no read reaches, and each case an earlier case covers.
6
6
  */
7
- import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isCallExpression, isCaseClause, isClassDeclaration, isClassExpression, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
7
+ import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isCallExpression, isCaseClause, isClassDeclaration, isClassExpression, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isQualifiedName, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isTypeReferenceNode, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
8
8
  import { nodeKey } from "./inventory.js";
9
9
  import { positionKey, renderPosition } from "./position.js";
10
- import { DEFAULT_BATCH_CAP } from "./references.js";
10
+ import { isAnswered, UNANSWERED } from "./query.js";
11
11
  import { signatureOf } from "./signatures.js";
12
12
  /** The codes of the parts this module reads. */
13
13
  export const UNUSED_PARAMETER = "DS1801";
@@ -195,8 +195,15 @@ function extendedNames(files) {
195
195
  const visit = (node) => {
196
196
  if (isHeritageClause(node) && node.token === SyntaxKind.ExtendsKeyword) {
197
197
  for (const type of node.types) {
198
- const expression = unwrapped(type.expression);
199
- const named = isPropertyAccessExpression(expression) ? expression.name : expression;
198
+ // A class's clause holds expressions; an interface's holds type references.
199
+ let named;
200
+ if (isTypeReferenceNode(type)) {
201
+ named = isQualifiedName(type.typeName) ? type.typeName.right : type.typeName;
202
+ }
203
+ else {
204
+ const expression = unwrapped(type.expression);
205
+ named = isPropertyAccessExpression(expression) ? expression.name : expression;
206
+ }
200
207
  if (isIdentifier(named)) {
201
208
  names.add(named.text);
202
209
  }
@@ -230,31 +237,33 @@ function hierarchyFixes(declared, extended) {
230
237
  }
231
238
  return owner.name !== undefined && extended.has(owner.name.text);
232
239
  }
233
- /** The symbol identifier each of a batch of identifiers resolves to, where the batch answers. */
240
+ /**
241
+ * The symbol identifier each of a batch of identifiers resolves to, where the batch
242
+ * resolves it, and {@link UNANSWERED} where the checker did not answer.
243
+ */
234
244
  function resolvedIds(project, nodes) {
235
245
  const ids = new Map();
236
- for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
237
- const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
238
- project.symbolsAt(run.map((node) => project.handle(node))).forEach((symbol, index) => {
239
- const node = run[index];
240
- if (symbol !== undefined && node !== undefined) {
241
- ids.set(node, symbol.id);
242
- }
243
- });
244
- }
246
+ project.symbolsAt(nodes.map((node) => project.handle(node))).forEach((symbol, index) => {
247
+ const node = nodes[index];
248
+ if (node !== undefined && symbol !== undefined) {
249
+ ids.set(node, symbol === UNANSWERED ? UNANSWERED : symbol.id);
250
+ }
251
+ });
245
252
  return ids;
246
253
  }
247
254
  /**
248
255
  * Whether one identifier reads the binding a name node declares: it resolves to the
249
- * same symbol, or the checker resolves it to nothing, which is read as a use rather than
250
- * risk a finding; a shorthand property reads the binding of its own name.
256
+ * same symbol, or the checker resolves it to nothing or does not answer for either
257
+ * name, which is read as a use rather than risk a finding; a shorthand property reads
258
+ * the binding of its own name.
251
259
  */
252
260
  function readsBinding(use, declared, ids) {
253
261
  if (isShorthandPropertyAssignment(use.parent)) {
254
262
  return true;
255
263
  }
256
264
  const at = ids.get(use);
257
- return at === undefined || at === ids.get(declared);
265
+ const own = ids.get(declared);
266
+ return at === undefined || at === UNANSWERED || own === UNANSWERED || at === own;
258
267
  }
259
268
  /**
260
269
  * The parts of one project's own declarations. `references` are the project's own
@@ -575,9 +584,17 @@ function resultParts(project, root, sites, references, calls) {
575
584
  if (!discardedAt.has(id) || generator || !returnsAValue(signature)) {
576
585
  continue;
577
586
  }
578
- const typed = project.checker.getSignatureFromDeclaration(signature);
579
- const result = typed === undefined ? undefined : project.checker.getReturnTypeOfSignature(typed);
580
- if (result === undefined || NO_VALUE.has(project.checker.typeToString(result))) {
587
+ // A question the checker leaves unanswered withholds the finding.
588
+ const typed = project.queries.signatureOf(signature);
589
+ if (typed === undefined || !isAnswered(typed)) {
590
+ continue;
591
+ }
592
+ const result = project.queries.returnTypeOf(typed);
593
+ if (result === undefined || !isAnswered(result)) {
594
+ continue;
595
+ }
596
+ const spelled = project.queries.typeToString(result);
597
+ if (!isAnswered(spelled) || NO_VALUE.has(spelled)) {
581
598
  continue;
582
599
  }
583
600
  const at = signature.type ?? node.name ?? node;
@@ -2,6 +2,7 @@ import { isClassDeclaration, isComputedPropertyName, isEnumDeclaration, isEnumMe
2
2
  import { dirnamePath, joinPath, relativePath } from "./paths.js";
3
3
  import { byPosition, positionKey, renderPosition } from "./position.js";
4
4
  import { computedComponent, nameComponent, renderRef } from "./ref.js";
5
+ import { isAnswered, UNANSWERED } from "./query.js";
5
6
  const MANIFEST = "package.json";
6
7
  /**
7
8
  * Resolves each file's package scope: the name of the nearest manifest at or above
@@ -493,12 +494,28 @@ export function inventory(project, host, targetRoot) {
493
494
  const handles = batched.map((node) => project.handle(node));
494
495
  const resolved = project.symbolsAt(handles);
495
496
  const symbolOf = new Map();
497
+ // A scope or a container whose symbol or table went unanswered keeps what its syntax
498
+ // declares, and everything under it is recorded unanswered.
499
+ const unansweredOwners = new Set();
496
500
  batched.forEach((node, index) => {
497
501
  const symbol = resolved[index];
498
- if (symbol !== undefined) {
502
+ if (typeof symbol === "object") {
499
503
  symbolOf.set(node, symbol);
500
504
  }
501
505
  });
506
+ resolved.slice(0, exportScopes.length).forEach((symbol, index) => {
507
+ const scope = exportScopes[index];
508
+ if (symbol === UNANSWERED && scope !== undefined) {
509
+ unansweredOwners.add(scope.owner);
510
+ }
511
+ });
512
+ containers
513
+ .filter((container) => container.nameNode !== undefined)
514
+ .forEach((container, index) => {
515
+ if (resolved[exportScopes.length + index] === UNANSWERED) {
516
+ unansweredOwners.add(container.owner);
517
+ }
518
+ });
502
519
  let exportTables = 0;
503
520
  for (const scope of exportScopes) {
504
521
  const symbol = symbolOf.get(scope.nameNode);
@@ -506,7 +523,11 @@ export function inventory(project, host, targetRoot) {
506
523
  continue;
507
524
  }
508
525
  exportTables += 1;
509
- for (const [, exported] of symbol.getExports()) {
526
+ const table = project.queries.exportsOf(symbol);
527
+ if (!isAnswered(table)) {
528
+ unansweredOwners.add(scope.owner);
529
+ }
530
+ for (const [, exported] of isAnswered(table) ? table : []) {
510
531
  for (const declaration of exported.declarations) {
511
532
  const held = project.declarationAt(declaration);
512
533
  if (held === undefined) {
@@ -527,11 +548,23 @@ export function inventory(project, host, targetRoot) {
527
548
  if (symbol !== undefined) {
528
549
  if (!container.membersAreExports) {
529
550
  memberTables += 1;
530
- outsideOwnFiles += readTable(container, symbol.getMembers(), seen, false);
551
+ const table = project.queries.membersOf(symbol);
552
+ if (isAnswered(table)) {
553
+ outsideOwnFiles += readTable(container, table, seen, false);
554
+ }
555
+ else {
556
+ unansweredOwners.add(container.owner);
557
+ }
531
558
  }
532
559
  if (container.membersAreExports || container.isClass) {
533
560
  memberTables += 1;
534
- outsideOwnFiles += readTable(container, symbol.getExports(), seen, container.isClass);
561
+ const table = project.queries.exportsOf(symbol);
562
+ if (isAnswered(table)) {
563
+ outsideOwnFiles += readTable(container, table, seen, container.isClass);
564
+ }
565
+ else {
566
+ unansweredOwners.add(container.owner);
567
+ }
535
568
  }
536
569
  }
537
570
  completeFromTree(container, seen);
@@ -635,6 +668,7 @@ export function inventory(project, host, targetRoot) {
635
668
  }
636
669
  return undefined;
637
670
  }
671
+ const byId = new Map(building.map((record) => [positionKey(record.position), record]));
638
672
  const symbols = building
639
673
  .map(render)
640
674
  .sort((a, b) => byPosition(a.position, b.position) || (a.ref < b.ref ? -1 : 1));
@@ -645,8 +679,47 @@ export function inventory(project, host, targetRoot) {
645
679
  cost: { batches: 1, exportTables, memberTables },
646
680
  starReExports,
647
681
  outsideOwnFiles,
682
+ unanswered: symbols
683
+ .filter((symbol) => underUnanswered(byId.get(symbol.id), unansweredOwners))
684
+ .map((symbol) => symbol.id),
685
+ byName: namesOf(building),
648
686
  };
649
687
  }
688
+ /** Whether one declaration is, or is declared under, a scope or container left unanswered. */
689
+ function underUnanswered(record, owners) {
690
+ for (let at = record; at !== undefined; at = at.parent) {
691
+ if (owners.has(at)) {
692
+ return true;
693
+ }
694
+ }
695
+ return false;
696
+ }
697
+ /** Every declaration but a file, by each name a use may spell it with. */
698
+ function namesOf(building) {
699
+ const byName = new Map();
700
+ for (const record of building) {
701
+ if (record.kind === "file") {
702
+ continue;
703
+ }
704
+ const id = positionKey(record.position);
705
+ const names = record.typeParameter === undefined
706
+ ? [record.component.text, record.localName]
707
+ : [record.typeParameter];
708
+ for (const name of new Set(names)) {
709
+ if (name === undefined) {
710
+ continue;
711
+ }
712
+ const ids = byName.get(name);
713
+ if (ids === undefined) {
714
+ byName.set(name, [id]);
715
+ }
716
+ else if (!ids.includes(id)) {
717
+ ids.push(id);
718
+ }
719
+ }
720
+ }
721
+ return byName;
722
+ }
650
723
  /** The chain of components that names one declaration, read from the module inward. */
651
724
  function chainOf(record) {
652
725
  return [...record.parentChain, record.component];
@@ -691,3 +764,11 @@ function refOf(record, chain) {
691
764
  ...(record.typeParameter === undefined ? {} : { typeParameter: record.typeParameter }),
692
765
  });
693
766
  }
767
+ /**
768
+ * Every declaration of an inventory other than a file, by each name a use may spell it
769
+ * with. It is how a reference the checker could not resolve is still counted, as a
770
+ * reference to every declaration that bears the name it spells.
771
+ */
772
+ export function declarationsByName(held) {
773
+ return held.byName;
774
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * A document written in pieces, so no stage of writing it holds the whole document as
3
+ * one string: a string has a length limit far below what a large report reaches.
4
+ */
5
+ /** The length a piece grows to before it is handed on. */
6
+ export const CHUNK_LENGTH = 1 << 16;
7
+ /**
8
+ * The pieces of `pieces` joined into runs of about {@link CHUNK_LENGTH}, the last one
9
+ * shorter. A piece longer than that is handed on whole.
10
+ */
11
+ export function* coalesced(pieces) {
12
+ let held = [];
13
+ let length = 0;
14
+ for (const piece of pieces) {
15
+ held.push(piece);
16
+ length += piece.length;
17
+ if (length >= CHUNK_LENGTH) {
18
+ yield held.join("");
19
+ held = [];
20
+ length = 0;
21
+ }
22
+ }
23
+ if (length > 0) {
24
+ yield held.join("");
25
+ }
26
+ }
27
+ /** Whether `JSON.stringify` writes nothing for a value where an object member holds it. */
28
+ function omitted(value) {
29
+ return value === undefined || typeof value === "function" || typeof value === "symbol";
30
+ }
31
+ function* valueOf(value, prefix, step) {
32
+ // With no indentation the document is compact: no line breaks, and no space after a key.
33
+ const nl = step === "" ? "" : "\n";
34
+ if (Array.isArray(value)) {
35
+ if (value.length === 0) {
36
+ yield "[]";
37
+ return;
38
+ }
39
+ const inner = prefix + step;
40
+ yield "[";
41
+ for (let at = 0; at < value.length; at += 1) {
42
+ yield at === 0 ? `${nl}${inner}` : `,${nl}${inner}`;
43
+ const element = value[at];
44
+ yield* omitted(element) ? ["null"] : valueOf(element, inner, step);
45
+ }
46
+ yield `${nl}${prefix}]`;
47
+ return;
48
+ }
49
+ if (typeof value === "object" && value !== null) {
50
+ const inner = prefix + step;
51
+ let first = true;
52
+ for (const [key, member] of Object.entries(value)) {
53
+ if (omitted(member)) {
54
+ continue;
55
+ }
56
+ yield `${first ? "{" : ","}${nl}${inner}${JSON.stringify(key)}:${step === "" ? "" : " "}`;
57
+ first = false;
58
+ yield* valueOf(member, inner, step);
59
+ }
60
+ yield first ? "{}" : `${nl}${prefix}}`;
61
+ return;
62
+ }
63
+ yield JSON.stringify(value);
64
+ }
65
+ /**
66
+ * One JSON document in pieces whose concatenation is the value as `JSON.stringify`
67
+ * writes it at `indent` spaces, followed by a newline. The value is plain data: objects,
68
+ * arrays, strings, finite numbers, booleans and null, an object member holding nothing
69
+ * being left out as `JSON.stringify` leaves it out.
70
+ */
71
+ export function jsonChunks(value, indent) {
72
+ return coalesced((function* document() {
73
+ yield* valueOf(value, "", " ".repeat(indent));
74
+ yield "\n";
75
+ })());
76
+ }
@@ -0,0 +1,23 @@
1
+ /** A count with its noun, so a line reads for one as well as for several. */
2
+ function counted(n, one, many) {
3
+ return `${String(n)} ${n === 1 ? one : many}`;
4
+ }
5
+ /**
6
+ * One line per configuration the run dropped, and one counting the questions the checker
7
+ * left unanswered by project, so a reader of the error stream sees where the analysis was
8
+ * partial. The lines carry no prefix; the caller writes them as its own diagnostics.
9
+ */
10
+ export function partialNotes(partial) {
11
+ const notes = [...partial.notBuilt]
12
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
13
+ .map((one) => `the derived configuration ${one.id} was not built and is not analyzed: ${one.error}`);
14
+ if (partial.unanswered.length > 0) {
15
+ const byProject = new Map();
16
+ for (const question of partial.unanswered) {
17
+ byProject.set(question.configFile, (byProject.get(question.configFile) ?? 0) + 1);
18
+ }
19
+ const where = [...byProject].map(([configFile, n]) => `${configFile}: ${String(n)}`).join(", ");
20
+ notes.push(`the checker answered ${counted(partial.unanswered.length, "question", "questions")} with a failure (${where}); every declaration an answer could have kept live is kept live and reported by nothing`);
21
+ }
22
+ return notes;
23
+ }