@cplieger/deadset-ts 7.0.0-dev.1 → 7.0.0-dev.3
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 +95 -60
- package/dist/src/augmentations.js +14 -12
- package/dist/src/component-layer.js +1 -1
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +230 -30
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +104 -13
- package/dist/src/convention-rows.js +10 -0
- package/dist/src/conventions.js +35 -1
- package/dist/src/dependencies.js +90 -32
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/element-access.js +341 -0
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +78 -16
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -5
- package/dist/src/findings/self-check.js +27 -10
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -119
- package/dist/src/intra-function-parts.js +25 -11
- package/dist/src/inventory.js +28 -12
- package/dist/src/keyof-comparisons.js +170 -0
- package/dist/src/kinds.js +17 -7
- package/dist/src/manifest.js +6 -16
- package/dist/src/matrix.js +20 -2
- package/dist/src/query.js +3 -0
- package/dist/src/references.js +14 -3
- package/dist/src/report.js +4 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/roots.js +0 -1
- package/dist/src/run.js +0 -2
- package/dist/src/schema.js +1 -1
- package/dist/src/scope.js +2 -2
- package/dist/src/serialization-contract.js +43 -14
- package/dist/src/session.js +12 -8
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +14 -5
- package/dist/src/suppress.js +64 -35
- package/dist/src/unimplemented-callees.js +312 -0
- package/dist/src/value-reach.js +7 -7
- package/dist/src/verbs/analyze.js +11 -9
- package/dist/src/workspace-programs.js +0 -3
- package/dist/src/workspace-resolution.js +0 -2
- package/dist/src/workspace.js +0 -3
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +145 -82
- package/src/augmentations.ts +14 -11
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +104 -22
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +250 -31
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +135 -17
- package/src/convention-rows.ts +23 -0
- package/src/conventions.ts +53 -1
- package/src/dependencies.ts +119 -38
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/element-access.ts +446 -0
- package/src/entry-points.ts +1 -1
- package/src/erased-stores.ts +413 -0
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +3 -1
- package/src/finding.ts +3 -1
- package/src/findings/completion.ts +97 -21
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +4 -9
- package/src/findings/self-check.ts +31 -14
- package/src/findings-pass.ts +14 -7
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +1 -1
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +303 -137
- package/src/intra-function-parts.ts +41 -16
- package/src/inventory.ts +37 -33
- package/src/json-document.ts +1 -1
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +7 -22
- package/src/matrix.ts +34 -2
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/query.ts +10 -0
- package/src/references.ts +30 -7
- package/src/report.ts +11 -5
- package/src/reporters/sarif.ts +4 -0
- package/src/roots.ts +0 -3
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/serialization-contract.ts +53 -14
- package/src/session.ts +34 -31
- package/src/stores.ts +1 -1
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +17 -7
- package/src/suppress.ts +79 -47
- package/src/template-field.ts +1 -1
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-reach.ts +12 -8
- package/src/verbs/analyze.ts +15 -9
- package/src/verbs/verb.ts +1 -4
- package/src/workspace-programs.ts +2 -9
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members an element access reads by the literal type of its index: `o[k]` with `k`
|
|
3
|
+
* typed `"a" | "b"` reads `a` and `b` of the type of `o`, an index typed `keyof T` reads
|
|
4
|
+
* every member of `T`, and an index typed as a type parameter reads what its constraint does. Where the index is a parameter of a function every call of which the
|
|
5
|
+
* target holds, the members are read at each call, by the type of the argument it passes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
isArrayLiteralExpression,
|
|
10
|
+
isArrowFunction,
|
|
11
|
+
isBinaryExpression,
|
|
12
|
+
isCallExpression,
|
|
13
|
+
isElementAccessExpression,
|
|
14
|
+
isForInStatement,
|
|
15
|
+
isForOfStatement,
|
|
16
|
+
isFunctionDeclaration,
|
|
17
|
+
isFunctionExpression,
|
|
18
|
+
isIdentifier,
|
|
19
|
+
isNoSubstitutionTemplateLiteral,
|
|
20
|
+
isNumericLiteral,
|
|
21
|
+
isObjectLiteralExpression,
|
|
22
|
+
isParenthesizedExpression,
|
|
23
|
+
isPostfixUnaryExpression,
|
|
24
|
+
isPrefixUnaryExpression,
|
|
25
|
+
isPropertyAssignment,
|
|
26
|
+
isShorthandPropertyAssignment,
|
|
27
|
+
isSpreadAssignment,
|
|
28
|
+
isSpreadElement,
|
|
29
|
+
isStringLiteral,
|
|
30
|
+
isVariableDeclaration,
|
|
31
|
+
isVariableDeclarationList,
|
|
32
|
+
isVariableStatement,
|
|
33
|
+
NodeFlags,
|
|
34
|
+
SyntaxKind,
|
|
35
|
+
type CallExpression,
|
|
36
|
+
type ElementAccessExpression,
|
|
37
|
+
type Expression,
|
|
38
|
+
type Identifier,
|
|
39
|
+
type Node,
|
|
40
|
+
type ParameterDeclaration,
|
|
41
|
+
type SourceFile,
|
|
42
|
+
} from "@typescript/native/unstable/ast";
|
|
43
|
+
import type { Type } from "@typescript/native/unstable/sync";
|
|
44
|
+
import type { AliasChains } from "./alias-chain.ts";
|
|
45
|
+
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
46
|
+
import { renderPosition } from "./position.ts";
|
|
47
|
+
import { UNANSWERED } from "./query.ts";
|
|
48
|
+
import type { Reference } from "./references.ts";
|
|
49
|
+
import type { ProjectView } from "./session.ts";
|
|
50
|
+
|
|
51
|
+
type Use = "read" | "write" | "both";
|
|
52
|
+
|
|
53
|
+
/** The assignment operators that read their target before they write it. */
|
|
54
|
+
const COMPOUND: ReadonlySet<SyntaxKind> = new Set([
|
|
55
|
+
SyntaxKind.PlusEqualsToken,
|
|
56
|
+
SyntaxKind.MinusEqualsToken,
|
|
57
|
+
SyntaxKind.AsteriskEqualsToken,
|
|
58
|
+
SyntaxKind.AsteriskAsteriskEqualsToken,
|
|
59
|
+
SyntaxKind.SlashEqualsToken,
|
|
60
|
+
SyntaxKind.PercentEqualsToken,
|
|
61
|
+
SyntaxKind.LessThanLessThanEqualsToken,
|
|
62
|
+
SyntaxKind.GreaterThanGreaterThanEqualsToken,
|
|
63
|
+
SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
|
|
64
|
+
SyntaxKind.AmpersandEqualsToken,
|
|
65
|
+
SyntaxKind.BarEqualsToken,
|
|
66
|
+
SyntaxKind.CaretEqualsToken,
|
|
67
|
+
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
68
|
+
SyntaxKind.BarBarEqualsToken,
|
|
69
|
+
SyntaxKind.QuestionQuestionEqualsToken,
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
/** How an expression is used where it is written: read, written, or both. */
|
|
73
|
+
function useOf(node: Node): Use {
|
|
74
|
+
let at = node;
|
|
75
|
+
while (isParenthesizedExpression(at.parent)) {
|
|
76
|
+
at = at.parent;
|
|
77
|
+
}
|
|
78
|
+
const parent = at.parent;
|
|
79
|
+
if (isBinaryExpression(parent) && parent.left === at) {
|
|
80
|
+
if (parent.operatorToken.kind === SyntaxKind.EqualsToken) {
|
|
81
|
+
return "write";
|
|
82
|
+
}
|
|
83
|
+
if (COMPOUND.has(parent.operatorToken.kind)) {
|
|
84
|
+
return "both";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
(isPrefixUnaryExpression(parent) || isPostfixUnaryExpression(parent)) &&
|
|
89
|
+
(parent.operator === SyntaxKind.PlusPlusToken || parent.operator === SyntaxKind.MinusMinusToken)
|
|
90
|
+
) {
|
|
91
|
+
return "both";
|
|
92
|
+
}
|
|
93
|
+
return "read";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function unwrapped(node: Expression): Expression {
|
|
97
|
+
return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Whether an index is a literal the checker resolves to a member by its own text. */
|
|
101
|
+
function isLiteralIndex(node: Expression): boolean {
|
|
102
|
+
const at = unwrapped(node);
|
|
103
|
+
return isStringLiteral(at) || isNumericLiteral(at) || isNoSubstitutionTemplateLiteral(at);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A function whose calls may decide an access, by the name its calls spell. */
|
|
107
|
+
interface Callable {
|
|
108
|
+
readonly name: Identifier;
|
|
109
|
+
readonly parameters: readonly ParameterDeclaration[];
|
|
110
|
+
readonly body: Node | undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The function `node` is, where every call of it is written in its own file under its own
|
|
115
|
+
* name: a function declaration or a `const` initialized with an arrow function or a function
|
|
116
|
+
* expression, neither exported. A method may be dispatched to through a base class or an
|
|
117
|
+
* interface, so it is none.
|
|
118
|
+
*/
|
|
119
|
+
function callableOf(node: Node): Callable | undefined {
|
|
120
|
+
const exported = (holder: Node): boolean =>
|
|
121
|
+
((holder as { readonly modifiers?: readonly Node[] }).modifiers ?? []).some(
|
|
122
|
+
(modifier) =>
|
|
123
|
+
modifier.kind === SyntaxKind.ExportKeyword || modifier.kind === SyntaxKind.DefaultKeyword,
|
|
124
|
+
);
|
|
125
|
+
if (isFunctionDeclaration(node)) {
|
|
126
|
+
return node.name === undefined || exported(node)
|
|
127
|
+
? undefined
|
|
128
|
+
: { name: node.name, parameters: node.parameters, body: node.body };
|
|
129
|
+
}
|
|
130
|
+
if (!isArrowFunction(node) && !isFunctionExpression(node)) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
const declaration = node.parent;
|
|
134
|
+
const list = declaration.parent;
|
|
135
|
+
if (
|
|
136
|
+
!isVariableDeclaration(declaration) ||
|
|
137
|
+
declaration.initializer !== node ||
|
|
138
|
+
!isIdentifier(declaration.name) ||
|
|
139
|
+
!isVariableDeclarationList(list) ||
|
|
140
|
+
(list.flags & NodeFlags.Const) === 0 ||
|
|
141
|
+
!isVariableStatement(list.parent) ||
|
|
142
|
+
exported(list.parent)
|
|
143
|
+
) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
return { name: declaration.name, parameters: node.parameters, body: node.body };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The innermost function enclosing a node, or none. */
|
|
150
|
+
function enclosingFunction(node: Node): Node | undefined {
|
|
151
|
+
for (let at = node.parent as Node | undefined; at !== undefined; at = at.parent) {
|
|
152
|
+
if (
|
|
153
|
+
isFunctionDeclaration(at) ||
|
|
154
|
+
isArrowFunction(at) ||
|
|
155
|
+
isFunctionExpression(at) ||
|
|
156
|
+
at.kind === SyntaxKind.MethodDeclaration ||
|
|
157
|
+
at.kind === SyntaxKind.Constructor ||
|
|
158
|
+
at.kind === SyntaxKind.GetAccessor ||
|
|
159
|
+
at.kind === SyntaxKind.SetAccessor
|
|
160
|
+
) {
|
|
161
|
+
return at;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Whether an assignment target, a destructuring pattern at any depth included, assigns a name. */
|
|
168
|
+
function targetAssigns(target: Node, name: string): boolean {
|
|
169
|
+
if (isParenthesizedExpression(target)) {
|
|
170
|
+
return targetAssigns(target.expression, name);
|
|
171
|
+
}
|
|
172
|
+
if (isIdentifier(target)) {
|
|
173
|
+
return target.text === name;
|
|
174
|
+
}
|
|
175
|
+
if (isArrayLiteralExpression(target)) {
|
|
176
|
+
return target.elements.some((element) => targetAssigns(element, name));
|
|
177
|
+
}
|
|
178
|
+
if (isObjectLiteralExpression(target)) {
|
|
179
|
+
return target.properties.some((property) =>
|
|
180
|
+
isPropertyAssignment(property)
|
|
181
|
+
? targetAssigns(property.initializer, name)
|
|
182
|
+
: isShorthandPropertyAssignment(property)
|
|
183
|
+
? isIdentifier(property.name) && property.name.text === name
|
|
184
|
+
: isSpreadAssignment(property) && targetAssigns(property.expression, name),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (isSpreadElement(target)) {
|
|
188
|
+
return targetAssigns(target.expression, name);
|
|
189
|
+
}
|
|
190
|
+
// A pattern element with a default value, `[k = "a"] = …`, assigns its left side.
|
|
191
|
+
return (
|
|
192
|
+
isBinaryExpression(target) &&
|
|
193
|
+
target.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
194
|
+
targetAssigns(target.left, name)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Whether a body assigns a name: by an assignment, a destructuring one included, an
|
|
200
|
+
* increment or a decrement, or as the target of a `for…in` or `for…of` loop.
|
|
201
|
+
*/
|
|
202
|
+
function assigns(body: Node | undefined, name: string): boolean {
|
|
203
|
+
let found = false;
|
|
204
|
+
const visit = (node: Node): void => {
|
|
205
|
+
if (found) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const target =
|
|
209
|
+
isBinaryExpression(node) &&
|
|
210
|
+
(node.operatorToken.kind === SyntaxKind.EqualsToken || COMPOUND.has(node.operatorToken.kind))
|
|
211
|
+
? node.left
|
|
212
|
+
: (isPrefixUnaryExpression(node) || isPostfixUnaryExpression(node)) &&
|
|
213
|
+
(node.operator === SyntaxKind.PlusPlusToken ||
|
|
214
|
+
node.operator === SyntaxKind.MinusMinusToken)
|
|
215
|
+
? node.operand
|
|
216
|
+
: (isForInStatement(node) || isForOfStatement(node)) &&
|
|
217
|
+
node.initializer.kind !== SyntaxKind.VariableDeclarationList
|
|
218
|
+
? node.initializer
|
|
219
|
+
: undefined;
|
|
220
|
+
found = target !== undefined && targetAssigns(target, name);
|
|
221
|
+
node.forEachChild(visit);
|
|
222
|
+
};
|
|
223
|
+
if (body !== undefined) {
|
|
224
|
+
visit(body);
|
|
225
|
+
}
|
|
226
|
+
return found;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** One lookup: the index whose literal type names members of the object's type, read where and by whom. */
|
|
230
|
+
interface Lookup {
|
|
231
|
+
readonly file: SourceFile;
|
|
232
|
+
readonly from: string;
|
|
233
|
+
readonly at: Node;
|
|
234
|
+
readonly object: Expression;
|
|
235
|
+
readonly index: Expression;
|
|
236
|
+
readonly use: Use;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** One access whose index may be a parameter decided at the function's calls. */
|
|
240
|
+
interface Candidate {
|
|
241
|
+
readonly file: SourceFile;
|
|
242
|
+
readonly from: string;
|
|
243
|
+
readonly access: ElementAccessExpression;
|
|
244
|
+
readonly use: Use;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The references the element accesses of one project's own files make to the members their
|
|
249
|
+
* index's literal type names, each made by the declaration holding the access or the call.
|
|
250
|
+
*/
|
|
251
|
+
export function elementAccessReferences<Brand>(
|
|
252
|
+
project: ProjectView<Brand>,
|
|
253
|
+
held: Inventory,
|
|
254
|
+
chains: AliasChains,
|
|
255
|
+
root: string,
|
|
256
|
+
testFiles: ReadonlySet<string>,
|
|
257
|
+
): readonly Reference[] {
|
|
258
|
+
const queries = project.queries;
|
|
259
|
+
const candidates: Candidate[] = [];
|
|
260
|
+
const calls = new Map<Node, { readonly file: SourceFile; readonly from: string }>();
|
|
261
|
+
const names: Identifier[] = [];
|
|
262
|
+
|
|
263
|
+
for (const file of project.ownSourceFiles()) {
|
|
264
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
265
|
+
if (fileId === undefined) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const visit = (node: Node, from: string): void => {
|
|
269
|
+
const holder = held.declarations.get(nodeKey(file, node)) ?? from;
|
|
270
|
+
if (isElementAccessExpression(node) && !isLiteralIndex(node.argumentExpression)) {
|
|
271
|
+
candidates.push({ file, from: holder, access: node, use: useOf(node) });
|
|
272
|
+
}
|
|
273
|
+
if (isCallExpression(node)) {
|
|
274
|
+
calls.set(node, { file, from: holder });
|
|
275
|
+
}
|
|
276
|
+
if (isIdentifier(node)) {
|
|
277
|
+
names.push(node);
|
|
278
|
+
}
|
|
279
|
+
node.forEachChild((child) => {
|
|
280
|
+
visit(child, holder);
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
file.forEachChild((child) => {
|
|
284
|
+
visit(child, fileId);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (candidates.length === 0) {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// A parameter index is decided at the calls only where the function is a callable every
|
|
292
|
+
// use of which is a call, so the symbols of the parameters and of every name are read.
|
|
293
|
+
const parameterOf = new Map<Candidate, { callable: Callable; at: number }>();
|
|
294
|
+
const byCallable = new Map<Identifier, Candidate[]>();
|
|
295
|
+
for (const candidate of candidates) {
|
|
296
|
+
const index = unwrapped(candidate.access.argumentExpression);
|
|
297
|
+
const fn = isIdentifier(index) ? enclosingFunction(candidate.access) : undefined;
|
|
298
|
+
const callable = fn === undefined ? undefined : callableOf(fn);
|
|
299
|
+
if (callable === undefined || !isIdentifier(index)) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const at = callable.parameters.findIndex(
|
|
303
|
+
(parameter) => isIdentifier(parameter.name) && parameter.name.text === index.text,
|
|
304
|
+
);
|
|
305
|
+
if (at < 0 || assigns(callable.body, index.text)) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
parameterOf.set(candidate, { callable, at });
|
|
309
|
+
byCallable.set(callable.name, [...(byCallable.get(callable.name) ?? []), candidate]);
|
|
310
|
+
}
|
|
311
|
+
const callableNames = [...byCallable.keys()];
|
|
312
|
+
const spelled = new Set(callableNames.map((name) => name.text));
|
|
313
|
+
const sameName = names.filter((name) => spelled.has(name.text));
|
|
314
|
+
const symbols = queries.symbolsAt([...callableNames, ...sameName]);
|
|
315
|
+
const symbolOf = new Map<Node, unknown>();
|
|
316
|
+
[...callableNames, ...sameName].forEach((node, at) => {
|
|
317
|
+
const symbol = symbols[at];
|
|
318
|
+
if (symbol !== UNANSWERED && symbol !== undefined) {
|
|
319
|
+
symbolOf.set(node, symbol);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
const callsOf = new Map<Node, CallExpression[] | undefined>();
|
|
323
|
+
for (const name of callableNames) {
|
|
324
|
+
const symbol = symbolOf.get(name);
|
|
325
|
+
let found: CallExpression[] | undefined = symbol === undefined ? undefined : [];
|
|
326
|
+
for (const use of sameName) {
|
|
327
|
+
if (found === undefined) {
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
if (use === name || symbolOf.get(use) !== symbol) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const parent = use.parent;
|
|
334
|
+
found =
|
|
335
|
+
isCallExpression(parent) && parent.expression === use ? [...found, parent] : undefined;
|
|
336
|
+
}
|
|
337
|
+
callsOf.set(name, found);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const lookups: Lookup[] = [];
|
|
341
|
+
for (const candidate of candidates) {
|
|
342
|
+
const deferred = parameterOf.get(candidate);
|
|
343
|
+
const sites = deferred === undefined ? undefined : callsOf.get(deferred.callable.name);
|
|
344
|
+
if (deferred === undefined || sites === undefined) {
|
|
345
|
+
lookups.push({
|
|
346
|
+
file: candidate.file,
|
|
347
|
+
from: candidate.from,
|
|
348
|
+
at: candidate.access.argumentExpression,
|
|
349
|
+
object: candidate.access.expression,
|
|
350
|
+
index: candidate.access.argumentExpression,
|
|
351
|
+
use: candidate.use,
|
|
352
|
+
});
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
// An object that is a parameter too is read as the argument each call passes there.
|
|
356
|
+
const object = unwrapped(candidate.access.expression);
|
|
357
|
+
const objectAt = isIdentifier(object)
|
|
358
|
+
? deferred.callable.parameters.findIndex(
|
|
359
|
+
(parameter) => isIdentifier(parameter.name) && parameter.name.text === object.text,
|
|
360
|
+
)
|
|
361
|
+
: -1;
|
|
362
|
+
for (const call of sites) {
|
|
363
|
+
const parameter = deferred.callable.parameters[deferred.at];
|
|
364
|
+
const index = call.arguments[deferred.at] ?? parameter?.initializer;
|
|
365
|
+
const site = calls.get(call);
|
|
366
|
+
if (index === undefined || site === undefined) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
lookups.push({
|
|
370
|
+
file: site.file,
|
|
371
|
+
from: site.from,
|
|
372
|
+
at: call,
|
|
373
|
+
object:
|
|
374
|
+
(objectAt < 0 ? undefined : call.arguments[objectAt]) ?? candidate.access.expression,
|
|
375
|
+
index,
|
|
376
|
+
use: candidate.use,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const nodes = lookups.flatMap((one) => [one.index, one.object]);
|
|
382
|
+
const types = queries.typesAt(nodes);
|
|
383
|
+
const typeOf = (node: Node, at: number): Type | undefined => {
|
|
384
|
+
const type = types[at];
|
|
385
|
+
return nodes[at] === node && type !== UNANSWERED ? type : undefined;
|
|
386
|
+
};
|
|
387
|
+
// A type parameter's body serves every instantiation, so it reads what its constraint admits.
|
|
388
|
+
const keysOf = (type: Type): Type => {
|
|
389
|
+
if (!type.isTypeParameter()) {
|
|
390
|
+
return type;
|
|
391
|
+
}
|
|
392
|
+
const constraint = queries.constraintOf(type);
|
|
393
|
+
return constraint === UNANSWERED || constraint === undefined ? type : constraint;
|
|
394
|
+
};
|
|
395
|
+
const partsOf = (type: Type): readonly Type[] => {
|
|
396
|
+
const parts = queries.constituents(type);
|
|
397
|
+
return parts === UNANSWERED ? [type] : parts;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const found: Reference[] = [];
|
|
401
|
+
lookups.forEach((lookup, at) => {
|
|
402
|
+
const indexType = typeOf(lookup.index, 2 * at);
|
|
403
|
+
const objectType = typeOf(lookup.object, 2 * at + 1);
|
|
404
|
+
const literals = indexType === undefined ? undefined : literalNames(partsOf(keysOf(indexType)));
|
|
405
|
+
if (objectType === undefined || literals === undefined) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const position = renderPosition(lookup.file, root, lookup.at.getStart());
|
|
409
|
+
for (const part of partsOf(objectType)) {
|
|
410
|
+
const properties = queries.propertiesOf(part);
|
|
411
|
+
for (const property of properties === UNANSWERED ? [] : properties) {
|
|
412
|
+
if (!literals.has(property.escapedName)) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
for (const to of chains.declarationsOf(property)) {
|
|
416
|
+
for (const use of lookup.use === "both" ? (["read", "write"] as const) : [lookup.use]) {
|
|
417
|
+
found.push({
|
|
418
|
+
from: lookup.from,
|
|
419
|
+
to,
|
|
420
|
+
position,
|
|
421
|
+
use,
|
|
422
|
+
resolution: "keyed",
|
|
423
|
+
test: testFiles.has(position.path),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
return found;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** The names a union of literal types spells, or none where a constituent is no string or number literal. */
|
|
434
|
+
function literalNames(parts: readonly Type[]): ReadonlySet<string> | undefined {
|
|
435
|
+
const names = new Set<string>();
|
|
436
|
+
for (const part of parts) {
|
|
437
|
+
if (part.isStringLiteralType()) {
|
|
438
|
+
names.add(part.value);
|
|
439
|
+
} else if (part.isNumberLiteralType()) {
|
|
440
|
+
names.add(String(part.value));
|
|
441
|
+
} else {
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return names;
|
|
446
|
+
}
|