@cplieger/deadset-ts 3.9.0 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/destructuring.js +273 -0
- package/dist/src/references.js +36 -3
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/destructuring.ts +353 -0
- package/src/references.ts +57 -4
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members a destructuring reads. An object pattern reads each property it names
|
|
3
|
+
* from the value it destructures, so the property's name in the pattern is a read of
|
|
4
|
+
* the member the value's type declares under that name, whether the pattern binds it
|
|
5
|
+
* to a local of the same name, renames it, gives it a default or nests a pattern under
|
|
6
|
+
* it. A rest element names no property: what it copies is read where the copy is.
|
|
7
|
+
*/
|
|
8
|
+
import { isArrayLiteralExpression, isBinaryExpression, isForOfStatement, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAssignment, isShorthandPropertyAssignment, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { TypeFlags } from "@typescript/native/unstable/sync";
|
|
10
|
+
/** The text a property name spells, or `undefined` for a computed name. */
|
|
11
|
+
function nameText(node) {
|
|
12
|
+
if (node === undefined) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
if (isIdentifier(node) ||
|
|
16
|
+
isStringLiteral(node) ||
|
|
17
|
+
isNoSubstitutionTemplateLiteral(node) ||
|
|
18
|
+
isNumericLiteral(node)) {
|
|
19
|
+
return node.text;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
/** The node one expression is written inside, through however many parentheses enclose it. */
|
|
24
|
+
function container(node) {
|
|
25
|
+
let child = node;
|
|
26
|
+
while (isParenthesizedExpression(child.parent)) {
|
|
27
|
+
child = child.parent;
|
|
28
|
+
}
|
|
29
|
+
return { child, parent: child.parent };
|
|
30
|
+
}
|
|
31
|
+
/** The plain assignment one node is the left side of, if it is one. */
|
|
32
|
+
function assignmentOf(node, parent) {
|
|
33
|
+
return isBinaryExpression(parent) &&
|
|
34
|
+
parent.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
35
|
+
parent.left === node
|
|
36
|
+
? parent
|
|
37
|
+
: undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether an object or array literal is a destructuring target rather than a value: the
|
|
41
|
+
* left side of an assignment, the target of a `for…of`, or an element of a literal that
|
|
42
|
+
* is one, with or without a default.
|
|
43
|
+
*/
|
|
44
|
+
function isDestructuringTarget(literal) {
|
|
45
|
+
const { child, parent } = container(literal);
|
|
46
|
+
if (assignmentOf(child, parent) !== undefined) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (isForOfStatement(parent)) {
|
|
50
|
+
return parent.initializer === child;
|
|
51
|
+
}
|
|
52
|
+
return isElement(child, parent);
|
|
53
|
+
}
|
|
54
|
+
/** Whether one node is an element of a destructuring target: a property's value, or an array element. */
|
|
55
|
+
function isElement(child, parent) {
|
|
56
|
+
if (isPropertyAssignment(parent)) {
|
|
57
|
+
return parent.initializer === child && isDestructuringTarget(parent.parent);
|
|
58
|
+
}
|
|
59
|
+
return isArrayLiteralExpression(parent) && isDestructuringTarget(parent);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Whether an object binding pattern runs: a pattern in the parameter list of a signature
|
|
63
|
+
* with no body, a function type or an overload among them, binds nothing at runtime.
|
|
64
|
+
*/
|
|
65
|
+
function binds(pattern) {
|
|
66
|
+
let at = pattern.parent;
|
|
67
|
+
while (at.kind === SyntaxKind.BindingElement) {
|
|
68
|
+
at = at.parent.parent;
|
|
69
|
+
}
|
|
70
|
+
if (!isParameterDeclaration(at)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return at.parent.body !== undefined;
|
|
74
|
+
}
|
|
75
|
+
/** The properties one node reads, where it is an object pattern; none otherwise. */
|
|
76
|
+
export function propertyReadsOf(node) {
|
|
77
|
+
const reads = [];
|
|
78
|
+
if (isObjectBindingPattern(node)) {
|
|
79
|
+
if (!binds(node)) {
|
|
80
|
+
return reads;
|
|
81
|
+
}
|
|
82
|
+
for (const element of node.elements) {
|
|
83
|
+
const key = element.propertyName ?? element.name;
|
|
84
|
+
const name = nameText(key);
|
|
85
|
+
if (element.dotDotDotToken === undefined && key !== undefined && name !== undefined) {
|
|
86
|
+
reads.push({ pattern: node, key, name });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return reads;
|
|
90
|
+
}
|
|
91
|
+
if (isObjectLiteralExpression(node) && isDestructuringTarget(node)) {
|
|
92
|
+
for (const property of node.properties) {
|
|
93
|
+
if (isShorthandPropertyAssignment(property) || isPropertyAssignment(property)) {
|
|
94
|
+
const name = nameText(property.name);
|
|
95
|
+
if (name !== undefined) {
|
|
96
|
+
reads.push({ pattern: node, key: property.name, name });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return reads;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Where the value one destructuring target receives comes from. The checker answers for
|
|
105
|
+
* a binding pattern's type itself, so this is asked of an assignment target only, whose
|
|
106
|
+
* own type is the literal's rather than the assigned value's. A default does not move
|
|
107
|
+
* the source: `{ a: { b } = fallback }` reads `b` from `a`. A target inside a spread
|
|
108
|
+
* takes the rest of a value and has no source here.
|
|
109
|
+
*/
|
|
110
|
+
function sourceOf(target) {
|
|
111
|
+
const steps = [];
|
|
112
|
+
let at = target;
|
|
113
|
+
for (;;) {
|
|
114
|
+
const { child, parent } = container(at);
|
|
115
|
+
const assignment = assignmentOf(child, parent);
|
|
116
|
+
if (assignment !== undefined) {
|
|
117
|
+
const outer = container(assignment);
|
|
118
|
+
if (!isElement(outer.child, outer.parent)) {
|
|
119
|
+
return { expression: assignment.right, steps: steps.reverse() };
|
|
120
|
+
}
|
|
121
|
+
at = assignment;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (isForOfStatement(parent) && parent.initializer === child) {
|
|
125
|
+
steps.push({ kind: "iterated" });
|
|
126
|
+
return { expression: parent.expression, steps: steps.reverse() };
|
|
127
|
+
}
|
|
128
|
+
if (isPropertyAssignment(parent) && parent.initializer === child) {
|
|
129
|
+
const name = nameText(parent.name);
|
|
130
|
+
if (name === undefined) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
steps.push({ kind: "property", name });
|
|
134
|
+
at = parent.parent;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (isArrayLiteralExpression(parent)) {
|
|
138
|
+
steps.push({
|
|
139
|
+
kind: "element",
|
|
140
|
+
index: parent.elements.findIndex((element) => element === child),
|
|
141
|
+
});
|
|
142
|
+
at = parent;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The resolution over one project, its batches capped at `cap`. Property tables are
|
|
150
|
+
* read once per type for the whole project, since one type is destructured in many
|
|
151
|
+
* places and a table answers every name of it.
|
|
152
|
+
*/
|
|
153
|
+
export function destructuring(project, cap) {
|
|
154
|
+
const checker = project.checker;
|
|
155
|
+
const tables = new Map();
|
|
156
|
+
let patternBatches = 0;
|
|
157
|
+
let patternLookups = 0;
|
|
158
|
+
const typesAt = (nodes) => {
|
|
159
|
+
const types = [];
|
|
160
|
+
for (let from = 0; from < nodes.length; from += cap) {
|
|
161
|
+
types.push(...checker.getTypeAtLocation(nodes.slice(from, from + cap)));
|
|
162
|
+
patternBatches += 1;
|
|
163
|
+
}
|
|
164
|
+
return types;
|
|
165
|
+
};
|
|
166
|
+
const tableOf = (type) => {
|
|
167
|
+
const known = tables.get(type.id);
|
|
168
|
+
if (known !== undefined) {
|
|
169
|
+
return known;
|
|
170
|
+
}
|
|
171
|
+
const table = new Map();
|
|
172
|
+
if ((type.flags & TypeFlags.AnyOrUnknown) === 0) {
|
|
173
|
+
patternLookups += 1;
|
|
174
|
+
for (const property of checker.getPropertiesOfType(type)) {
|
|
175
|
+
table.set(property.name, property);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
tables.set(type.id, table);
|
|
179
|
+
return table;
|
|
180
|
+
};
|
|
181
|
+
const definedOf = (type) => {
|
|
182
|
+
if ((type.flags & TypeFlags.Union) === 0) {
|
|
183
|
+
return type;
|
|
184
|
+
}
|
|
185
|
+
patternLookups += 1;
|
|
186
|
+
return checker.getNonNullableType(type);
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* The type one element of an array or tuple value has: a tuple's element at `index`,
|
|
190
|
+
* an array's element type at any index, and `undefined` for a tuple iterated whole.
|
|
191
|
+
*/
|
|
192
|
+
const elementOf = (type, index) => {
|
|
193
|
+
if (!type.isTypeReference()) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
patternLookups += 1;
|
|
197
|
+
if (checker.isTupleType(type)) {
|
|
198
|
+
if (index === undefined) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
patternLookups += 1;
|
|
202
|
+
return checker.getTypeArguments(type)[index];
|
|
203
|
+
}
|
|
204
|
+
patternLookups += 1;
|
|
205
|
+
if (!checker.isArrayType(type)) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
patternLookups += 1;
|
|
209
|
+
return checker.getTypeArguments(type)[0];
|
|
210
|
+
};
|
|
211
|
+
const step = (type, next) => {
|
|
212
|
+
const defined = definedOf(type);
|
|
213
|
+
if (defined === undefined) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
if (next.kind === "property") {
|
|
217
|
+
const property = tableOf(defined).get(next.name);
|
|
218
|
+
if (property === undefined) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
patternLookups += 1;
|
|
222
|
+
return checker.getTypeOfSymbol(property);
|
|
223
|
+
}
|
|
224
|
+
return elementOf(defined, next.kind === "element" ? next.index : undefined);
|
|
225
|
+
};
|
|
226
|
+
const resolve = (reads) => {
|
|
227
|
+
const patterns = [...new Set(reads.map((read) => read.pattern))];
|
|
228
|
+
const sources = new Map();
|
|
229
|
+
const asking = new Set();
|
|
230
|
+
for (const pattern of patterns) {
|
|
231
|
+
if (isObjectBindingPattern(pattern)) {
|
|
232
|
+
asking.add(pattern);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const source = sourceOf(pattern);
|
|
236
|
+
sources.set(pattern, source);
|
|
237
|
+
if (source !== undefined) {
|
|
238
|
+
asking.add(source.expression);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const answered = new Map();
|
|
242
|
+
const asked = [...asking];
|
|
243
|
+
typesAt(asked).forEach((type, index) => {
|
|
244
|
+
const node = asked[index];
|
|
245
|
+
if (type !== undefined && node !== undefined) {
|
|
246
|
+
answered.set(node, type);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
const patternTypes = new Map();
|
|
250
|
+
for (const pattern of patterns) {
|
|
251
|
+
const source = sources.get(pattern);
|
|
252
|
+
if (source === undefined) {
|
|
253
|
+
patternTypes.set(pattern, answered.get(pattern));
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
let type = answered.get(source.expression);
|
|
257
|
+
for (const next of source.steps) {
|
|
258
|
+
type = type === undefined ? undefined : step(type, next);
|
|
259
|
+
}
|
|
260
|
+
patternTypes.set(pattern, type === undefined ? undefined : definedOf(type));
|
|
261
|
+
}
|
|
262
|
+
return reads.map((read) => {
|
|
263
|
+
const type = patternTypes.get(read.pattern);
|
|
264
|
+
return type === undefined ? undefined : tableOf(type).get(read.name);
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
return {
|
|
268
|
+
resolve,
|
|
269
|
+
get cost() {
|
|
270
|
+
return { patternBatches, patternLookups };
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
package/dist/src/references.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExternalModuleReference, isForInStatement, isForOfStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
2
2
|
import { aliasChains } from "./alias-chain.js";
|
|
3
|
+
import { destructuring, propertyReadsOf } from "./destructuring.js";
|
|
3
4
|
import { globExpression } from "./glob.js";
|
|
4
5
|
import { nodeKey } from "./inventory.js";
|
|
5
6
|
import { byPosition, renderPosition } from "./position.js";
|
|
@@ -205,6 +206,7 @@ function linksOf(file, node, declarations, enclosing) {
|
|
|
205
206
|
*/
|
|
206
207
|
function sitesOf(file, declarations, fileId) {
|
|
207
208
|
const found = [];
|
|
209
|
+
const reads = [];
|
|
208
210
|
const links = [];
|
|
209
211
|
const evaluations = [];
|
|
210
212
|
const decorated = [];
|
|
@@ -243,6 +245,9 @@ function sitesOf(file, declarations, fileId) {
|
|
|
243
245
|
}
|
|
244
246
|
markNames(node);
|
|
245
247
|
markStores(node, writes);
|
|
248
|
+
for (const read of propertyReadsOf(node)) {
|
|
249
|
+
reads.push({ ...read, from: enclosing });
|
|
250
|
+
}
|
|
246
251
|
links.push(...linksOf(file, node, declarations, enclosing));
|
|
247
252
|
const evaluated = evaluatedBy(node);
|
|
248
253
|
if (evaluated !== undefined) {
|
|
@@ -262,7 +267,7 @@ function sitesOf(file, declarations, fileId) {
|
|
|
262
267
|
enclosing = outer;
|
|
263
268
|
};
|
|
264
269
|
file.forEachChild(visit);
|
|
265
|
-
return { uses: found, links, evaluations, decorated };
|
|
270
|
+
return { uses: found, reads, links, evaluations, decorated };
|
|
266
271
|
}
|
|
267
272
|
/**
|
|
268
273
|
* The name one test-file rule takes. A rule's name is a word list the report's own
|
|
@@ -316,6 +321,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
316
321
|
const found = [];
|
|
317
322
|
const tests = [];
|
|
318
323
|
const chains = aliasChains(project, held);
|
|
324
|
+
const destructured = destructuring(project, cap);
|
|
319
325
|
let batched = 0;
|
|
320
326
|
let fileBatches = 0;
|
|
321
327
|
let residueFallbacks = 0;
|
|
@@ -351,7 +357,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
351
357
|
if (test) {
|
|
352
358
|
tests.push(path);
|
|
353
359
|
}
|
|
354
|
-
const { uses, links, evaluations, decorated } = sitesOf(file, declarations, declaredAt(file) ?? "");
|
|
360
|
+
const { uses, reads, links, evaluations, decorated } = sitesOf(file, declarations, declaredAt(file) ?? "");
|
|
355
361
|
// A decorator is written inside the declaration it is attached to and receives it,
|
|
356
362
|
// so the declaration is used where the decorator is written, whatever the
|
|
357
363
|
// decorator does with it.
|
|
@@ -426,6 +432,25 @@ export function references(project, held, targetRoot, options) {
|
|
|
426
432
|
}
|
|
427
433
|
}
|
|
428
434
|
}
|
|
435
|
+
// A pattern names a property of the value it destructures, which no name node of
|
|
436
|
+
// the pattern resolves to: a shorthand's name is the local it binds.
|
|
437
|
+
destructured.resolve(reads).forEach((symbol, index) => {
|
|
438
|
+
const read = reads[index];
|
|
439
|
+
if (symbol === undefined || read === undefined) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const position = renderPosition(file, targetRoot, read.key.getStart());
|
|
443
|
+
for (const target of chains.chainOf(symbol)) {
|
|
444
|
+
found.push({
|
|
445
|
+
from: read.from,
|
|
446
|
+
to: target.id,
|
|
447
|
+
position,
|
|
448
|
+
use: "read",
|
|
449
|
+
resolution: "destructured",
|
|
450
|
+
test,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
});
|
|
429
454
|
for (const site of uses) {
|
|
430
455
|
const direct = site.shorthand !== undefined
|
|
431
456
|
? "shorthand"
|
|
@@ -459,6 +484,14 @@ export function references(project, held, targetRoot, options) {
|
|
|
459
484
|
configFile: project.configFile,
|
|
460
485
|
references: found,
|
|
461
486
|
testFilePaths: tests.sort(compare),
|
|
462
|
-
cost: {
|
|
487
|
+
cost: {
|
|
488
|
+
batched,
|
|
489
|
+
fileBatches,
|
|
490
|
+
residueFallbacks,
|
|
491
|
+
shorthandLookups,
|
|
492
|
+
aliasSteps: chains.steps,
|
|
493
|
+
patternBatches: destructured.cost.patternBatches,
|
|
494
|
+
patternLookups: destructured.cost.patternLookups,
|
|
495
|
+
},
|
|
463
496
|
};
|
|
464
497
|
}
|
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members a destructuring reads. An object pattern reads each property it names
|
|
3
|
+
* from the value it destructures, so the property's name in the pattern is a read of
|
|
4
|
+
* the member the value's type declares under that name, whether the pattern binds it
|
|
5
|
+
* to a local of the same name, renames it, gives it a default or nests a pattern under
|
|
6
|
+
* it. A rest element names no property: what it copies is read where the copy is.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
isArrayLiteralExpression,
|
|
11
|
+
isBinaryExpression,
|
|
12
|
+
isForOfStatement,
|
|
13
|
+
isIdentifier,
|
|
14
|
+
isNoSubstitutionTemplateLiteral,
|
|
15
|
+
isNumericLiteral,
|
|
16
|
+
isObjectBindingPattern,
|
|
17
|
+
isObjectLiteralExpression,
|
|
18
|
+
isParameterDeclaration,
|
|
19
|
+
isParenthesizedExpression,
|
|
20
|
+
isPropertyAssignment,
|
|
21
|
+
isShorthandPropertyAssignment,
|
|
22
|
+
isStringLiteral,
|
|
23
|
+
SyntaxKind,
|
|
24
|
+
type BinaryExpression,
|
|
25
|
+
type Node,
|
|
26
|
+
} from "@typescript/native/unstable/ast";
|
|
27
|
+
import { TypeFlags, type Symbol as TSSymbol, type Type } from "@typescript/native/unstable/sync";
|
|
28
|
+
import type { ProjectView } from "./session.ts";
|
|
29
|
+
|
|
30
|
+
/** One property one object pattern reads. */
|
|
31
|
+
export interface PropertyRead {
|
|
32
|
+
/** The pattern, whose destructured value declares the property. */
|
|
33
|
+
readonly pattern: Node;
|
|
34
|
+
/** The property's name as the pattern writes it, which is where the read is written. */
|
|
35
|
+
readonly key: Node;
|
|
36
|
+
/** The property's name. */
|
|
37
|
+
readonly name: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The text a property name spells, or `undefined` for a computed name. */
|
|
41
|
+
function nameText(node: Node | undefined): string | undefined {
|
|
42
|
+
if (node === undefined) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
isIdentifier(node) ||
|
|
47
|
+
isStringLiteral(node) ||
|
|
48
|
+
isNoSubstitutionTemplateLiteral(node) ||
|
|
49
|
+
isNumericLiteral(node)
|
|
50
|
+
) {
|
|
51
|
+
return node.text;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The node one expression is written inside, through however many parentheses enclose it. */
|
|
57
|
+
function container(node: Node): { readonly child: Node; readonly parent: Node } {
|
|
58
|
+
let child = node;
|
|
59
|
+
while (isParenthesizedExpression(child.parent)) {
|
|
60
|
+
child = child.parent;
|
|
61
|
+
}
|
|
62
|
+
return { child, parent: child.parent };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The plain assignment one node is the left side of, if it is one. */
|
|
66
|
+
function assignmentOf(node: Node, parent: Node): BinaryExpression | undefined {
|
|
67
|
+
return isBinaryExpression(parent) &&
|
|
68
|
+
parent.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
69
|
+
parent.left === node
|
|
70
|
+
? parent
|
|
71
|
+
: undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether an object or array literal is a destructuring target rather than a value: the
|
|
76
|
+
* left side of an assignment, the target of a `for…of`, or an element of a literal that
|
|
77
|
+
* is one, with or without a default.
|
|
78
|
+
*/
|
|
79
|
+
function isDestructuringTarget(literal: Node): boolean {
|
|
80
|
+
const { child, parent } = container(literal);
|
|
81
|
+
if (assignmentOf(child, parent) !== undefined) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (isForOfStatement(parent)) {
|
|
85
|
+
return parent.initializer === child;
|
|
86
|
+
}
|
|
87
|
+
return isElement(child, parent);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Whether one node is an element of a destructuring target: a property's value, or an array element. */
|
|
91
|
+
function isElement(child: Node, parent: Node): boolean {
|
|
92
|
+
if (isPropertyAssignment(parent)) {
|
|
93
|
+
return parent.initializer === child && isDestructuringTarget(parent.parent);
|
|
94
|
+
}
|
|
95
|
+
return isArrayLiteralExpression(parent) && isDestructuringTarget(parent);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whether an object binding pattern runs: a pattern in the parameter list of a signature
|
|
100
|
+
* with no body, a function type or an overload among them, binds nothing at runtime.
|
|
101
|
+
*/
|
|
102
|
+
function binds(pattern: Node): boolean {
|
|
103
|
+
let at = pattern.parent;
|
|
104
|
+
while (at.kind === SyntaxKind.BindingElement) {
|
|
105
|
+
at = at.parent.parent;
|
|
106
|
+
}
|
|
107
|
+
if (!isParameterDeclaration(at)) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return (at.parent as { readonly body?: Node }).body !== undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The properties one node reads, where it is an object pattern; none otherwise. */
|
|
114
|
+
export function propertyReadsOf(node: Node): readonly PropertyRead[] {
|
|
115
|
+
const reads: PropertyRead[] = [];
|
|
116
|
+
if (isObjectBindingPattern(node)) {
|
|
117
|
+
if (!binds(node)) {
|
|
118
|
+
return reads;
|
|
119
|
+
}
|
|
120
|
+
for (const element of node.elements) {
|
|
121
|
+
const key = element.propertyName ?? element.name;
|
|
122
|
+
const name = nameText(key);
|
|
123
|
+
if (element.dotDotDotToken === undefined && key !== undefined && name !== undefined) {
|
|
124
|
+
reads.push({ pattern: node, key, name });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return reads;
|
|
128
|
+
}
|
|
129
|
+
if (isObjectLiteralExpression(node) && isDestructuringTarget(node)) {
|
|
130
|
+
for (const property of node.properties) {
|
|
131
|
+
if (isShorthandPropertyAssignment(property) || isPropertyAssignment(property)) {
|
|
132
|
+
const name = nameText(property.name);
|
|
133
|
+
if (name !== undefined) {
|
|
134
|
+
reads.push({ pattern: node, key: property.name, name });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return reads;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** One step from a destructured value to the value a nested target destructures. */
|
|
143
|
+
type Step =
|
|
144
|
+
| { readonly kind: "property"; readonly name: string }
|
|
145
|
+
| { readonly kind: "element"; readonly index: number }
|
|
146
|
+
| { readonly kind: "iterated" };
|
|
147
|
+
|
|
148
|
+
/** Where an assignment target's value comes from: an expression, and the steps into it. */
|
|
149
|
+
interface Source {
|
|
150
|
+
readonly expression: Node;
|
|
151
|
+
readonly steps: readonly Step[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Where the value one destructuring target receives comes from. The checker answers for
|
|
156
|
+
* a binding pattern's type itself, so this is asked of an assignment target only, whose
|
|
157
|
+
* own type is the literal's rather than the assigned value's. A default does not move
|
|
158
|
+
* the source: `{ a: { b } = fallback }` reads `b` from `a`. A target inside a spread
|
|
159
|
+
* takes the rest of a value and has no source here.
|
|
160
|
+
*/
|
|
161
|
+
function sourceOf(target: Node): Source | undefined {
|
|
162
|
+
const steps: Step[] = [];
|
|
163
|
+
let at = target;
|
|
164
|
+
for (;;) {
|
|
165
|
+
const { child, parent } = container(at);
|
|
166
|
+
const assignment = assignmentOf(child, parent);
|
|
167
|
+
if (assignment !== undefined) {
|
|
168
|
+
const outer = container(assignment);
|
|
169
|
+
if (!isElement(outer.child, outer.parent)) {
|
|
170
|
+
return { expression: assignment.right, steps: steps.reverse() };
|
|
171
|
+
}
|
|
172
|
+
at = assignment;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (isForOfStatement(parent) && parent.initializer === child) {
|
|
176
|
+
steps.push({ kind: "iterated" });
|
|
177
|
+
return { expression: parent.expression, steps: steps.reverse() };
|
|
178
|
+
}
|
|
179
|
+
if (isPropertyAssignment(parent) && parent.initializer === child) {
|
|
180
|
+
const name = nameText(parent.name);
|
|
181
|
+
if (name === undefined) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
steps.push({ kind: "property", name });
|
|
185
|
+
at = parent.parent;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (isArrayLiteralExpression(parent)) {
|
|
189
|
+
steps.push({
|
|
190
|
+
kind: "element",
|
|
191
|
+
index: parent.elements.findIndex((element) => element === child),
|
|
192
|
+
});
|
|
193
|
+
at = parent;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** What resolving one project's property reads cost at the client boundary. */
|
|
201
|
+
interface DestructuringCost {
|
|
202
|
+
/** Batched type lookups, one per capped run of a file's patterns and assigned values. */
|
|
203
|
+
readonly patternBatches: number;
|
|
204
|
+
/**
|
|
205
|
+
* Per-type lookups: one property table per distinct destructured type, and each step
|
|
206
|
+
* an assignment target takes into its assigned value.
|
|
207
|
+
*/
|
|
208
|
+
readonly patternLookups: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Resolves property reads to the symbols the destructured values' types declare. */
|
|
212
|
+
interface Destructuring {
|
|
213
|
+
/**
|
|
214
|
+
* The property symbol each read names, in the order given, `undefined` where the
|
|
215
|
+
* destructured value's type declares no property of that name or is not known.
|
|
216
|
+
*/
|
|
217
|
+
resolve(reads: readonly PropertyRead[]): readonly (TSSymbol | undefined)[];
|
|
218
|
+
readonly cost: DestructuringCost;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The resolution over one project, its batches capped at `cap`. Property tables are
|
|
223
|
+
* read once per type for the whole project, since one type is destructured in many
|
|
224
|
+
* places and a table answers every name of it.
|
|
225
|
+
*/
|
|
226
|
+
export function destructuring<Brand>(project: ProjectView<Brand>, cap: number): Destructuring {
|
|
227
|
+
const checker = project.checker;
|
|
228
|
+
const tables = new Map<number, ReadonlyMap<string, TSSymbol>>();
|
|
229
|
+
let patternBatches = 0;
|
|
230
|
+
let patternLookups = 0;
|
|
231
|
+
|
|
232
|
+
const typesAt = (nodes: readonly Node[]): (Type | undefined)[] => {
|
|
233
|
+
const types: (Type | undefined)[] = [];
|
|
234
|
+
for (let from = 0; from < nodes.length; from += cap) {
|
|
235
|
+
types.push(...checker.getTypeAtLocation(nodes.slice(from, from + cap)));
|
|
236
|
+
patternBatches += 1;
|
|
237
|
+
}
|
|
238
|
+
return types;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const tableOf = (type: Type): ReadonlyMap<string, TSSymbol> => {
|
|
242
|
+
const known = tables.get(type.id);
|
|
243
|
+
if (known !== undefined) {
|
|
244
|
+
return known;
|
|
245
|
+
}
|
|
246
|
+
const table = new Map<string, TSSymbol>();
|
|
247
|
+
if ((type.flags & TypeFlags.AnyOrUnknown) === 0) {
|
|
248
|
+
patternLookups += 1;
|
|
249
|
+
for (const property of checker.getPropertiesOfType(type)) {
|
|
250
|
+
table.set(property.name, property);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
tables.set(type.id, table);
|
|
254
|
+
return table;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const definedOf = (type: Type): Type | undefined => {
|
|
258
|
+
if ((type.flags & TypeFlags.Union) === 0) {
|
|
259
|
+
return type;
|
|
260
|
+
}
|
|
261
|
+
patternLookups += 1;
|
|
262
|
+
return checker.getNonNullableType(type);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The type one element of an array or tuple value has: a tuple's element at `index`,
|
|
267
|
+
* an array's element type at any index, and `undefined` for a tuple iterated whole.
|
|
268
|
+
*/
|
|
269
|
+
const elementOf = (type: Type, index: number | undefined): Type | undefined => {
|
|
270
|
+
if (!type.isTypeReference()) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
patternLookups += 1;
|
|
274
|
+
if (checker.isTupleType(type)) {
|
|
275
|
+
if (index === undefined) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
patternLookups += 1;
|
|
279
|
+
return checker.getTypeArguments(type)[index];
|
|
280
|
+
}
|
|
281
|
+
patternLookups += 1;
|
|
282
|
+
if (!checker.isArrayType(type)) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
patternLookups += 1;
|
|
286
|
+
return checker.getTypeArguments(type)[0];
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const step = (type: Type, next: Step): Type | undefined => {
|
|
290
|
+
const defined = definedOf(type);
|
|
291
|
+
if (defined === undefined) {
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
if (next.kind === "property") {
|
|
295
|
+
const property = tableOf(defined).get(next.name);
|
|
296
|
+
if (property === undefined) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
patternLookups += 1;
|
|
300
|
+
return checker.getTypeOfSymbol(property);
|
|
301
|
+
}
|
|
302
|
+
return elementOf(defined, next.kind === "element" ? next.index : undefined);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const resolve = (reads: readonly PropertyRead[]): readonly (TSSymbol | undefined)[] => {
|
|
306
|
+
const patterns = [...new Set(reads.map((read) => read.pattern))];
|
|
307
|
+
const sources = new Map<Node, Source | undefined>();
|
|
308
|
+
const asking = new Set<Node>();
|
|
309
|
+
for (const pattern of patterns) {
|
|
310
|
+
if (isObjectBindingPattern(pattern)) {
|
|
311
|
+
asking.add(pattern);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const source = sourceOf(pattern);
|
|
315
|
+
sources.set(pattern, source);
|
|
316
|
+
if (source !== undefined) {
|
|
317
|
+
asking.add(source.expression);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const answered = new Map<Node, Type>();
|
|
321
|
+
const asked = [...asking];
|
|
322
|
+
typesAt(asked).forEach((type, index) => {
|
|
323
|
+
const node = asked[index];
|
|
324
|
+
if (type !== undefined && node !== undefined) {
|
|
325
|
+
answered.set(node, type);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
const patternTypes = new Map<Node, Type | undefined>();
|
|
329
|
+
for (const pattern of patterns) {
|
|
330
|
+
const source = sources.get(pattern);
|
|
331
|
+
if (source === undefined) {
|
|
332
|
+
patternTypes.set(pattern, answered.get(pattern));
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
let type = answered.get(source.expression);
|
|
336
|
+
for (const next of source.steps) {
|
|
337
|
+
type = type === undefined ? undefined : step(type, next);
|
|
338
|
+
}
|
|
339
|
+
patternTypes.set(pattern, type === undefined ? undefined : definedOf(type));
|
|
340
|
+
}
|
|
341
|
+
return reads.map((read) => {
|
|
342
|
+
const type = patternTypes.get(read.pattern);
|
|
343
|
+
return type === undefined ? undefined : tableOf(type).get(read.name);
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
resolve,
|
|
349
|
+
get cost() {
|
|
350
|
+
return { patternBatches, patternLookups };
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
package/src/references.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from "@typescript/native/unstable/ast";
|
|
30
30
|
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
31
31
|
import { aliasChains } from "./alias-chain.ts";
|
|
32
|
+
import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuring.ts";
|
|
32
33
|
import { globExpression } from "./glob.ts";
|
|
33
34
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
34
35
|
import { byPosition, renderPosition, type Position } from "./position.ts";
|
|
@@ -71,8 +72,11 @@ export type Use = "read" | "write" | "evaluation" | "decorator";
|
|
|
71
72
|
* the literal declares rather than to the declaration that name reads.
|
|
72
73
|
* - `alias` is a step along an import or export chain, from one link to what it names.
|
|
73
74
|
* - `syntax` is the tree alone, which says what a decorator is attached to.
|
|
75
|
+
* - `destructured` is the property a pattern names, looked up on the type of the value
|
|
76
|
+
* the pattern destructures.
|
|
74
77
|
*/
|
|
75
|
-
export type Resolution =
|
|
78
|
+
export type Resolution =
|
|
79
|
+
"batch" | "resolved-symbol" | "shorthand" | "alias" | "syntax" | "destructured";
|
|
76
80
|
|
|
77
81
|
/** One use of one declaration by one declaration. */
|
|
78
82
|
export interface Reference {
|
|
@@ -119,6 +123,16 @@ export interface ReferenceCost {
|
|
|
119
123
|
readonly shorthandLookups: number;
|
|
120
124
|
/** Per-symbol lookups that step from one alias to what it names, one per alias. */
|
|
121
125
|
readonly aliasSteps: number;
|
|
126
|
+
/**
|
|
127
|
+
* Batched type lookups for the object patterns: per file, the patterns and the values
|
|
128
|
+
* assigned to destructuring targets, divided by the batch cap, rounded up.
|
|
129
|
+
*/
|
|
130
|
+
readonly patternBatches: number;
|
|
131
|
+
/**
|
|
132
|
+
* Per-type lookups for the object patterns: one property table per distinct type a
|
|
133
|
+
* pattern destructures, and each step a nested assignment target takes into its value.
|
|
134
|
+
*/
|
|
135
|
+
readonly patternLookups: number;
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
/** One project's references, in position order, and what reading them cost. */
|
|
@@ -326,9 +340,15 @@ interface Decorated {
|
|
|
326
340
|
readonly declaration: string;
|
|
327
341
|
}
|
|
328
342
|
|
|
343
|
+
/** One property a pattern reads, and the declaration the pattern is written inside. */
|
|
344
|
+
interface Read extends PropertyRead {
|
|
345
|
+
readonly from: string;
|
|
346
|
+
}
|
|
347
|
+
|
|
329
348
|
/** Everything one file's walk found to resolve, and the decorators it needs no resolution for. */
|
|
330
349
|
interface FileSites {
|
|
331
350
|
readonly uses: readonly Site[];
|
|
351
|
+
readonly reads: readonly Read[];
|
|
332
352
|
readonly links: readonly Link[];
|
|
333
353
|
readonly evaluations: readonly Evaluation[];
|
|
334
354
|
readonly decorated: readonly Decorated[];
|
|
@@ -425,6 +445,7 @@ function sitesOf(
|
|
|
425
445
|
fileId: string,
|
|
426
446
|
): FileSites {
|
|
427
447
|
const found: Site[] = [];
|
|
448
|
+
const reads: Read[] = [];
|
|
428
449
|
const links: Link[] = [];
|
|
429
450
|
const evaluations: Evaluation[] = [];
|
|
430
451
|
const decorated: Decorated[] = [];
|
|
@@ -465,6 +486,9 @@ function sitesOf(
|
|
|
465
486
|
}
|
|
466
487
|
markNames(node);
|
|
467
488
|
markStores(node, writes);
|
|
489
|
+
for (const read of propertyReadsOf(node)) {
|
|
490
|
+
reads.push({ ...read, from: enclosing });
|
|
491
|
+
}
|
|
468
492
|
links.push(...linksOf(file, node, declarations, enclosing));
|
|
469
493
|
const evaluated = evaluatedBy(node);
|
|
470
494
|
if (evaluated !== undefined) {
|
|
@@ -485,7 +509,7 @@ function sitesOf(
|
|
|
485
509
|
};
|
|
486
510
|
|
|
487
511
|
file.forEachChild(visit);
|
|
488
|
-
return { uses: found, links, evaluations, decorated };
|
|
512
|
+
return { uses: found, reads, links, evaluations, decorated };
|
|
489
513
|
}
|
|
490
514
|
|
|
491
515
|
/**
|
|
@@ -551,6 +575,7 @@ export function references<Brand>(
|
|
|
551
575
|
const found: Reference[] = [];
|
|
552
576
|
const tests: string[] = [];
|
|
553
577
|
const chains = aliasChains(project, held);
|
|
578
|
+
const destructured = destructuring(project, cap);
|
|
554
579
|
let batched = 0;
|
|
555
580
|
let fileBatches = 0;
|
|
556
581
|
let residueFallbacks = 0;
|
|
@@ -595,7 +620,7 @@ export function references<Brand>(
|
|
|
595
620
|
if (test) {
|
|
596
621
|
tests.push(path);
|
|
597
622
|
}
|
|
598
|
-
const { uses, links, evaluations, decorated } = sitesOf(
|
|
623
|
+
const { uses, reads, links, evaluations, decorated } = sitesOf(
|
|
599
624
|
file,
|
|
600
625
|
declarations,
|
|
601
626
|
declaredAt(file) ?? "",
|
|
@@ -679,6 +704,26 @@ export function references<Brand>(
|
|
|
679
704
|
}
|
|
680
705
|
}
|
|
681
706
|
|
|
707
|
+
// A pattern names a property of the value it destructures, which no name node of
|
|
708
|
+
// the pattern resolves to: a shorthand's name is the local it binds.
|
|
709
|
+
destructured.resolve(reads).forEach((symbol, index) => {
|
|
710
|
+
const read = reads[index];
|
|
711
|
+
if (symbol === undefined || read === undefined) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const position = renderPosition(file, targetRoot, read.key.getStart());
|
|
715
|
+
for (const target of chains.chainOf(symbol)) {
|
|
716
|
+
found.push({
|
|
717
|
+
from: read.from,
|
|
718
|
+
to: target.id,
|
|
719
|
+
position,
|
|
720
|
+
use: "read",
|
|
721
|
+
resolution: "destructured",
|
|
722
|
+
test,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
|
|
682
727
|
for (const site of uses) {
|
|
683
728
|
const direct: Resolution =
|
|
684
729
|
site.shorthand !== undefined
|
|
@@ -718,6 +763,14 @@ export function references<Brand>(
|
|
|
718
763
|
configFile: project.configFile,
|
|
719
764
|
references: found,
|
|
720
765
|
testFilePaths: tests.sort(compare),
|
|
721
|
-
cost: {
|
|
766
|
+
cost: {
|
|
767
|
+
batched,
|
|
768
|
+
fileBatches,
|
|
769
|
+
residueFallbacks,
|
|
770
|
+
shorthandLookups,
|
|
771
|
+
aliasSteps: chains.steps,
|
|
772
|
+
patternBatches: destructured.cost.patternBatches,
|
|
773
|
+
patternLookups: destructured.cost.patternLookups,
|
|
774
|
+
},
|
|
722
775
|
};
|
|
723
776
|
}
|