@cplieger/deadset-ts 5.2.3 → 6.0.1

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 (84) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +28 -5
  3. package/dist/src/augmentations.js +50 -0
  4. package/dist/src/components.js +15 -15
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/convention-rows.js +17 -2
  7. package/dist/src/conventions.js +82 -1
  8. package/dist/src/dependencies.js +42 -26
  9. package/dist/src/entry-points.js +28 -14
  10. package/dist/src/exempt.js +1 -0
  11. package/dist/src/findings/completion.js +9 -4
  12. package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
  13. package/dist/src/findings/emitters.js +64 -25
  14. package/dist/src/findings/interfaces.js +4 -3
  15. package/dist/src/findings/intra-function.js +15 -2
  16. package/dist/src/findings/reads-and-writes.js +3 -2
  17. package/dist/src/findings/unused-declarations.js +13 -1
  18. package/dist/src/findings/visibility-narrowing.js +2 -1
  19. package/dist/src/findings-pass.js +1 -0
  20. package/dist/src/graph.js +6 -6
  21. package/dist/src/implementations.js +1 -1
  22. package/dist/src/indexed-access.js +215 -0
  23. package/dist/src/interface-satisfaction.js +85 -23
  24. package/dist/src/intra-function-parts.js +139 -30
  25. package/dist/src/inventory.js +6 -1
  26. package/dist/src/kinds.js +13 -12
  27. package/dist/src/matrix.js +34 -9
  28. package/dist/src/module-declarations.js +180 -0
  29. package/dist/src/outside-tests.js +17 -15
  30. package/dist/src/overrides.js +70 -9
  31. package/dist/src/query.js +7 -0
  32. package/dist/src/ref.js +9 -0
  33. package/dist/src/references.js +6 -0
  34. package/dist/src/report.js +6 -1
  35. package/dist/src/reporters/sarif.js +6 -2
  36. package/dist/src/reporters/text.js +23 -3
  37. package/dist/src/roots.js +50 -10
  38. package/dist/src/suppress-file.js +3 -5
  39. package/dist/src/version.js +1 -1
  40. package/dist/src/workflow-steps.js +82 -0
  41. package/dist/src/workspace-programs.js +6 -31
  42. package/jsr.json +1 -1
  43. package/package.json +8 -2
  44. package/src/analysis.ts +40 -4
  45. package/src/augmentations.ts +72 -0
  46. package/src/components.ts +15 -13
  47. package/src/conformance-record.ts +2 -2
  48. package/src/convention-rows.ts +25 -3
  49. package/src/conventions.ts +106 -2
  50. package/src/dependencies.ts +51 -31
  51. package/src/entry-points.ts +32 -19
  52. package/src/exempt.ts +4 -1
  53. package/src/findings/completion.ts +11 -4
  54. package/src/findings/dependencies-and-module-machinery.ts +2 -2
  55. package/src/findings/emitters.ts +89 -27
  56. package/src/findings/interfaces.ts +7 -6
  57. package/src/findings/intra-function.ts +20 -2
  58. package/src/findings/reads-and-writes.ts +7 -2
  59. package/src/findings/unused-declarations.ts +16 -1
  60. package/src/findings/visibility-narrowing.ts +2 -1
  61. package/src/findings-pass.ts +4 -1
  62. package/src/graph.ts +6 -6
  63. package/src/implementations.ts +1 -1
  64. package/src/indexed-access.ts +295 -0
  65. package/src/interface-satisfaction.ts +118 -27
  66. package/src/intra-function-parts.ts +196 -35
  67. package/src/inventory.ts +8 -0
  68. package/src/kinds.ts +14 -12
  69. package/src/matrix.ts +51 -8
  70. package/src/module-declarations.ts +241 -0
  71. package/src/outside-tests.ts +18 -16
  72. package/src/overrides.ts +80 -14
  73. package/src/query.ts +28 -0
  74. package/src/ref.ts +10 -0
  75. package/src/references.ts +6 -0
  76. package/src/report.ts +11 -1
  77. package/src/reporters/sarif.ts +9 -2
  78. package/src/reporters/text.ts +25 -3
  79. package/src/roots.ts +56 -7
  80. package/src/suppress-file.ts +3 -6
  81. package/src/sweep.ts +12 -0
  82. package/src/version.ts +1 -1
  83. package/src/workflow-steps.ts +104 -0
  84. package/src/workspace-programs.ts +6 -35
@@ -0,0 +1,295 @@
1
+ /**
2
+ * The members an indexed-access type names. `T["a" | "b"]` reads the members `a` and `b`
3
+ * of `T` as a property access by either name would. Where the index is a type parameter
4
+ * a call fixes, the call names the members the literal type argument names, written or
5
+ * inferred, which is read off the parameter the type parameter annotates whole. The type
6
+ * query of a type-query alias is no reference.
7
+ */
8
+
9
+ import {
10
+ isArrowFunction,
11
+ isCallExpression,
12
+ isFunctionDeclaration,
13
+ isFunctionExpression,
14
+ isIdentifier,
15
+ isIndexedAccessTypeNode,
16
+ isLiteralTypeNode,
17
+ isMethodDeclaration,
18
+ isNewExpression,
19
+ isNoSubstitutionTemplateLiteral,
20
+ isNumericLiteral,
21
+ isParenthesizedTypeNode,
22
+ isPropertyAccessExpression,
23
+ isStringLiteral,
24
+ isTypeReferenceNode,
25
+ isUnionTypeNode,
26
+ SyntaxKind,
27
+ type Node,
28
+ type SourceFile,
29
+ type TypeNode,
30
+ } from "@typescript/native/unstable/ast";
31
+ import type { Type } from "@typescript/native/unstable/sync";
32
+ import type { AliasChains } from "./alias-chain.ts";
33
+ import { nodeKey, type Inventory } from "./inventory.ts";
34
+ import { renderPosition } from "./position.ts";
35
+ import { UNANSWERED } from "./query.ts";
36
+ import type { Reference } from "./references.ts";
37
+ import type { ProjectView } from "./session.ts";
38
+ import { typeQueryAliases } from "./type-query-alias.ts";
39
+
40
+ /** A function whose signature declares type parameters, by the kinds of node that can. */
41
+ type Generic = Node & {
42
+ readonly typeParameters?: readonly { readonly name: Node }[];
43
+ readonly parameters: readonly { readonly name: Node; readonly type?: TypeNode }[];
44
+ };
45
+
46
+ function isGeneric(node: Node): node is Generic {
47
+ return (
48
+ (isFunctionDeclaration(node) ||
49
+ isMethodDeclaration(node) ||
50
+ isArrowFunction(node) ||
51
+ isFunctionExpression(node)) &&
52
+ (node as { readonly typeParameters?: readonly unknown[] }).typeParameters !== undefined
53
+ );
54
+ }
55
+
56
+ function unwrappedType(node: TypeNode): TypeNode {
57
+ return isParenthesizedTypeNode(node) ? unwrappedType(node.type) : node;
58
+ }
59
+
60
+ /** The names a literal index type spells, or none where the index is any other type. */
61
+ function literalNames(index: TypeNode): readonly string[] | undefined {
62
+ const node = unwrappedType(index);
63
+ if (isUnionTypeNode(node)) {
64
+ const names = node.types.map(literalNames);
65
+ return names.every((one) => one !== undefined) ? names.flat() : undefined;
66
+ }
67
+ if (!isLiteralTypeNode(node)) {
68
+ return undefined;
69
+ }
70
+ const literal = node.literal;
71
+ if (isStringLiteral(literal) || isNoSubstitutionTemplateLiteral(literal)) {
72
+ return [literal.text];
73
+ }
74
+ return isNumericLiteral(literal) ? [String(Number(literal.text))] : undefined;
75
+ }
76
+
77
+ /** The name a literal type carries, a union's each, or none where a constituent is no literal. */
78
+ function literalTypeNames(
79
+ type: Type,
80
+ parts: (type: Type) => readonly Type[],
81
+ ): string[] | undefined {
82
+ const names: string[] = [];
83
+ for (const part of parts(type)) {
84
+ if (part.isStringLiteralType()) {
85
+ names.push(part.value);
86
+ } else if (part.isNumberLiteralType()) {
87
+ names.push(String(part.value));
88
+ } else {
89
+ return undefined;
90
+ }
91
+ }
92
+ return names;
93
+ }
94
+
95
+ /** One indexed-access type whose index is a type parameter its function declares. */
96
+ interface ByParameter {
97
+ readonly object: TypeNode;
98
+ /** The index of the parameter the type parameter annotates whole. */
99
+ readonly parameter: number;
100
+ }
101
+
102
+ /** An indexed access to resolve: the object type, the names, and where the reference is made. */
103
+ interface Access {
104
+ readonly file: SourceFile;
105
+ readonly from: string;
106
+ readonly at: Node;
107
+ readonly object: TypeNode | Type;
108
+ readonly names: readonly string[];
109
+ }
110
+
111
+ /**
112
+ * The references the indexed-access types of one project's own files make to the members
113
+ * they name, each made by the declaration that holds the type or the call, or by the file.
114
+ */
115
+ export function indexedAccessReferences<Brand>(
116
+ project: ProjectView<Brand>,
117
+ held: Inventory,
118
+ chains: AliasChains,
119
+ root: string,
120
+ testFiles: ReadonlySet<string>,
121
+ ): readonly Reference[] {
122
+ const queries = project.queries;
123
+ const accesses: Access[] = [];
124
+ const generics = new Map<string, ByParameter[]>();
125
+ const calls: { file: SourceFile; from: string; call: Node; name: string }[] = [];
126
+ const names = new Set<string>();
127
+
128
+ for (const file of project.ownSourceFiles()) {
129
+ const fileId = held.declarations.get(nodeKey(file, file));
130
+ if (fileId === undefined) {
131
+ continue;
132
+ }
133
+ const aliasTypes = new Set(typeQueryAliases(file).map((alias) => alias.type));
134
+ const visit = (node: Node, from: string, enclosing: readonly Generic[]): void => {
135
+ const own = held.declarations.get(nodeKey(file, node));
136
+ const holder = own ?? from;
137
+ const inside = isGeneric(node) ? [...enclosing, node] : enclosing;
138
+ if (isIndexedAccessTypeNode(node) && !aliasTypes.has(node)) {
139
+ const literal = literalNames(node.indexType);
140
+ if (literal !== undefined) {
141
+ accesses.push({ file, from: holder, at: node, object: node.objectType, names: literal });
142
+ } else {
143
+ parameterIndex(node, inside, file, generics, names);
144
+ }
145
+ }
146
+ if (isCallExpression(node) || isNewExpression(node)) {
147
+ const callee = node.expression;
148
+ const name = isIdentifier(callee)
149
+ ? callee.text
150
+ : isPropertyAccessExpression(callee) && isIdentifier(callee.name)
151
+ ? callee.name.text
152
+ : undefined;
153
+ if (name !== undefined) {
154
+ calls.push({ file, from: holder, call: node, name });
155
+ }
156
+ }
157
+ node.forEachChild((child) => {
158
+ visit(child, holder, inside);
159
+ });
160
+ };
161
+ file.forEachChild((child) => {
162
+ visit(child, fileId, []);
163
+ });
164
+ }
165
+
166
+ for (const { file, from, call, name } of calls) {
167
+ if (!names.has(name)) {
168
+ continue;
169
+ }
170
+ const resolved = queries.resolvedSignature(call);
171
+ if (resolved === UNANSWERED || resolved?.declaration === undefined) {
172
+ continue;
173
+ }
174
+ const declaration = project.declarationAt(resolved.declaration)?.node;
175
+ const byParameter =
176
+ declaration === undefined
177
+ ? undefined
178
+ : generics.get(nodeKey(declaration.getSourceFile(), declaration));
179
+ if (byParameter === undefined) {
180
+ continue;
181
+ }
182
+ for (const { object, parameter } of byParameter) {
183
+ const fixed = queries.typeAtPosition(resolved, parameter);
184
+ const literal =
185
+ fixed === UNANSWERED ? undefined : literalTypeNames(fixed, (type) => partsOf(type));
186
+ if (literal !== undefined) {
187
+ accesses.push({ file, from, at: call, object, names: literal });
188
+ }
189
+ }
190
+ }
191
+
192
+ function partsOf(type: Type): readonly Type[] {
193
+ const parts = queries.constituents(type);
194
+ return parts === UNANSWERED ? [type] : parts;
195
+ }
196
+
197
+ const objectNodes = accesses.flatMap((one) => ("kind" in one.object ? [one.object] : []));
198
+ const typeOf = new Map<Node, Type>();
199
+ queries.typesAt(objectNodes).forEach((answer, index) => {
200
+ const node = objectNodes[index];
201
+ if (answer !== UNANSWERED && answer !== undefined && node !== undefined) {
202
+ typeOf.set(node, answer);
203
+ }
204
+ });
205
+
206
+ const found: Reference[] = [];
207
+ for (const access of accesses) {
208
+ const type = "kind" in access.object ? typeOf.get(access.object) : access.object;
209
+ if (type === undefined) {
210
+ continue;
211
+ }
212
+ const position = renderPosition(access.file, root, access.at.getStart());
213
+ for (const part of partsOf(type)) {
214
+ const properties = queries.propertiesOf(part);
215
+ if (properties === UNANSWERED) {
216
+ continue;
217
+ }
218
+ for (const property of properties) {
219
+ if (!access.names.includes(property.escapedName)) {
220
+ continue;
221
+ }
222
+ for (const to of chains.declarationsOf(property)) {
223
+ found.push({
224
+ from: access.from,
225
+ to,
226
+ position,
227
+ use: "read",
228
+ resolution: "batch",
229
+ test: testFiles.has(position.path),
230
+ });
231
+ }
232
+ }
233
+ }
234
+ }
235
+ return found;
236
+ }
237
+
238
+ /**
239
+ * Records an indexed-access type whose index names a type parameter of an enclosing
240
+ * function that a parameter of that function is annotated with whole, so a call's
241
+ * argument for that parameter fixes the index.
242
+ */
243
+ function parameterIndex(
244
+ node: Node & { readonly objectType: TypeNode; readonly indexType: TypeNode },
245
+ enclosing: readonly Generic[],
246
+ file: SourceFile,
247
+ generics: Map<string, ByParameter[]>,
248
+ names: Set<string>,
249
+ ): void {
250
+ const index = unwrappedType(node.indexType);
251
+ if (!isTypeReferenceNode(index) || !isIdentifier(index.typeName)) {
252
+ return;
253
+ }
254
+ const spelled = index.typeName.text;
255
+ for (const generic of [...enclosing].reverse()) {
256
+ const declares = (generic.typeParameters ?? []).some(
257
+ (parameter) => isIdentifier(parameter.name) && parameter.name.text === spelled,
258
+ );
259
+ if (!declares) {
260
+ continue;
261
+ }
262
+ const at = generic.parameters.findIndex((parameter) => {
263
+ const type = parameter.type === undefined ? undefined : unwrappedType(parameter.type);
264
+ return (
265
+ type !== undefined &&
266
+ isTypeReferenceNode(type) &&
267
+ isIdentifier(type.typeName) &&
268
+ type.typeName.text === spelled
269
+ );
270
+ });
271
+ if (at >= 0) {
272
+ const key = nodeKey(file, generic);
273
+ generics.set(key, [...(generics.get(key) ?? []), { object: node.objectType, parameter: at }]);
274
+ const name = calledName(generic);
275
+ if (name !== undefined) {
276
+ names.add(name);
277
+ }
278
+ }
279
+ return;
280
+ }
281
+ }
282
+
283
+ /** The name a call writes to call one generic function: its own, or its variable's. */
284
+ function calledName(generic: Generic): string | undefined {
285
+ const own = (generic as { readonly name?: Node }).name;
286
+ if (own !== undefined && isIdentifier(own)) {
287
+ return own.text;
288
+ }
289
+ const parent = generic.parent as { readonly kind: SyntaxKind; readonly name?: Node };
290
+ return parent.kind === SyntaxKind.VariableDeclaration &&
291
+ parent.name !== undefined &&
292
+ isIdentifier(parent.name)
293
+ ? parent.name.text
294
+ : undefined;
295
+ }
@@ -1,19 +1,14 @@
1
1
  /**
2
- * The interface-satisfaction class: a class member is held back when a value of the
3
- * class reaches a position the checker types as an interface requiring a member of
4
- * that name, so a caller reaches the member through the interface while no name in
5
- * the program resolves to it.
6
- *
7
- * A value reaches such a position as an `implements` entry, the initializer of a
8
- * declaration whose type is written, an assignment, an argument, a returned value, an
9
- * array element, an object property, or the operand of `as` or `satisfies`, read
10
- * through parentheses, conditionals and the operators whose result is an operand.
11
- * `this` in an instance member is a value of its class. Types are asked in batches;
12
- * the contextual lookup has none, so it is asked only of a value whose type is a class
13
- * of the target, and each distinct pair, interface and class is asked once.
2
+ * The interface-satisfaction class: a member is held back when a value of its class
3
+ * reaches a position the checker types as an interface requiring that member, through an
4
+ * `implements` entry, a typed initializer, an assignment, an argument, a return, an array
5
+ * element, an object property or an `as` or `satisfies` operand. `this` is a value of its
6
+ * class, and a position typed by a type parameter reaches its constraint. The contextual
7
+ * lookup has no batch form, so it is asked only of a value typed as a target class.
14
8
  */
15
9
 
16
10
  import {
11
+ isCallExpression,
17
12
  isClassDeclaration,
18
13
  isClassExpression,
19
14
  isClassStaticBlockDeclaration,
@@ -22,6 +17,7 @@ import {
22
17
  isGetAccessorDeclaration,
23
18
  isHeritageClause,
24
19
  isMethodDeclaration,
20
+ isNewExpression,
25
21
  isObjectLiteralExpression,
26
22
  isSetAccessorDeclaration,
27
23
  ModifierFlags,
@@ -32,8 +28,11 @@ import {
32
28
  } from "@typescript/native/unstable/ast";
33
29
  import { SymbolFlags, type Type } from "@typescript/native/unstable/sync";
34
30
  import { aliasChains } from "./alias-chain.ts";
31
+ import { emptyImplementation } from "./implementations.ts";
35
32
  import type { DetectorInput, Evidence } from "./exempt.ts";
36
- import { nodeKey } from "./inventory.ts";
33
+ import type { Reference } from "./references.ts";
34
+ import type { Exemption } from "./sweep.ts";
35
+ import { nodeKey, type Inventory } from "./inventory.ts";
37
36
  import { renderPosition } from "./position.ts";
38
37
  import { must } from "./query.ts";
39
38
  import { handedOn, valuesOf } from "./value-flow.ts";
@@ -60,6 +59,12 @@ interface Clause {
60
59
  readonly entry: Node;
61
60
  }
62
61
 
62
+ /** A class member answering an interface member: its declarations, and whether it is empty. */
63
+ interface Answering {
64
+ readonly ids: readonly string[];
65
+ readonly empty: boolean;
66
+ }
67
+
63
68
  /** One class type reaching one interface type, and the places it does. */
64
69
  interface Pair {
65
70
  readonly owner: ClassAt;
@@ -223,7 +228,11 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
223
228
  const interfaceNames = new Map<number, string | undefined>();
224
229
  /** The interface types one position's type is or holds, each with the interface's name. */
225
230
  const interfacesOf = (type: Type): readonly { type: Type; name: string }[] =>
226
- partsOf(type).flatMap((part) => {
231
+ partsOf(type).flatMap((part): readonly { type: Type; name: string }[] => {
232
+ if (part.isTypeParameter()) {
233
+ const constraint = must(queries.constraintOf(part));
234
+ return constraint === undefined ? [] : interfacesOf(constraint);
235
+ }
227
236
  if (!interfaceNames.has(part.id)) {
228
237
  const symbol = part.isObjectType() ? must(queries.symbolOfType(part)) : undefined;
229
238
  interfaceNames.set(
@@ -237,6 +246,22 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
237
246
  return name === undefined ? [] : [{ type: part, name }];
238
247
  });
239
248
 
249
+ /**
250
+ * The type an argument's parameter declares in the generic signature a call
251
+ * instantiates, which is a type parameter where the argument is a type argument's value:
252
+ * the contextual type of such an argument is the instantiation, not the parameter.
253
+ */
254
+ const genericPosition = (value: Expression): Type | undefined => {
255
+ const call = value.parent;
256
+ if (!isCallExpression(call) && !isNewExpression(call)) {
257
+ return undefined;
258
+ }
259
+ const at = (call.arguments ?? []).findIndex((argument) => argument === value);
260
+ const resolved = at < 0 ? undefined : must(queries.resolvedSignature(call));
261
+ const generic = resolved === undefined ? undefined : must(queries.genericOf(resolved));
262
+ return generic === undefined ? undefined : must(queries.typeAtPosition(generic, at));
263
+ };
264
+
240
265
  const pairs = new Map<string, Pair>();
241
266
  const reach = (owner: ClassAt, source: Type, target: Type, name: string, site: Node): void => {
242
267
  const key = `${String(source.id)}:${String(target.id)}`;
@@ -274,19 +299,18 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
274
299
  const positions = queries.contextualTypes(carrying.map((one) => one.flow.value));
275
300
  carrying.forEach(({ flow, sources }, index) => {
276
301
  const position = must(positions[index]);
277
- if (position === undefined) {
278
- return;
279
- }
280
- for (const reached of interfacesOf(position)) {
281
- for (const source of sources) {
282
- reach(source.owner, source.type, reached.type, reached.name, flow.value);
302
+ for (const type of [position, genericPosition(flow.value)]) {
303
+ for (const reached of type === undefined ? [] : interfacesOf(type)) {
304
+ for (const source of sources) {
305
+ reach(source.owner, source.type, reached.type, reached.name, flow.value);
306
+ }
283
307
  }
284
308
  }
285
309
  });
286
310
 
287
311
  const chains = aliasChains(project, held);
288
- const required = new Map<number, ReadonlySet<string>>();
289
- const members = new Map<string, ReadonlyMap<string, readonly string[]>>();
312
+ const required = new Map<number, ReadonlyMap<string, readonly string[]>>();
313
+ const members = new Map<string, ReadonlyMap<string, Answering>>();
290
314
  const found: Evidence[] = [];
291
315
  const asking = [...pairs.values()];
292
316
  const assignable = queries.assignableEach(asking);
@@ -296,8 +320,11 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
296
320
  }
297
321
  let names = required.get(pair.target.id);
298
322
  if (names === undefined) {
299
- names = new Set(
300
- must(queries.propertiesOf(pair.target)).map((property) => property.escapedName),
323
+ names = new Map(
324
+ must(queries.propertiesOf(pair.target)).map((property) => [
325
+ property.escapedName,
326
+ chains.declarationsOf(property),
327
+ ]),
301
328
  );
302
329
  required.set(pair.target.id, names);
303
330
  }
@@ -307,19 +334,24 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
307
334
  answering = new Map(
308
335
  must(queries.propertiesOf(instance)).map((property) => [
309
336
  property.escapedName,
310
- chains.declarationsOf(property),
337
+ {
338
+ ids: chains.declarationsOf(property),
339
+ empty: emptyImplementation(project, property),
340
+ },
311
341
  ]),
312
342
  );
313
343
  members.set(pair.owner.id, answering);
314
344
  }
315
345
  const detail = `satisfies ${pair.name}`;
316
- for (const name of names) {
317
- for (const id of answering.get(name) ?? []) {
346
+ for (const [name, methods] of names) {
347
+ const member = answering.get(name);
348
+ for (const id of member?.ids ?? []) {
318
349
  for (const site of pair.sites) {
319
350
  found.push({
320
351
  id,
321
352
  detail,
322
353
  site: renderPosition(site.getSourceFile(), targetRoot, site.getStart()),
354
+ implementing: { methods, empty: member?.empty ?? false },
323
355
  });
324
356
  }
325
357
  }
@@ -327,3 +359,62 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
327
359
  }
328
360
  return found;
329
361
  }
362
+
363
+ /** One project's interface-satisfaction records settled against the run's calls. */
364
+ interface Settled {
365
+ readonly evidence: readonly Exemption[];
366
+ /** A reference from each uncalled interface method to each implementation it alone keeps. */
367
+ readonly references: readonly Reference[];
368
+ }
369
+
370
+ /**
371
+ * Each project's records with every record dropped whose member implements only interface
372
+ * methods nothing calls, unless every implementation of such a method is empty, and a
373
+ * reference from each such method to the member, which then falls with the method.
374
+ * `references` is every reference of the run, a consumer's included.
375
+ */
376
+ export function settledSatisfaction(
377
+ projects: readonly { readonly evidence: readonly Exemption[]; readonly held: Inventory }[],
378
+ references: readonly Reference[],
379
+ ): readonly Settled[] {
380
+ const methods = new Set(
381
+ projects.flatMap((one) =>
382
+ one.held.symbols.filter((symbol) => symbol.kind === "interface-method").map((one) => one.id),
383
+ ),
384
+ );
385
+ const called = new Set(references.map((reference) => reference.to));
386
+ const bodies = new Map<string, boolean>();
387
+ for (const record of projects.flatMap((one) => one.evidence)) {
388
+ for (const method of record.implementing?.methods ?? []) {
389
+ bodies.set(method, (bodies.get(method) ?? true) && record.implementing?.empty === true);
390
+ }
391
+ }
392
+ const uncalled = (method: string): boolean =>
393
+ methods.has(method) && !called.has(method) && bodies.get(method) !== true;
394
+ return projects.map(({ evidence }) => {
395
+ const kept: Exemption[] = [];
396
+ const added: Reference[] = [];
397
+ for (const record of evidence) {
398
+ const implemented = record.implementing?.methods ?? [];
399
+ if (
400
+ record.class !== "interface-satisfaction" ||
401
+ implemented.length === 0 ||
402
+ !implemented.every(uncalled)
403
+ ) {
404
+ kept.push(record);
405
+ continue;
406
+ }
407
+ for (const method of implemented) {
408
+ added.push({
409
+ from: method,
410
+ to: record.id,
411
+ position: record.site,
412
+ use: "read",
413
+ resolution: "override",
414
+ test: false,
415
+ });
416
+ }
417
+ }
418
+ return { evidence: kept, references: added };
419
+ });
420
+ }