@cplieger/deadset-ts 8.0.0-dev.20 → 8.0.0-dev.22

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 (72) hide show
  1. package/dist/src/alias-chain.js +33 -2
  2. package/dist/src/analysis.js +18 -77
  3. package/dist/src/conformance-record.js +2 -2
  4. package/dist/src/contract/contract.generated.js +5 -4
  5. package/dist/src/dependencies.js +1 -1
  6. package/dist/src/exemptions/argument-places.js +131 -0
  7. package/dist/src/{calls.js → exemptions/calls.js} +126 -179
  8. package/dist/src/{exempt-classes.js → exemptions/classes.js} +1 -1
  9. package/dist/src/{decorator.js → exemptions/decorator.js} +19 -30
  10. package/dist/src/exemptions/detectors.js +48 -0
  11. package/dist/src/{enum-group.js → exemptions/enum-group.js} +7 -7
  12. package/dist/src/{erased-stores.js → exemptions/erased-stores.js} +4 -6
  13. package/dist/src/{framework-lifecycle.js → exemptions/framework-lifecycle.js} +31 -36
  14. package/dist/src/{generated-file.js → exemptions/generated-file.js} +4 -4
  15. package/dist/src/{injection-container.js → exemptions/injection-container.js} +19 -25
  16. package/dist/src/exemptions/installed-globals.js +173 -0
  17. package/dist/src/{interface-satisfaction.js → exemptions/interface-satisfaction.js} +52 -121
  18. package/dist/src/exemptions/lifecycle.js +150 -0
  19. package/dist/src/{reflective-lookup.js → exemptions/reflective-lookup.js} +3 -3
  20. package/dist/src/{serialization-contract.js → exemptions/serialization-contract.js} +415 -329
  21. package/dist/src/exemptions/sites.js +182 -0
  22. package/dist/src/{template-field.js → exemptions/template-field.js} +7 -7
  23. package/dist/src/{value-reach.js → exemptions/value-reach.js} +14 -7
  24. package/dist/src/findings/intra-function.js +50 -37
  25. package/dist/src/index/sites.js +1 -1
  26. package/dist/src/index/values.js +124 -0
  27. package/dist/src/intra-function-parts.js +1149 -222
  28. package/dist/src/liveness/sweep.js +2 -2
  29. package/dist/src/query.js +48 -0
  30. package/dist/src/roots/configured-declarations.js +1 -1
  31. package/dist/src/verbs/print-retained.js +23 -1
  32. package/jsr.json +1 -1
  33. package/package.json +2 -2
  34. package/src/alias-chain.ts +38 -2
  35. package/src/analysis.ts +19 -100
  36. package/src/bindings.ts +1 -1
  37. package/src/conformance-record.ts +2 -2
  38. package/src/contract/contract.generated.ts +5 -4
  39. package/src/dependencies.ts +1 -1
  40. package/src/exemptions/argument-places.ts +198 -0
  41. package/src/{calls.ts → exemptions/calls.ts} +190 -224
  42. package/src/{exempt-classes.ts → exemptions/classes.ts} +1 -1
  43. package/src/exemptions/decorator.ts +56 -0
  44. package/src/exemptions/detectors.ts +75 -0
  45. package/src/{enum-group.ts → exemptions/enum-group.ts} +9 -9
  46. package/src/{erased-stores.ts → exemptions/erased-stores.ts} +5 -8
  47. package/src/{framework-lifecycle.ts → exemptions/framework-lifecycle.ts} +39 -48
  48. package/src/{generated-file.ts → exemptions/generated-file.ts} +6 -6
  49. package/src/{injection-container.ts → exemptions/injection-container.ts} +28 -33
  50. package/src/exemptions/installed-globals.ts +255 -0
  51. package/src/{interface-satisfaction.ts → exemptions/interface-satisfaction.ts} +69 -160
  52. package/src/exemptions/lifecycle.ts +286 -0
  53. package/src/{reflective-lookup.ts → exemptions/reflective-lookup.ts} +5 -5
  54. package/src/{serialization-contract.ts → exemptions/serialization-contract.ts} +563 -429
  55. package/src/exemptions/sites.ts +293 -0
  56. package/src/{template-field.ts → exemptions/template-field.ts} +11 -11
  57. package/src/{value-reach.ts → exemptions/value-reach.ts} +16 -8
  58. package/src/facts.ts +1 -1
  59. package/src/findings/intra-function.ts +59 -37
  60. package/src/index/sites.ts +1 -1
  61. package/src/index/values.ts +170 -0
  62. package/src/intra-function-parts.ts +1435 -247
  63. package/src/liveness/sweep.ts +2 -2
  64. package/src/query.ts +109 -10
  65. package/src/roots/configured-declarations.ts +1 -2
  66. package/src/stores.ts +1 -1
  67. package/src/verbs/print-retained.ts +29 -1
  68. package/dist/src/exempt.js +0 -164
  69. package/dist/src/value-flow.js +0 -74
  70. package/src/decorator.ts +0 -78
  71. package/src/exempt.ts +0 -295
  72. package/src/value-flow.ts +0 -104
@@ -0,0 +1,255 @@
1
+ /**
2
+ * The classes a program installs as globals: a class value written into a property of
3
+ * the global object, which code outside the analysis that names the global constructs.
4
+ * The checker finds each write: the receiver's type is the global object's, the callee is
5
+ * the standard library's `Object.defineProperty` or a test runner's stub, and the
6
+ * property is the one of the key's name that `typeof globalThis` declares.
7
+ */
8
+
9
+ import {
10
+ isIdentifier,
11
+ isNoSubstitutionTemplateLiteral,
12
+ isObjectLiteralExpression,
13
+ isPropertyAssignment,
14
+ isShorthandPropertyAssignment,
15
+ isStringLiteral,
16
+ type Expression,
17
+ type Node,
18
+ } from "@typescript/native/ast";
19
+ import {
20
+ SignatureKind,
21
+ SymbolFlags,
22
+ type Symbol as TSSymbol,
23
+ type Type,
24
+ } from "@typescript/native/sync";
25
+ import type { AliasChains } from "../alias-chain.ts";
26
+ import { typesAt } from "../class-members.ts";
27
+ import { packageOfPath } from "../dependencies.ts";
28
+ import type { Inventory } from "../inventory.ts";
29
+ import { must } from "../query.ts";
30
+ import type { ProjectView } from "../session.ts";
31
+ import { parenthesesRemoved, unwrapped } from "../unwrap.ts";
32
+ import { spansOf } from "./argument-places.ts";
33
+ import { classesCarried } from "./calls.ts";
34
+ import type { ProjectSites } from "./sites.ts";
35
+
36
+ /** A call that writes a value into a property of an object, by the places of its arguments. */
37
+ interface Writer {
38
+ /** The object's place, none where the object is the global one. */
39
+ readonly object: number | undefined;
40
+ readonly key: number;
41
+ readonly value: number;
42
+ /** Whether the value is the `value` member of a property descriptor. */
43
+ readonly described: boolean;
44
+ }
45
+
46
+ /**
47
+ * The test runners' stubs of a global, by method name and the packages that declare it:
48
+ * `vi.stubGlobal` of vitest, and `jest.replaceProperty`, which `@jest/environment` types
49
+ * for `@jest/globals` and `@types/jest` declares for the `jest` global.
50
+ */
51
+ const STUBS: ReadonlyMap<string, Writer & { readonly packages: ReadonlySet<string> }> = new Map([
52
+ [
53
+ "stubGlobal",
54
+ { packages: new Set(["vitest"]), object: undefined, key: 0, value: 1, described: false },
55
+ ],
56
+ [
57
+ "replaceProperty",
58
+ {
59
+ packages: new Set(["@jest/environment", "@jest/globals", "@types/jest"]),
60
+ object: 0,
61
+ key: 1,
62
+ value: 2,
63
+ described: false,
64
+ },
65
+ ],
66
+ ]);
67
+
68
+ /** `Object.defineProperty(object, key, descriptor)`. */
69
+ const DEFINE: Writer = { object: 0, key: 1, value: 2, described: true };
70
+
71
+ /** One write of a value into a property a written key names. */
72
+ interface Write {
73
+ readonly site: Node;
74
+ /** The object written to, none where it is the global one. */
75
+ readonly receiver: Expression | undefined;
76
+ readonly key: string;
77
+ readonly value: Expression;
78
+ }
79
+
80
+ /** One class a write installs as a global, and the instance types the global constructs. */
81
+ interface Installed {
82
+ readonly id: string;
83
+ readonly site: Node;
84
+ readonly global: string;
85
+ readonly instances: readonly Type[];
86
+ }
87
+
88
+ /** The text of a key written as a string literal. */
89
+ function keyText(node: Expression | undefined): string | undefined {
90
+ const key = node === undefined ? undefined : parenthesesRemoved(node);
91
+ return key !== undefined && (isStringLiteral(key) || isNoSubstitutionTemplateLiteral(key))
92
+ ? key.text
93
+ : undefined;
94
+ }
95
+
96
+ /** Whether a property's name is `value`. */
97
+ function named(name: Node): boolean {
98
+ return (isIdentifier(name) || isStringLiteral(name)) && name.text === "value";
99
+ }
100
+
101
+ /** The value a property descriptor written as an object literal gives. */
102
+ function describedValue(node: Expression | undefined): Expression | undefined {
103
+ const descriptor = node === undefined ? undefined : parenthesesRemoved(node);
104
+ if (descriptor === undefined || !isObjectLiteralExpression(descriptor)) {
105
+ return undefined;
106
+ }
107
+ for (const property of descriptor.properties) {
108
+ if (isPropertyAssignment(property) && named(property.name)) {
109
+ return property.initializer;
110
+ }
111
+ if (
112
+ isShorthandPropertyAssignment(property) &&
113
+ isIdentifier(property.name) &&
114
+ named(property.name)
115
+ ) {
116
+ return property.name;
117
+ }
118
+ }
119
+ return undefined;
120
+ }
121
+
122
+ /** How a call's callee writes a property: the standard library's `Object.defineProperty` or a runner's stub. */
123
+ function writerOf<Brand>(project: ProjectView<Brand>, method: TSSymbol): Writer | undefined {
124
+ if (method.name === "defineProperty") {
125
+ const standard = method.declarations.some(
126
+ (handle) => project.program.getSourceFileMetadata(handle.path)?.isDefaultLibrary === true,
127
+ );
128
+ return standard && must(project.queries.parentOf(method))?.name === "ObjectConstructor"
129
+ ? DEFINE
130
+ : undefined;
131
+ }
132
+ const stub = STUBS.get(method.name);
133
+ return stub !== undefined &&
134
+ method.declarations.some((handle) => stub.packages.has(packageOfPath(handle.path) ?? ""))
135
+ ? stub
136
+ : undefined;
137
+ }
138
+
139
+ /** Each write of one project's own files that may install a value as a global, read from syntax and the callee. */
140
+ function writesOf<Brand>(project: ProjectView<Brand>, sites: ProjectSites): readonly Write[] {
141
+ const found: Write[] = sites.stores.map((store) => ({
142
+ site: store.store,
143
+ receiver: store.receiver,
144
+ key: store.key,
145
+ value: store.value,
146
+ }));
147
+ for (const { call } of sites.calls) {
148
+ const method = sites.invocations.callee(call);
149
+ const writer = method === undefined ? undefined : writerOf(project, method);
150
+ if (writer === undefined) {
151
+ continue;
152
+ }
153
+ const invoked = sites.invocations.of(call);
154
+ // With no types asked, a spread has no known length, so only a place no spread
155
+ // comes at or before is read.
156
+ const spans = spansOf(project, invoked.arguments, invoked.listed, []);
157
+ const at = (place: number): Expression | undefined => {
158
+ const span = spans.find((one) => !one.open && one.first === place);
159
+ return span === undefined ? undefined : invoked.arguments[span.argument];
160
+ };
161
+ const key = keyText(at(writer.key));
162
+ const receiver = writer.object === undefined ? undefined : at(writer.object);
163
+ const value = writer.described ? describedValue(at(writer.value)) : at(writer.value);
164
+ if (
165
+ key !== undefined &&
166
+ value !== undefined &&
167
+ (writer.object === undefined || receiver !== undefined)
168
+ ) {
169
+ found.push({ site: call, receiver, key, value });
170
+ }
171
+ }
172
+ return found;
173
+ }
174
+
175
+ /**
176
+ * The classes one project installs as globals. A write installs one where its object is
177
+ * the global object, as the checker types it (`window` and `self` are intersections that
178
+ * hold `typeof globalThis`, and an object asserted to `any` is read inside the
179
+ * assertion), and where the global object declares a property of the key's name with a
180
+ * construct signature, whose instances code outside the analysis makes.
181
+ */
182
+ export function installedGlobals<Brand>(
183
+ project: ProjectView<Brand>,
184
+ held: Inventory,
185
+ chains: AliasChains,
186
+ sites: ProjectSites,
187
+ ): readonly Installed[] {
188
+ const { queries } = project;
189
+ const writes = writesOf(project, sites);
190
+ if (writes.length === 0) {
191
+ return [];
192
+ }
193
+ const global = must(queries.resolveName("globalThis", SymbolFlags.Value, undefined, false));
194
+ const globalType = global === undefined ? undefined : must(queries.typeOfSymbol(global));
195
+ if (global === undefined || globalType === undefined) {
196
+ return [];
197
+ }
198
+ const properties = new Map(
199
+ must(queries.propertiesOf(globalType)).map((property) => [property.name, property]),
200
+ );
201
+ const keyed = writes.filter((write) => properties.has(write.key));
202
+ const objects = keyed.flatMap((write) =>
203
+ write.receiver === undefined ? [] : [unwrapped(write.receiver)],
204
+ );
205
+ const objectTypes = new Map(
206
+ typesAt(project, objects).map((type, at) => [objects[at], type] as const),
207
+ );
208
+ const globals = new Map<number, boolean>();
209
+ const isGlobal = (type: Type | undefined): boolean => {
210
+ if (type === undefined) {
211
+ return false;
212
+ }
213
+ let known = globals.get(type.id);
214
+ if (known === undefined) {
215
+ const parts =
216
+ type.isUnionType() || type.isIntersectionType() ? must(queries.constituents(type)) : [type];
217
+ known = parts.some((part) => must(queries.symbolOfType(part))?.id === global.id);
218
+ globals.set(type.id, known);
219
+ }
220
+ return known;
221
+ };
222
+ const installs = keyed.filter(
223
+ (write) => write.receiver === undefined || isGlobal(objectTypes.get(unwrapped(write.receiver))),
224
+ );
225
+ if (installs.length === 0) {
226
+ return [];
227
+ }
228
+ const instances = new Map<string, readonly Type[]>();
229
+ const instancesOf = (key: string): readonly Type[] => {
230
+ let made = instances.get(key);
231
+ if (made === undefined) {
232
+ const property = properties.get(key);
233
+ const type = property === undefined ? undefined : must(queries.typeOfSymbol(property));
234
+ made = (
235
+ type === undefined ? [] : must(queries.signaturesOf(type, SignatureKind.Construct))
236
+ ).flatMap((signature) => must(queries.returnTypeOf(signature)) ?? []);
237
+ instances.set(key, made);
238
+ }
239
+ return made;
240
+ };
241
+ const carried = classesCarried(
242
+ project,
243
+ held,
244
+ chains,
245
+ sites,
246
+ installs.map((write) => [write.value]),
247
+ );
248
+ return installs.flatMap((write, at) => {
249
+ const uses = carried[at] ?? [];
250
+ const made = uses.length === 0 ? [] : instancesOf(write.key);
251
+ return made.length === 0
252
+ ? []
253
+ : uses.map((use) => ({ id: use.id, site: write.site, global: write.key, instances: made }));
254
+ });
255
+ }
@@ -3,40 +3,30 @@
3
3
  * reaches a position typed as an interface, or an object type a type alias names, that
4
4
  * requires the member: an `implements` entry, a typed initializer, an assignment, an
5
5
  * argument, a return, an array element, an object property or an `as` or `satisfies`
6
- * operand. `this` is a value of its class; a type parameter reaches its constraint. The
6
+ * operand. `this` is a value of its class; a type parameter reaches its constraint. A
7
+ * class installed as a global reaches the instance type the global constructs, whether
8
+ * or not it is assignable to it, because a stub answers only what its caller uses. The
7
9
  * contextual lookup has no batch form, so it is asked only of a value of a target class.
8
10
  */
9
11
 
10
12
  import {
11
13
  isCallExpression,
12
- isClassDeclaration,
13
- isClassExpression,
14
- isClassStaticBlockDeclaration,
15
- isFunctionDeclaration,
16
- isFunctionExpression,
17
- isGetAccessorDeclaration,
18
14
  isHeritageClause,
19
- isMethodDeclaration,
20
15
  isNewExpression,
21
- isObjectLiteralExpression,
22
- isSetAccessorDeclaration,
23
16
  isTypeLiteralNode,
24
- ModifierFlags,
25
17
  SyntaxKind,
18
+ type ClassLikeDeclaration,
26
19
  type Expression,
27
20
  type Node,
28
- type SourceFile,
29
21
  } from "@typescript/native/ast";
30
22
  import { SymbolFlags, type Type } from "@typescript/native/sync";
31
- import { aliasChains } from "./alias-chain.ts";
32
- import { emptyImplementation } from "./findings/implementations.ts";
33
- import type { DetectorInput, Evidence } from "./exempt.ts";
34
- import type { Reference } from "./references.ts";
35
- import type { Exemption } from "./exempt.ts";
36
- import { nodeKey, type Inventory } from "./inventory.ts";
37
- import { renderPosition } from "./position.ts";
38
- import { must } from "./query.ts";
39
- import { handedOn, valuesOf } from "./value-flow.ts";
23
+ import { aliasChains } from "../alias-chain.ts";
24
+ import { emptyImplementation } from "../findings/implementations.ts";
25
+ import { installedGlobals } from "./installed-globals.ts";
26
+ import type { DetectorInput, Evidence } from "./lifecycle.ts";
27
+ import { renderPosition } from "../position.ts";
28
+ import { must } from "../query.ts";
29
+ import type { ProjectSites } from "./sites.ts";
40
30
 
41
31
  /** One class the inventory holds, by its declaration. */
42
32
  interface ClassAt {
@@ -45,6 +35,7 @@ interface ClassAt {
45
35
  readonly name: Node;
46
36
  /** Whether the class declares type parameters, so its values carry instantiations of it. */
47
37
  readonly generic: boolean;
38
+ readonly node: ClassLikeDeclaration;
48
39
  }
49
40
 
50
41
  /** One value that flows into a position the checker types from its context. */
@@ -73,6 +64,8 @@ interface Pair {
73
64
  readonly target: Type;
74
65
  readonly name: string;
75
66
  readonly sites: Node[];
67
+ /** The global the class is installed as, which holds the members without assignability. */
68
+ installed?: string;
76
69
  }
77
70
 
78
71
  /** Whether a type alias names one type: the client's type object carries the alias it was written through. */
@@ -80,23 +73,7 @@ function aliased(type: Type): boolean {
80
73
  return (type as { readonly aliasSymbol?: unknown }).aliasSymbol !== undefined;
81
74
  }
82
75
 
83
- /** Whether one node declares a static member, whose `this` is the class itself. */
84
- function isStatic(node: Node): boolean {
85
- const flags = (node as { readonly modifierFlags?: number }).modifierFlags ?? 0;
86
- return (flags & ModifierFlags.Static) !== 0;
87
- }
88
-
89
- /** Whether one node is a method or an accessor of an object literal, whose `this` is the literal. */
90
- function isObjectLiteralMethod(node: Node): boolean {
91
- return (
92
- (isMethodDeclaration(node) ||
93
- isGetAccessorDeclaration(node) ||
94
- isSetAccessorDeclaration(node)) &&
95
- isObjectLiteralExpression(node.parent)
96
- );
97
- }
98
-
99
- /** What one project's walk found to resolve. */
76
+ /** What one project's sites hold that this class reads. */
100
77
  interface Walked {
101
78
  readonly classes: readonly ClassAt[];
102
79
  readonly clauses: readonly Clause[];
@@ -104,59 +81,30 @@ interface Walked {
104
81
  }
105
82
 
106
83
  /**
107
- * The classes, the `implements` entries and the flowing values of one project's own
108
- * files. `this` is the class an instance member belongs to, so a function that is not
109
- * an arrow, a static member and a static block each stop it.
84
+ * The named classes, the `implements` entries and the flowing values of one project's own
85
+ * files, a value `this` standing for the class it is an instance of.
110
86
  */
111
- function walk(files: readonly SourceFile[], declarations: ReadonlyMap<string, string>): Walked {
112
- const classes: ClassAt[] = [];
113
- const clauses: Clause[] = [];
114
- const flows: Flow[] = [];
115
- for (const file of files) {
116
- const visit = (node: Node, self: ClassAt | undefined): void => {
117
- let inner = self;
118
- if (isClassDeclaration(node) || isClassExpression(node)) {
119
- const id = declarations.get(nodeKey(file, node));
120
- const name = node.name;
121
- const owner =
122
- id === undefined || name === undefined
123
- ? undefined
124
- : { id, name, generic: node.typeParameters !== undefined };
125
- if (owner !== undefined) {
126
- classes.push(owner);
127
- for (const clause of node.heritageClauses ?? []) {
128
- if (isHeritageClause(clause) && clause.token === SyntaxKind.ImplementsKeyword) {
129
- clauses.push(...clause.types.map((entry) => ({ owner, entry })));
130
- }
131
- }
132
- }
133
- inner = owner;
134
- } else if (
135
- isFunctionDeclaration(node) ||
136
- isFunctionExpression(node) ||
137
- isClassStaticBlockDeclaration(node) ||
138
- isStatic(node) ||
139
- isObjectLiteralMethod(node)
140
- ) {
141
- inner = undefined;
142
- }
143
- for (const written of handedOn(node)) {
144
- for (const value of valuesOf(written)) {
145
- flows.push({
146
- value,
147
- self: value.kind === SyntaxKind.ThisKeyword ? inner : undefined,
148
- });
149
- }
150
- }
151
- node.forEachChild((child) => {
152
- visit(child, inner);
153
- });
154
- };
155
- file.forEachChild((child) => {
156
- visit(child, undefined);
157
- });
158
- }
159
- return { classes, clauses, flows };
87
+ function walked(sites: ProjectSites): Walked {
88
+ const classes = sites.classes.flatMap(({ id, node }) =>
89
+ node.name === undefined
90
+ ? []
91
+ : [{ id, name: node.name, generic: node.typeParameters !== undefined, node }],
92
+ );
93
+ const byId = new Map(classes.map((owner) => [owner.id, owner]));
94
+ return {
95
+ classes,
96
+ clauses: classes.flatMap((owner) =>
97
+ (owner.node.heritageClauses ?? []).flatMap((clause) =>
98
+ isHeritageClause(clause) && clause.token === SyntaxKind.ImplementsKeyword
99
+ ? clause.types.map((entry) => ({ owner, entry }))
100
+ : [],
101
+ ),
102
+ ),
103
+ flows: sites.flows.map((flow) => ({
104
+ value: flow.value,
105
+ self: flow.self === undefined ? undefined : byId.get(flow.self),
106
+ })),
107
+ };
160
108
  }
161
109
 
162
110
  /**
@@ -167,8 +115,8 @@ function walk(files: readonly SourceFile[], declarations: ReadonlyMap<string, st
167
115
  export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
168
116
  const { project, held, targetRoot } = input;
169
117
  const queries = project.queries;
170
- const files = project.ownSourceFiles();
171
- const { classes, clauses, flows } = walk(files, held.declarations);
118
+ const chains = aliasChains(project, held);
119
+ const { classes, clauses, flows } = walked(input.sites);
172
120
  if (classes.length === 0) {
173
121
  return [];
174
122
  }
@@ -283,14 +231,16 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
283
231
  };
284
232
 
285
233
  const pairs = new Map<string, Pair>();
286
- const reach = (owner: ClassAt, source: Type, target: Type, name: string, site: Node): void => {
234
+ const reach = (owner: ClassAt, source: Type, target: Type, name: string, site: Node): Pair => {
287
235
  const key = `${String(source.id)}:${String(target.id)}`;
288
- const pair = pairs.get(key);
236
+ let pair = pairs.get(key);
289
237
  if (pair === undefined) {
290
- pairs.set(key, { owner, source, target, name, sites: [site] });
238
+ pair = { owner, source, target, name, sites: [site] };
239
+ pairs.set(key, pair);
291
240
  } else {
292
241
  pair.sites.push(site);
293
242
  }
243
+ return pair;
294
244
  };
295
245
 
296
246
  for (const clause of clauses) {
@@ -328,14 +278,29 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
328
278
  }
329
279
  });
330
280
 
331
- const chains = aliasChains(project, held);
281
+ const byId = new Map(classes.map((owner) => [owner.id, owner]));
282
+ for (const one of installedGlobals(project, held, chains, input.sites)) {
283
+ const owner = byId.get(one.id);
284
+ const source = instanceOf.get(one.id);
285
+ if (owner === undefined || source === undefined) {
286
+ continue;
287
+ }
288
+ for (const instance of one.instances) {
289
+ reach(owner, source, instance, one.global, one.site).installed = one.global;
290
+ }
291
+ }
292
+
332
293
  const required = new Map<number, ReadonlyMap<string, readonly string[]>>();
333
294
  const members = new Map<string, ReadonlyMap<string, Answering>>();
334
295
  const found: Evidence[] = [];
335
- const asking = [...pairs.values()];
336
- const assignable = queries.assignableEach(asking);
337
- for (const [index, pair] of asking.entries()) {
338
- if (!must(assignable[index] ?? false)) {
296
+ const asking = [...pairs.values()].filter((pair) => pair.installed === undefined);
297
+ const assignable = new Set(
298
+ queries
299
+ .assignableEach(asking)
300
+ .flatMap((answer, index) => (must(answer) ? [asking[index]] : [])),
301
+ );
302
+ for (const pair of pairs.values()) {
303
+ if (pair.installed === undefined && !assignable.has(pair)) {
339
304
  continue;
340
305
  }
341
306
  let names = required.get(pair.target.id);
@@ -362,7 +327,10 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
362
327
  );
363
328
  members.set(pair.owner.id, answering);
364
329
  }
365
- const detail = `satisfies ${pair.name}`;
330
+ const detail =
331
+ pair.installed === undefined
332
+ ? `satisfies ${pair.name}`
333
+ : `installed as the global ${pair.installed}`;
366
334
  for (const [name, methods] of names) {
367
335
  const member = answering.get(name);
368
336
  for (const id of member?.ids ?? []) {
@@ -379,62 +347,3 @@ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): reado
379
347
  }
380
348
  return found;
381
349
  }
382
-
383
- /** One project's interface-satisfaction records settled against the run's calls. */
384
- interface Settled {
385
- readonly evidence: readonly Exemption[];
386
- /** A reference from each uncalled interface method to each implementation it alone keeps. */
387
- readonly references: readonly Reference[];
388
- }
389
-
390
- /**
391
- * Each project's records with every record dropped whose member implements only interface
392
- * methods nothing calls, unless every implementation of such a method is empty, and a
393
- * reference from each such method to the member, which then falls with the method.
394
- * `references` is every reference of the run, a consumer's included.
395
- */
396
- export function settledSatisfaction(
397
- projects: readonly { readonly evidence: readonly Exemption[]; readonly held: Inventory }[],
398
- references: readonly Reference[],
399
- ): readonly Settled[] {
400
- const methods = new Set(
401
- projects.flatMap((one) =>
402
- one.held.symbols.filter((symbol) => symbol.kind === "interface-method").map((one) => one.id),
403
- ),
404
- );
405
- const called = new Set(references.map((reference) => reference.to));
406
- const bodies = new Map<string, boolean>();
407
- for (const record of projects.flatMap((one) => one.evidence)) {
408
- for (const method of record.implementing?.methods ?? []) {
409
- bodies.set(method, (bodies.get(method) ?? true) && record.implementing?.empty === true);
410
- }
411
- }
412
- const uncalled = (method: string): boolean =>
413
- methods.has(method) && !called.has(method) && bodies.get(method) !== true;
414
- return projects.map(({ evidence }) => {
415
- const kept: Exemption[] = [];
416
- const added: Reference[] = [];
417
- for (const record of evidence) {
418
- const implemented = record.implementing?.methods ?? [];
419
- if (
420
- record.class !== "interface-satisfaction" ||
421
- implemented.length === 0 ||
422
- !implemented.every(uncalled)
423
- ) {
424
- kept.push(record);
425
- continue;
426
- }
427
- for (const method of implemented) {
428
- added.push({
429
- from: method,
430
- to: record.id,
431
- position: record.site,
432
- use: "read",
433
- resolution: "override",
434
- test: false,
435
- });
436
- }
437
- }
438
- return { evidence: kept, references: added };
439
- });
440
- }