@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
@@ -0,0 +1,207 @@
1
+ /**
2
+ * The one way the analysis asks a project's checker anything. A question the compiler
3
+ * server panics on answers {@link UNANSWERED}, and a batch it panics on whole is asked
4
+ * again one question at a time; every caller reads an unanswered question as keeping a
5
+ * declaration live, and each is recorded once so a run can say where it was partial.
6
+ * Questions of one accessor about many locations go out together, through the array
7
+ * form where it has one and otherwise as one batch, one round trip per capped run.
8
+ */
9
+ import { isIdentifier } from "@typescript/native/unstable/ast";
10
+ /**
11
+ * The greatest number of locations one batched request asks about. A batch is
12
+ * answered by an array of its own length, so an uncapped batch over a large file
13
+ * makes one answer proportional to that file. The figure bounds the size of one
14
+ * answer rather than the work: the median file is one batch at this cap and the
15
+ * largest production file a handful.
16
+ */
17
+ export const DEFAULT_BATCH_CAP = 4096;
18
+ /** The answer to a question the checker could not answer. */
19
+ export const UNANSWERED = Symbol("unanswered");
20
+ /** Whether one answer is an answer. */
21
+ export function isAnswered(answer) {
22
+ return answer !== UNANSWERED;
23
+ }
24
+ /**
25
+ * The stop a pass raises where it cannot go on without an answer the checker did not
26
+ * give. Whoever runs the pass catches it and keeps live everything the pass could
27
+ * have kept live, so a pass that throws it never computes a finding from the gap.
28
+ */
29
+ export class Unanswerable extends Error {
30
+ constructor() {
31
+ super("the checker did not answer a question this pass needs");
32
+ this.name = "Unanswerable";
33
+ }
34
+ }
35
+ /** The answer itself, or the stop of a pass that cannot go on without it. */
36
+ export function must(answer) {
37
+ if (answer === UNANSWERED) {
38
+ throw new Unanswerable();
39
+ }
40
+ return answer;
41
+ }
42
+ /**
43
+ * Whether one failure is the server's report that a question panicked. The server
44
+ * recovers the panic and answers the question with its text, so the session
45
+ * survives it; any other failure, a closed channel among them, is not one this layer
46
+ * contains.
47
+ */
48
+ function panicked(error) {
49
+ return error instanceof Error && error.message.startsWith("panic:");
50
+ }
51
+ const ASK_DIRECTLY = (_accessor, _locations, question) => question();
52
+ /** What one run could not ask, each question once. */
53
+ export class UnansweredLog {
54
+ #seen = new Map();
55
+ /** Records one question, once however many times it is asked. */
56
+ note(question) {
57
+ const key = [question.configFile, question.accessor, question.location].join("\u0000");
58
+ if (!this.#seen.has(key)) {
59
+ this.#seen.set(key, question);
60
+ }
61
+ }
62
+ /** Every question recorded, in the order first asked. */
63
+ get questions() {
64
+ return [...this.#seen.values()];
65
+ }
66
+ }
67
+ function nodeLocation(node) {
68
+ return `${node.getSourceFile().fileName}:${String(node.pos)}`;
69
+ }
70
+ function symbolLocation(symbol) {
71
+ return `symbol ${String(symbol.id)} ${symbol.name}`;
72
+ }
73
+ function typeLocation(type) {
74
+ return `type ${String(type.id)}`;
75
+ }
76
+ /**
77
+ * The guarded questions over one project's checker. `configFile` names the project
78
+ * in the log, `batch` sends many questions in one round trip, `log` is the run's,
79
+ * shared by every project, and every question is put through `ask`.
80
+ */
81
+ export function queriesOf(checker, configFile, batch, log, ask = ASK_DIRECTLY) {
82
+ const note = (accessor, location) => {
83
+ log.note({ configFile, accessor, location });
84
+ };
85
+ const one = (accessor, location, question) => {
86
+ try {
87
+ return ask(accessor, () => [location()], question);
88
+ }
89
+ catch (error) {
90
+ if (!panicked(error)) {
91
+ throw error;
92
+ }
93
+ note(accessor, location());
94
+ return UNANSWERED;
95
+ }
96
+ };
97
+ /**
98
+ * One question per item of `items`, in runs of at most `cap`, each run in one
99
+ * round trip. A run fails as a whole only where the server panics outside any one
100
+ * question, and is then asked one question at a time.
101
+ */
102
+ const each = (accessor, items, cap, locate, question, alone) => {
103
+ function* guarded(item) {
104
+ try {
105
+ return yield* ask(accessor, () => [locate(item)], () => question(item));
106
+ }
107
+ catch (error) {
108
+ if (!panicked(error)) {
109
+ throw error;
110
+ }
111
+ note(accessor, locate(item));
112
+ return UNANSWERED;
113
+ }
114
+ }
115
+ const answers = [];
116
+ for (let from = 0; from < items.length; from += cap) {
117
+ const run = items.slice(from, from + cap);
118
+ try {
119
+ answers.push(...batch(run.map(guarded)));
120
+ }
121
+ catch (error) {
122
+ if (!panicked(error)) {
123
+ throw error;
124
+ }
125
+ answers.push(...run.map((item) => one(accessor, () => locate(item), () => alone(item))));
126
+ }
127
+ }
128
+ return answers;
129
+ };
130
+ /**
131
+ * One accessor's array form over `items`, in runs of at most `cap`. A run the server
132
+ * panics on is asked again as two halves, down to one item, so the items it can
133
+ * answer are answered and only an item that panics alone is unanswered.
134
+ */
135
+ const many = (accessor, items, cap, locate, question) => {
136
+ const answers = [];
137
+ const settle = (run) => {
138
+ try {
139
+ answers.push(...ask(accessor, () => run.map(locate), () => question(run)));
140
+ return;
141
+ }
142
+ catch (error) {
143
+ if (!panicked(error)) {
144
+ throw error;
145
+ }
146
+ }
147
+ const [only] = run;
148
+ if (run.length === 1 && only !== undefined) {
149
+ note(accessor, locate(only));
150
+ answers.push(UNANSWERED);
151
+ return;
152
+ }
153
+ const half = Math.ceil(run.length / 2);
154
+ settle(run.slice(0, half));
155
+ settle(run.slice(half));
156
+ };
157
+ for (let from = 0; from < items.length; from += cap) {
158
+ settle(items.slice(from, from + cap));
159
+ }
160
+ return answers;
161
+ };
162
+ return {
163
+ symbolsAt: (nodes, cap = DEFAULT_BATCH_CAP) => many("getSymbolAtLocation", nodes, cap, nodeLocation, (run) => checker.getSymbolAtLocation(run)),
164
+ resolvedSymbol: (node) => isIdentifier(node)
165
+ ? one("getResolvedSymbol", () => nodeLocation(node), () => checker.getResolvedSymbol(node))
166
+ : undefined,
167
+ shorthandValue: (node) => one("getShorthandAssignmentValueSymbol", () => nodeLocation(node), () => checker.getShorthandAssignmentValueSymbol(node)),
168
+ immediateAliased: (symbol) => one("getImmediateAliasedSymbol", () => symbolLocation(symbol), () => checker.getImmediateAliasedSymbol(symbol)),
169
+ aliased: (symbol) => one("getAliasedSymbol", () => symbolLocation(symbol), () => checker.getAliasedSymbol(symbol)),
170
+ resolveName: (name, meaning, location, excludeGlobals) => one("resolveName", () => (location === undefined ? `name ${name}` : nodeLocation(location)), () => checker.resolveName(name, meaning, location, excludeGlobals)),
171
+ exportsOfModule: (module) => one("getExportsOfModule", () => symbolLocation(module), () => checker.getExportsOfModule(module)),
172
+ membersOf: (symbol) => one("getMembers", () => symbolLocation(symbol), () => symbol.getMembers()),
173
+ exportsOf: (symbol) => one("getExports", () => symbolLocation(symbol), () => symbol.getExports()),
174
+ parentOf: (symbol) => one("getParent", () => symbolLocation(symbol), () => symbol.getParent()),
175
+ jsDocTags: (symbols, cap = DEFAULT_BATCH_CAP) => each("getJsDocTags", symbols, cap, symbolLocation, (symbol) => symbol.getJsDocTags.gen(checker), (symbol) => symbol.getJsDocTags(checker)),
176
+ typesAt: (nodes, cap = DEFAULT_BATCH_CAP) => many("getTypeAtLocation", nodes, cap, nodeLocation, (run) => checker.getTypeAtLocation(run)),
177
+ typeAt: (node) => one("getTypeAtLocation", () => nodeLocation(node), () => checker.getTypeAtLocation(node)),
178
+ typesOfSymbols: (symbols, cap = DEFAULT_BATCH_CAP) => many("getTypeOfSymbol", symbols, cap, symbolLocation, (run) => checker.getTypeOfSymbol(run)),
179
+ typeOfSymbol: (symbol) => one("getTypeOfSymbol", () => symbolLocation(symbol), () => checker.getTypeOfSymbol(symbol)),
180
+ declaredTypeOf: (symbol) => one("getDeclaredTypeOfSymbol", () => symbolLocation(symbol), () => checker.getDeclaredTypeOfSymbol(symbol)),
181
+ contextualTypes: (nodes, cap = DEFAULT_BATCH_CAP) => each("getContextualType", nodes, cap, nodeLocation, (node) => checker.getContextualType.gen(node), (node) => checker.getContextualType(node)),
182
+ propertiesOf: (type) => one("getPropertiesOfType", () => typeLocation(type), () => checker.getPropertiesOfType(type)),
183
+ indexInfosOf: (type) => one("getIndexInfosOfType", () => typeLocation(type), () => checker.getIndexInfosOfType(type)),
184
+ typeArguments: (type) => type.isTypeReference()
185
+ ? one("getTypeArguments", () => typeLocation(type), () => checker.getTypeArguments(type))
186
+ : [],
187
+ nonNullable: (type) => one("getNonNullableType", () => typeLocation(type), () => checker.getNonNullableType(type)),
188
+ isArray: (type) => one("isArrayType", () => typeLocation(type), () => checker.isArrayType(type)),
189
+ assignableEach: (pairs, cap = DEFAULT_BATCH_CAP) => each("isTypeAssignableTo", pairs, cap, (pair) => `${typeLocation(pair.source)} to ${typeLocation(pair.target)}`, (pair) => checker.isTypeAssignableTo.gen(pair.source, pair.target), (pair) => checker.isTypeAssignableTo(pair.source, pair.target)),
190
+ baseTypes: (type) => type.isClassOrInterface()
191
+ ? one("getBaseTypes", () => typeLocation(type), () => checker.getBaseTypes(type))
192
+ : [],
193
+ constituents: (type) => type.isUnionType() || type.isIntersectionType()
194
+ ? one("getTypes", () => typeLocation(type), () => type.getTypes())
195
+ : [type],
196
+ targetOf: (type) => type.isTypeReference()
197
+ ? one("getTarget", () => typeLocation(type), () => type.getTarget())
198
+ : type,
199
+ symbolOfType: (type) => one("getSymbol", () => typeLocation(type), () => type.getSymbol()),
200
+ signaturesOf: (type, kind) => one("getSignaturesOfType", () => typeLocation(type), () => checker.getSignaturesOfType(type, kind)),
201
+ resolvedSignature: (node) => one("getResolvedSignature", () => nodeLocation(node), () => checker.getResolvedSignature(node)),
202
+ parameterType: (signature, index) => one("getParameterType", () => `signature ${String(signature.id)} parameter ${String(index)}`, () => checker.getParameterType(signature, index)),
203
+ signatureOf: (declaration) => one("getSignatureFromDeclaration", () => nodeLocation(declaration), () => checker.getSignatureFromDeclaration(declaration)),
204
+ returnTypeOf: (signature) => one("getReturnTypeOfSignature", () => `signature ${String(signature.id)}`, () => checker.getReturnTypeOfSignature(signature)),
205
+ typeToString: (type) => one("typeToString", () => typeLocation(type), () => checker.typeToString(type)),
206
+ };
207
+ }
@@ -2,15 +2,10 @@ import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDecor
2
2
  import { aliasChains } from "./alias-chain.js";
3
3
  import { destructuring, propertyReadsOf } from "./destructuring.js";
4
4
  import { globExpression } from "./glob.js";
5
- import { nodeKey } from "./inventory.js";
5
+ import { declarationsByName, nodeKey } from "./inventory.js";
6
6
  import { relativePath } from "./paths.js";
7
7
  import { byPosition, renderPosition } from "./position.js";
8
- /**
9
- * The batch cap a run takes when the configuration names none. It bounds the size of
10
- * one answer rather than the work: the median file is one batch at this cap and the
11
- * largest production file a handful.
12
- */
13
- export const DEFAULT_BATCH_CAP = 4096;
8
+ import { DEFAULT_BATCH_CAP, UNANSWERED } from "./query.js";
14
9
  /**
15
10
  * The operators whose left side one assignment stores into. A compound assignment is
16
11
  * among them: it reads its target only to write the result back, so nothing else
@@ -294,6 +289,49 @@ export function testFileRulesOf(testFiles, paths) {
294
289
  })
295
290
  .sort((a, b) => compare(a.rule, b.rule));
296
291
  }
292
+ const importedCache = new WeakMap();
293
+ /**
294
+ * Each local name one file's imports bind, mapped to the names it imports: the
295
+ * exported name of a named import, `default` for a default import, and `undefined` for
296
+ * a namespace import or an import assignment, which binds a whole module.
297
+ */
298
+ function importedNames(file) {
299
+ const known = importedCache.get(file);
300
+ if (known !== undefined) {
301
+ return known;
302
+ }
303
+ const names = new Map();
304
+ const bind = (local, imported) => {
305
+ names.set(local, [...(names.get(local) ?? []), imported]);
306
+ };
307
+ for (const statement of file.statements) {
308
+ if (isImportEqualsDeclaration(statement)) {
309
+ bind(statement.name.text, undefined);
310
+ continue;
311
+ }
312
+ const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
313
+ if (clause === undefined) {
314
+ continue;
315
+ }
316
+ if (clause.name !== undefined) {
317
+ bind(clause.name.text, "default");
318
+ }
319
+ const bindings = clause.namedBindings;
320
+ if (bindings === undefined) {
321
+ continue;
322
+ }
323
+ if (bindings.kind === SyntaxKind.NamespaceImport) {
324
+ bind(bindings.name.text, undefined);
325
+ }
326
+ else {
327
+ for (const element of bindings.elements) {
328
+ bind(element.name.text, element.propertyName?.text ?? element.name.text);
329
+ }
330
+ }
331
+ }
332
+ importedCache.set(file, names);
333
+ return names;
334
+ }
297
335
  /** Two strings ordered bytewise, which is the order every set of a run is read in. */
298
336
  function compare(a, b) {
299
337
  if (a === b) {
@@ -347,7 +385,26 @@ export function references(project, held, targetRoot, options) {
347
385
  * Each later link is a declaration of its own that references the next, so one link
348
386
  * per re-export is what lets a chain be followed one declaration at a time.
349
387
  */
350
- const carriedBy = (link, symbol) => link.alias ? chains.nextDeclared(symbol) : chains.declarationsOf(symbol);
388
+ const carriedBy = (link, symbol) => link.alias
389
+ ? chains.nextDeclared(symbol)
390
+ : chains.declarationsOf(symbol).map((id) => ({ id, stepped: false, guessed: false }));
391
+ /**
392
+ * Every declaration bearing the name one node spells, or the name an import of its
393
+ * file binds that name to, which is what a reference the checker could not resolve is
394
+ * counted as: it may be a use of any of them, and a declaration it may use is kept
395
+ * live. A name a namespace import binds may be a use of any file.
396
+ */
397
+ const named = (node) => {
398
+ const byName = declarationsByName(held);
399
+ const text = node.getText();
400
+ const imported = importedNames(node.getSourceFile()).get(text) ?? [];
401
+ return [
402
+ ...new Set([
403
+ ...(byName.get(text) ?? []),
404
+ ...imported.flatMap((name) => name === undefined ? [...fileIds] : (byName.get(name) ?? [])),
405
+ ]),
406
+ ];
407
+ };
351
408
  /** The symbol one site resolves to, asking only the accessor the site's form names. */
352
409
  const symbolFor = (site, answered) => {
353
410
  if (site.shorthand !== undefined) {
@@ -355,7 +412,7 @@ export function references(project, held, targetRoot, options) {
355
412
  return project.shorthandValueAt(project.handle(site.shorthand));
356
413
  }
357
414
  const batched = answered.get(site.node);
358
- if (batched !== undefined) {
415
+ if (batched === UNANSWERED || typeof batched === "object") {
359
416
  return batched;
360
417
  }
361
418
  residueFallbacks += 1;
@@ -397,7 +454,7 @@ export function references(project, held, targetRoot, options) {
397
454
  batched += batching.length;
398
455
  for (let from = 0; from < batching.length; from += cap) {
399
456
  const run = batching.slice(from, from + cap);
400
- const answers = project.symbolsAt(run.map((node) => project.handle(node)));
457
+ const answers = project.symbolsAt(run.map((node) => project.handle(node)), cap);
401
458
  fileBatches += 1;
402
459
  run.forEach((node, index) => {
403
460
  const symbol = answers[index];
@@ -412,13 +469,16 @@ export function references(project, held, targetRoot, options) {
412
469
  continue;
413
470
  }
414
471
  const position = renderPosition(file, root, link.at.getStart());
415
- for (const id of carriedBy(link, symbol)) {
472
+ const carried = symbol === UNANSWERED
473
+ ? named(link.at).map((id) => ({ id, stepped: false, guessed: true }))
474
+ : carriedBy(link, symbol);
475
+ for (const target of carried) {
416
476
  found.push({
417
477
  from: link.from,
418
- to: id,
478
+ to: target.id,
419
479
  position,
420
480
  use: "read",
421
- resolution: link.alias ? "alias" : "batch",
481
+ resolution: target.guessed ? "by-name" : link.alias ? "alias" : "batch",
422
482
  test,
423
483
  });
424
484
  }
@@ -429,14 +489,16 @@ export function references(project, held, targetRoot, options) {
429
489
  continue;
430
490
  }
431
491
  const position = renderPosition(file, root, evaluation.node.getStart());
432
- for (const id of chains.declarationsOf(module)) {
492
+ // A module the checker could not resolve may be any file of the target.
493
+ const evaluated = module === UNANSWERED ? [...fileIds] : chains.declarationsOf(module);
494
+ for (const id of evaluated) {
433
495
  if (fileIds.has(id)) {
434
496
  found.push({
435
497
  from: evaluation.from,
436
498
  to: id,
437
499
  position,
438
500
  use: "evaluation",
439
- resolution: "batch",
501
+ resolution: module === UNANSWERED ? "by-name" : "batch",
440
502
  test,
441
503
  });
442
504
  }
@@ -450,13 +512,20 @@ export function references(project, held, targetRoot, options) {
450
512
  return;
451
513
  }
452
514
  const position = renderPosition(file, root, read.key.getStart());
453
- for (const target of chains.chainOf(symbol)) {
515
+ const targets = symbol === UNANSWERED
516
+ ? (declarationsByName(held).get(read.name) ?? []).map((id) => ({
517
+ id,
518
+ stepped: false,
519
+ guessed: true,
520
+ }))
521
+ : chains.chainOf(symbol);
522
+ for (const target of targets) {
454
523
  found.push({
455
524
  from: read.from,
456
525
  to: target.id,
457
526
  position,
458
527
  use: "read",
459
- resolution: "destructured",
528
+ resolution: target.guessed ? "by-name" : "destructured",
460
529
  test,
461
530
  });
462
531
  }
@@ -464,7 +533,7 @@ export function references(project, held, targetRoot, options) {
464
533
  for (const site of uses) {
465
534
  const direct = site.shorthand !== undefined
466
535
  ? "shorthand"
467
- : answered.has(site.node)
536
+ : typeof answered.get(site.node) === "object"
468
537
  ? "batch"
469
538
  : "resolved-symbol";
470
539
  const symbol = symbolFor(site, answered);
@@ -474,13 +543,16 @@ export function references(project, held, targetRoot, options) {
474
543
  const position = renderPosition(file, root, site.node.getStart());
475
544
  // A re-export something imports is used and so is the declaration behind it, so
476
545
  // the use names every link of the chain its name stands at the head of.
477
- for (const target of chains.chainOf(symbol)) {
546
+ const targets = symbol === UNANSWERED
547
+ ? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
548
+ : chains.chainOf(symbol);
549
+ for (const target of targets) {
478
550
  found.push({
479
551
  from: site.from,
480
552
  to: target.id,
481
553
  position,
482
554
  use: site.use,
483
- resolution: target.stepped ? "alias" : direct,
555
+ resolution: target.guessed ? "by-name" : target.stepped ? "alias" : direct,
484
556
  test,
485
557
  });
486
558
  }
@@ -8,11 +8,15 @@
8
8
  * string reaches every member of that name on any type, static members included,
9
9
  * and every record names the string's own position. A key that is not literal text
10
10
  * holds nothing back: a computed name is the case no exact rule serves. `Reflect` is
11
- * the global one only: a local declaration of that name is a different object.
11
+ * the global one only: a local declaration of that name is a different object. An
12
+ * element access through a receiver whose type declares a string index signature, and is
13
+ * not `any`, reads that signature and holds nothing back.
12
14
  */
13
15
  import { isCallExpression, isElementAccessExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/unstable/ast";
16
+ import { TypeFlags } from "@typescript/native/unstable/sync";
14
17
  import { memberNames } from "./member-names.js";
15
18
  import { renderPosition } from "./position.js";
19
+ import { isAnswered, UNANSWERED } from "./query.js";
16
20
  /** The `Reflect` methods whose second argument is a property key. */
17
21
  const REFLECT_KEYED = new Set(["get", "set", "has"]);
18
22
  /** The text one key denotes, where it is literal text. */
@@ -34,6 +38,7 @@ function lookupsOf(file) {
34
38
  name,
35
39
  callee: `[${JSON.stringify(name)}]`,
36
40
  reflect: undefined,
41
+ receiver: node.expression,
37
42
  });
38
43
  }
39
44
  }
@@ -48,7 +53,13 @@ function lookupsOf(file) {
48
53
  REFLECT_KEYED.has(method.text) &&
49
54
  key !== undefined &&
50
55
  name !== undefined) {
51
- found.push({ key, name, callee: `Reflect.${method.text}`, reflect: target });
56
+ found.push({
57
+ key,
58
+ name,
59
+ callee: `Reflect.${method.text}`,
60
+ reflect: target,
61
+ receiver: undefined,
62
+ });
52
63
  }
53
64
  }
54
65
  node.forEachChild(visit);
@@ -56,6 +67,18 @@ function lookupsOf(file) {
56
67
  file.forEachChild(visit);
57
68
  return found;
58
69
  }
70
+ /**
71
+ * Whether a receiver's type is not `any` and declares a string index signature. A type
72
+ * whose index signatures the checker did not answer for is read as declaring none, so
73
+ * the lookup retains what it names.
74
+ */
75
+ function readsAMapping(project, type) {
76
+ if ((type.flags & TypeFlags.Any) !== 0) {
77
+ return false;
78
+ }
79
+ const infos = project.queries.indexInfosOf(type);
80
+ return isAnswered(infos) && infos.some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
81
+ }
59
82
  /**
60
83
  * The reflective-lookup detector: every member whose name a literal key of a lookup
61
84
  * spells, recorded at the key.
@@ -69,24 +92,43 @@ export function reflectiveLookup(input) {
69
92
  }
70
93
  // Whether each written `Reflect` is the global one, asked in one batch: a
71
94
  // declaration of that name in one of the project's own files shadows it.
72
- const ownFiles = new Set(files.map((file) => file.fileName));
95
+ const ownFiles = project.ownPaths();
73
96
  const reflects = lookups.flatMap((lookup) => lookup.reflect === undefined ? [] : [lookup.reflect]);
74
97
  const global = new Set();
75
98
  if (reflects.length > 0) {
76
99
  project.symbolsAt(reflects.map((node) => project.handle(node))).forEach((symbol, index) => {
77
100
  const node = reflects[index];
78
- const declaredHere = symbol?.declarations.some((declaration) => ownFiles.has(declaration.path));
101
+ // A name the checker did not answer for is read as the global, which retains.
102
+ const declaredHere = symbol === UNANSWERED
103
+ ? false
104
+ : symbol?.declarations.some((declaration) => ownFiles.has(declaration.path));
79
105
  if (node !== undefined && declaredHere === false) {
80
106
  global.add(node);
81
107
  }
82
108
  });
83
109
  }
110
+ // An element access through a receiver that maps every string key to one value type
111
+ // reads that mapping rather than a member, asked in one batch.
112
+ const receivers = lookups.flatMap((lookup) => lookup.receiver === undefined ? [] : [lookup.receiver]);
113
+ const mapped = new Set();
114
+ project.queries.typesAt(receivers).forEach((type, index) => {
115
+ const receiver = receivers[index];
116
+ if (receiver !== undefined &&
117
+ type !== undefined &&
118
+ isAnswered(type) &&
119
+ readsAMapping(project, type)) {
120
+ mapped.add(receiver);
121
+ }
122
+ });
84
123
  const members = memberNames(files, held);
85
124
  const found = [];
86
125
  for (const lookup of lookups) {
87
126
  if (lookup.reflect !== undefined && !global.has(lookup.reflect)) {
88
127
  continue;
89
128
  }
129
+ if (lookup.receiver !== undefined && mapped.has(lookup.receiver)) {
130
+ continue;
131
+ }
90
132
  const site = renderPosition(lookup.key.getSourceFile(), targetRoot, lookup.key.getStart());
91
133
  for (const member of members.get(lookup.name) ?? []) {
92
134
  found.push({ id: member.id, detail: `looked up by ${lookup.callee}`, site });
@@ -4,7 +4,12 @@
4
4
  * renders this document, so no two renderings disagree about which findings exist.
5
5
  */
6
6
  /** The version of the report schema this analyzer writes a report to. */
7
- export const SCHEMA_VERSION = "6.0.0";
7
+ const SCHEMA_VERSION = "6.1.0";
8
+ /**
9
+ * Every report schema version this analyzer reads: the schema versions the Contract it
10
+ * implements admits, each an instance of the one it writes.
11
+ */
12
+ export const SCHEMA_VERSIONS_ACCEPTED = ["6.0.0", SCHEMA_VERSION];
8
13
  /** The name this analyzer writes every document under and mints every identifier with. */
9
14
  export const ANALYZER_NAME = "deadset-ts";
10
15
  /** The language this analyzer claims. */
@@ -52,6 +57,7 @@ function wireDetails(details) {
52
57
  ...(entry.reason === undefined ? {} : { reason: entry.reason }),
53
58
  },
54
59
  }),
60
+ ...(details.overlap === undefined ? {} : { overlap: details.overlap }),
55
61
  };
56
62
  }
57
63
  /** One finding as the finding schema spells it, member for member in its order. */
@@ -79,6 +85,9 @@ export function wireFinding(finding) {
79
85
  root: finding.component.root,
80
86
  symbol_count: finding.component.symbolCount,
81
87
  deletable_lines: finding.component.deletableLines,
88
+ ...(finding.component.members === undefined
89
+ ? {}
90
+ : { members: finding.component.members.map(wireSymbol) }),
82
91
  },
83
92
  retained_by: finding.retainedBy,
84
93
  configurations: finding.configurations,
@@ -198,7 +207,7 @@ export function buildReport(input) {
198
207
  name: ANALYZER_NAME,
199
208
  version: input.version,
200
209
  languages: [LANGUAGE],
201
- schema_versions_accepted: [SCHEMA_VERSION],
210
+ schema_versions_accepted: SCHEMA_VERSIONS_ACCEPTED,
202
211
  conformance: {
203
212
  corpus_version: input.conformance.corpusVersion,
204
213
  result: input.conformance.result,
@@ -209,7 +218,9 @@ export function buildReport(input) {
209
218
  configurations: [...input.configurations]
210
219
  .sort((a, b) => compare(a.id, b.id))
211
220
  .map((one) => ({ id: one.id, project: one.project })),
212
- configurations_not_built: [],
221
+ configurations_not_built: [...input.notBuilt]
222
+ .sort((a, b) => compare(a.id, b.id))
223
+ .map((one) => ({ id: one.id, project: one.project, error: one.error })),
213
224
  consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
214
225
  findings,
215
226
  edge_evaluations: input.result.edgeEvaluations.map(wireEvaluation).sort(byEdge),
@@ -1,4 +1,5 @@
1
1
  import { fails } from "../config.js";
2
+ import { coalesced } from "../json-chunks.js";
2
3
  import { KINDS } from "../kinds.js";
3
4
  /** The escapes a workflow command's message needs, so a line break does not end the command. */
4
5
  const DATA_ESCAPES = { "%": "%25", "\r": "%0D", "\n": "%0A" };
@@ -28,9 +29,12 @@ function command(level, at, endLine, title, message) {
28
29
  * because its kind is fixed on at the failing severity.
29
30
  */
30
31
  export function annotations(report, options) {
31
- const lines = report.findings.map((found) => command(fails(found.severity, options.failOn) ? "error" : "warning", found.position, found.position.end_line, `${found.code} ${found.kind}`, found.message));
32
- for (const stale of report.stale_suppressions) {
33
- lines.push(command("error", stale.position, stale.position.line, `${stale.code} ${kindName(stale.code)}`, stale.message));
34
- }
35
- return lines.map((line) => `${line}\n`).join("");
32
+ return coalesced((function* lines() {
33
+ for (const found of report.findings) {
34
+ yield `${command(fails(found.severity, options.failOn) ? "error" : "warning", found.position, found.position.end_line, `${found.code} ${found.kind}`, found.message)}\n`;
35
+ }
36
+ for (const stale of report.stale_suppressions) {
37
+ yield `${command("error", stale.position, stale.position.line, `${stale.code} ${kindName(stale.code)}`, stale.message)}\n`;
38
+ }
39
+ })());
36
40
  }
@@ -1,4 +1,5 @@
1
+ import { jsonChunks } from "../json-chunks.js";
1
2
  /** The indentation of the report document, which with the schema's member order fixes its bytes. */
2
3
  const INDENT = 2;
3
- /** The report document itself: the report, indented, with a closing newline. */
4
- export const json = (report) => `${JSON.stringify(report, null, INDENT)}\n`;
4
+ /** The report document itself, in pieces: the report, indented, with a closing newline. */
5
+ export const json = (report) => jsonChunks(report, INDENT);