@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
@@ -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
  }
@@ -14,9 +14,9 @@
14
14
  */
15
15
  import { isCallExpression, isElementAccessExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/unstable/ast";
16
16
  import { TypeFlags } from "@typescript/native/unstable/sync";
17
- import { typesAt } from "./class-members.js";
18
17
  import { memberNames } from "./member-names.js";
19
18
  import { renderPosition } from "./position.js";
19
+ import { isAnswered, UNANSWERED } from "./query.js";
20
20
  /** The `Reflect` methods whose second argument is a property key. */
21
21
  const REFLECT_KEYED = new Set(["get", "set", "has"]);
22
22
  /** The text one key denotes, where it is literal text. */
@@ -67,14 +67,17 @@ function lookupsOf(file) {
67
67
  file.forEachChild(visit);
68
68
  return found;
69
69
  }
70
- /** Whether a receiver's type is not `any` and declares a string index signature. */
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
+ */
71
75
  function readsAMapping(project, type) {
72
76
  if ((type.flags & TypeFlags.Any) !== 0) {
73
77
  return false;
74
78
  }
75
- return project.checker
76
- .getIndexInfosOfType(type)
77
- .some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
79
+ const infos = project.queries.indexInfosOf(type);
80
+ return isAnswered(infos) && infos.some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
78
81
  }
79
82
  /**
80
83
  * The reflective-lookup detector: every member whose name a literal key of a lookup
@@ -89,13 +92,16 @@ export function reflectiveLookup(input) {
89
92
  }
90
93
  // Whether each written `Reflect` is the global one, asked in one batch: a
91
94
  // declaration of that name in one of the project's own files shadows it.
92
- const ownFiles = new Set(files.map((file) => file.fileName));
95
+ const ownFiles = project.ownPaths();
93
96
  const reflects = lookups.flatMap((lookup) => lookup.reflect === undefined ? [] : [lookup.reflect]);
94
97
  const global = new Set();
95
98
  if (reflects.length > 0) {
96
99
  project.symbolsAt(reflects.map((node) => project.handle(node))).forEach((symbol, index) => {
97
100
  const node = reflects[index];
98
- 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));
99
105
  if (node !== undefined && declaredHere === false) {
100
106
  global.add(node);
101
107
  }
@@ -105,9 +111,12 @@ export function reflectiveLookup(input) {
105
111
  // reads that mapping rather than a member, asked in one batch.
106
112
  const receivers = lookups.flatMap((lookup) => lookup.receiver === undefined ? [] : [lookup.receiver]);
107
113
  const mapped = new Set();
108
- typesAt(project, receivers).forEach((type, index) => {
114
+ project.queries.typesAt(receivers).forEach((type, index) => {
109
115
  const receiver = receivers[index];
110
- if (receiver !== undefined && type !== undefined && readsAMapping(project, type)) {
116
+ if (receiver !== undefined &&
117
+ type !== undefined &&
118
+ isAnswered(type) &&
119
+ readsAMapping(project, type)) {
111
120
  mapped.add(receiver);
112
121
  }
113
122
  });
@@ -218,7 +218,9 @@ export function buildReport(input) {
218
218
  configurations: [...input.configurations]
219
219
  .sort((a, b) => compare(a.id, b.id))
220
220
  .map((one) => ({ id: one.id, project: one.project })),
221
- 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 })),
222
224
  consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
223
225
  findings,
224
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);
@@ -1,3 +1,4 @@
1
+ import { jsonChunks } from "../json-chunks.js";
1
2
  import { KINDS } from "../kinds.js";
2
3
  import { lineHashes, symbolFingerprint } from "./fingerprint.js";
3
4
  import { RenderError } from "./reporter.js";
@@ -236,5 +237,5 @@ export function sarif(report, options) {
236
237
  },
237
238
  ],
238
239
  };
239
- return `${JSON.stringify(log, null, INDENT)}\n`;
240
+ return jsonChunks(log, INDENT);
240
241
  }
@@ -6,6 +6,7 @@
6
6
  * and `println`. Any other function, `define`, `template` and `block` are refused at parse,
7
7
  * and a member the document does not carry fails the rendering.
8
8
  */
9
+ import { CHUNK_LENGTH } from "../json-chunks.js";
9
10
  import { RenderError } from "./reporter.js";
10
11
  import { utf8Bytes } from "./sha256.js";
11
12
  /** A template that does not parse, which refuses the invocation before any analysis. */
@@ -735,10 +736,22 @@ function call(name, args) {
735
736
  /** One execution of a template over a document. */
736
737
  class Execution {
737
738
  scopes;
738
- out = "";
739
+ /** The rendering so far, in pieces of about {@link CHUNK_LENGTH}, the last still growing. */
740
+ pieces = [""];
739
741
  constructor(root) {
740
742
  this.scopes = [new Map([["$", root]])];
741
743
  }
744
+ write(text) {
745
+ const last = this.pieces.length - 1;
746
+ const held = (this.pieces[last] ?? "") + text;
747
+ if (held.length >= CHUNK_LENGTH) {
748
+ this.pieces[last] = held;
749
+ this.pieces.push("");
750
+ }
751
+ else {
752
+ this.pieces[last] = held;
753
+ }
754
+ }
742
755
  lookup(name) {
743
756
  for (let i = this.scopes.length - 1; i >= 0; i -= 1) {
744
757
  const scope = this.scopes[i];
@@ -762,7 +775,7 @@ class Execution {
762
775
  let held = value;
763
776
  for (const name of names) {
764
777
  if (!isObject(held)) {
765
- throw new RenderError(`can't evaluate field ${name} in a ${kindOf(held)}`);
778
+ throw new RenderError(`cannot evaluate field ${name} in a ${kindOf(held)}`);
766
779
  }
767
780
  if (!Object.hasOwn(held, name)) {
768
781
  throw new RenderError(`the document has no member ${JSON.stringify(name)} here`);
@@ -794,7 +807,7 @@ class Execution {
794
807
  }
795
808
  if (head.k !== "func") {
796
809
  if (rest.length > 0 || piped !== undefined) {
797
- throw new RenderError("can't give an argument to a non-function");
810
+ throw new RenderError("cannot give an argument to a non-function");
798
811
  }
799
812
  return this.arg(head, dot);
800
813
  }
@@ -842,12 +855,12 @@ class Execution {
842
855
  for (const node of nodes) {
843
856
  switch (node.k) {
844
857
  case "text":
845
- this.out += node.text;
858
+ this.write(node.text);
846
859
  break;
847
860
  case "action": {
848
861
  const value = this.pipeline(node.pipe, dot, true);
849
862
  if (node.pipe.vars.length === 0) {
850
- this.out += formatted(value, false);
863
+ this.write(formatted(value, false));
851
864
  }
852
865
  break;
853
866
  }
@@ -890,7 +903,7 @@ class Execution {
890
903
  .map((key) => [key, value[key] ?? null]);
891
904
  }
892
905
  else {
893
- throw new RenderError(`range can't iterate over a ${kindOf(value)}`);
906
+ throw new RenderError(`range cannot iterate over a ${kindOf(value)}`);
894
907
  }
895
908
  if (entries.length === 0) {
896
909
  this.run(node.otherwise, dot);
@@ -925,12 +938,40 @@ class Execution {
925
938
  }
926
939
  }
927
940
  /**
928
- * The template's rendering of one document. The rendering is built whole, so a template
929
- * that fails partway renders nothing, and the failure names what it could not evaluate.
941
+ * One document as the JSON value it encodes: what `JSON.stringify` would write for it,
942
+ * read back, without writing it. A member holding nothing is left out, and an array
943
+ * element holding nothing, like a number that is not finite, is null.
944
+ */
945
+ function plainOf(value) {
946
+ if (Array.isArray(value)) {
947
+ return value.map((element) => plainOf(element) ?? null);
948
+ }
949
+ if (typeof value === "object" && value !== null) {
950
+ const held = {};
951
+ for (const [key, member] of Object.entries(value)) {
952
+ const plain = plainOf(member);
953
+ if (plain !== undefined) {
954
+ held[key] = plain;
955
+ }
956
+ }
957
+ return held;
958
+ }
959
+ if (typeof value === "number") {
960
+ return Number.isFinite(value) ? value : null;
961
+ }
962
+ if (typeof value === "string" || typeof value === "boolean" || value === null) {
963
+ return value;
964
+ }
965
+ return undefined;
966
+ }
967
+ /**
968
+ * The template's rendering of one document, in pieces of about {@link CHUNK_LENGTH}.
969
+ * The rendering is complete before it is answered, so a template that fails partway
970
+ * renders nothing, and the failure names what it could not evaluate.
930
971
  */
931
972
  export function renderTemplate(template, document) {
932
- const root = JSON.parse(JSON.stringify(document));
973
+ const root = plainOf(document) ?? null;
933
974
  const execution = new Execution(root);
934
975
  execution.run(template.nodes, root);
935
- return execution.out;
976
+ return execution.pieces.filter((piece) => piece !== "");
936
977
  }