@cplieger/deadset-ts 1.2.0 → 2.1.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.
@@ -0,0 +1,606 @@
1
+ import {
2
+ isArrayLiteralExpression,
3
+ isBinaryExpression,
4
+ isDeleteExpression,
5
+ isElementAccessExpression,
6
+ isForInStatement,
7
+ isForOfStatement,
8
+ isObjectLiteralExpression,
9
+ isParenthesizedExpression,
10
+ isPostfixUnaryExpression,
11
+ isPrefixUnaryExpression,
12
+ isPropertyAccessExpression,
13
+ isPropertyAssignment,
14
+ isShorthandPropertyAssignment,
15
+ isSpreadAssignment,
16
+ isSpreadElement,
17
+ SyntaxKind,
18
+ type Node,
19
+ type SourceFile,
20
+ } from "@typescript/native/unstable/ast";
21
+ import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
22
+ import { nodeKey, type Inventory } from "./inventory.ts";
23
+ import { byPosition, renderPosition, type Position } from "./position.ts";
24
+ import type { ProjectView } from "./session.ts";
25
+
26
+ /**
27
+ * Every use one project's own files make of the project's own declarations.
28
+ *
29
+ * The pass is one local walk per file and one batched resolution per file, never a
30
+ * query per symbol: a per-symbol reference query is the shape whose answers depend on
31
+ * the order the symbols are asked about, so no answer here depends on that order. The
32
+ * tree is on this side of the client boundary and the resolution is not, which is why a
33
+ * file's name nodes are gathered whole and handed over together.
34
+ *
35
+ * What the pass records and what it does not: a reference carries its use, its
36
+ * position, the declaration that encloses it and the classification of the file that
37
+ * made it. It carries no mode. Which of the references a run counts is the reading the
38
+ * sweep makes of this set, so the mode is one value the run decides once and this pass
39
+ * never sees it.
40
+ */
41
+
42
+ /** How one reference uses the declaration it names. */
43
+ export type Use = "read" | "write";
44
+
45
+ /**
46
+ * Which accessor answered for one reference, so that the cost of a run is attributable
47
+ * and a table of references shows the path each row took.
48
+ *
49
+ * - `batch` is the batched lookup over a file's name nodes, which answers for almost
50
+ * every one of them.
51
+ * - `resolved-symbol` is the per-node lookup for the residue a batch left.
52
+ * - `shorthand` is the per-node lookup for `{ a }`, whose name resolves to the property
53
+ * the literal declares rather than to the declaration that name reads.
54
+ * - `alias` is a step along an import or export chain, from one link to what it names.
55
+ */
56
+ export type Resolution = "batch" | "resolved-symbol" | "shorthand" | "alias";
57
+
58
+ /** One use of one declaration by one declaration. */
59
+ export interface Reference {
60
+ /**
61
+ * The identifier of the declaration the referencing name is written inside. A
62
+ * reference written outside every declaration of a file belongs to the file and
63
+ * carries the file's own identifier, the way an import belongs to the file that
64
+ * writes it rather than to the first declaration below it.
65
+ */
66
+ readonly from: string;
67
+ /** The identifier of the declaration the reference names. */
68
+ readonly to: string;
69
+ /** Where the referencing name is written. */
70
+ readonly position: Position;
71
+ readonly use: Use;
72
+ readonly resolution: Resolution;
73
+ /** Whether the file that made the reference is classified as a test file. */
74
+ readonly test: boolean;
75
+ }
76
+
77
+ /** One rule that classified files as test files, and how many files it matched. */
78
+ export interface TestFileRule {
79
+ /** The rule's name, as a report's own rule list spells one. */
80
+ readonly rule: string;
81
+ readonly matched: number;
82
+ }
83
+
84
+ /** What one project's reference pass cost at the client boundary. */
85
+ export interface ReferenceCost {
86
+ /**
87
+ * The name nodes the batches asked about, summed over the files. It is the size of
88
+ * the question rather than a cost of its own, and it is reported so that the number
89
+ * of batches is readable against it.
90
+ */
91
+ readonly batched: number;
92
+ /**
93
+ * Batched lookups: per file, the name-node count divided by the batch cap, rounded
94
+ * up. It is what keeps the pass's cost off the identifier count.
95
+ */
96
+ readonly fileBatches: number;
97
+ /** Per-node lookups for the name nodes a batch left unresolved. */
98
+ readonly residueFallbacks: number;
99
+ /** Per-node lookups for a shorthand property assignment's value. */
100
+ readonly shorthandLookups: number;
101
+ /** Per-symbol lookups that step from one alias to what it names, one per alias. */
102
+ readonly aliasSteps: number;
103
+ }
104
+
105
+ /** One project's references, in position order, and what reading them cost. */
106
+ export interface References {
107
+ /** The compiler configuration the project was opened from. */
108
+ readonly configFile: string;
109
+ readonly references: readonly Reference[];
110
+ /** The rules that classified files as test files, ordered by rule. */
111
+ readonly testFileRules: readonly TestFileRule[];
112
+ readonly cost: ReferenceCost;
113
+ }
114
+
115
+ /** How one run resolves references. */
116
+ export interface ReferenceOptions {
117
+ /**
118
+ * The greatest number of name nodes one batched lookup asks about. A batch is
119
+ * answered by an array of its own length, so an uncapped batch over a large file
120
+ * makes one answer proportional to that file. Absent takes {@link DEFAULT_BATCH_CAP}.
121
+ */
122
+ readonly batchCap?: number;
123
+ /**
124
+ * Glob patterns, relative to the target root, naming the files that are test files.
125
+ * Each pattern is a rule of its own and reports the files it matched.
126
+ */
127
+ readonly testFiles: readonly string[];
128
+ }
129
+
130
+ /**
131
+ * The batch cap a run takes when the configuration names none. It bounds the size of
132
+ * one answer rather than the work: the median file is one batch at this cap and the
133
+ * largest production file a handful.
134
+ */
135
+ export const DEFAULT_BATCH_CAP = 4096;
136
+
137
+ /**
138
+ * The operators whose left side one assignment stores into. A compound assignment is
139
+ * among them: it reads its target only to write the result back, so nothing else
140
+ * receives what it read, and a declaration whose every other reference is one of these
141
+ * carries no information out of itself.
142
+ */
143
+ const ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
144
+ SyntaxKind.EqualsToken,
145
+ SyntaxKind.PlusEqualsToken,
146
+ SyntaxKind.MinusEqualsToken,
147
+ SyntaxKind.AsteriskEqualsToken,
148
+ SyntaxKind.AsteriskAsteriskEqualsToken,
149
+ SyntaxKind.SlashEqualsToken,
150
+ SyntaxKind.PercentEqualsToken,
151
+ SyntaxKind.LessThanLessThanEqualsToken,
152
+ SyntaxKind.GreaterThanGreaterThanEqualsToken,
153
+ SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
154
+ SyntaxKind.AmpersandEqualsToken,
155
+ SyntaxKind.BarEqualsToken,
156
+ SyntaxKind.BarBarEqualsToken,
157
+ SyntaxKind.AmpersandAmpersandEqualsToken,
158
+ SyntaxKind.QuestionQuestionEqualsToken,
159
+ SyntaxKind.CaretEqualsToken,
160
+ ]);
161
+
162
+ /**
163
+ * The declaration forms that name something declared elsewhere. Each is a link rather
164
+ * than a declaration of what it names, so a reference through one names the next link
165
+ * and, at the end of the chain, the declaration itself: a re-export something imports
166
+ * is used, and so is the declaration behind it.
167
+ */
168
+ const ALIASES: ReadonlySet<SyntaxKind> = new Set([
169
+ SyntaxKind.ImportClause,
170
+ SyntaxKind.ImportEqualsDeclaration,
171
+ SyntaxKind.ImportSpecifier,
172
+ SyntaxKind.NamespaceImport,
173
+ SyntaxKind.ExportAssignment,
174
+ SyntaxKind.ExportSpecifier,
175
+ SyntaxKind.NamespaceExport,
176
+ ]);
177
+
178
+ /** Whether one node is an identifier or a private name, the two forms a name takes. */
179
+ function isName(node: Node): boolean {
180
+ return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
181
+ }
182
+
183
+ /** The expression one node is, through however many parentheses are written around it. */
184
+ function unparenthesized(node: Node): Node {
185
+ let held = node;
186
+ while (isParenthesizedExpression(held)) {
187
+ held = held.expression;
188
+ }
189
+ return held;
190
+ }
191
+
192
+ /**
193
+ * Records the name one store writes into.
194
+ *
195
+ * A property access writes the member it names. An element access writes the collection
196
+ * it stores into, because a collection a module only ever stores into holds nothing
197
+ * anything reads, and the key is no declaration of the target. A destructuring target
198
+ * writes each name it binds, through the spreads and the nested patterns it binds them
199
+ * in.
200
+ */
201
+ function markWrite(target: Node, writes: Set<number>): void {
202
+ const held = unparenthesized(target);
203
+ if (isName(held)) {
204
+ writes.add(held.pos);
205
+ return;
206
+ }
207
+ if (isPropertyAccessExpression(held)) {
208
+ if (isName(held.name)) {
209
+ writes.add(held.name.pos);
210
+ }
211
+ return;
212
+ }
213
+ if (isElementAccessExpression(held)) {
214
+ markWrite(held.expression, writes);
215
+ return;
216
+ }
217
+ if (isArrayLiteralExpression(held)) {
218
+ for (const element of held.elements) {
219
+ markWrite(element, writes);
220
+ }
221
+ return;
222
+ }
223
+ if (isObjectLiteralExpression(held)) {
224
+ for (const property of held.properties) {
225
+ if (isPropertyAssignment(property)) {
226
+ markWrite(property.initializer, writes);
227
+ continue;
228
+ }
229
+ if (isShorthandPropertyAssignment(property)) {
230
+ markWrite(property.name, writes);
231
+ continue;
232
+ }
233
+ if (isSpreadAssignment(property)) {
234
+ markWrite(property.expression, writes);
235
+ }
236
+ }
237
+ return;
238
+ }
239
+ if (isSpreadElement(held)) {
240
+ markWrite(held.expression, writes);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Records every store one node performs: an assignment, a compound assignment, an
246
+ * increment or a decrement, a `delete`, and an iteration that assigns to a name declared
247
+ * elsewhere. An iteration that declares its own name stores into nothing the inventory
248
+ * holds.
249
+ */
250
+ function markStores(node: Node, writes: Set<number>): void {
251
+ if (isBinaryExpression(node)) {
252
+ if (ASSIGNMENTS.has(node.operatorToken.kind)) {
253
+ markWrite(node.left, writes);
254
+ }
255
+ return;
256
+ }
257
+ if (isPrefixUnaryExpression(node) || isPostfixUnaryExpression(node)) {
258
+ if (
259
+ node.operator === SyntaxKind.PlusPlusToken ||
260
+ node.operator === SyntaxKind.MinusMinusToken
261
+ ) {
262
+ markWrite(node.operand, writes);
263
+ }
264
+ return;
265
+ }
266
+ if (isDeleteExpression(node)) {
267
+ markWrite(node.expression, writes);
268
+ return;
269
+ }
270
+ if (isForInStatement(node) || isForOfStatement(node)) {
271
+ if (node.initializer.kind !== SyntaxKind.VariableDeclarationList) {
272
+ markWrite(node.initializer, writes);
273
+ }
274
+ }
275
+ }
276
+
277
+ /** One name node the walk found, and what the walk knows about it. */
278
+ interface Site {
279
+ /** The name node, whose position the reference carries. */
280
+ readonly node: Node;
281
+ /**
282
+ * The shorthand property assignment this name belongs to, where it belongs to one.
283
+ * Its value is the declaration the name reads, while the name itself declares the
284
+ * property the literal carries.
285
+ */
286
+ readonly shorthand: Node | undefined;
287
+ readonly from: string;
288
+ readonly use: Use;
289
+ }
290
+
291
+ /**
292
+ * Every name node one file holds that is a use rather than a declaration, in source
293
+ * order, each with the declaration that encloses it and the use it makes.
294
+ *
295
+ * One walk answers all three, because the three facts are positional and a second walk
296
+ * would have to agree with this one about which nodes are names. A name that is the
297
+ * declared name of the declaration it belongs to is left out: it is where that
298
+ * declaration is written rather than a use of it, which is what makes a declaration
299
+ * referenced only from its own site an unreferenced declaration.
300
+ */
301
+ function sitesOf(
302
+ file: SourceFile,
303
+ declarations: ReadonlyMap<string, string>,
304
+ fileId: string,
305
+ ): Site[] {
306
+ const found: Site[] = [];
307
+ const writes = new Set<number>();
308
+ const declared = new Set<number>();
309
+ const shorthands = new Map<number, Node>();
310
+ let enclosing = fileId;
311
+
312
+ const markNames = (node: Node): void => {
313
+ if (isShorthandPropertyAssignment(node)) {
314
+ shorthands.set(node.name.pos, node);
315
+ return;
316
+ }
317
+ // A property access carries the member it names where a declaration carries its
318
+ // declared name, and it is the one form of the grammar that does.
319
+ if (isPropertyAccessExpression(node)) {
320
+ return;
321
+ }
322
+ const named = node as { readonly name?: Node; readonly propertyName?: Node };
323
+ for (const name of [named.name, named.propertyName]) {
324
+ if (name !== undefined && isName(name)) {
325
+ declared.add(name.pos);
326
+ }
327
+ }
328
+ };
329
+
330
+ const visit = (node: Node): void => {
331
+ if (isName(node)) {
332
+ if (!declared.has(node.pos)) {
333
+ found.push({
334
+ node,
335
+ shorthand: shorthands.get(node.pos),
336
+ from: enclosing,
337
+ use: writes.has(node.pos) ? "write" : "read",
338
+ });
339
+ }
340
+ return;
341
+ }
342
+ markNames(node);
343
+ markStores(node, writes);
344
+ const held = declarations.get(nodeKey(file, node));
345
+ const outer = enclosing;
346
+ if (held !== undefined) {
347
+ enclosing = held;
348
+ }
349
+ node.forEachChild(visit);
350
+ enclosing = outer;
351
+ };
352
+
353
+ file.forEachChild(visit);
354
+ return found;
355
+ }
356
+
357
+ /**
358
+ * One literal run of a pattern, with every character the expression syntax claims
359
+ * escaped.
360
+ *
361
+ * The set is the syntax characters and the solidus, and it is closed: an escape of any
362
+ * other character is refused outright by the expression syntax under the unicode flag,
363
+ * so escaping a hyphen, which needs none outside a character class, would refuse every
364
+ * pattern that names a hyphenated path.
365
+ */
366
+ function quoted(text: string): string {
367
+ return text.replaceAll(/[.*+?^${}()|[\]\\/]/gu, (character) => `\\${character}`);
368
+ }
369
+
370
+ /**
371
+ * The expression one glob pattern denotes. `**` spans path separators while `*` and `?`
372
+ * do not, a brace list is an alternation of its members, and every other character
373
+ * stands for itself.
374
+ */
375
+ function expressionOf(pattern: string): RegExp {
376
+ let source = "^";
377
+ let at = 0;
378
+ while (at < pattern.length) {
379
+ const rest = pattern.slice(at);
380
+ if (rest.startsWith("**/")) {
381
+ source += "(?:[^/]*/)*";
382
+ at += 3;
383
+ continue;
384
+ }
385
+ if (rest.startsWith("**")) {
386
+ source += ".*";
387
+ at += 2;
388
+ continue;
389
+ }
390
+ const character = pattern[at] ?? "";
391
+ at += 1;
392
+ if (character === "*") {
393
+ source += "[^/]*";
394
+ continue;
395
+ }
396
+ if (character === "?") {
397
+ source += "[^/]";
398
+ continue;
399
+ }
400
+ if (character === "{") {
401
+ const close = pattern.indexOf("}", at);
402
+ if (close < 0) {
403
+ source += "\\{";
404
+ continue;
405
+ }
406
+ source += `(?:${pattern.slice(at, close).split(",").map(quoted).join("|")})`;
407
+ at = close + 1;
408
+ continue;
409
+ }
410
+ source += quoted(character);
411
+ }
412
+ return new RegExp(`${source}$`, "u");
413
+ }
414
+
415
+ /**
416
+ * The name one test-file rule takes. A rule's name is a word list the report's own
417
+ * vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
418
+ * its pattern in the configured list.
419
+ */
420
+ function patternRule(index: number): string {
421
+ return `test-file-pattern-${String(index + 1)}`;
422
+ }
423
+
424
+ /** Two strings ordered bytewise, which is the order every set of a run is read in. */
425
+ function compare(a: string, b: string): number {
426
+ if (a === b) {
427
+ return 0;
428
+ }
429
+ return a < b ? -1 : 1;
430
+ }
431
+
432
+ /** One declaration one symbol reaches, and whether a step along an alias reached it. */
433
+ interface Target {
434
+ readonly id: string;
435
+ readonly stepped: boolean;
436
+ }
437
+
438
+ /**
439
+ * Every reference one project's own files make to the project's own declarations, in
440
+ * position order, and the test-file rules that classified those files.
441
+ *
442
+ * `held` is the inventory of the same project, whose declaration map is what a resolved
443
+ * symbol is looked up in; `targetRoot` is the absolute path every position is rendered
444
+ * against, which is the root the inventory was read against.
445
+ *
446
+ * The order is by the referencing position, then by the declaration named, the
447
+ * declaration referencing it and the use. That is a total order because one name
448
+ * references one declaration once from one declaration, while a chain of aliases reaches
449
+ * several declarations at one position.
450
+ */
451
+ export function references<Brand>(
452
+ project: ProjectView<Brand>,
453
+ held: Inventory,
454
+ targetRoot: string,
455
+ options: ReferenceOptions,
456
+ ): References {
457
+ const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
458
+ const declarations = held.declarations;
459
+ const patterns = options.testFiles.map(expressionOf);
460
+ const matched = options.testFiles.map(() => 0);
461
+ const found: Reference[] = [];
462
+ const reached = new Map<number, readonly Target[]>();
463
+ let batched = 0;
464
+ let fileBatches = 0;
465
+ let residueFallbacks = 0;
466
+ let shorthandLookups = 0;
467
+ let aliasSteps = 0;
468
+
469
+ const files = project.ownSourceFiles();
470
+ const ownFiles = new Set(files.map((file) => file.fileName));
471
+
472
+ /** The identifier of the declaration one node of this program is, where it is one. */
473
+ const declaredAt = (node: Node): string | undefined =>
474
+ declarations.get(nodeKey(node.getSourceFile(), node));
475
+
476
+ /**
477
+ * The declarations one symbol names: the declaration it is, and every link of the
478
+ * alias chain it stands at the head of. A re-export something imports is used and so
479
+ * is the declaration behind it, so both are named; a link this project does not
480
+ * declare is stepped over rather than recorded.
481
+ *
482
+ * A declaration handle carries the file it is in and the syntax it is written as, so
483
+ * both questions the walk asks of a declaration are answered before it is resolved: a
484
+ * declaration outside the project's own files belongs to another program and is never
485
+ * read, which is what keeps the pass from fetching a library file to discover that the
486
+ * inventory does not hold what is in it.
487
+ */
488
+ const targetsOf = (symbol: TSSymbol): readonly Target[] => {
489
+ const cached = reached.get(symbol.id);
490
+ if (cached !== undefined) {
491
+ return cached;
492
+ }
493
+ const targets: Target[] = [];
494
+ const walked = new Set<number>();
495
+ let at: TSSymbol | undefined = symbol;
496
+ let stepped = false;
497
+ while (at !== undefined && !walked.has(at.id)) {
498
+ walked.add(at.id);
499
+ let alias = false;
500
+ for (const handle of at.declarations) {
501
+ alias = alias || ALIASES.has(handle.kind);
502
+ if (!ownFiles.has(handle.path)) {
503
+ continue;
504
+ }
505
+ const node = project.declarationAt(handle)?.node;
506
+ const id = node === undefined ? undefined : declaredAt(node);
507
+ if (id !== undefined && !targets.some((target) => target.id === id)) {
508
+ targets.push({ id, stepped });
509
+ }
510
+ }
511
+ if (!alias) {
512
+ break;
513
+ }
514
+ at = project.aliasStepOf(at);
515
+ aliasSteps += 1;
516
+ stepped = true;
517
+ }
518
+ reached.set(symbol.id, targets);
519
+ return targets;
520
+ };
521
+
522
+ /** The symbol one site resolves to, asking only the accessor the site's form names. */
523
+ const symbolFor = (site: Site, answered: ReadonlyMap<number, TSSymbol>): TSSymbol | undefined => {
524
+ if (site.shorthand !== undefined) {
525
+ shorthandLookups += 1;
526
+ return project.shorthandValueAt(project.handle(site.shorthand));
527
+ }
528
+ const batched = answered.get(site.node.pos);
529
+ if (batched !== undefined) {
530
+ return batched;
531
+ }
532
+ residueFallbacks += 1;
533
+ return project.resolvedSymbolAt(project.handle(site.node));
534
+ };
535
+
536
+ for (const file of files) {
537
+ const path = renderPosition(file, targetRoot, 0).path;
538
+ let test = false;
539
+ patterns.forEach((pattern, index) => {
540
+ if (pattern.test(path)) {
541
+ matched[index] = (matched[index] ?? 0) + 1;
542
+ test = true;
543
+ }
544
+ });
545
+ const sites = sitesOf(file, declarations, declaredAt(file) ?? "");
546
+
547
+ // One batch per capped run of the file's name nodes. A shorthand is left out of
548
+ // the batch: its name resolves to the property the literal declares, so the batch's
549
+ // answer for it would name something this project's inventory never holds.
550
+ const batching = sites.filter((site) => site.shorthand === undefined);
551
+ const answered = new Map<number, TSSymbol>();
552
+ batched += batching.length;
553
+ for (let from = 0; from < batching.length; from += cap) {
554
+ const run = batching.slice(from, from + cap);
555
+ const answers = project.symbolsAt(run.map((site) => project.handle(site.node)));
556
+ fileBatches += 1;
557
+ run.forEach((site, index) => {
558
+ const symbol = answers[index];
559
+ if (symbol !== undefined) {
560
+ answered.set(site.node.pos, symbol);
561
+ }
562
+ });
563
+ }
564
+
565
+ for (const site of sites) {
566
+ const direct: Resolution =
567
+ site.shorthand !== undefined
568
+ ? "shorthand"
569
+ : answered.has(site.node.pos)
570
+ ? "batch"
571
+ : "resolved-symbol";
572
+ const symbol = symbolFor(site, answered);
573
+ if (symbol === undefined) {
574
+ continue;
575
+ }
576
+ const position = renderPosition(file, targetRoot, site.node.getStart());
577
+ for (const target of targetsOf(symbol)) {
578
+ found.push({
579
+ from: site.from,
580
+ to: target.id,
581
+ position,
582
+ use: site.use,
583
+ resolution: target.stepped ? "alias" : direct,
584
+ test,
585
+ });
586
+ }
587
+ }
588
+ }
589
+
590
+ found.sort(
591
+ (a, b) =>
592
+ byPosition(a.position, b.position) ||
593
+ compare(a.to, b.to) ||
594
+ compare(a.from, b.from) ||
595
+ compare(a.use, b.use),
596
+ );
597
+
598
+ return {
599
+ configFile: project.configFile,
600
+ references: found,
601
+ testFileRules: options.testFiles
602
+ .map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
603
+ .sort((a, b) => compare(a.rule, b.rule)),
604
+ cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps },
605
+ };
606
+ }
package/src/run.ts CHANGED
@@ -11,7 +11,7 @@ import { printConfig } from "./print.ts";
11
11
  import { resolve } from "./resolve.ts";
12
12
  import { readScope, ScopeError, scopeForDir, type Scope } from "./scope.ts";
13
13
  import { diagnosticsOf, openEngine, runSession, type Engine } from "./session.ts";
14
- import { CONTRACT_VERSION, version } from "./version.ts";
14
+ import { CONTRACT_VERSION } from "./version.ts";
15
15
 
16
16
  /** One output stream of the command line. `process.stdout` and `process.stderr` satisfy it. */
17
17
  export interface Writer {
@@ -186,8 +186,8 @@ function scopeOf(host: Host, options: Options): Scope {
186
186
  * An option asking for a source edit is refused before the verb is read: this
187
187
  * analyzer reports and never edits a source file.
188
188
  *
189
- * `host` is the filesystem the run reads and the directory it reads relative paths
190
- * against; `openClient` opens the compiler client a verb that reads projects needs.
189
+ * `host` is the platform the run reads, the version it reports included;
190
+ * `openClient` opens the compiler client a verb that reads projects needs.
191
191
  * Both are parameters because both are the platform, which no module below this one
192
192
  * names: the command entry binds them, and a caller that wants to watch what a run
193
193
  * reads supplies its own.
@@ -210,7 +210,7 @@ export function run(
210
210
 
211
211
  const verb = args[0];
212
212
  if (verb === "version") {
213
- out.write(`deadset-ts ${version()}\ncontract ${CONTRACT_VERSION}\n`);
213
+ out.write(`deadset-ts ${host.analyzerVersion()}\ncontract ${CONTRACT_VERSION}\n`);
214
214
  return EXIT_CLEAN;
215
215
  }
216
216
  if (verb === "print-config") {