@cplieger/deadset-ts 8.0.0-dev.20 → 8.0.0-dev.21
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.
- package/dist/src/alias-chain.js +33 -2
- package/dist/src/analysis.js +18 -77
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contract/contract.generated.js +4 -3
- package/dist/src/dependencies.js +1 -1
- package/dist/src/exemptions/argument-places.js +131 -0
- package/dist/src/{calls.js → exemptions/calls.js} +126 -179
- package/dist/src/{exempt-classes.js → exemptions/classes.js} +1 -1
- package/dist/src/{decorator.js → exemptions/decorator.js} +19 -30
- package/dist/src/exemptions/detectors.js +48 -0
- package/dist/src/{enum-group.js → exemptions/enum-group.js} +7 -7
- package/dist/src/{erased-stores.js → exemptions/erased-stores.js} +4 -6
- package/dist/src/{framework-lifecycle.js → exemptions/framework-lifecycle.js} +31 -36
- package/dist/src/{generated-file.js → exemptions/generated-file.js} +4 -4
- package/dist/src/{injection-container.js → exemptions/injection-container.js} +19 -25
- package/dist/src/exemptions/installed-globals.js +173 -0
- package/dist/src/{interface-satisfaction.js → exemptions/interface-satisfaction.js} +52 -121
- package/dist/src/exemptions/lifecycle.js +150 -0
- package/dist/src/{reflective-lookup.js → exemptions/reflective-lookup.js} +3 -3
- package/dist/src/{serialization-contract.js → exemptions/serialization-contract.js} +415 -329
- package/dist/src/exemptions/sites.js +182 -0
- package/dist/src/{template-field.js → exemptions/template-field.js} +7 -7
- package/dist/src/{value-reach.js → exemptions/value-reach.js} +14 -7
- package/dist/src/index/sites.js +1 -1
- package/dist/src/index/values.js +124 -0
- package/dist/src/liveness/sweep.js +2 -2
- package/dist/src/query.js +48 -0
- package/dist/src/roots/configured-declarations.js +1 -1
- package/dist/src/verbs/print-retained.js +23 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/alias-chain.ts +38 -2
- package/src/analysis.ts +19 -100
- package/src/bindings.ts +1 -1
- package/src/conformance-record.ts +2 -2
- package/src/contract/contract.generated.ts +4 -3
- package/src/dependencies.ts +1 -1
- package/src/exemptions/argument-places.ts +198 -0
- package/src/{calls.ts → exemptions/calls.ts} +190 -224
- package/src/{exempt-classes.ts → exemptions/classes.ts} +1 -1
- package/src/exemptions/decorator.ts +56 -0
- package/src/exemptions/detectors.ts +75 -0
- package/src/{enum-group.ts → exemptions/enum-group.ts} +9 -9
- package/src/{erased-stores.ts → exemptions/erased-stores.ts} +5 -8
- package/src/{framework-lifecycle.ts → exemptions/framework-lifecycle.ts} +39 -48
- package/src/{generated-file.ts → exemptions/generated-file.ts} +6 -6
- package/src/{injection-container.ts → exemptions/injection-container.ts} +28 -33
- package/src/exemptions/installed-globals.ts +255 -0
- package/src/{interface-satisfaction.ts → exemptions/interface-satisfaction.ts} +69 -160
- package/src/exemptions/lifecycle.ts +286 -0
- package/src/{reflective-lookup.ts → exemptions/reflective-lookup.ts} +5 -5
- package/src/{serialization-contract.ts → exemptions/serialization-contract.ts} +563 -429
- package/src/exemptions/sites.ts +293 -0
- package/src/{template-field.ts → exemptions/template-field.ts} +11 -11
- package/src/{value-reach.ts → exemptions/value-reach.ts} +16 -8
- package/src/facts.ts +1 -1
- package/src/index/sites.ts +1 -1
- package/src/index/values.ts +170 -0
- package/src/liveness/sweep.ts +2 -2
- package/src/query.ts +109 -10
- package/src/roots/configured-declarations.ts +1 -2
- package/src/stores.ts +1 -1
- package/src/verbs/print-retained.ts +29 -1
- package/dist/src/exempt.js +0 -164
- package/dist/src/value-flow.js +0 -74
- package/src/decorator.ts +0 -78
- package/src/exempt.ts +0 -295
- 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.
|
|
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 "
|
|
32
|
-
import { emptyImplementation } from "
|
|
33
|
-
import
|
|
34
|
-
import type {
|
|
35
|
-
import
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
112
|
-
const classes
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
171
|
-
const { classes, clauses, flows } =
|
|
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):
|
|
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
|
-
|
|
236
|
+
let pair = pairs.get(key);
|
|
289
237
|
if (pair === undefined) {
|
|
290
|
-
|
|
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
|
|
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 =
|
|
337
|
-
|
|
338
|
-
|
|
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 =
|
|
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
|
-
}
|