@cplieger/deadset-ts 1.1.0 → 2.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/README.md +46 -5
- package/{bin/deadset-ts.ts → dist/bin/deadset-ts.js} +2 -3
- package/dist/bin/node-host.js +74 -0
- package/dist/src/config.js +63 -0
- package/dist/src/discover.js +171 -0
- package/dist/src/host.js +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/inventory.js +676 -0
- package/dist/src/kinds.js +12 -0
- package/dist/src/paths.js +86 -0
- package/dist/src/position.js +52 -0
- package/dist/src/print.js +56 -0
- package/dist/src/ref.js +117 -0
- package/dist/src/references.js +420 -0
- package/dist/src/resolve.js +482 -0
- package/dist/src/run.js +249 -0
- package/dist/src/schema.js +335 -0
- package/dist/src/scope.js +116 -0
- package/dist/src/session.js +101 -0
- package/dist/src/version.js +7 -0
- package/jsr.json +1 -1
- package/package.json +10 -4
- package/src/host.ts +10 -2
- package/src/index.ts +39 -1
- package/src/inventory.ts +964 -0
- package/src/paths.ts +20 -0
- package/src/position.ts +75 -0
- package/src/ref.ts +179 -0
- package/src/references.ts +606 -0
- package/src/run.ts +4 -4
- package/src/session.ts +57 -22
- package/src/version.ts +3 -18
- package/bin/node-host.ts +0 -28
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { isArrayLiteralExpression, isBinaryExpression, isDeleteExpression, isElementAccessExpression, isForInStatement, isForOfStatement, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
2
|
+
import { nodeKey } from "./inventory.js";
|
|
3
|
+
import { byPosition, renderPosition } from "./position.js";
|
|
4
|
+
/**
|
|
5
|
+
* The batch cap a run takes when the configuration names none. It bounds the size of
|
|
6
|
+
* one answer rather than the work: the median file is one batch at this cap and the
|
|
7
|
+
* largest production file a handful.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_BATCH_CAP = 4096;
|
|
10
|
+
/**
|
|
11
|
+
* The operators whose left side one assignment stores into. A compound assignment is
|
|
12
|
+
* among them: it reads its target only to write the result back, so nothing else
|
|
13
|
+
* receives what it read, and a declaration whose every other reference is one of these
|
|
14
|
+
* carries no information out of itself.
|
|
15
|
+
*/
|
|
16
|
+
const ASSIGNMENTS = new Set([
|
|
17
|
+
SyntaxKind.EqualsToken,
|
|
18
|
+
SyntaxKind.PlusEqualsToken,
|
|
19
|
+
SyntaxKind.MinusEqualsToken,
|
|
20
|
+
SyntaxKind.AsteriskEqualsToken,
|
|
21
|
+
SyntaxKind.AsteriskAsteriskEqualsToken,
|
|
22
|
+
SyntaxKind.SlashEqualsToken,
|
|
23
|
+
SyntaxKind.PercentEqualsToken,
|
|
24
|
+
SyntaxKind.LessThanLessThanEqualsToken,
|
|
25
|
+
SyntaxKind.GreaterThanGreaterThanEqualsToken,
|
|
26
|
+
SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
|
|
27
|
+
SyntaxKind.AmpersandEqualsToken,
|
|
28
|
+
SyntaxKind.BarEqualsToken,
|
|
29
|
+
SyntaxKind.BarBarEqualsToken,
|
|
30
|
+
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
31
|
+
SyntaxKind.QuestionQuestionEqualsToken,
|
|
32
|
+
SyntaxKind.CaretEqualsToken,
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* The declaration forms that name something declared elsewhere. Each is a link rather
|
|
36
|
+
* than a declaration of what it names, so a reference through one names the next link
|
|
37
|
+
* and, at the end of the chain, the declaration itself: a re-export something imports
|
|
38
|
+
* is used, and so is the declaration behind it.
|
|
39
|
+
*/
|
|
40
|
+
const ALIASES = new Set([
|
|
41
|
+
SyntaxKind.ImportClause,
|
|
42
|
+
SyntaxKind.ImportEqualsDeclaration,
|
|
43
|
+
SyntaxKind.ImportSpecifier,
|
|
44
|
+
SyntaxKind.NamespaceImport,
|
|
45
|
+
SyntaxKind.ExportAssignment,
|
|
46
|
+
SyntaxKind.ExportSpecifier,
|
|
47
|
+
SyntaxKind.NamespaceExport,
|
|
48
|
+
]);
|
|
49
|
+
/** Whether one node is an identifier or a private name, the two forms a name takes. */
|
|
50
|
+
function isName(node) {
|
|
51
|
+
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
|
|
52
|
+
}
|
|
53
|
+
/** The expression one node is, through however many parentheses are written around it. */
|
|
54
|
+
function unparenthesized(node) {
|
|
55
|
+
let held = node;
|
|
56
|
+
while (isParenthesizedExpression(held)) {
|
|
57
|
+
held = held.expression;
|
|
58
|
+
}
|
|
59
|
+
return held;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Records the name one store writes into.
|
|
63
|
+
*
|
|
64
|
+
* A property access writes the member it names. An element access writes the collection
|
|
65
|
+
* it stores into, because a collection a module only ever stores into holds nothing
|
|
66
|
+
* anything reads, and the key is no declaration of the target. A destructuring target
|
|
67
|
+
* writes each name it binds, through the spreads and the nested patterns it binds them
|
|
68
|
+
* in.
|
|
69
|
+
*/
|
|
70
|
+
function markWrite(target, writes) {
|
|
71
|
+
const held = unparenthesized(target);
|
|
72
|
+
if (isName(held)) {
|
|
73
|
+
writes.add(held.pos);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (isPropertyAccessExpression(held)) {
|
|
77
|
+
if (isName(held.name)) {
|
|
78
|
+
writes.add(held.name.pos);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (isElementAccessExpression(held)) {
|
|
83
|
+
markWrite(held.expression, writes);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (isArrayLiteralExpression(held)) {
|
|
87
|
+
for (const element of held.elements) {
|
|
88
|
+
markWrite(element, writes);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (isObjectLiteralExpression(held)) {
|
|
93
|
+
for (const property of held.properties) {
|
|
94
|
+
if (isPropertyAssignment(property)) {
|
|
95
|
+
markWrite(property.initializer, writes);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (isShorthandPropertyAssignment(property)) {
|
|
99
|
+
markWrite(property.name, writes);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (isSpreadAssignment(property)) {
|
|
103
|
+
markWrite(property.expression, writes);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (isSpreadElement(held)) {
|
|
109
|
+
markWrite(held.expression, writes);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Records every store one node performs: an assignment, a compound assignment, an
|
|
114
|
+
* increment or a decrement, a `delete`, and an iteration that assigns to a name declared
|
|
115
|
+
* elsewhere. An iteration that declares its own name stores into nothing the inventory
|
|
116
|
+
* holds.
|
|
117
|
+
*/
|
|
118
|
+
function markStores(node, writes) {
|
|
119
|
+
if (isBinaryExpression(node)) {
|
|
120
|
+
if (ASSIGNMENTS.has(node.operatorToken.kind)) {
|
|
121
|
+
markWrite(node.left, writes);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (isPrefixUnaryExpression(node) || isPostfixUnaryExpression(node)) {
|
|
126
|
+
if (node.operator === SyntaxKind.PlusPlusToken ||
|
|
127
|
+
node.operator === SyntaxKind.MinusMinusToken) {
|
|
128
|
+
markWrite(node.operand, writes);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (isDeleteExpression(node)) {
|
|
133
|
+
markWrite(node.expression, writes);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (isForInStatement(node) || isForOfStatement(node)) {
|
|
137
|
+
if (node.initializer.kind !== SyntaxKind.VariableDeclarationList) {
|
|
138
|
+
markWrite(node.initializer, writes);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Every name node one file holds that is a use rather than a declaration, in source
|
|
144
|
+
* order, each with the declaration that encloses it and the use it makes.
|
|
145
|
+
*
|
|
146
|
+
* One walk answers all three, because the three facts are positional and a second walk
|
|
147
|
+
* would have to agree with this one about which nodes are names. A name that is the
|
|
148
|
+
* declared name of the declaration it belongs to is left out: it is where that
|
|
149
|
+
* declaration is written rather than a use of it, which is what makes a declaration
|
|
150
|
+
* referenced only from its own site an unreferenced declaration.
|
|
151
|
+
*/
|
|
152
|
+
function sitesOf(file, declarations, fileId) {
|
|
153
|
+
const found = [];
|
|
154
|
+
const writes = new Set();
|
|
155
|
+
const declared = new Set();
|
|
156
|
+
const shorthands = new Map();
|
|
157
|
+
let enclosing = fileId;
|
|
158
|
+
const markNames = (node) => {
|
|
159
|
+
if (isShorthandPropertyAssignment(node)) {
|
|
160
|
+
shorthands.set(node.name.pos, node);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// A property access carries the member it names where a declaration carries its
|
|
164
|
+
// declared name, and it is the one form of the grammar that does.
|
|
165
|
+
if (isPropertyAccessExpression(node)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const named = node;
|
|
169
|
+
for (const name of [named.name, named.propertyName]) {
|
|
170
|
+
if (name !== undefined && isName(name)) {
|
|
171
|
+
declared.add(name.pos);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const visit = (node) => {
|
|
176
|
+
if (isName(node)) {
|
|
177
|
+
if (!declared.has(node.pos)) {
|
|
178
|
+
found.push({
|
|
179
|
+
node,
|
|
180
|
+
shorthand: shorthands.get(node.pos),
|
|
181
|
+
from: enclosing,
|
|
182
|
+
use: writes.has(node.pos) ? "write" : "read",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
markNames(node);
|
|
188
|
+
markStores(node, writes);
|
|
189
|
+
const held = declarations.get(nodeKey(file, node));
|
|
190
|
+
const outer = enclosing;
|
|
191
|
+
if (held !== undefined) {
|
|
192
|
+
enclosing = held;
|
|
193
|
+
}
|
|
194
|
+
node.forEachChild(visit);
|
|
195
|
+
enclosing = outer;
|
|
196
|
+
};
|
|
197
|
+
file.forEachChild(visit);
|
|
198
|
+
return found;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* One literal run of a pattern, with every character the expression syntax claims
|
|
202
|
+
* escaped.
|
|
203
|
+
*
|
|
204
|
+
* The set is the syntax characters and the solidus, and it is closed: an escape of any
|
|
205
|
+
* other character is refused outright by the expression syntax under the unicode flag,
|
|
206
|
+
* so escaping a hyphen, which needs none outside a character class, would refuse every
|
|
207
|
+
* pattern that names a hyphenated path.
|
|
208
|
+
*/
|
|
209
|
+
function quoted(text) {
|
|
210
|
+
return text.replaceAll(/[.*+?^${}()|[\]\\/]/gu, (character) => `\\${character}`);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The expression one glob pattern denotes. `**` spans path separators while `*` and `?`
|
|
214
|
+
* do not, a brace list is an alternation of its members, and every other character
|
|
215
|
+
* stands for itself.
|
|
216
|
+
*/
|
|
217
|
+
function expressionOf(pattern) {
|
|
218
|
+
let source = "^";
|
|
219
|
+
let at = 0;
|
|
220
|
+
while (at < pattern.length) {
|
|
221
|
+
const rest = pattern.slice(at);
|
|
222
|
+
if (rest.startsWith("**/")) {
|
|
223
|
+
source += "(?:[^/]*/)*";
|
|
224
|
+
at += 3;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (rest.startsWith("**")) {
|
|
228
|
+
source += ".*";
|
|
229
|
+
at += 2;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const character = pattern[at] ?? "";
|
|
233
|
+
at += 1;
|
|
234
|
+
if (character === "*") {
|
|
235
|
+
source += "[^/]*";
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (character === "?") {
|
|
239
|
+
source += "[^/]";
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (character === "{") {
|
|
243
|
+
const close = pattern.indexOf("}", at);
|
|
244
|
+
if (close < 0) {
|
|
245
|
+
source += "\\{";
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
source += `(?:${pattern.slice(at, close).split(",").map(quoted).join("|")})`;
|
|
249
|
+
at = close + 1;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
source += quoted(character);
|
|
253
|
+
}
|
|
254
|
+
return new RegExp(`${source}$`, "u");
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The name one test-file rule takes. A rule's name is a word list the report's own
|
|
258
|
+
* vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
|
|
259
|
+
* its pattern in the configured list.
|
|
260
|
+
*/
|
|
261
|
+
function patternRule(index) {
|
|
262
|
+
return `test-file-pattern-${String(index + 1)}`;
|
|
263
|
+
}
|
|
264
|
+
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
265
|
+
function compare(a, b) {
|
|
266
|
+
if (a === b) {
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
return a < b ? -1 : 1;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Every reference one project's own files make to the project's own declarations, in
|
|
273
|
+
* position order, and the test-file rules that classified those files.
|
|
274
|
+
*
|
|
275
|
+
* `held` is the inventory of the same project, whose declaration map is what a resolved
|
|
276
|
+
* symbol is looked up in; `targetRoot` is the absolute path every position is rendered
|
|
277
|
+
* against, which is the root the inventory was read against.
|
|
278
|
+
*
|
|
279
|
+
* The order is by the referencing position, then by the declaration named, the
|
|
280
|
+
* declaration referencing it and the use. That is a total order because one name
|
|
281
|
+
* references one declaration once from one declaration, while a chain of aliases reaches
|
|
282
|
+
* several declarations at one position.
|
|
283
|
+
*/
|
|
284
|
+
export function references(project, held, targetRoot, options) {
|
|
285
|
+
const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
|
|
286
|
+
const declarations = held.declarations;
|
|
287
|
+
const patterns = options.testFiles.map(expressionOf);
|
|
288
|
+
const matched = options.testFiles.map(() => 0);
|
|
289
|
+
const found = [];
|
|
290
|
+
const reached = new Map();
|
|
291
|
+
let batched = 0;
|
|
292
|
+
let fileBatches = 0;
|
|
293
|
+
let residueFallbacks = 0;
|
|
294
|
+
let shorthandLookups = 0;
|
|
295
|
+
let aliasSteps = 0;
|
|
296
|
+
const files = project.ownSourceFiles();
|
|
297
|
+
const ownFiles = new Set(files.map((file) => file.fileName));
|
|
298
|
+
/** The identifier of the declaration one node of this program is, where it is one. */
|
|
299
|
+
const declaredAt = (node) => declarations.get(nodeKey(node.getSourceFile(), node));
|
|
300
|
+
/**
|
|
301
|
+
* The declarations one symbol names: the declaration it is, and every link of the
|
|
302
|
+
* alias chain it stands at the head of. A re-export something imports is used and so
|
|
303
|
+
* is the declaration behind it, so both are named; a link this project does not
|
|
304
|
+
* declare is stepped over rather than recorded.
|
|
305
|
+
*
|
|
306
|
+
* A declaration handle carries the file it is in and the syntax it is written as, so
|
|
307
|
+
* both questions the walk asks of a declaration are answered before it is resolved: a
|
|
308
|
+
* declaration outside the project's own files belongs to another program and is never
|
|
309
|
+
* read, which is what keeps the pass from fetching a library file to discover that the
|
|
310
|
+
* inventory does not hold what is in it.
|
|
311
|
+
*/
|
|
312
|
+
const targetsOf = (symbol) => {
|
|
313
|
+
const cached = reached.get(symbol.id);
|
|
314
|
+
if (cached !== undefined) {
|
|
315
|
+
return cached;
|
|
316
|
+
}
|
|
317
|
+
const targets = [];
|
|
318
|
+
const walked = new Set();
|
|
319
|
+
let at = symbol;
|
|
320
|
+
let stepped = false;
|
|
321
|
+
while (at !== undefined && !walked.has(at.id)) {
|
|
322
|
+
walked.add(at.id);
|
|
323
|
+
let alias = false;
|
|
324
|
+
for (const handle of at.declarations) {
|
|
325
|
+
alias = alias || ALIASES.has(handle.kind);
|
|
326
|
+
if (!ownFiles.has(handle.path)) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const node = project.declarationAt(handle)?.node;
|
|
330
|
+
const id = node === undefined ? undefined : declaredAt(node);
|
|
331
|
+
if (id !== undefined && !targets.some((target) => target.id === id)) {
|
|
332
|
+
targets.push({ id, stepped });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!alias) {
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
at = project.aliasStepOf(at);
|
|
339
|
+
aliasSteps += 1;
|
|
340
|
+
stepped = true;
|
|
341
|
+
}
|
|
342
|
+
reached.set(symbol.id, targets);
|
|
343
|
+
return targets;
|
|
344
|
+
};
|
|
345
|
+
/** The symbol one site resolves to, asking only the accessor the site's form names. */
|
|
346
|
+
const symbolFor = (site, answered) => {
|
|
347
|
+
if (site.shorthand !== undefined) {
|
|
348
|
+
shorthandLookups += 1;
|
|
349
|
+
return project.shorthandValueAt(project.handle(site.shorthand));
|
|
350
|
+
}
|
|
351
|
+
const batched = answered.get(site.node.pos);
|
|
352
|
+
if (batched !== undefined) {
|
|
353
|
+
return batched;
|
|
354
|
+
}
|
|
355
|
+
residueFallbacks += 1;
|
|
356
|
+
return project.resolvedSymbolAt(project.handle(site.node));
|
|
357
|
+
};
|
|
358
|
+
for (const file of files) {
|
|
359
|
+
const path = renderPosition(file, targetRoot, 0).path;
|
|
360
|
+
let test = false;
|
|
361
|
+
patterns.forEach((pattern, index) => {
|
|
362
|
+
if (pattern.test(path)) {
|
|
363
|
+
matched[index] = (matched[index] ?? 0) + 1;
|
|
364
|
+
test = true;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
const sites = sitesOf(file, declarations, declaredAt(file) ?? "");
|
|
368
|
+
// One batch per capped run of the file's name nodes. A shorthand is left out of
|
|
369
|
+
// the batch: its name resolves to the property the literal declares, so the batch's
|
|
370
|
+
// answer for it would name something this project's inventory never holds.
|
|
371
|
+
const batching = sites.filter((site) => site.shorthand === undefined);
|
|
372
|
+
const answered = new Map();
|
|
373
|
+
batched += batching.length;
|
|
374
|
+
for (let from = 0; from < batching.length; from += cap) {
|
|
375
|
+
const run = batching.slice(from, from + cap);
|
|
376
|
+
const answers = project.symbolsAt(run.map((site) => project.handle(site.node)));
|
|
377
|
+
fileBatches += 1;
|
|
378
|
+
run.forEach((site, index) => {
|
|
379
|
+
const symbol = answers[index];
|
|
380
|
+
if (symbol !== undefined) {
|
|
381
|
+
answered.set(site.node.pos, symbol);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
for (const site of sites) {
|
|
386
|
+
const direct = site.shorthand !== undefined
|
|
387
|
+
? "shorthand"
|
|
388
|
+
: answered.has(site.node.pos)
|
|
389
|
+
? "batch"
|
|
390
|
+
: "resolved-symbol";
|
|
391
|
+
const symbol = symbolFor(site, answered);
|
|
392
|
+
if (symbol === undefined) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const position = renderPosition(file, targetRoot, site.node.getStart());
|
|
396
|
+
for (const target of targetsOf(symbol)) {
|
|
397
|
+
found.push({
|
|
398
|
+
from: site.from,
|
|
399
|
+
to: target.id,
|
|
400
|
+
position,
|
|
401
|
+
use: site.use,
|
|
402
|
+
resolution: target.stepped ? "alias" : direct,
|
|
403
|
+
test,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
found.sort((a, b) => byPosition(a.position, b.position) ||
|
|
409
|
+
compare(a.to, b.to) ||
|
|
410
|
+
compare(a.from, b.from) ||
|
|
411
|
+
compare(a.use, b.use));
|
|
412
|
+
return {
|
|
413
|
+
configFile: project.configFile,
|
|
414
|
+
references: found,
|
|
415
|
+
testFileRules: options.testFiles
|
|
416
|
+
.map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
|
|
417
|
+
.sort((a, b) => compare(a.rule, b.rule)),
|
|
418
|
+
cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps },
|
|
419
|
+
};
|
|
420
|
+
}
|