@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.18

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 (60) hide show
  1. package/dist/src/analysis.js +26 -71
  2. package/dist/src/augmentations.js +1 -1
  3. package/dist/src/calls.js +267 -48
  4. package/dist/src/conformance-record.js +1 -1
  5. package/dist/src/contract/contract.generated.js +2 -2
  6. package/dist/src/exempt.js +0 -26
  7. package/dist/src/exposure.js +400 -0
  8. package/dist/src/findings/completion.js +4 -17
  9. package/dist/src/findings/emitters.js +22 -33
  10. package/dist/src/findings/intra-function.js +3 -3
  11. package/dist/src/findings/reads-and-writes.js +71 -15
  12. package/dist/src/findings/unused-declarations.js +2 -22
  13. package/dist/src/findings/visibility-narrowing.js +14 -20
  14. package/dist/src/inventory.js +18 -0
  15. package/dist/src/{components.js → liveness/components.js} +1 -1
  16. package/dist/src/{graph.js → liveness/graph.js} +38 -21
  17. package/dist/src/liveness/matrix.js +99 -0
  18. package/dist/src/liveness/sweep.js +558 -0
  19. package/dist/src/query.js +46 -0
  20. package/dist/src/references/sink.js +87 -1
  21. package/dist/src/references.js +1 -1
  22. package/dist/src/roots/published-reach.js +4 -4
  23. package/dist/src/verbs/explain.js +73 -152
  24. package/dist/src/verbs/print-retained.js +1 -1
  25. package/jsr.json +1 -1
  26. package/package.json +1 -1
  27. package/src/analysis.ts +41 -107
  28. package/src/augmentations.ts +1 -1
  29. package/src/calls.ts +326 -52
  30. package/src/conformance-record.ts +1 -1
  31. package/src/contract/contract.generated.ts +2 -2
  32. package/src/exempt.ts +31 -46
  33. package/src/exposure.ts +492 -0
  34. package/src/finding.ts +1 -1
  35. package/src/findings/boundary.ts +7 -0
  36. package/src/findings/completion.ts +5 -19
  37. package/src/findings/emitters.ts +23 -41
  38. package/src/findings/interfaces.ts +3 -3
  39. package/src/findings/intra-function.ts +3 -3
  40. package/src/findings/reads-and-writes.ts +99 -26
  41. package/src/findings/unused-declarations.ts +4 -28
  42. package/src/findings/visibility-narrowing.ts +25 -26
  43. package/src/interface-satisfaction.ts +1 -1
  44. package/src/inventory.ts +19 -0
  45. package/src/{components.ts → liveness/components.ts} +3 -3
  46. package/src/{graph.ts → liveness/graph.ts} +74 -26
  47. package/src/liveness/matrix.ts +162 -0
  48. package/src/liveness/sweep.ts +855 -0
  49. package/src/query.ts +81 -0
  50. package/src/references/sink.ts +101 -0
  51. package/src/references.ts +8 -1
  52. package/src/roots/published-reach.ts +9 -4
  53. package/src/stores.ts +2 -1
  54. package/src/tests/file-class.ts +19 -2
  55. package/src/verbs/explain.ts +81 -199
  56. package/src/verbs/print-retained.ts +1 -1
  57. package/dist/src/matrix.js +0 -275
  58. package/dist/src/sweep.js +0 -234
  59. package/src/matrix.ts +0 -466
  60. package/src/sweep.ts +0 -384
package/src/calls.ts CHANGED
@@ -1,30 +1,48 @@
1
1
  /**
2
2
  * The calls and decorators of one project that the configured classes read: what each
3
3
  * callee resolves to, and which of the target's classes a call passes or a decorator
4
- * is attached to. A class is passed as an argument, or as an element of an array
5
- * literal or a property value of an object literal written as one, at any depth, named
6
- * directly or through variables, and a decorator written as a call is a call.
4
+ * is attached to. A class is passed by each value an argument can be, at any depth inside
5
+ * the array and object literals it writes, named directly or through variables, and a
6
+ * decorator written as a call is a call.
7
7
  */
8
8
 
9
9
  import {
10
10
  isArrayLiteralExpression,
11
+ isArrowFunction,
12
+ isBinaryExpression,
13
+ isBindingElement,
11
14
  isCallExpression,
12
15
  isClassDeclaration,
16
+ isConditionalExpression,
13
17
  isDecorator,
18
+ isFunctionDeclaration,
19
+ isFunctionExpression,
14
20
  isIdentifier,
21
+ isMethodDeclaration,
15
22
  isObjectLiteralExpression,
23
+ isParameterDeclaration,
16
24
  isParenthesizedExpression,
17
25
  isPropertyAccessExpression,
18
26
  isPropertyAssignment,
27
+ isPropertyDeclaration,
19
28
  isShorthandPropertyAssignment,
29
+ isOmittedExpression,
30
+ isSpreadAssignment,
20
31
  isSpreadElement,
21
32
  isVariableDeclaration,
22
33
  type CallExpression,
23
34
  type Decorator,
24
35
  type Expression,
25
36
  type Node,
37
+ type ParameterDeclaration,
38
+ SyntaxKind,
26
39
  } from "@typescript/native/ast";
27
- import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/sync";
40
+ import {
41
+ ObjectFlags,
42
+ SymbolFlags,
43
+ type Symbol as TSSymbol,
44
+ type Type,
45
+ } from "@typescript/native/sync";
28
46
  import type { AliasChains } from "./alias-chain.ts";
29
47
  import { nodeKey, type Inventory } from "./inventory.ts";
30
48
  import { must } from "./query.ts";
@@ -35,11 +53,14 @@ import { unwrapped } from "./unwrap.ts";
35
53
  interface PassedValue {
36
54
  /** Where the value is written, which is where a record of it sits. */
37
55
  readonly site: Node;
38
- /** The name the value is resolved through, or the shorthand assignment that holds it. */
39
- readonly name: Node;
56
+ /** The name the value is resolved through, the shorthand assignment that holds it, or none. */
57
+ readonly name: Node | undefined;
40
58
  readonly shorthand: boolean;
41
59
  }
42
60
 
61
+ /** A passed value that names its declaration. */
62
+ type NamedValue = PassedValue & { readonly name: Node };
63
+
43
64
  /** One class of the target a call passes or a decorator is attached to, and where. */
44
65
  interface ClassUse {
45
66
  /** The inventory's identifier of the class. */
@@ -111,46 +132,114 @@ export function resolvedTargets<Brand>(
111
132
  return targets;
112
133
  }
113
134
 
114
- /** The values one expression passes: itself, or what an array or object literal holds. */
115
- function passedIn(expression: Expression, found: PassedValue[]): void {
116
- if (isParenthesizedExpression(expression)) {
117
- passedIn(expression.expression, found);
118
- return;
119
- }
120
- if (isArrayLiteralExpression(expression)) {
121
- for (const element of expression.elements) {
122
- passedIn(isSpreadElement(element) ? element.expression : element, found);
123
- }
124
- return;
135
+ /** The declaration a shorthand property's value resolves to, an import followed to what it names. */
136
+ function shorthandTarget<Brand>(project: ProjectView<Brand>, node: Node): TSSymbol | undefined {
137
+ const value = must(project.shorthandValueAt(project.handle(node)));
138
+ if (value === undefined || (value.flags & SymbolFlags.Alias) === 0) {
139
+ return value;
125
140
  }
126
- if (isObjectLiteralExpression(expression)) {
127
- for (const property of expression.properties) {
128
- if (isPropertyAssignment(property)) {
129
- passedIn(property.initializer, found);
130
- } else if (isShorthandPropertyAssignment(property)) {
131
- found.push({ site: property.name, name: property, shorthand: true });
141
+ const aliased = must(project.queries.aliased(value));
142
+ return aliased.declarations.length === 0 ? undefined : aliased;
143
+ }
144
+
145
+ /**
146
+ * The values one expression passes: each value it can be ({@link leavesOf}), or what an
147
+ * array or object literal it can be holds, at any depth.
148
+ */
149
+ function passedIn(written: Expression, found: PassedValue[]): void {
150
+ for (const expression of leavesOf(written)) {
151
+ if (isArrayLiteralExpression(expression)) {
152
+ for (const element of expression.elements) {
153
+ passedIn(element, found);
154
+ }
155
+ } else if (isObjectLiteralExpression(expression)) {
156
+ for (const property of expression.properties) {
157
+ if (isPropertyAssignment(property)) {
158
+ passedIn(property.initializer, found);
159
+ } else if (isSpreadAssignment(property)) {
160
+ passedIn(property.expression, found);
161
+ } else if (isShorthandPropertyAssignment(property)) {
162
+ found.push({ site: property.name, name: property, shorthand: true });
163
+ }
132
164
  }
165
+ } else if (!isOmittedExpression(expression)) {
166
+ found.push({ site: expression, name: calleeName(expression), shorthand: false });
133
167
  }
134
- return;
135
168
  }
136
- const name = calleeName(expression);
137
- if (name !== undefined) {
138
- found.push({ site: expression, name, shorthand: false });
169
+ }
170
+
171
+ /** The operators whose value is one of their operands. */
172
+ const CHOOSING: ReadonlySet<SyntaxKind> = new Set([
173
+ SyntaxKind.QuestionQuestionToken,
174
+ SyntaxKind.BarBarToken,
175
+ SyntaxKind.AmpersandAmpersandToken,
176
+ ]);
177
+
178
+ /**
179
+ * The expressions one value can be: itself, or each branch of a conditional, each operand
180
+ * of `??`, `||` and `&&` and the last of a comma, inside any wrapper that changes no value.
181
+ * Each is read alone because the checker reduces a union by subtype, so the type at the
182
+ * whole drops a class structurally equal to, or a subtype of, another branch's.
183
+ */
184
+ function leavesOf(written: Expression): readonly Expression[] {
185
+ const expression = unwrapped(isSpreadElement(written) ? written.expression : written);
186
+ if (isConditionalExpression(expression)) {
187
+ return [...leavesOf(expression.whenTrue), ...leavesOf(expression.whenFalse)];
188
+ }
189
+ if (isBinaryExpression(expression)) {
190
+ const operator = expression.operatorToken.kind;
191
+ if (CHOOSING.has(operator)) {
192
+ return [...leavesOf(expression.left), ...leavesOf(expression.right)];
193
+ }
194
+ if (operator === SyntaxKind.CommaToken) {
195
+ return leavesOf(expression.right);
196
+ }
139
197
  }
198
+ return [expression];
140
199
  }
141
200
 
142
201
  /** The values one argument passes, a spread argument's included. */
143
- export function valuesPassed(argument: Expression): readonly PassedValue[] {
202
+ function leavesPassed(argument: Expression): readonly PassedValue[] {
144
203
  const found: PassedValue[] = [];
145
- passedIn(isSpreadElement(argument) ? argument.expression : argument, found);
204
+ passedIn(argument, found);
146
205
  return found;
147
206
  }
148
207
 
208
+ /** The values one argument passes that name their declaration. */
209
+ export function valuesPassed(argument: Expression): readonly NamedValue[] {
210
+ return leavesPassed(argument).filter((value): value is NamedValue => value.name !== undefined);
211
+ }
212
+
213
+ /**
214
+ * What one passed value holds: the declaration its name resolves to, and the classes and
215
+ * functions the checker's type at it holds ({@link constructedAt}). `known` holds names
216
+ * already resolved in a batch.
217
+ */
218
+ function heldBy<Brand>(
219
+ project: ProjectView<Brand>,
220
+ value: PassedValue,
221
+ known?: ReadonlyMap<Node, TSSymbol>,
222
+ ): { readonly named: TSSymbol | undefined; readonly typed: readonly TSSymbol[] } {
223
+ const { name } = value;
224
+ if (name === undefined) {
225
+ return { named: undefined, typed: constructedAt(project, value.site) };
226
+ }
227
+ if (value.shorthand) {
228
+ return { named: shorthandTarget(project, name), typed: [] };
229
+ }
230
+ return {
231
+ named: known?.get(name) ?? resolvedTargets(project, [name]).get(name),
232
+ typed: constructedAt(project, value.site),
233
+ };
234
+ }
235
+
149
236
  /**
150
- * The classes of the target each call passes, by the call. A name that is no class is
151
- * followed through the initializer of each variable it declares, so a class bound to a
152
- * variable first is passed. A value that names no class of the target, a class of
153
- * another program included, passes nothing here.
237
+ * The classes of the target each call passes, by the call: each class a value an argument
238
+ * can be holds as a constructor, as the checker types it. A name whose type erases the
239
+ * class is followed through what its declaration binds ({@link boundTo}) and, for a
240
+ * parameter, through what each call of its function passes at its place, so a class handed
241
+ * to a wrapper is passed. A class is used at each value that passes it. A class of another
242
+ * program passes nothing.
154
243
  */
155
244
  export function classesPassed<Brand>(
156
245
  project: ProjectView<Brand>,
@@ -158,45 +247,230 @@ export function classesPassed<Brand>(
158
247
  chains: AliasChains,
159
248
  calls: readonly CallExpression[],
160
249
  ): ReadonlyMap<CallExpression, readonly ClassUse[]> {
161
- const passed = new Map(
162
- calls.map((call) => [call, call.arguments.flatMap((argument) => valuesPassed(argument))]),
163
- );
164
- const all = [...passed.values()].flat();
250
+ const passed = new Map(calls.map((call) => [call, call.arguments.flatMap(leavesPassed)]));
165
251
  const targets = resolvedTargets(
166
252
  project,
167
- all.filter((value) => !value.shorthand).map((value) => value.name),
253
+ [...passed.values()].flat().flatMap((value) => (value.shorthand ? [] : (value.name ?? []))),
168
254
  );
169
255
  const classes = new Set(
170
256
  held.symbols.filter((symbol) => symbol.kind === "class").map((symbol) => symbol.id),
171
257
  );
258
+ let calling: CallsOf | undefined;
259
+ const callsOfFunction = (symbol: TSSymbol): readonly CallExpression[] => {
260
+ calling ??= callsByDeclaration(project);
261
+ return symbol.declarations.flatMap((handle) => calling?.get(keyOf(handle)) ?? []);
262
+ };
263
+ const classesOf = (value: PassedValue, seen: Set<number>): readonly string[] => {
264
+ const { named, typed } = heldBy(project, value, targets);
265
+ return [
266
+ ...typed.flatMap((symbol) => chains.declarationsOf(symbol).filter((id) => classes.has(id))),
267
+ ...(named === undefined ? [] : reached(named, seen)),
268
+ ];
269
+ };
172
270
  const reached = (symbol: TSSymbol, seen: Set<number>): readonly string[] => {
173
271
  const ids = chains.declarationsOf(symbol).filter((id) => classes.has(id));
174
272
  if (ids.length > 0 || seen.has(symbol.id)) {
175
273
  return ids;
176
274
  }
177
275
  seen.add(symbol.id);
276
+ const through = (written: Expression): readonly string[] =>
277
+ leavesPassed(written).flatMap((value) => classesOf(value, seen));
178
278
  return symbol.declarations.flatMap((handle) => {
179
279
  const node = project.declarationAt(handle)?.node;
180
- if (node === undefined || !isVariableDeclaration(node) || node.initializer === undefined) {
280
+ if (node === undefined) {
181
281
  return [];
182
282
  }
183
- const name = calleeName(unwrapped(node.initializer));
184
- const next = name === undefined ? undefined : resolvedTargets(project, [name]).get(name);
185
- return next === undefined ? [] : reached(next, seen);
283
+ if (isParameterDeclaration(node)) {
284
+ return argumentsAt(project, node, callsOfFunction).flatMap(through);
285
+ }
286
+ const bound = boundTo(project, node);
287
+ return [
288
+ ...(bound.written === undefined ? [] : through(bound.written)),
289
+ ...bound.symbols.flatMap((next) => reached(next, seen)),
290
+ ];
186
291
  });
187
292
  };
188
- const classesOf = (value: PassedValue): readonly string[] => {
189
- const symbol = value.shorthand
190
- ? must(project.shorthandValueAt(project.handle(value.name)))
191
- : targets.get(value.name);
192
- return symbol === undefined ? [] : reached(symbol, new Set());
193
- };
194
293
  return new Map(
195
- [...passed].map(([call, values]) => [
196
- call,
197
- values.flatMap((value) => classesOf(value).map((id) => ({ id, site: value.site }))),
198
- ]),
294
+ [...passed].map(([call, values]) => {
295
+ const uses = new Map<string, ClassUse>();
296
+ for (const value of values) {
297
+ for (const id of classesOf(value, new Set())) {
298
+ uses.set(`${id}\u0000${String(value.site.pos)}`, { id, site: value.site });
299
+ }
300
+ }
301
+ return [call, [...uses.values()]];
302
+ }),
303
+ );
304
+ }
305
+
306
+ /**
307
+ * The symbols of the classes and functions the checker's type at one expression holds as
308
+ * values, each member of a union: the static side of a class or a function is an
309
+ * anonymous type its declaration's symbol names, which an instance's type is not.
310
+ */
311
+ function constructedAt<Brand>(project: ProjectView<Brand>, written: Node): readonly TSSymbol[] {
312
+ return heldIn(project, must(project.queries.typeAt(unwrapped(written))));
313
+ }
314
+
315
+ /** The classes and functions one type holds as values, as {@link constructedAt} reads them. */
316
+ function heldIn<Brand>(project: ProjectView<Brand>, type: Type | undefined): readonly TSSymbol[] {
317
+ const { queries } = project;
318
+ return (type === undefined ? [] : must(queries.constituents(type))).flatMap((part) => {
319
+ if (!part.isObjectType() || (part.objectFlags & ObjectFlags.Anonymous) === 0) {
320
+ return [];
321
+ }
322
+ return must(queries.symbolOfType(part)) ?? [];
323
+ });
324
+ }
325
+
326
+ /** What one declaration binds its name to: the value it writes, or the symbols it copies. */
327
+ interface Bound {
328
+ readonly written?: Expression;
329
+ readonly symbols: readonly TSSymbol[];
330
+ }
331
+
332
+ /**
333
+ * What a variable, a property or a destructured binding binds its name to: a variable's
334
+ * or a property's initializer, the value a shorthand property names, or the property of
335
+ * the destructured value a binding element reads.
336
+ */
337
+ function boundTo<Brand>(project: ProjectView<Brand>, node: Node): Bound {
338
+ if (isShorthandPropertyAssignment(node)) {
339
+ const value = shorthandTarget(project, node);
340
+ return { symbols: value === undefined ? [] : [value] };
341
+ }
342
+ if (isBindingElement(node)) {
343
+ const key = node.propertyName ?? node.name;
344
+ if (key === undefined || !isIdentifier(key)) {
345
+ return { symbols: [] };
346
+ }
347
+ const source = must(project.queries.typeAt(node.parent));
348
+ const property =
349
+ source === undefined
350
+ ? undefined
351
+ : must(project.queries.propertiesOf(source)).find((one) => one.name === key.text);
352
+ return { symbols: property === undefined ? [] : [property] };
353
+ }
354
+ const written = initializerOf(node);
355
+ return written === undefined ? { symbols: [] } : { written, symbols: [] };
356
+ }
357
+
358
+ /** The value a variable or a property declaration binds its name to, where it writes one. */
359
+ function initializerOf(node: Node): Expression | undefined {
360
+ return isVariableDeclaration(node) || isPropertyAssignment(node) || isPropertyDeclaration(node)
361
+ ? node.initializer
362
+ : undefined;
363
+ }
364
+
365
+ /** The project's calls, by each declaration their callee resolves to. */
366
+ type CallsOf = ReadonlyMap<string, readonly CallExpression[]>;
367
+
368
+ /** The key of one declaration a symbol names, the same for each symbol the checker makes of it. */
369
+ function keyOf(handle: { readonly path: string; readonly index: number }): string {
370
+ return `${handle.path}#${String(handle.index)}`;
371
+ }
372
+
373
+ /**
374
+ * Every call of the project by the declarations its callee can be: the function or the
375
+ * method it names, or else each function the checker's type at it holds, wherever that
376
+ * value came from. A name is also followed through what its binding copies
377
+ * ({@link boundTo}), which is how a call reaches a function an annotation erases.
378
+ * Only the target's own declarations are keys, as only its own functions are asked for.
379
+ */
380
+ function callsByDeclaration<Brand>(project: ProjectView<Brand>): CallsOf {
381
+ const calls = callsOf(project);
382
+ const own = project.ownPaths();
383
+ const callees = resolvedTargets(
384
+ project,
385
+ calls.flatMap((call) => calleeName(call.expression) ?? []),
386
+ );
387
+ const named = new Map<number, readonly string[]>();
388
+ const keysOf = (symbol: TSSymbol, seen: Set<number>): readonly string[] => {
389
+ const known = named.get(symbol.id);
390
+ if (known !== undefined || seen.has(symbol.id)) {
391
+ return known ?? [];
392
+ }
393
+ seen.add(symbol.id);
394
+ const keys = symbol.declarations
395
+ .filter((handle) => own.has(handle.path))
396
+ .flatMap((handle) => {
397
+ const node = project.declarationAt(handle)?.node;
398
+ const bound = node === undefined ? { symbols: [] } : boundTo(project, node);
399
+ const copied = [
400
+ ...bound.symbols,
401
+ ...(bound.written === undefined ? [] : leavesPassed(bound.written)).flatMap((value) => {
402
+ const { named: next, typed } = heldBy(project, value);
403
+ return [...typed, ...(next === undefined ? [] : [next])];
404
+ }),
405
+ ];
406
+ return [keyOf(handle), ...copied.flatMap((next) => keysOf(next, seen))];
407
+ });
408
+ named.set(symbol.id, keys);
409
+ return keys;
410
+ };
411
+ const calleeOf = (call: CallExpression): TSSymbol | undefined => {
412
+ const name = calleeName(call.expression);
413
+ return name === undefined ? undefined : callees.get(name);
414
+ };
415
+ const declared = (call: CallExpression): boolean =>
416
+ ((calleeOf(call)?.flags ?? 0) & (SymbolFlags.Function | SymbolFlags.Method)) !== 0;
417
+ const typed = calls.filter((call) => !declared(call));
418
+ const types = new Map(
419
+ project.queries
420
+ .typesAt(typed.map((call) => unwrapped(call.expression)))
421
+ .map((type, index) => [typed[index], must(type)]),
199
422
  );
423
+ const found = new Map<string, CallExpression[]>();
424
+ for (const call of calls) {
425
+ const callee = calleeOf(call);
426
+ const held = [
427
+ ...(callee === undefined ? [] : [callee]),
428
+ ...(types.has(call) ? heldIn(project, types.get(call)) : []),
429
+ ];
430
+ for (const key of new Set(held.flatMap((symbol) => keysOf(symbol, new Set())))) {
431
+ found.set(key, [...(found.get(key) ?? []), call]);
432
+ }
433
+ }
434
+ return found;
435
+ }
436
+
437
+ /**
438
+ * What each call of the function a parameter belongs to passes at the parameter's place,
439
+ * every argument from there on for a rest parameter. The function is the one the checker's
440
+ * type at its own declaration names, so a function with no name of its own is called too.
441
+ * A `this` parameter takes no place, and an argument at or after a spread argument passes
442
+ * at every place the spread's length can move it to.
443
+ */
444
+ function argumentsAt<Brand>(
445
+ project: ProjectView<Brand>,
446
+ parameter: ParameterDeclaration,
447
+ callsOfFunction: (symbol: TSSymbol) => readonly CallExpression[],
448
+ ): readonly Expression[] {
449
+ const owner = parameter.parent;
450
+ if (
451
+ !isFunctionDeclaration(owner) &&
452
+ !isMethodDeclaration(owner) &&
453
+ !isArrowFunction(owner) &&
454
+ !isFunctionExpression(owner)
455
+ ) {
456
+ return [];
457
+ }
458
+ const calls = new Set(constructedAt(project, owner).flatMap(callsOfFunction));
459
+ const [first] = owner.parameters;
460
+ const erased = first !== undefined && isIdentifier(first.name) && first.name.text === "this";
461
+ const place = owner.parameters.indexOf(parameter) - (erased ? 1 : 0);
462
+ const rest = parameter.dotDotDotToken !== undefined;
463
+ return [...calls].flatMap((call) => {
464
+ let spreads = 0;
465
+ return call.arguments.flatMap((argument, index) => {
466
+ const spread = isSpreadElement(argument);
467
+ const earliest = index - spreads;
468
+ spreads += spread ? 1 : 0;
469
+ const open = spread || earliest < index;
470
+ const lands = open ? rest || earliest <= place : rest ? index >= place : index === place;
471
+ return lands ? [spread ? argument.expression : argument] : [];
472
+ });
473
+ });
200
474
  }
201
475
 
202
476
  /** One class declaration of the target and the decorators written on it. */
@@ -4,7 +4,7 @@ export const RECORD = {
4
4
  conformance: {
5
5
  corpusVersion: "6.0.0",
6
6
  result: "pass",
7
- digest: "sha256:a4ffbc1c2d9dc7903ccbf52af0e1a22094e0b50363f59365f6aa37b9437d3427",
7
+ digest: "sha256:140f095d52fa8140de070f46d13a132b723c634ecf24a8b96e6a92c410c4b848",
8
8
  },
9
9
  gaps: [],
10
10
  } as const;
@@ -423,7 +423,7 @@ export const KIND_ROWS = [
423
423
  holdsLive: true,
424
424
  rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface, and so is a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, because that interface fixes them whether or not the program converts the method's type to it. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending. A reference from a Go external test package, a test file whose package clause names its directory's package with the suffix _test, is a reference from outside the package, because that test builds as another package and needs the export.",
425
425
  precondition:
426
- "Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the package references that function or method, because the caller holds values of the type, and unexporting it would leave an exported signature naming a type its callers cannot name. A package-level declaration whose name a string literal spells carries the class grammar/analysis/confidence.md gives it.",
426
+ "Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type is not reported when a reference from outside the package reaches its values through its own package's exported declarations: a reference to an exported function or method whose parameters or results name the type, to an exported variable or constant whose type names it, to an exported field or property whose type names it, an embedded field included, or to an exported member of the type itself, a field promoted through an embedding included. A type a reference from outside the package names, or whose values such a reference reaches, passes the reach on to each type its definition names other than through an unexported member, whether it is exported or not, so an unexported type an exported function returns passes it to the types its exported fields name. A type names another through every composite that spells it: a pointer, an array, a slice, a map key or value, a channel, a function signature and a type argument. The referencing code holds values of the type, so unexporting it would leave an exported declaration naming a type its users cannot name. A package-level declaration whose name a string literal spells carries the class grammar/analysis/confidence.md gives it.",
427
427
  details: { narrower_visibility: "required", name_literal: "optional" },
428
428
  defaultEnabled: true,
429
429
  defaultSeverity: "warn",
@@ -439,7 +439,7 @@ export const KIND_ROWS = [
439
439
  holdsLive: true,
440
440
  rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface, and so is a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, because that interface fixes them whether or not the program converts the method's type to it. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending. A reference from a Go external test package, a test file whose package clause names its directory's package with the suffix _test, is a reference from outside the package, because that test builds as another package and needs the export.",
441
441
  precondition:
442
- "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the target module references that function or method, because the caller holds values of the type, and moving it behind an internal boundary would leave an exported signature naming a type its callers cannot name.",
442
+ "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type is not reported when a reference from outside the target module reaches its values through its own package's exported declarations, as DS1501 defines that reach, because the referencing code holds values of the type, and moving it behind an internal boundary would leave an exported declaration naming a type its users cannot name.",
443
443
  details: { narrower_visibility: "required", name_literal: "optional" },
444
444
  defaultEnabled: true,
445
445
  defaultSeverity: "warn",
package/src/exempt.ts CHANGED
@@ -18,14 +18,43 @@ import {
18
18
  type TypeScriptVisibility,
19
19
  } from "./exempt-classes.ts";
20
20
  import type { Inventory, InventorySymbol, Visibility } from "./inventory.ts";
21
- import type { Matrix } from "./matrix.ts";
22
21
  import { byPosition, positionKey, type Position } from "./position.ts";
23
22
  import { Unanswerable } from "./query.ts";
24
23
  import type { ProjectView } from "./session.ts";
25
24
  import type { Templates } from "./template-field.ts";
26
- import { sweep, type Exemption, type Implementing, type Mode, type SweepInput } from "./sweep.ts";
25
+ import type { Mode } from "./tests/file-class.ts";
27
26
  import { bytewise } from "./text/compare.ts";
28
27
 
28
+ /** One declaration an exemption class holds back, and the evidence it was found by. */
29
+ export interface Exemption {
30
+ /** The identifier of the declaration held back. */
31
+ readonly id: string;
32
+ readonly class: TSExemptionClass;
33
+ /** One clause naming the relation, then the thing it relates to: `passed to JSON.stringify`. */
34
+ readonly detail: string;
35
+ /** Where the evidence is written. */
36
+ readonly site: Position;
37
+ /**
38
+ * The component file, below the target root, whose markup holds the evidence. The
39
+ * record holds only while that file is live.
40
+ */
41
+ readonly whileLive?: string;
42
+ /**
43
+ * The interface methods the retained member implements, and whether its body is empty.
44
+ * The record holds only while one of them is called through its interface.
45
+ */
46
+ readonly implementing?: Implementing;
47
+ /** Whether the record holds only where a write the mode counts stores into the declaration. */
48
+ readonly whileWritten?: true;
49
+ }
50
+
51
+ /** The interface methods one retained class member implements, and whether its body is empty. */
52
+ interface Implementing {
53
+ /** The declarations of the interface's methods the member answers, by identifier. */
54
+ readonly methods: readonly string[];
55
+ readonly empty: boolean;
56
+ }
57
+
29
58
  /** One declaration a detector holds back, and where and why. */
30
59
  export interface Evidence {
31
60
  /** The identifier of the declaration held back. */
@@ -228,21 +257,6 @@ export function exemptionsOf(found: readonly Exemption[]): readonly Exemption[]
228
257
  });
229
258
  }
230
259
 
231
- /**
232
- * The records that hold while the component files at `live` are live: every record that
233
- * holds whatever is live, and each one a live file's markup holds, as a record of the run.
234
- */
235
- export function holdingWhile(
236
- records: readonly Exemption[],
237
- live: ReadonlySet<string>,
238
- ): readonly Exemption[] {
239
- return exemptionsOf(
240
- records.flatMap(({ whileLive, ...record }) =>
241
- whileLive === undefined || live.has(whileLive) ? [record] : [],
242
- ),
243
- );
244
- }
245
-
246
260
  /** The records per declaration, each declaration's in the order given. */
247
261
  function byDeclaration(records: readonly Exemption[]): ReadonlyMap<string, readonly Exemption[]> {
248
262
  const grouped = new Map<string, Exemption[]>();
@@ -257,35 +271,6 @@ function byDeclaration(records: readonly Exemption[]): ReadonlyMap<string, reado
257
271
  return grouped;
258
272
  }
259
273
 
260
- /**
261
- * The records that held back a declaration: one some configuration's graph, swept
262
- * again with no exemption and its marks, judges a candidate, whatever another
263
- * configuration answers, or one in `writeOnly`, which the run would report as written
264
- * and never read without the record's read. A declaration a relation or a mark holds
265
- * live in every configuration is held back by no record. The order is the
266
- * declarations' site order, and for one declaration the input's.
267
- */
268
- export function retainedIn(
269
- matrix: Matrix,
270
- input: SweepInput,
271
- writeOnly: ReadonlySet<string> = new Set(),
272
- ): readonly Exemption[] {
273
- const exempt = input.exempt ?? [];
274
- if (exempt.length === 0) {
275
- return [];
276
- }
277
- const bare: SweepInput = { marked: input.marked, mode: input.mode };
278
- const dead = new Set(
279
- matrix.graphs.flatMap((graph) =>
280
- sweep(graph, bare).candidates.map((candidate) => candidate.id),
281
- ),
282
- );
283
- const held = byDeclaration(
284
- exempt.filter((record) => dead.has(record.id) || writeOnly.has(record.id)),
285
- );
286
- return matrix.union.symbols.flatMap((symbol) => held.get(symbol.id) ?? []);
287
- }
288
-
289
274
  /**
290
275
  * One line per held-back declaration, in site order: its reference and position, then
291
276
  * each record's class, site and detail, all separated by tabs.