@cplieger/deadset-ts 7.0.0-dev.1 → 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 (113) hide show
  1. package/dist/src/analysis.js +95 -60
  2. package/dist/src/augmentations.js +14 -12
  3. package/dist/src/component-layer.js +1 -1
  4. package/dist/src/components.js +62 -20
  5. package/dist/src/configuration-files.js +230 -30
  6. package/dist/src/conformance-record.js +1 -1
  7. package/dist/src/contextual-uses.js +104 -13
  8. package/dist/src/convention-rows.js +10 -0
  9. package/dist/src/conventions.js +35 -1
  10. package/dist/src/dependencies.js +90 -32
  11. package/dist/src/discover.js +1 -1
  12. package/dist/src/edges.js +1 -1
  13. package/dist/src/element-access.js +341 -0
  14. package/dist/src/erased-stores.js +317 -0
  15. package/dist/src/exempt-classes.js +9 -22
  16. package/dist/src/exempt.js +3 -15
  17. package/dist/src/findings/completion.js +78 -16
  18. package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
  19. package/dist/src/findings/emitters.js +1 -5
  20. package/dist/src/findings/self-check.js +27 -10
  21. package/dist/src/findings-pass.js +6 -5
  22. package/dist/src/in-operands.js +30 -0
  23. package/dist/src/indexed-access.js +264 -119
  24. package/dist/src/intra-function-parts.js +25 -11
  25. package/dist/src/inventory.js +28 -12
  26. package/dist/src/keyof-comparisons.js +170 -0
  27. package/dist/src/kinds.js +17 -7
  28. package/dist/src/manifest.js +6 -16
  29. package/dist/src/matrix.js +20 -2
  30. package/dist/src/query.js +3 -0
  31. package/dist/src/references.js +14 -3
  32. package/dist/src/report.js +4 -1
  33. package/dist/src/reporters/sarif.js +4 -0
  34. package/dist/src/roots.js +0 -1
  35. package/dist/src/run.js +0 -2
  36. package/dist/src/schema.js +1 -1
  37. package/dist/src/scope.js +2 -2
  38. package/dist/src/serialization-contract.js +43 -14
  39. package/dist/src/session.js +12 -8
  40. package/dist/src/suppress-file.js +58 -6
  41. package/dist/src/suppress-inline.js +14 -5
  42. package/dist/src/suppress.js +64 -35
  43. package/dist/src/unimplemented-callees.js +312 -0
  44. package/dist/src/value-reach.js +7 -7
  45. package/dist/src/verbs/analyze.js +11 -9
  46. package/dist/src/workspace-programs.js +0 -3
  47. package/dist/src/workspace-resolution.js +0 -2
  48. package/dist/src/workspace.js +0 -3
  49. package/jsr.json +1 -1
  50. package/package.json +3 -2
  51. package/src/analysis.ts +145 -82
  52. package/src/augmentations.ts +14 -11
  53. package/src/calls.ts +3 -3
  54. package/src/component-files.ts +7 -7
  55. package/src/component-layer.ts +2 -2
  56. package/src/components.ts +104 -22
  57. package/src/config.ts +4 -4
  58. package/src/configuration-files.ts +250 -31
  59. package/src/conformance-record.ts +1 -1
  60. package/src/contextual-uses.ts +135 -17
  61. package/src/convention-rows.ts +23 -0
  62. package/src/conventions.ts +53 -1
  63. package/src/dependencies.ts +119 -38
  64. package/src/discover.ts +3 -3
  65. package/src/edges.ts +3 -3
  66. package/src/element-access.ts +446 -0
  67. package/src/entry-points.ts +1 -1
  68. package/src/erased-stores.ts +413 -0
  69. package/src/exempt-classes.ts +10 -24
  70. package/src/exempt.ts +5 -29
  71. package/src/file-facts.ts +3 -1
  72. package/src/finding.ts +3 -1
  73. package/src/findings/completion.ts +97 -21
  74. package/src/findings/dependencies-and-module-machinery.ts +1 -1
  75. package/src/findings/emitters.ts +4 -9
  76. package/src/findings/self-check.ts +31 -14
  77. package/src/findings-pass.ts +14 -7
  78. package/src/graph.ts +2 -2
  79. package/src/html-entries.ts +1 -1
  80. package/src/in-operands.ts +45 -0
  81. package/src/indexed-access.ts +303 -137
  82. package/src/intra-function-parts.ts +41 -16
  83. package/src/inventory.ts +37 -33
  84. package/src/json-document.ts +1 -1
  85. package/src/keyof-comparisons.ts +211 -0
  86. package/src/kinds.ts +20 -7
  87. package/src/manifest.ts +7 -22
  88. package/src/matrix.ts +34 -2
  89. package/src/namespace-objects.ts +1 -1
  90. package/src/pnpm-workspace.ts +1 -1
  91. package/src/query.ts +10 -0
  92. package/src/references.ts +30 -7
  93. package/src/report.ts +11 -5
  94. package/src/reporters/sarif.ts +4 -0
  95. package/src/roots.ts +0 -3
  96. package/src/run.ts +0 -2
  97. package/src/schema.ts +2 -2
  98. package/src/scope.ts +2 -2
  99. package/src/serialization-contract.ts +53 -14
  100. package/src/session.ts +34 -31
  101. package/src/stores.ts +1 -1
  102. package/src/suppress-file.ts +68 -5
  103. package/src/suppress-inline.ts +17 -7
  104. package/src/suppress.ts +79 -47
  105. package/src/template-field.ts +1 -1
  106. package/src/unimplemented-callees.ts +411 -0
  107. package/src/value-reach.ts +12 -8
  108. package/src/verbs/analyze.ts +15 -9
  109. package/src/verbs/verb.ts +1 -4
  110. package/src/workspace-programs.ts +2 -9
  111. package/src/workspace-resolution.ts +0 -3
  112. package/src/workspace-run.ts +1 -1
  113. package/src/workspace.ts +1 -9
@@ -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.
@@ -417,7 +414,6 @@ export function ownedAs<Brand>(
417
414
 
418
415
  /** Every diagnostic set a project is refused for carrying an error in. */
419
416
  export interface ProjectDiagnostics {
420
- readonly configFile: string;
421
417
  readonly syntactic: readonly Diagnostic[];
422
418
  readonly semantic: readonly Diagnostic[];
423
419
  readonly configParsing: readonly Diagnostic[];
@@ -431,7 +427,6 @@ export interface ProjectDiagnostics {
431
427
  /** The three diagnostic sets one project carries, read before any analysis. */
432
428
  export function diagnosticsOf<Brand>(project: ProjectView<Brand>): ProjectDiagnostics {
433
429
  return {
434
- configFile: project.configFile,
435
430
  syntactic: project.program.getSyntacticDiagnostics(),
436
431
  semantic: project.program.getSemanticDiagnostics(),
437
432
  configParsing: project.program.getConfigFileParsingDiagnostics(),
@@ -489,10 +484,10 @@ function projectsOf(
489
484
  /**
490
485
  * Runs `visit` over every project the compiler configurations name, inside one never updated
491
486
  * snapshot of one client, and answers in their order. A configuration `programs` describes is
492
- * built as that program, or opened from the configuration it writes, instead of as a project.
493
- * One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
494
- * under `"record"`. `last` runs after the last visit while the client still answers; the
495
- * 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.
496
491
  */
497
492
  export function runSession<Result>(
498
493
  engine: Engine,
@@ -501,6 +496,7 @@ export function runSession<Result>(
501
496
  unopened: Unopened = "refuse",
502
497
  programs: ReadonlyMap<string, WorkspaceProgram> = new Map(),
503
498
  last: () => void = () => undefined,
499
+ first: (opened: readonly OpenedFiles[]) => void = () => undefined,
504
500
  ): SessionResult<Result> {
505
501
  try {
506
502
  const built: string[] = [];
@@ -525,7 +521,7 @@ export function runSession<Result>(
525
521
  try {
526
522
  const opened = projectsOf(snapshot, built, written);
527
523
  const log = new UnansweredLog();
528
- const projects: Result[] = [];
524
+ const views: ProjectView<unknown>[] = [];
529
525
  const missing: string[] = [];
530
526
  for (const configFile of configFiles) {
531
527
  const project = opened.get(configFile);
@@ -536,8 +532,15 @@ export function runSession<Result>(
536
532
  missing.push(configFile);
537
533
  continue;
538
534
  }
539
- projects.push(visit(viewOf(project, configFile, engine, log)));
535
+ views.push(viewOf(project, configFile, engine, log));
540
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));
541
544
  last();
542
545
  return {
543
546
  projects,
package/src/stores.ts CHANGED
@@ -37,7 +37,7 @@ export interface Stores {
37
37
  }
38
38
 
39
39
  /** What one configuration contributes to the run's stores. */
40
- export interface ConfigurationStores {
40
+ interface ConfigurationStores {
41
41
  readonly references: readonly Reference[];
42
42
  readonly accessors: readonly string[];
43
43
  }
@@ -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";
@@ -37,7 +37,7 @@ const NO_REASON = new RegExp(DIRECTIVE_EXPRESSIONS.noReason);
37
37
  const DIRECTIVE_FORM = "// deadset:ignore DS0000[,DS0000...] -- <reason>";
38
38
 
39
39
  /** What the grammar's decision procedure makes of one comment. */
40
- export type Directive =
40
+ type Directive =
41
41
  | { readonly is: "none" }
42
42
  | { readonly is: "directive"; readonly codes: readonly string[]; readonly reason: string }
43
43
  | { readonly is: "no-reason"; readonly codes: readonly string[] }
@@ -64,7 +64,7 @@ export function classifyComment(text: string): Directive {
64
64
  }
65
65
 
66
66
  /** One `//` line comment: the offset of its first solidus, and its text to the end of its line. */
67
- export interface LineComment {
67
+ interface LineComment {
68
68
  readonly offset: number;
69
69
  readonly text: string;
70
70
  }
@@ -123,7 +123,7 @@ export function tokenStart(text: string, from: number): number {
123
123
  * syntax tree holds are skipped, so a `//` inside a string, a template or a regular
124
124
  * expression starts nothing.
125
125
  */
126
- export function lineComments(file: SourceFile): readonly LineComment[] {
126
+ function lineComments(file: SourceFile): readonly LineComment[] {
127
127
  const text = file.text;
128
128
  const spans: { readonly start: number; readonly end: number }[] = [];
129
129
  const visit = (node: Node): void => {
@@ -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.