@cplieger/deadset-ts 3.12.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/node-host.js +10 -7
  3. package/dist/src/alias-chain.js +59 -7
  4. package/dist/src/analysis.js +217 -48
  5. package/dist/src/calls.js +23 -22
  6. package/dist/src/class-members.js +10 -11
  7. package/dist/src/components.js +4 -2
  8. package/dist/src/configured-declarations.js +56 -7
  9. package/dist/src/conformance-record.js +2 -2
  10. package/dist/src/dependencies.js +34 -17
  11. package/dist/src/deprecation.js +35 -24
  12. package/dist/src/destructuring.js +37 -29
  13. package/dist/src/discover.js +105 -28
  14. package/dist/src/edges.js +4 -1
  15. package/dist/src/entry-points.js +7 -0
  16. package/dist/src/enum-group.js +46 -40
  17. package/dist/src/exempt.js +42 -1
  18. package/dist/src/findings/completion.js +37 -8
  19. package/dist/src/findings/emitters.js +20 -6
  20. package/dist/src/findings/interfaces.js +7 -1
  21. package/dist/src/findings/intra-function.js +91 -2
  22. package/dist/src/findings/self-check.js +30 -7
  23. package/dist/src/findings/unused-declarations.js +15 -3
  24. package/dist/src/findings-pass.js +18 -11
  25. package/dist/src/framework-lifecycle.js +8 -6
  26. package/dist/src/implementations.js +45 -23
  27. package/dist/src/interface-satisfaction.js +30 -27
  28. package/dist/src/intra-function-parts.js +660 -0
  29. package/dist/src/inventory.js +85 -4
  30. package/dist/src/json-chunks.js +76 -0
  31. package/dist/src/kinds.js +337 -35
  32. package/dist/src/partial.js +23 -0
  33. package/dist/src/query.js +207 -0
  34. package/dist/src/references.js +92 -20
  35. package/dist/src/reflective-lookup.js +46 -4
  36. package/dist/src/report.js +14 -3
  37. package/dist/src/reporters/annotations.js +9 -5
  38. package/dist/src/reporters/json.js +3 -2
  39. package/dist/src/reporters/sarif.js +22 -16
  40. package/dist/src/reporters/template.js +112 -12
  41. package/dist/src/reporters/text.js +8 -6
  42. package/dist/src/roots.js +27 -11
  43. package/dist/src/serialization-contract.js +53 -32
  44. package/dist/src/session.js +79 -39
  45. package/dist/src/signatures.js +32 -0
  46. package/dist/src/suppress-file.js +29 -7
  47. package/dist/src/suppress-inline.js +8 -1
  48. package/dist/src/suppress.js +27 -0
  49. package/dist/src/sweep.js +4 -1
  50. package/dist/src/value-reach.js +44 -25
  51. package/dist/src/verbs/analyze.js +39 -14
  52. package/dist/src/verbs/describe.js +12 -8
  53. package/dist/src/verbs/explain.js +22 -9
  54. package/dist/src/verbs/print-projects.js +40 -7
  55. package/dist/src/verbs/print-retained.js +6 -2
  56. package/dist/src/verbs/print-roots.js +5 -1
  57. package/dist/src/version.js +1 -1
  58. package/jsr.json +1 -1
  59. package/package.json +3 -3
  60. package/src/alias-chain.ts +75 -11
  61. package/src/analysis.ts +385 -104
  62. package/src/calls.ts +10 -9
  63. package/src/class-members.ts +10 -11
  64. package/src/components.ts +4 -2
  65. package/src/configured-declarations.ts +80 -8
  66. package/src/conformance-record.ts +2 -2
  67. package/src/dependencies.ts +42 -20
  68. package/src/deprecation.ts +53 -27
  69. package/src/destructuring.ts +50 -39
  70. package/src/discover.ts +177 -33
  71. package/src/edges.ts +4 -1
  72. package/src/entry-points.ts +7 -0
  73. package/src/enum-group.ts +56 -42
  74. package/src/exempt.ts +50 -2
  75. package/src/finding.ts +5 -1
  76. package/src/findings/completion.ts +46 -10
  77. package/src/findings/emitter.ts +3 -0
  78. package/src/findings/emitters.ts +27 -6
  79. package/src/findings/interfaces.ts +7 -1
  80. package/src/findings/intra-function.ts +106 -3
  81. package/src/findings/self-check.ts +48 -7
  82. package/src/findings/unused-declarations.ts +18 -4
  83. package/src/findings-pass.ts +26 -11
  84. package/src/framework-lifecycle.ts +10 -6
  85. package/src/host.ts +5 -3
  86. package/src/implementations.ts +59 -23
  87. package/src/interface-satisfaction.ts +30 -27
  88. package/src/intra-function-parts.ts +889 -0
  89. package/src/inventory.ts +97 -4
  90. package/src/json-chunks.ts +87 -0
  91. package/src/kinds.ts +355 -41
  92. package/src/partial.ts +40 -0
  93. package/src/query.ts +522 -0
  94. package/src/references.ts +119 -38
  95. package/src/reflective-lookup.ts +56 -6
  96. package/src/report.ts +32 -4
  97. package/src/reporters/annotations.ts +23 -21
  98. package/src/reporters/json.ts +3 -2
  99. package/src/reporters/reporter.ts +2 -2
  100. package/src/reporters/sarif.ts +27 -18
  101. package/src/reporters/template.ts +119 -14
  102. package/src/reporters/text.ts +12 -12
  103. package/src/roots.ts +34 -12
  104. package/src/serialization-contract.ts +59 -40
  105. package/src/session.ts +146 -58
  106. package/src/signatures.ts +55 -0
  107. package/src/suppress-file.ts +40 -8
  108. package/src/suppress-inline.ts +8 -1
  109. package/src/suppress.ts +31 -0
  110. package/src/sweep.ts +9 -4
  111. package/src/value-reach.ts +66 -28
  112. package/src/verbs/analyze.ts +51 -14
  113. package/src/verbs/describe.ts +13 -9
  114. package/src/verbs/explain.ts +20 -9
  115. package/src/verbs/print-projects.ts +53 -7
  116. package/src/verbs/print-retained.ts +6 -2
  117. package/src/verbs/print-roots.ts +5 -1
  118. package/src/version.ts +1 -1
  119. package/dist/src/reporters/rules.js +0 -178
  120. package/src/reporters/rules.ts +0 -195
package/src/session.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { API, SymbolFlags } from "@typescript/native/unstable/sync";
2
2
  import type {
3
- Checker,
3
+ APIRequestGenerator,
4
4
  Diagnostic,
5
5
  DocumentIdentifier,
6
6
  NodeHandle,
@@ -10,9 +10,15 @@ import type {
10
10
  Symbol as TSSymbol,
11
11
  TimingInfo,
12
12
  } from "@typescript/native/unstable/sync";
13
- import { isIdentifier } from "@typescript/native/unstable/ast";
14
13
  import type { Node, SourceFile } from "@typescript/native/unstable/ast";
15
14
  import { DiscoveryError } from "./discover.ts";
15
+ import {
16
+ queriesOf,
17
+ UnansweredLog,
18
+ type Answer,
19
+ type Queries,
20
+ type UnansweredQuestion,
21
+ } from "./query.ts";
16
22
 
17
23
  /**
18
24
  * The compiler the analysis reads through. It is a client over a protocol to a
@@ -31,7 +37,15 @@ export interface Engine {
31
37
  */
32
38
  parseConfigFile(file: DocumentIdentifier): ReturnType<API["parseConfigFile"]>;
33
39
  /** Opens one snapshot holding every project named. */
34
- updateSnapshot(openProjects: readonly string[]): Snapshot;
40
+ createSnapshot(openProjects: readonly string[]): Snapshot;
41
+ /** Sends many questions in one round trip and answers each, in order. */
42
+ batch<T>(questions: readonly APIRequestGenerator<T>[]): T[];
43
+ /**
44
+ * Puts one question to a project's checker, by the accessor's name and the locations
45
+ * it asks about. Every guarded question passes through here, so a caller that
46
+ * observes or fails questions does it at this seam.
47
+ */
48
+ ask<T>(accessor: string, locations: () => readonly string[], question: () => T): T;
35
49
  /** Round-trip latency, bytes transferred and server time, when collection is on. */
36
50
  getTimingInfo(): TimingInfo;
37
51
  /** Releases the client and the process behind it. */
@@ -56,7 +70,10 @@ export function openEngine(options: EngineOptions): Engine {
56
70
  let closed = false;
57
71
  return {
58
72
  parseConfigFile: (file) => api.parseConfigFile(file),
59
- updateSnapshot: (openProjects) => api.updateSnapshot({ openProjects: [...openProjects] }),
73
+ createSnapshot: (openProjects) => api.createSnapshot({ openProjects: [...openProjects] }),
74
+ batch: <T>(questions: readonly APIRequestGenerator<T>[]) =>
75
+ api.batch<readonly APIRequestGenerator<T>[]>(...questions),
76
+ ask: (_accessor, _locations, question) => question(),
60
77
  getTimingInfo: () => api.getTimingInfo(),
61
78
  close: () => {
62
79
  if (closed) {
@@ -90,38 +107,45 @@ export interface ProjectView<Brand> {
90
107
  readonly configFile: string;
91
108
  /** The project's program, whose walk is local to this process. */
92
109
  readonly program: Program;
93
- /** The project's checker. Every call on it is a round trip. */
94
- readonly checker: Checker;
110
+ /**
111
+ * The questions the project's checker answers, each guarded. Every one is a round
112
+ * trip, and one may go unanswered.
113
+ */
114
+ readonly queries: Queries;
95
115
  /** The source files the program holds that are the target's own. */
96
116
  ownSourceFiles(): readonly SourceFile[];
117
+ /**
118
+ * The keys of the target's own files, which is how a declaration handle names the
119
+ * file it is in.
120
+ */
121
+ ownPaths(): ReadonlySet<string>;
97
122
  /** A handle to one node of this project's program. */
98
123
  handle(node: Node): Handle<Brand>;
99
124
  /**
100
- * The symbols a batch of this project's nodes resolve to, one round trip for
101
- * the batch, in the order the handles were given. The batch is how a file's
102
- * identifiers reach the checker, and `undefined` marks a node the batch did not
103
- * resolve.
125
+ * The symbols a batch of this project's nodes resolve to, in the order the handles
126
+ * were given, in runs of at most `cap`. The batch is how a file's identifiers reach
127
+ * the checker, and `undefined` marks a node the batch did not resolve.
104
128
  */
105
- symbolsAt(handles: readonly Handle<Brand>[]): readonly (TSSymbol | undefined)[];
129
+ symbolsAt(handles: readonly Handle<Brand>[], cap?: number): Answer<TSSymbol | undefined>[];
106
130
  /**
107
131
  * The symbol one node resolves to, asked for that node alone. The accessor behind
108
- * it takes one identifier and offers no array overload, so it is one round trip per
109
- * node and it answers only for the residue a batch left unresolved. A node that is
110
- * not an identifier has no such answer.
132
+ * it takes one identifier and offers no array form, so it answers only for the
133
+ * residue a batch left unresolved. A node that is not an identifier has no such
134
+ * answer.
111
135
  */
112
- resolvedSymbolAt(handle: Handle<Brand>): TSSymbol | undefined;
136
+ resolvedSymbolAt(handle: Handle<Brand>): Answer<TSSymbol | undefined>;
113
137
  /**
114
138
  * The symbol the value of one shorthand property assignment names, which is the
115
139
  * declaration `{ a }` reads; the symbol the property's own name resolves to is the
116
140
  * property the literal declares. The handle names the assignment, not its name.
117
141
  */
118
- shorthandValueAt(handle: Handle<Brand>): TSSymbol | undefined;
142
+ shorthandValueAt(handle: Handle<Brand>): Answer<TSSymbol | undefined>;
119
143
  /**
120
144
  * The symbol one alias names, one link along: an import or export specifier
121
145
  * resolves to the declaration it carries forward, or to the next alias of a chain.
122
146
  * `undefined` where the symbol is no alias, or where the alias resolves to nothing.
123
147
  */
124
- aliasStepOf(symbol: TSSymbol): TSSymbol | undefined;
148
+ aliasStepOf(symbol: TSSymbol): Answer<TSSymbol | undefined>;
125
149
  /**
126
150
  * The node one symbol's declaration handle names, read in this project's program,
127
151
  * or `undefined` where this project's program does not hold that file.
@@ -142,49 +166,70 @@ export interface ProjectView<Brand> {
142
166
  */
143
167
  export type ProjectVisitor<Result> = <Brand>(project: ProjectView<Brand>) => Result;
144
168
 
145
- function viewOf<Brand>(project: Project): ProjectView<Brand> {
169
+ function viewOf<Brand>(
170
+ project: Project,
171
+ configFile: string,
172
+ engine: Engine,
173
+ log: UnansweredLog,
174
+ ): ProjectView<Brand> {
175
+ const queries = queriesOf(
176
+ project.checker,
177
+ configFile,
178
+ (questions) => engine.batch(questions),
179
+ log,
180
+ (accessor, locations, question) => engine.ask(accessor, locations, question),
181
+ );
146
182
  // The snapshot is never updated, so a project's own file set is the same answer
147
183
  // every time it is asked for. It is read once because the question is not free:
148
184
  // deciding it consults the metadata of every file the program holds, the compiler's
149
185
  // own library files included, and a run asks for the set once per pass.
150
186
  let ownFiles: readonly SourceFile[] | undefined;
187
+ let ownKeys: ReadonlySet<string> | undefined;
188
+ const ownSourceFiles = (): readonly SourceFile[] => {
189
+ ownFiles ??= project.program
190
+ .getSourceFileNames()
191
+ // The metadata decides which files belong to the target, and it is read by
192
+ // name. Fetching each file first and filtering afterwards costs one round
193
+ // trip per library file the answer then discards, and a program holds far
194
+ // more library files than target files.
195
+ .filter((name) => {
196
+ const metadata = project.program.getSourceFileMetadata(name);
197
+ return (
198
+ metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary
199
+ );
200
+ })
201
+ .map((name) => project.program.getSourceFile(name))
202
+ .filter((file): file is SourceFile => file !== undefined);
203
+ return ownFiles;
204
+ };
151
205
  return {
152
- configFile: project.configFileName,
206
+ configFile,
153
207
  program: project.program,
154
- checker: project.checker,
155
- ownSourceFiles: () => {
156
- ownFiles ??= project.program
157
- .getSourceFileNames()
158
- // The metadata decides which files belong to the target, and it is read by
159
- // name. Fetching each file first and filtering afterwards costs one round
160
- // trip per library file the answer then discards, and a program holds far
161
- // more library files than target files.
162
- .filter((name) => {
163
- const metadata = project.program.getSourceFileMetadata(name);
164
- return (
165
- metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary
166
- );
167
- })
168
- .map((name) => project.program.getSourceFile(name))
169
- .filter((file): file is SourceFile => file !== undefined);
170
- return ownFiles;
208
+ queries,
209
+ ownSourceFiles,
210
+ ownPaths: () => {
211
+ ownKeys ??= new Set(ownSourceFiles().map((file) => file.path));
212
+ return ownKeys;
171
213
  },
172
214
  handle: (node) => ({ node }),
173
- symbolsAt: (handles) => project.checker.getSymbolAtLocation(handles.map(({ node }) => node)),
174
- resolvedSymbolAt: ({ node }) =>
175
- isIdentifier(node) ? project.checker.getResolvedSymbol(node) : undefined,
176
- shorthandValueAt: ({ node }) => project.checker.getShorthandAssignmentValueSymbol(node),
215
+ symbolsAt: (handles, cap) =>
216
+ queries.symbolsAt(
217
+ handles.map(({ node }) => node),
218
+ cap,
219
+ ),
220
+ resolvedSymbolAt: ({ node }) => queries.resolvedSymbol(node),
221
+ shorthandValueAt: ({ node }) => queries.shorthandValue(node),
177
222
  aliasStepOf: (symbol) => {
178
223
  // The step is asked of an alias only: asked of any other symbol, the compiler
179
224
  // fails an assertion and the session ends.
180
225
  if ((symbol.flags & SymbolFlags.Alias) === 0) {
181
226
  return undefined;
182
227
  }
183
- const held = project.checker.getImmediateAliasedSymbol(symbol);
228
+ const held = queries.immediateAliased(symbol);
184
229
  // An alias that resolves to nothing answers with the checker's own unknown
185
230
  // symbol, which declares nothing; a symbol that is no alias answers with
186
231
  // nothing at all.
187
- return held === undefined || held.declarations.length === 0 ? undefined : held;
232
+ return typeof held === "object" && held.declarations.length === 0 ? undefined : held;
188
233
  },
189
234
  declarationAt: (handle) => {
190
235
  const node = handle.resolve(project);
@@ -203,11 +248,17 @@ export function narrowedTo<Brand>(
203
248
  keep: (file: SourceFile) => boolean,
204
249
  ): ProjectView<Brand> {
205
250
  let kept: readonly SourceFile[] | undefined;
251
+ let keys: ReadonlySet<string> | undefined;
252
+ const ownSourceFiles = (): readonly SourceFile[] => {
253
+ kept ??= project.ownSourceFiles().filter(keep);
254
+ return kept;
255
+ };
206
256
  return {
207
257
  ...project,
208
- ownSourceFiles: () => {
209
- kept ??= project.ownSourceFiles().filter(keep);
210
- return kept;
258
+ ownSourceFiles,
259
+ ownPaths: () => {
260
+ keys ??= new Set(ownSourceFiles().map((file) => file.path));
261
+ return keys;
211
262
  },
212
263
  };
213
264
  }
@@ -218,6 +269,11 @@ export interface ProjectDiagnostics {
218
269
  readonly syntactic: readonly Diagnostic[];
219
270
  readonly semantic: readonly Diagnostic[];
220
271
  readonly configParsing: readonly Diagnostic[];
272
+ /** The line and column of one offset into one of the program's files, counted from one. */
273
+ readonly locate: (
274
+ fileName: string,
275
+ offset: number,
276
+ ) => { line: number; column: number } | undefined;
221
277
  }
222
278
 
223
279
  /** The three diagnostic sets one project carries, read before any analysis. */
@@ -227,15 +283,31 @@ export function diagnosticsOf<Brand>(project: ProjectView<Brand>): ProjectDiagno
227
283
  syntactic: project.program.getSyntacticDiagnostics(),
228
284
  semantic: project.program.getSemanticDiagnostics(),
229
285
  configParsing: project.program.getConfigFileParsingDiagnostics(),
286
+ locate: (fileName, offset) => {
287
+ const file = project.program.getSourceFile(fileName);
288
+ if (file === undefined || offset < 0 || offset > file.text.length) {
289
+ return undefined;
290
+ }
291
+ const { line, character } = file.getLineAndCharacterOfPosition(offset);
292
+ return { line: line + 1, column: character + 1 };
293
+ },
230
294
  };
231
295
  }
232
296
 
233
297
  /** One run's result: what the visitor answered for each project, and what it cost. */
234
298
  export interface SessionResult<Result> {
299
+ /** One answer per configuration the snapshot opened a project for, in the order named. */
235
300
  readonly projects: readonly Result[];
301
+ /** The configurations the snapshot opened no project for, in the order named. */
302
+ readonly unopened: readonly string[];
236
303
  readonly timing: TimingInfo;
304
+ /** Every question the checker could not answer, each once, in the order first asked. */
305
+ readonly unanswered: readonly UnansweredQuestion[];
237
306
  }
238
307
 
308
+ /** What a run does about a configuration the snapshot opened no project for. */
309
+ type Unopened = "refuse" | "record";
310
+
239
311
  /**
240
312
  * Runs `visit` over every project the compiler configurations name, inside one
241
313
  * snapshot of one client, in the order they are named, and answers in that order.
@@ -244,30 +316,46 @@ export interface SessionResult<Result> {
244
316
  * with its own program and checker; it is never updated, because the analysis
245
317
  * reads and the tree does not change under it. The order the snapshot lists its
246
318
  * projects in is the compiler's own, so each named configuration is looked up in it
247
- * instead, and a configuration the snapshot opened no project for ends the run: the
248
- * run would otherwise answer for fewer projects than it was asked to analyze. The
249
- * snapshot is disposed once when the last project has been visited, and the client
250
- * is released after it, whether the visit completed or threw.
319
+ * instead. A configuration the snapshot opened no project for ends the run under
320
+ * `"refuse"` and is named in the result under `"record"`. The snapshot is disposed once
321
+ * the last project has been visited, and the client is released after it, whether the
322
+ * visit completed or threw.
251
323
  */
252
324
  export function runSession<Result>(
253
325
  engine: Engine,
254
326
  configFiles: readonly string[],
255
327
  visit: ProjectVisitor<Result>,
328
+ unopened: Unopened = "refuse",
256
329
  ): SessionResult<Result> {
257
330
  try {
258
- const snapshot = engine.updateSnapshot(configFiles);
331
+ const snapshot = engine.createSnapshot(configFiles);
259
332
  try {
260
- const opened = new Map(
261
- snapshot.getProjects().map((project) => [project.configFileName, project]),
262
- );
263
- const projects = configFiles.map((configFile) => {
333
+ const opened = new Map<string, Project>();
334
+ for (const project of snapshot.getProjects()) {
335
+ if (project.configFileName !== undefined) {
336
+ opened.set(project.configFileName, project);
337
+ }
338
+ }
339
+ const log = new UnansweredLog();
340
+ const projects: Result[] = [];
341
+ const missing: string[] = [];
342
+ for (const configFile of configFiles) {
264
343
  const project = opened.get(configFile);
265
344
  if (project === undefined) {
266
- throw new DiscoveryError(`${configFile} opened no project`, []);
345
+ if (unopened === "refuse") {
346
+ throw new DiscoveryError(`${configFile} opened no project`, []);
347
+ }
348
+ missing.push(configFile);
349
+ continue;
267
350
  }
268
- return visit(viewOf(project));
269
- });
270
- return { projects, timing: engine.getTimingInfo() };
351
+ projects.push(visit(viewOf(project, configFile, engine, log)));
352
+ }
353
+ return {
354
+ projects,
355
+ unopened: missing,
356
+ timing: engine.getTimingInfo(),
357
+ unanswered: log.questions,
358
+ };
271
359
  } finally {
272
360
  snapshot.dispose();
273
361
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The function a declaration of the inventory declares, where it declares one: the node
3
+ * whose parameters, body and result are the declaration's own parts.
4
+ */
5
+
6
+ import {
7
+ isArrowFunction,
8
+ isClassDeclaration,
9
+ isClassExpression,
10
+ isFunctionDeclaration,
11
+ isFunctionExpression,
12
+ isMethodDeclaration,
13
+ isPropertyDeclaration,
14
+ isVariableDeclaration,
15
+ type ArrowFunction,
16
+ type FunctionDeclaration,
17
+ type FunctionExpression,
18
+ type MethodDeclaration,
19
+ type Node,
20
+ } from "@typescript/native/unstable/ast";
21
+
22
+ /** A function-like node that carries a body of its own. */
23
+ export type SignatureNode =
24
+ FunctionDeclaration | MethodDeclaration | ArrowFunction | FunctionExpression;
25
+
26
+ /** Whether one node is a function value written in place: an arrow or a function expression. */
27
+ function isFunctionValue(node: Node | undefined): node is ArrowFunction | FunctionExpression {
28
+ return node !== undefined && (isArrowFunction(node) || isFunctionExpression(node));
29
+ }
30
+
31
+ /**
32
+ * The function one declaration declares: a function declaration and a class method with
33
+ * a body are their own; a variable and a class property whose initializer is an arrow or
34
+ * a function expression declare that function. Any other declaration declares none, an
35
+ * overload signature and an abstract or ambient method among them, because no body is
36
+ * there to read its parts.
37
+ */
38
+ export function signatureOf(node: Node): SignatureNode | undefined {
39
+ if (isFunctionDeclaration(node)) {
40
+ return node.body === undefined ? undefined : node;
41
+ }
42
+ if (isMethodDeclaration(node)) {
43
+ const owner = node.parent;
44
+ return node.body !== undefined && (isClassDeclaration(owner) || isClassExpression(owner))
45
+ ? node
46
+ : undefined;
47
+ }
48
+ if (
49
+ (isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
50
+ isFunctionValue(node.initializer)
51
+ ) {
52
+ return node.initializer;
53
+ }
54
+ return undefined;
55
+ }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { Host } from "./host.ts";
9
9
  import type { InventorySymbol } from "./inventory.ts";
10
+ import { jsonChunks } from "./json-chunks.ts";
10
11
  import { joinPath } from "./paths.ts";
11
12
  import { positionAt, walkDocument } from "./json-document.ts";
12
13
  import type { Position } from "./position.ts";
@@ -154,6 +155,15 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
154
155
  return { records: named.map((one) => ({ ...written, bound: one.id })), refusals: [] };
155
156
  }
156
157
 
158
+ function byReferenceOf(symbols: readonly InventorySymbol[]): ByReference {
159
+ const byReference = new Map<string, InventorySymbol[]>();
160
+ for (const symbol of symbols) {
161
+ const key = referenceKey(symbol.ref, symbol.position.path);
162
+ byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
163
+ }
164
+ return byReference;
165
+ }
166
+
157
167
  /** Reads one document at the target root, binding each record against the run's declarations. */
158
168
  function readDocument(
159
169
  host: Host,
@@ -196,11 +206,7 @@ function readDocument(
196
206
  return malformed(shape.file);
197
207
  }
198
208
 
199
- const byReference = new Map<string, InventorySymbol[]>();
200
- for (const symbol of symbols) {
201
- const key = referenceKey(symbol.ref, symbol.position.path);
202
- byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
203
- }
209
+ const byReference = byReferenceOf(symbols);
204
210
  const records: SuppressionRecord[] = [];
205
211
  const refusals: Refusal[] = [];
206
212
  (held as readonly unknown[]).forEach((value, index) => {
@@ -247,6 +253,24 @@ export interface Recorded {
247
253
  readonly path: string;
248
254
  }
249
255
 
256
+ /**
257
+ * Rows a baseline write holds in memory, bound against the run's declarations as the same
258
+ * rows read back from the document would be, each carrying the reason given.
259
+ */
260
+ export function bindRows(
261
+ rows: readonly Recorded[],
262
+ reason: string,
263
+ symbols: readonly InventorySymbol[],
264
+ ): Suppressions {
265
+ const byReference = byReferenceOf(symbols);
266
+ const site = documentSite(BASELINE_SHAPE);
267
+ const read = rows.map((row) => readRecord({ ...row, reason }, site, byReference, BASELINE_SHAPE));
268
+ return {
269
+ records: read.flatMap((one) => one.records),
270
+ refusals: read.flatMap((one) => one.refusals),
271
+ };
272
+ }
273
+
250
274
  /** The analyzer a written row names as its provenance, as the same run's report names it. */
251
275
  export interface Provenance {
252
276
  readonly analyzer: string;
@@ -258,19 +282,27 @@ const BASELINE_DESCRIPTION =
258
282
  "Recorded findings: a run fails only on a finding this document does not hold. " +
259
283
  "Every reason is the analyzer that recorded the row, never an adjudication.";
260
284
 
285
+ /** The reason every row a baseline write records carries. */
286
+ export function rowReason(provenance: Provenance): string {
287
+ return `recorded by ${provenance.analyzer} ${provenance.version}`;
288
+ }
289
+
261
290
  /**
262
291
  * A baseline holding one row per finding, in the order given, which is the report's. Every
263
292
  * row carries the provenance as its reason. A finding missing a value a row needs, and an
264
293
  * identity missing its name or version, are refused rather than written, because the
265
294
  * grammar requires a reason on every row.
266
295
  */
267
- export function writeBaseline(findings: readonly Recorded[], provenance: Provenance): string {
296
+ export function writeBaseline(
297
+ findings: readonly Recorded[],
298
+ provenance: Provenance,
299
+ ): Iterable<string> {
268
300
  if (provenance.analyzer === "" || provenance.version === "") {
269
301
  throw new Error(
270
302
  `a baseline row would carry no reason: the analyzer identity names ${JSON.stringify(provenance.analyzer)} at version ${JSON.stringify(provenance.version)}`,
271
303
  );
272
304
  }
273
- const reason = `recorded by ${provenance.analyzer} ${provenance.version}`;
305
+ const reason = rowReason(provenance);
274
306
  const baseline = findings.map((found) => {
275
307
  if (found.code === "" || found.symbol === "" || found.path === "") {
276
308
  throw new Error(
@@ -279,5 +311,5 @@ export function writeBaseline(findings: readonly Recorded[], provenance: Provena
279
311
  }
280
312
  return { code: found.code, symbol: found.symbol, path: found.path, reason };
281
313
  });
282
- return `${JSON.stringify({ description: BASELINE_DESCRIPTION, baseline }, null, 2)}\n`;
314
+ return jsonChunks({ description: BASELINE_DESCRIPTION, baseline }, 2);
283
315
  }
@@ -8,6 +8,7 @@ import { SyntaxKind, type Node, type SourceFile } from "@typescript/native/unsta
8
8
  import { nodeKey, type Inventory } from "./inventory.ts";
9
9
  import { byPosition, positionKey, renderPosition, type Position } from "./position.ts";
10
10
  import type { ProjectView } from "./session.ts";
11
+ import { signatureOf } from "./signatures.ts";
11
12
  import {
12
13
  SUPPRESSION_WITHOUT_REASON,
13
14
  SuppressionError,
@@ -168,7 +169,8 @@ interface Declared {
168
169
  /**
169
170
  * The declarations of one file by the line each begins on, the line of its first token: a
170
171
  * decorator, a modifier or the keyword. A variable's first token is its statement's
171
- * keyword, and a variable written on a later line of the statement also begins there.
172
+ * keyword, and a variable written on a later line of the statement also begins there. A
173
+ * function also begins, for a directive, on the line of each of its parameters.
172
174
  */
173
175
  function declarationLines(
174
176
  file: SourceFile,
@@ -189,6 +191,11 @@ function declarationLines(
189
191
  const one = id === undefined ? undefined : symbols.get(id);
190
192
  if (one !== undefined) {
191
193
  add(node.pos, one);
194
+ // A parameter on a later line of a wrapped signature takes its directive on the
195
+ // line above it, inside the parameter list, and the directive binds the function.
196
+ for (const parameter of signatureOf(node)?.parameters ?? []) {
197
+ add(parameter.pos, one);
198
+ }
192
199
  const list = node.parent;
193
200
  if (
194
201
  node.kind === SyntaxKind.VariableDeclaration &&
package/src/suppress.ts CHANGED
@@ -169,6 +169,7 @@ const ROW_SUBJECTS: ReadonlySet<string> = new Set([
169
169
  "dependency",
170
170
  "module-directive",
171
171
  "root",
172
+ "configured-declaration",
172
173
  "suppression",
173
174
  "edge",
174
175
  ]);
@@ -178,6 +179,36 @@ export function isRowSubject(kind: string): boolean {
178
179
  return ROW_SUBJECTS.has(kind);
179
180
  }
180
181
 
182
+ /**
183
+ * The subject kinds that are a part of a declaration rather than a declaration: a part
184
+ * has no reference of its own, so a finding about one names the declaration that holds
185
+ * it, and the part is decided inside that declaration whatever uses it.
186
+ */
187
+ const PART_SUBJECTS: ReadonlySet<string> = new Set([
188
+ "parameter",
189
+ "receiver",
190
+ "result",
191
+ "statement",
192
+ "case",
193
+ "store",
194
+ ]);
195
+
196
+ /** Whether a finding's subject is a part of the declaration its reference names. */
197
+ export function isPartSubject(kind: string): boolean {
198
+ return PART_SUBJECTS.has(kind);
199
+ }
200
+
201
+ /** The prefix of the codes of the part kinds, the intra-function range. */
202
+ const PART_KIND_PREFIX = "DS18";
203
+
204
+ /**
205
+ * Whether a code is a part kind, whose record withholds its finding and marks nothing:
206
+ * the record says the part is wanted, not that the declaration holding it is.
207
+ */
208
+ export function isPartKind(code: string): boolean {
209
+ return code.startsWith(PART_KIND_PREFIX);
210
+ }
211
+
181
212
  /** One finding's identity: its code and the position of the thing it names. */
182
213
  function claimKey(finding: Finding): string {
183
214
  return `${finding.code}\u0000${positionKey(finding.position)}`;
package/src/sweep.ts CHANGED
@@ -51,6 +51,11 @@ export interface SweepInput {
51
51
  * holds nothing back.
52
52
  */
53
53
  readonly exempt?: readonly Exemption[];
54
+ /**
55
+ * The declarations a question the checker did not answer could have kept live. Each
56
+ * is held as an exempt declaration is. Absent, the sweep holds nothing back for one.
57
+ */
58
+ readonly unanswered?: readonly string[];
54
59
  readonly mode: Mode;
55
60
  }
56
61
 
@@ -94,10 +99,10 @@ export interface Liveness {
94
99
  */
95
100
  export function sweep(graph: Graph, input: SweepInput): Liveness {
96
101
  const marked = flagged(graph, input.marked);
97
- const exempt = flagged(
98
- graph,
99
- (input.exempt ?? []).map((record) => record.id),
100
- );
102
+ const exempt = flagged(graph, [
103
+ ...(input.exempt ?? []).map((record) => record.id),
104
+ ...(input.unanswered ?? []),
105
+ ]);
101
106
  const held = marked.map((mark, at) => mark || exempt[at] === true);
102
107
  // Every root, every mark and every exemption seeds reachability. A root of a kind that
103
108
  // names a caller, and a mark, are live under reference counting as well; a root that