@cplieger/deadset-ts 7.0.0-dev.2 → 7.0.0-dev.3

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 (75) hide show
  1. package/dist/src/analysis.js +89 -44
  2. package/dist/src/augmentations.js +14 -12
  3. package/dist/src/components.js +62 -20
  4. package/dist/src/configuration-files.js +5 -5
  5. package/dist/src/conformance-record.js +1 -1
  6. package/dist/src/contextual-uses.js +71 -14
  7. package/dist/src/convention-rows.js +6 -2
  8. package/dist/src/conventions.js +3 -1
  9. package/dist/src/dependencies.js +80 -26
  10. package/dist/src/element-access.js +341 -0
  11. package/dist/src/erased-stores.js +317 -0
  12. package/dist/src/findings/completion.js +46 -15
  13. package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
  14. package/dist/src/findings/emitters.js +1 -1
  15. package/dist/src/findings/self-check.js +17 -4
  16. package/dist/src/findings-pass.js +6 -5
  17. package/dist/src/in-operands.js +30 -0
  18. package/dist/src/indexed-access.js +264 -125
  19. package/dist/src/intra-function-parts.js +25 -11
  20. package/dist/src/inventory.js +25 -0
  21. package/dist/src/keyof-comparisons.js +170 -0
  22. package/dist/src/kinds.js +17 -7
  23. package/dist/src/manifest.js +6 -16
  24. package/dist/src/matrix.js +20 -2
  25. package/dist/src/query.js +3 -0
  26. package/dist/src/references.js +12 -1
  27. package/dist/src/report.js +1 -1
  28. package/dist/src/reporters/sarif.js +4 -0
  29. package/dist/src/serialization-contract.js +43 -14
  30. package/dist/src/session.js +12 -7
  31. package/dist/src/suppress-file.js +58 -6
  32. package/dist/src/suppress-inline.js +13 -4
  33. package/dist/src/suppress.js +54 -24
  34. package/dist/src/unimplemented-callees.js +312 -0
  35. package/dist/src/value-reach.js +7 -7
  36. package/dist/src/verbs/analyze.js +11 -9
  37. package/jsr.json +1 -1
  38. package/package.json +2 -2
  39. package/src/analysis.ts +135 -52
  40. package/src/augmentations.ts +14 -11
  41. package/src/components.ts +103 -21
  42. package/src/configuration-files.ts +5 -4
  43. package/src/conformance-record.ts +1 -1
  44. package/src/contextual-uses.ts +81 -17
  45. package/src/convention-rows.ts +6 -2
  46. package/src/conventions.ts +10 -1
  47. package/src/dependencies.ts +107 -30
  48. package/src/element-access.ts +446 -0
  49. package/src/erased-stores.ts +413 -0
  50. package/src/file-facts.ts +2 -0
  51. package/src/findings/completion.ts +54 -20
  52. package/src/findings/dependencies-and-module-machinery.ts +1 -1
  53. package/src/findings/emitters.ts +1 -1
  54. package/src/findings/self-check.ts +18 -4
  55. package/src/findings-pass.ts +12 -5
  56. package/src/in-operands.ts +45 -0
  57. package/src/indexed-access.ts +303 -145
  58. package/src/intra-function-parts.ts +41 -16
  59. package/src/inventory.ts +32 -0
  60. package/src/keyof-comparisons.ts +211 -0
  61. package/src/kinds.ts +20 -7
  62. package/src/manifest.ts +6 -21
  63. package/src/matrix.ts +34 -2
  64. package/src/query.ts +10 -0
  65. package/src/references.ts +23 -1
  66. package/src/report.ts +3 -1
  67. package/src/reporters/sarif.ts +4 -0
  68. package/src/serialization-contract.ts +53 -14
  69. package/src/session.ts +34 -29
  70. package/src/suppress-file.ts +68 -5
  71. package/src/suppress-inline.ts +14 -4
  72. package/src/suppress.ts +63 -28
  73. package/src/unimplemented-callees.ts +411 -0
  74. package/src/value-reach.ts +11 -7
  75. package/src/verbs/analyze.ts +15 -9
package/src/matrix.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * and intersected, with the dead components computed once over every configuration.
5
5
  */
6
6
 
7
- import { componentsOf, type Component } from "./components.ts";
7
+ import { componentsOf, type Component, type Positional } from "./components.ts";
8
8
  import { graphOf, OUTSIDE, type Graph } from "./graph.ts";
9
9
  import type { InventorySymbol } from "./inventory.ts";
10
10
  import { byPosition } from "./position.ts";
@@ -36,6 +36,8 @@ export interface Configured {
36
36
  * finding.
37
37
  */
38
38
  readonly referenceOnly?: readonly string[] | undefined;
39
+ /** Each declaration's dependents, by id, as the configuration's inventory states them. */
40
+ readonly dependents?: ReadonlyMap<string, readonly string[]> | undefined;
39
41
  }
40
42
 
41
43
  /** The run's configurations, indexed for the sweep. */
@@ -56,6 +58,8 @@ export interface Matrix {
56
58
  * whose project holds it, in the run's order.
57
59
  */
58
60
  readonly heldIn: readonly (readonly string[])[];
61
+ /** Each declaration's dependents, by id, as the first configuration holding it states them. */
62
+ readonly dependents: ReadonlyMap<string, readonly string[]>;
59
63
  }
60
64
 
61
65
  /** One declaration dead in every configuration that holds it. */
@@ -128,7 +132,13 @@ export function matrixOf(per: readonly Configured[]): Matrix {
128
132
  const references: Reference[] = [];
129
133
  const firstSeen = new Map<string, number>();
130
134
  const judged = new Set<string>();
135
+ const dependents = new Map<string, readonly string[]>();
131
136
  per.forEach((one, index) => {
137
+ for (const [id, after] of one.dependents ?? []) {
138
+ if (!dependents.has(id)) {
139
+ dependents.set(id, after);
140
+ }
141
+ }
132
142
  const referenceOnly = new Set(one.referenceOnly);
133
143
  for (const symbol of one.symbols) {
134
144
  const held = holders.get(symbol.id);
@@ -168,6 +178,7 @@ export function matrixOf(per: readonly Configured[]): Matrix {
168
178
  [...new Set(per.flatMap((one) => one.supportFiles ?? []))],
169
179
  ),
170
180
  heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
181
+ dependents,
171
182
  };
172
183
  }
173
184
 
@@ -257,14 +268,16 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
257
268
  });
258
269
  });
259
270
 
271
+ const liveUnder = liveAnywhere(union.symbols, per);
260
272
  return {
261
- liveUnder: liveAnywhere(union.symbols, per),
273
+ liveUnder,
262
274
  candidates,
263
275
  components: componentsOf(
264
276
  union,
265
277
  dead.map((one, at) => one && heldBy[at] === undefined),
266
278
  testOfDeadCode,
267
279
  unreferencedTest,
280
+ positionsOf(union, matrix.dependents, liveUnder),
268
281
  ),
269
282
  };
270
283
  }
@@ -286,6 +299,25 @@ export function liveFilesAt(
286
299
  );
287
300
  }
288
301
 
302
+ /**
303
+ * Each declaration's dependents by its place in the union, and per place whether the
304
+ * declaration is live under reachability in some configuration.
305
+ */
306
+ function positionsOf(
307
+ union: Graph,
308
+ dependents: ReadonlyMap<string, readonly string[]>,
309
+ liveUnder: ReadonlyMap<string, readonly Relation[]>,
310
+ ): Positional {
311
+ return {
312
+ dependents: union.symbols.map((symbol) =>
313
+ (dependents.get(symbol.id) ?? []).map((id) => union.at(id)).filter((at) => at !== OUTSIDE),
314
+ ),
315
+ live: union.symbols.map(
316
+ (symbol) => liveUnder.get(symbol.id)?.includes("reachability") === true,
317
+ ),
318
+ };
319
+ }
320
+
289
321
  /** The relations in the order a declaration's entry lists them. */
290
322
  const RELATIONS: readonly Relation[] = ["reference-counting", "reachability"];
291
323
 
package/src/query.ts CHANGED
@@ -205,6 +205,8 @@ export interface Queries {
205
205
  targetOf(type: Type): Answer<Type>;
206
206
  /** The constraint of a type parameter, where it declares one; none for another type. */
207
207
  constraintOf(type: Type): Answer<Type | undefined>;
208
+ /** The object and the index of an indexed-access type; none for another type. */
209
+ indexedAccessOf(type: Type): Answer<{ readonly object: Type; readonly index: Type } | undefined>;
208
210
  /** The symbol a type is declared by. */
209
211
  symbolOfType(type: Type): Answer<TSSymbol | undefined>;
210
212
  signaturesOf(type: Type, kind: SignatureKind): Answer<readonly Signature[]>;
@@ -516,6 +518,14 @@ export function queriesOf(
516
518
  () => type.getConstraint(),
517
519
  )
518
520
  : undefined,
521
+ indexedAccessOf: (type) =>
522
+ type.isIndexedAccessType()
523
+ ? one(
524
+ "getObjectType",
525
+ () => typeLocation(type),
526
+ () => ({ object: type.getObjectType(), index: type.getIndexType() }),
527
+ )
528
+ : undefined,
519
529
  symbolOfType: (type) =>
520
530
  one(
521
531
  "getSymbol",
package/src/references.ts CHANGED
@@ -43,7 +43,10 @@ import { namespaceUses } from "./namespace-objects.ts";
43
43
  import { globExpression } from "./glob.ts";
44
44
  import { declarationsByName, nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
45
45
  import { augmentationReferences } from "./augmentations.ts";
46
+ import { elementAccessReferences } from "./element-access.ts";
46
47
  import { indexedAccessReferences } from "./indexed-access.ts";
48
+ import { keyofComparisonReferences } from "./keyof-comparisons.ts";
49
+ import { unimplementedCalleeReads } from "./unimplemented-callees.ts";
47
50
  import { moduleDeclarationReferences } from "./module-declarations.ts";
48
51
  import { overrideReferences } from "./overrides.ts";
49
52
  import { dirnamePath, queriedModulePath, relativePath, resolvePath } from "./paths.ts";
@@ -185,6 +188,13 @@ export interface ReferenceCost {
185
188
  readonly contextualBatches: number;
186
189
  /** Per-type lookups for the contextual uses: each distinct type's constituents, properties and symbol. */
187
190
  readonly contextualLookups: number;
191
+ /**
192
+ * The requests the passes that read members through the checker's types make: element
193
+ * accesses, key comparisons, instantiated signatures and values handed to unimplemented
194
+ * callees. They are
195
+ * counted where the caller measures the client, through `requestCount`, and are 0 otherwise.
196
+ */
197
+ readonly typedLookups: number;
188
198
  }
189
199
 
190
200
  /** One project's references, in position order, and what reading them cost. */
@@ -221,6 +231,8 @@ export interface ReferenceOptions {
221
231
  * reference to a declaration of `held`, the target's, is recorded.
222
232
  */
223
233
  readonly consumer?: ConsumerSide;
234
+ /** The client's running request count, where the caller measures the client. */
235
+ readonly requestCount?: () => number;
224
236
  }
225
237
 
226
238
  /**
@@ -1556,12 +1568,21 @@ export function references<Brand>(
1556
1568
  }
1557
1569
  }
1558
1570
 
1571
+ let typedLookups = 0;
1559
1572
  if (consumer === undefined) {
1560
1573
  found.push(...overrideReferences(project, held, chains, new Set(tests)));
1574
+ const before = options.requestCount?.() ?? 0;
1561
1575
  found.push(...indexedAccessReferences(project, held, chains, root, new Set(tests)));
1576
+ found.push(...elementAccessReferences(project, held, chains, root, new Set(tests)));
1577
+ found.push(...keyofComparisonReferences(project, held, chains, root, new Set(tests)));
1578
+ found.push(...unimplementedCalleeReads(project, held, chains, root, new Set(tests)));
1579
+ typedLookups = (options.requestCount?.() ?? 0) - before;
1562
1580
  found.push(...moduleDeclarationReferences(project, held, root, new Set(tests)));
1563
1581
  }
1564
- found.push(...augmentationReferences(held, found));
1582
+ const scripts = new Set(
1583
+ walked.filter((one) => one.file.externalModuleIndicator === undefined).map((one) => one.fileId),
1584
+ );
1585
+ found.push(...augmentationReferences(held, found, scripts));
1565
1586
  found.sort(
1566
1587
  (a, b) =>
1567
1588
  byPosition(a.position, b.position) ||
@@ -1586,6 +1607,7 @@ export function references<Brand>(
1586
1607
  patternLookups: destructured.cost.patternLookups,
1587
1608
  contextualBatches: contextual.batches,
1588
1609
  contextualLookups: contextual.lookups,
1610
+ typedLookups,
1589
1611
  },
1590
1612
  };
1591
1613
  }
package/src/report.ts CHANGED
@@ -19,7 +19,7 @@ import type { StaleSuppression } from "./findings/self-check.ts";
19
19
  import type { TestFileRule } from "./references.ts";
20
20
 
21
21
  /** The version of the report schema this analyzer writes a report to. */
22
- const SCHEMA_VERSION = "8.0.0";
22
+ const SCHEMA_VERSION = "8.1.0";
23
23
 
24
24
  /** Every report schema version this analyzer reads: the one the Contract it implements admits. */
25
25
  export const SCHEMA_VERSIONS_ACCEPTED: readonly string[] = [SCHEMA_VERSION];
@@ -45,6 +45,8 @@ interface WireDetails {
45
45
  readonly position: WirePosition;
46
46
  }[];
47
47
  readonly write_positions?: readonly WirePosition[];
48
+ /** Set under the name-literal rule, which no finding of this analyzer meets; SARIF still maps it. */
49
+ readonly name_literal?: WirePosition;
48
50
  readonly dependency_class?: string;
49
51
  readonly mechanism?: string;
50
52
  readonly entry?: {
@@ -160,6 +160,10 @@ function relatedOf(found: WireFinding): readonly Related[] {
160
160
  for (const at of found.details.write_positions ?? []) {
161
161
  related.push({ label: "write", at, endLine: at.end_line });
162
162
  }
163
+ if (found.details.name_literal !== undefined) {
164
+ const at = found.details.name_literal;
165
+ related.push({ label: "name", at, endLine: at.end_line });
166
+ }
163
167
  for (const one of found.component.members ?? []) {
164
168
  if (
165
169
  declarationKey(one.ref, one.position.path) !==
@@ -1,12 +1,10 @@
1
1
  /**
2
- * The serialization-contract class: the data members and `toJSON` of every class,
3
- * interface and object type a value's static type names, and of every such type its
4
- * data members' types reach, where the value reaches `JSON.stringify` or a call
5
- * `ts.serializers` names, and those and `toString` where it is passed to an `unknown`
6
- * or `any` parameter of a function outside the analysis. A function of the project
7
- * handing such a parameter of its own to a destination is one, holding back what that
8
- * destination does, to a fixpoint. A function invoked through `Function.prototype.call`,
9
- * `apply` or `bind` is the callee, given the arguments those methods pass it.
2
+ * The serialization-contract class: the data members and `toJSON` of every type a value's
3
+ * static type reaches, where the value reaches `JSON.stringify` or a call `ts.serializers`
4
+ * names, and `toString` too where it reaches an `unknown` or `any` parameter of a function
5
+ * outside the analysis or an erased property of a type the program does not declare. A
6
+ * function of the project handing such a parameter to a destination is one, to a fixpoint,
7
+ * and a function invoked through `call`, `apply` or `bind` is the callee.
10
8
  */
11
9
 
12
10
  import {
@@ -59,6 +57,7 @@ import { nodeKey, type Inventory } from "./inventory.ts";
59
57
  import { renderPosition } from "./position.ts";
60
58
  import { must } from "./query.ts";
61
59
  import type { ProjectView } from "./session.ts";
60
+ import { erasedStores, type ErasedStores } from "./erased-stores.ts";
62
61
  import { valueReach, type Reached } from "./value-reach.ts";
63
62
 
64
63
  /** The serializer every run reads, whatever the configuration names. */
@@ -527,13 +526,20 @@ function destinations<Brand>(
527
526
  return { invocation, at, mayReceive };
528
527
  }
529
528
 
530
- /** One argument whose value carries classes and object types of the target into a destination. */
529
+ /** One value that carries classes and object types of the target into a destination. */
531
530
  interface Flow {
532
531
  readonly site: Node;
533
532
  readonly classes: ReadonlySet<string>;
534
533
  /** The members of the interfaces and object types the value carries. */
535
534
  readonly members: readonly TSSymbol[];
536
535
  readonly to: Destination;
536
+ /** What a retention names the destination by. */
537
+ readonly detail: string;
538
+ }
539
+
540
+ /** What two reaches carry together. */
541
+ function joined(a: Reached, b: Reached): Reached {
542
+ return { classes: new Set([...a.classes, ...b.classes]), members: [...a.members, ...b.members] };
537
543
  }
538
544
 
539
545
  /** Each argument of a call that can receive one whose value carries a class of the target. */
@@ -542,8 +548,9 @@ function flowsOf<Brand>(
542
548
  held: Inventory,
543
549
  sites: readonly Site[],
544
550
  to: Destinations,
551
+ stores: ErasedStores,
545
552
  ): readonly Flow[] {
546
- const reach = valueReach(project, held);
553
+ const reach = valueReach(project, held, stores.inside);
547
554
  const arguments_ = sites.flatMap((site) =>
548
555
  to.mayReceive(site.call)
549
556
  ? to.invocation(site.call).arguments.flatMap((argument, index) => {
@@ -560,7 +567,7 @@ function flowsOf<Brand>(
560
567
  const typeOf = new Map<Expression, Type | undefined>(
561
568
  asked.map((one, index) => [one.value, types[index]]),
562
569
  );
563
- return arguments_.flatMap((one) => {
570
+ const passed = arguments_.flatMap((one) => {
564
571
  const type = typeOf.get(one.value);
565
572
  let reached: Reached = { classes: new Set<string>(), members: [] };
566
573
  if (one.value.kind === SyntaxKind.ThisKeyword) {
@@ -571,12 +578,38 @@ function flowsOf<Brand>(
571
578
  } else if (type !== undefined) {
572
579
  reached = reach.reachedFrom(type);
573
580
  }
581
+ for (const stored of stores.ofVariable(one.value) ?? []) {
582
+ reached = joined(reached, reach.reachedFrom(stored));
583
+ }
574
584
  const carries = reached.classes.size > 0 || reached.members.length > 0;
575
585
  const destination = carries ? to.at(one.site.call, one.argument) : undefined;
576
586
  return destination === undefined
577
587
  ? []
578
- : [{ site: one.value, classes: reached.classes, members: reached.members, to: destination }];
588
+ : [
589
+ {
590
+ site: one.value,
591
+ classes: reached.classes,
592
+ members: reached.members,
593
+ to: destination,
594
+ detail: `passed to ${destination.callee}`,
595
+ },
596
+ ];
597
+ });
598
+ const left = stores.outside.flatMap((store) => {
599
+ const reached = reach.reachedFrom(store.type);
600
+ return reached.classes.size === 0 && reached.members.length === 0
601
+ ? []
602
+ : [
603
+ {
604
+ site: store.value,
605
+ classes: reached.classes,
606
+ members: reached.members,
607
+ to: { reach: "conversions" as const, callee: store.property },
608
+ detail: `stored in the property ${store.property} of a type the program does not declare`,
609
+ },
610
+ ];
579
611
  });
612
+ return [...passed, ...left];
580
613
  }
581
614
 
582
615
  /**
@@ -589,7 +622,13 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
589
622
  const { project, held, targetRoot } = input;
590
623
  const walked = walkProject(project, held);
591
624
  const chains = aliasChains(project, held);
592
- const flows = flowsOf(project, held, walked.sites, destinations(input, chains, walked));
625
+ const flows = flowsOf(
626
+ project,
627
+ held,
628
+ walked.sites,
629
+ destinations(input, chains, walked),
630
+ erasedStores(project),
631
+ );
593
632
  if (flows.length === 0) {
594
633
  return [];
595
634
  }
@@ -609,7 +648,7 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
609
648
  const found: Evidence[] = [];
610
649
  for (const flow of flows) {
611
650
  const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
612
- const detail = `passed to ${flow.to.callee}`;
651
+ const { detail } = flow;
613
652
  for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
614
653
  const symbol = byId.get(member.id);
615
654
  const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
package/src/session.ts CHANGED
@@ -28,13 +28,10 @@ import {
28
28
  } from "./query.ts";
29
29
 
30
30
  /**
31
- * The compiler the analysis reads through. It is a client over a protocol to a
32
- * separate process, so every accessor call is a round trip and a run holds one
33
- * client and one snapshot.
34
- *
35
- * It is an interface because the run's own accounting for that lifetime is not on
36
- * the client's surface: a snapshot reports whether it is disposed and not how many
37
- * a client opened, so the count is observed at this seam.
31
+ * The compiler the analysis reads through: a client over a protocol to a separate
32
+ * process, so every accessor call is a round trip and a run holds one client and one
33
+ * snapshot. It is an interface because a snapshot reports whether it is disposed and not
34
+ * how many a client opened, so the run's count of them is observed at this seam.
38
35
  */
39
36
  export interface Engine {
40
37
  /**
@@ -187,14 +184,10 @@ export function openEngine(options: EngineOptions): Engine {
187
184
  }
188
185
 
189
186
  /**
190
- * A node of one project's program, carried with that project's identity.
191
- *
192
- * `Brand` is a type parameter and never a value: a view is handed to a visitor
193
- * that introduces its own `Brand`, so two projects' views have handle types the
194
- * compiler will not exchange and a handle cannot reach another project's checker.
195
- * Handles are only meaningful inside the program that produced them, and the
196
- * cross-project step of the analysis compares rendered positions rather than
197
- * handles.
187
+ * A node of one project's program, carried with that project's identity. `Brand` is a
188
+ * type parameter and never a value: each visitor introduces its own, so two projects'
189
+ * handle types do not exchange and a handle cannot reach another project's checker. The
190
+ * cross-project step compares rendered positions, never handles.
198
191
  */
199
192
  export interface Handle<Brand> {
200
193
  readonly node: Node;
@@ -253,14 +246,11 @@ export interface ProjectView<Brand> {
253
246
  */
254
247
  aliasStepOf(symbol: TSSymbol): Answer<TSSymbol | undefined>;
255
248
  /**
256
- * The node one symbol's declaration handle names, read in this project's program,
257
- * or `undefined` where this project's program does not hold that file.
258
- *
259
- * A symbol is shared by every project of the snapshot and remembers the project it
260
- * was first seen in, so resolving its handle without saying which program to read
261
- * would answer out of whichever project reached the symbol first. The project is
262
- * named here instead, and the answer becomes a handle of this project like any
263
- * other. The read is local once the file has been fetched.
249
+ * The node one symbol's declaration handle names, read in this project's program, or
250
+ * `undefined` where this program does not hold that file. A symbol is shared by every
251
+ * project of the snapshot and remembers the one it was first seen in, so the project is
252
+ * named here, or the answer would come from whichever project reached the symbol first.
253
+ * The read is local once the file has been fetched.
264
254
  */
265
255
  declarationAt(handle: NodeHandle): Handle<Brand> | undefined;
266
256
  }
@@ -272,6 +262,13 @@ export interface ProjectView<Brand> {
272
262
  */
273
263
  export type ProjectVisitor<Result> = <Brand>(project: ProjectView<Brand>) => Result;
274
264
 
265
+ /** One project a session opened: its configuration and the files of its own program. */
266
+ export interface OpenedFiles {
267
+ readonly configFile: string;
268
+ /** The program's source files no default library and no external library holds. */
269
+ readonly fileNames: () => readonly string[];
270
+ }
271
+
275
272
  /**
276
273
  * The source files of one program that `own` admits, in the program's order, none of
277
274
  * them one of the compiler's default libraries.
@@ -487,10 +484,10 @@ function projectsOf(
487
484
  /**
488
485
  * Runs `visit` over every project the compiler configurations name, inside one never updated
489
486
  * snapshot of one client, and answers in their order. A configuration `programs` describes is
490
- * built as that program, or opened from the configuration it writes, instead of as a project.
491
- * One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
492
- * under `"record"`. `last` runs after the last visit while the client still answers; the
493
- * snapshot is disposed and the client released after it, whether the visit completed or threw.
487
+ * built as that program, or opened from the configuration it writes. One the snapshot holds
488
+ * nothing for ends the run under `"refuse"` and is named in the result under `"record"`.
489
+ * `first` runs over every opened project before the first visit and `last` after the last,
490
+ * both while the client answers; the snapshot and the client are then released.
494
491
  */
495
492
  export function runSession<Result>(
496
493
  engine: Engine,
@@ -499,6 +496,7 @@ export function runSession<Result>(
499
496
  unopened: Unopened = "refuse",
500
497
  programs: ReadonlyMap<string, WorkspaceProgram> = new Map(),
501
498
  last: () => void = () => undefined,
499
+ first: (opened: readonly OpenedFiles[]) => void = () => undefined,
502
500
  ): SessionResult<Result> {
503
501
  try {
504
502
  const built: string[] = [];
@@ -523,7 +521,7 @@ export function runSession<Result>(
523
521
  try {
524
522
  const opened = projectsOf(snapshot, built, written);
525
523
  const log = new UnansweredLog();
526
- const projects: Result[] = [];
524
+ const views: ProjectView<unknown>[] = [];
527
525
  const missing: string[] = [];
528
526
  for (const configFile of configFiles) {
529
527
  const project = opened.get(configFile);
@@ -534,8 +532,15 @@ export function runSession<Result>(
534
532
  missing.push(configFile);
535
533
  continue;
536
534
  }
537
- projects.push(visit(viewOf(project, configFile, engine, log)));
535
+ views.push(viewOf(project, configFile, engine, log));
538
536
  }
537
+ first(
538
+ views.map((view) => ({
539
+ configFile: view.configFile,
540
+ fileNames: () => view.ownSourceFiles().map((file) => file.fileName),
541
+ })),
542
+ );
543
+ const projects = views.map((view) => visit(view));
539
544
  last();
540
545
  return {
541
546
  projects,
@@ -59,6 +59,13 @@ const BASELINE_SHAPE: Shape = {
59
59
  recordWant: "a row naming code, symbol, path and reason, each a string",
60
60
  };
61
61
 
62
+ /**
63
+ * The reference scheme of this analyzer's language. A record of the other scheme is
64
+ * form-checked and then left to the analyzer of its language: it binds nothing here, counts
65
+ * in neither total and is reported under no code.
66
+ */
67
+ const OWN_SCHEME = "ts://";
68
+
62
69
  /** The closed key list of one entry or row. */
63
70
  const RECORD_KEYS: ReadonlySet<string> = new Set(["code", "symbol", "path", "reason"]);
64
71
 
@@ -126,6 +133,9 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
126
133
  if (path !== undefined && path !== "" && !isEntryPath(path)) {
127
134
  return refuse(JSON.stringify(path), PATH_WANT);
128
135
  }
136
+ if (!symbol.startsWith(OWN_SCHEME)) {
137
+ return { records: [], refusals: [] };
138
+ }
129
139
 
130
140
  const written = {
131
141
  code,
@@ -284,15 +294,68 @@ export function rowReason(provenance: Provenance): string {
284
294
  return `recorded by ${provenance.analyzer} ${provenance.version}`;
285
295
  }
286
296
 
297
+ /** A row of the other language's scheme a write keeps: the members the document held, and no other. */
298
+ interface KeptRow {
299
+ readonly code: string;
300
+ readonly symbol: string;
301
+ readonly path?: string;
302
+ readonly reason?: string;
303
+ }
304
+
305
+ /**
306
+ * The rows of the other language's scheme that the baseline document `text` holds, in
307
+ * document order and as written, which a write keeps. A document that is not a baseline,
308
+ * or a row in a shape no record takes, is refused rather than dropped.
309
+ */
310
+ export function otherSchemeRows(text: string): readonly KeptRow[] {
311
+ const refuse = (detail: string): never => {
312
+ throw new Error(
313
+ `${BASELINE_FILE} cannot be rewritten keeping the other language's rows: ${detail}`,
314
+ );
315
+ };
316
+ let parsed: unknown;
317
+ try {
318
+ parsed = JSON.parse(text);
319
+ } catch (error: unknown) {
320
+ return refuse(error instanceof Error ? error.message : String(error));
321
+ }
322
+ const held = isObject(parsed) ? parsed[BASELINE_SHAPE.array] : undefined;
323
+ if (!Array.isArray(held)) {
324
+ return refuse(BASELINE_SHAPE.documentWant);
325
+ }
326
+ return (held as readonly unknown[]).flatMap((value): KeptRow[] => {
327
+ try {
328
+ readRecord(value, documentSite(BASELINE_SHAPE), new Map(), BASELINE_SHAPE);
329
+ } catch (error: unknown) {
330
+ return refuse(error instanceof Error ? error.message : String(error));
331
+ }
332
+ // readRecord has checked every member is a string and the code and symbol are present.
333
+ const { code, symbol, path, reason } = value as Readonly<Record<string, string>>;
334
+ if (code === undefined || symbol === undefined || symbol.startsWith(OWN_SCHEME)) {
335
+ return [];
336
+ }
337
+ return [
338
+ {
339
+ code,
340
+ symbol,
341
+ ...(path === undefined ? {} : { path }),
342
+ ...(reason === undefined ? {} : { reason }),
343
+ },
344
+ ];
345
+ });
346
+ }
347
+
287
348
  /**
288
- * A baseline holding one row per finding, in the order given, which is the report's. Every
289
- * row carries the provenance as its reason. A finding missing a value a row needs, and an
290
- * identity missing its name or version, are refused rather than written, because the
291
- * grammar requires a reason on every row.
349
+ * A baseline holding one row per finding, in the order given, which is the report's, after
350
+ * the other language's rows `kept`, so two analyzers writing one document in either order
351
+ * write the same bytes. Every recorded row carries the provenance as its reason. A finding
352
+ * missing a value a row needs, and an identity missing its name or version, are refused
353
+ * rather than written, because the grammar requires a reason on every row.
292
354
  */
293
355
  export function writeBaseline(
294
356
  findings: readonly Recorded[],
295
357
  provenance: Provenance,
358
+ kept: readonly KeptRow[] = [],
296
359
  ): Iterable<string> {
297
360
  if (provenance.analyzer === "" || provenance.version === "") {
298
361
  throw new Error(
@@ -308,5 +371,5 @@ export function writeBaseline(
308
371
  }
309
372
  return { code: found.code, symbol: found.symbol, path: found.path, reason };
310
373
  });
311
- return jsonChunks({ description: BASELINE_DESCRIPTION, baseline }, 2);
374
+ return jsonChunks({ description: BASELINE_DESCRIPTION, baseline: [...kept, ...baseline] }, 2);
312
375
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The inline directive: `deadset:ignore`, the first token of a `//` line comment, naming
3
3
  * one or more codes and a reason, and binding to every declaration that begins on the line
4
- * below it.
4
+ * below it, or on its own line where it trails a token there.
5
5
  */
6
6
 
7
7
  import { SyntaxKind, type Node, type SourceFile } from "@typescript/native/unstable/ast";
@@ -211,6 +211,15 @@ function declarationLines(
211
211
  return below;
212
212
  }
213
213
 
214
+ /**
215
+ * Whether the comment at `offset` follows a token on its own line, so it binds the
216
+ * declarations that begin on that line rather than on the next.
217
+ */
218
+ function trails(file: SourceFile, offset: number): boolean {
219
+ const { line } = file.getLineAndCharacterOfPosition(offset);
220
+ return tokenStart(file.text, file.getPositionOfLineAndCharacter(line, 0)) < offset;
221
+ }
222
+
214
223
  /** One comment in the namespace, what the grammar made of it, and what it bound. */
215
224
  export interface InlineDirective {
216
225
  readonly site: Position;
@@ -242,11 +251,12 @@ export function inlineDirectives<Brand>(
242
251
  const below = declarationLines(file, held, symbols);
243
252
  for (const comment of comments) {
244
253
  const site = renderPosition(file, targetRoot, comment.offset);
254
+ const target = trails(file, comment.offset) ? site.line : site.line + 1;
245
255
  found.push({
246
256
  site,
247
257
  text: comment.text,
248
258
  directive: classifyComment(comment.text),
249
- bound: [...(below.get(site.line + 1) ?? [])],
259
+ bound: [...(below.get(target) ?? [])],
250
260
  });
251
261
  }
252
262
  }
@@ -254,8 +264,8 @@ export function inlineDirectives<Brand>(
254
264
  }
255
265
 
256
266
  /**
257
- * The inline directives of a run's projects, each bound to the declarations that begin on
258
- * the line below it, one directive binding the union of what each project holding its file
267
+ * The inline directives of a run's projects, each bound to the declarations the placement
268
+ * rule gives it, one directive binding the union of what each project holding its file
259
269
  * declares there. A directive that lacks only its reason is one {@link Refusal} per code;
260
270
  * any other malformed comment in the namespace ends the read with a
261
271
  * {@link SuppressionError} naming the first one written.