@cplieger/deadset-ts 5.2.3 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/analysis.js +28 -5
- package/dist/src/augmentations.js +50 -0
- package/dist/src/components.js +15 -15
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +17 -2
- package/dist/src/conventions.js +82 -1
- package/dist/src/dependencies.js +42 -26
- package/dist/src/entry-points.js +28 -14
- package/dist/src/exempt.js +1 -0
- package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
- package/dist/src/findings/emitters.js +47 -12
- package/dist/src/findings/interfaces.js +4 -3
- package/dist/src/findings/intra-function.js +15 -2
- package/dist/src/findings/unused-declarations.js +13 -1
- package/dist/src/findings-pass.js +1 -0
- package/dist/src/graph.js +6 -6
- package/dist/src/implementations.js +1 -1
- package/dist/src/indexed-access.js +215 -0
- package/dist/src/interface-satisfaction.js +85 -23
- package/dist/src/intra-function-parts.js +139 -30
- package/dist/src/inventory.js +6 -1
- package/dist/src/kinds.js +13 -12
- package/dist/src/matrix.js +34 -9
- package/dist/src/module-declarations.js +180 -0
- package/dist/src/outside-tests.js +17 -15
- package/dist/src/overrides.js +70 -9
- package/dist/src/query.js +7 -0
- package/dist/src/ref.js +1 -0
- package/dist/src/references.js +6 -0
- package/dist/src/report.js +6 -1
- package/dist/src/reporters/sarif.js +3 -1
- package/dist/src/reporters/text.js +23 -3
- package/dist/src/roots.js +50 -10
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +82 -0
- package/jsr.json +1 -1
- package/package.json +7 -1
- package/src/analysis.ts +40 -4
- package/src/augmentations.ts +72 -0
- package/src/components.ts +15 -13
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +25 -3
- package/src/conventions.ts +106 -2
- package/src/dependencies.ts +51 -31
- package/src/entry-points.ts +32 -19
- package/src/exempt.ts +4 -1
- package/src/findings/dependencies-and-module-machinery.ts +2 -2
- package/src/findings/emitters.ts +71 -12
- package/src/findings/interfaces.ts +7 -6
- package/src/findings/intra-function.ts +20 -2
- package/src/findings/unused-declarations.ts +16 -1
- package/src/findings-pass.ts +4 -1
- package/src/graph.ts +6 -6
- package/src/implementations.ts +1 -1
- package/src/indexed-access.ts +295 -0
- package/src/interface-satisfaction.ts +118 -27
- package/src/intra-function-parts.ts +196 -35
- package/src/inventory.ts +8 -0
- package/src/kinds.ts +14 -12
- package/src/matrix.ts +51 -8
- package/src/module-declarations.ts +241 -0
- package/src/outside-tests.ts +18 -16
- package/src/overrides.ts +80 -14
- package/src/query.ts +28 -0
- package/src/ref.ts +1 -0
- package/src/references.ts +6 -0
- package/src/report.ts +11 -1
- package/src/reporters/sarif.ts +4 -1
- package/src/reporters/text.ts +25 -3
- package/src/roots.ts +56 -7
- package/src/sweep.ts +12 -0
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +104 -0
|
@@ -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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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,
|
|
289
|
-
const members = new Map<string, ReadonlyMap<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
|
|
300
|
-
must(queries.propertiesOf(pair.target)).map((property) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|