test-inventory 0.6.1 → 0.8.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/CHANGELOG.md +100 -0
- package/README.md +4 -3
- package/dist/case-titles.d.ts +27 -0
- package/dist/case-titles.js +329 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -5
- package/dist/config-dependencies.js +8 -3
- package/dist/config-runs.d.ts +22 -0
- package/dist/config-runs.js +263 -3
- package/dist/config.d.ts +18 -1
- package/dist/config.js +160 -14
- package/dist/dependency-hash.d.ts +13 -5
- package/dist/dependency-hash.js +56 -12
- package/dist/diagnostics.js +3 -0
- package/dist/extract.d.ts +19 -1
- package/dist/extract.js +391 -41
- package/dist/files.js +3 -2
- package/dist/filter.js +1 -0
- package/dist/framework-version.d.ts +13 -2
- package/dist/framework-version.js +64 -5
- package/dist/frameworks/bindings.d.ts +14 -0
- package/dist/frameworks/bindings.js +99 -6
- package/dist/frameworks/common.d.ts +6 -0
- package/dist/frameworks/common.js +27 -0
- package/dist/frameworks/jest.d.ts +46 -0
- package/dist/frameworks/jest.js +442 -0
- package/dist/frameworks/playwright.d.ts +3 -1
- package/dist/frameworks/playwright.js +14 -5
- package/dist/frameworks/test-functions.d.ts +169 -0
- package/dist/frameworks/test-functions.js +492 -0
- package/dist/frameworks/vitest.d.ts +13 -58
- package/dist/frameworks/vitest.js +84 -378
- package/dist/imported-values.d.ts +22 -0
- package/dist/imported-values.js +149 -0
- package/dist/model.d.ts +11 -0
- package/dist/options.d.ts +16 -1
- package/dist/options.js +9 -2
- package/dist/parse.js +17 -2
- package/dist/records.js +57 -15
- package/dist/resolve.js +39 -36
- package/dist/scan-file.d.ts +5 -0
- package/dist/scan-file.js +15 -8
- package/dist/scan-source.js +3 -1
- package/dist/scan.js +31 -10
- package/dist/types.d.ts +24 -6
- package/dist/values.d.ts +49 -1
- package/dist/values.js +531 -19
- package/package.json +1 -1
- package/schema/v1.json +33 -4
package/dist/values.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { propertyName, unwrap } from "./ast.js";
|
|
1
|
+
import { childNodes, isFunctionNode, propertyKey, propertyName, unwrap } from "./ast.js";
|
|
2
2
|
const UNRESOLVED = { ok: false, importedFrom: null };
|
|
3
3
|
function isPrimitive(value) {
|
|
4
4
|
return typeof value !== 'object' || value === null;
|
|
@@ -58,6 +58,13 @@ export class Values {
|
|
|
58
58
|
pending = new Set();
|
|
59
59
|
/** The functions being read at one of their calls, innermost last. */
|
|
60
60
|
calls = [];
|
|
61
|
+
imports;
|
|
62
|
+
assigned = new WeakMap();
|
|
63
|
+
changed = new WeakMap();
|
|
64
|
+
/** Without `imports`, a value imported from another module is not worked out. */
|
|
65
|
+
constructor(imports = null) {
|
|
66
|
+
this.imports = imports;
|
|
67
|
+
}
|
|
61
68
|
/**
|
|
62
69
|
* Runs `read` with a function's parameters bound to the arguments of one call, worked out where the call is, so that
|
|
63
70
|
* titles and values built from them resolve, as in `test(\`${side} rejects a zero quantity\`)` inside
|
|
@@ -67,16 +74,33 @@ export class Values {
|
|
|
67
74
|
withArguments(fn, args, scopes, read) {
|
|
68
75
|
const parameters = new Map();
|
|
69
76
|
for (const [index, parameter] of fn.params.entries()) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
this.bind(parameter, args.at(index), parameters, [...scopes, fn]);
|
|
78
|
+
}
|
|
79
|
+
// A parameter the function assigns to may hold another value by the time it is read.
|
|
80
|
+
for (const name of this.assignedIn(fn.body))
|
|
81
|
+
parameters.delete(name);
|
|
82
|
+
return this.withBound(fn, parameters, read);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Runs `read` with the names a loop declares bound to one of the values it goes over, as in
|
|
86
|
+
* `for (const [tier, bps] of Object.entries(FEES))`. `declarator` is the loop's variable declarator.
|
|
87
|
+
*/
|
|
88
|
+
withLoopValue(declarator, pattern, value, body, scopes, read) {
|
|
89
|
+
const names = new Map();
|
|
90
|
+
this.bind(pattern, value, names, scopes);
|
|
91
|
+
for (const name of this.assignedIn(body))
|
|
92
|
+
names.delete(name);
|
|
93
|
+
return this.withBound(declarator, names, read);
|
|
94
|
+
}
|
|
95
|
+
assignedIn(body) {
|
|
96
|
+
let names = this.assigned.get(body);
|
|
97
|
+
if (!names) {
|
|
98
|
+
names = assignedNames(body);
|
|
99
|
+
this.assigned.set(body, names);
|
|
79
100
|
}
|
|
101
|
+
return names;
|
|
102
|
+
}
|
|
103
|
+
withBound(fn, parameters, read) {
|
|
80
104
|
this.calls.push({ fn, parameters, cache: new Map() });
|
|
81
105
|
try {
|
|
82
106
|
return read();
|
|
@@ -85,6 +109,64 @@ export class Values {
|
|
|
85
109
|
this.calls.pop();
|
|
86
110
|
}
|
|
87
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Binds the names of a parameter or a loop variable to the parts of `value` they take, as destructuring does. A
|
|
114
|
+
* missing `value`, after a spread argument, leaves them unknown, unless a default gives them one.
|
|
115
|
+
*/
|
|
116
|
+
bind(pattern, value, names, scopes) {
|
|
117
|
+
switch (pattern.type) {
|
|
118
|
+
case 'Identifier':
|
|
119
|
+
if (value)
|
|
120
|
+
names.set(pattern.name, () => value);
|
|
121
|
+
return;
|
|
122
|
+
case 'AssignmentPattern': {
|
|
123
|
+
const useDefault = !value || (value.ok && value.value === undefined);
|
|
124
|
+
if (!useDefault) {
|
|
125
|
+
this.bind(pattern.left, value, names, scopes);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const fallback = pattern.right;
|
|
129
|
+
if (pattern.left.type === 'Identifier') {
|
|
130
|
+
names.set(pattern.left.name, () => this.resolve(fallback, scopes));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.bind(pattern.left, this.resolve(fallback, scopes), names, scopes);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
case 'ArrayPattern':
|
|
137
|
+
for (const [index, element] of pattern.elements.entries()) {
|
|
138
|
+
if (!element || !value)
|
|
139
|
+
continue;
|
|
140
|
+
const list = value.ok && Array.isArray(value.value) ? value.value : null;
|
|
141
|
+
const part = (rest) => list ? { ok: true, value: rest ? list.slice(index) : list[index] } : value.ok ? UNRESOLVED : value;
|
|
142
|
+
if (element.type === 'RestElement')
|
|
143
|
+
this.bind(element.argument, part(true), names, scopes);
|
|
144
|
+
else
|
|
145
|
+
this.bind(element, part(false), names, scopes);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
case 'ObjectPattern': {
|
|
149
|
+
if (!value)
|
|
150
|
+
return;
|
|
151
|
+
const object = value.ok && isPlainObject(value.value) ? value.value : null;
|
|
152
|
+
const taken = [];
|
|
153
|
+
for (const property of pattern.properties) {
|
|
154
|
+
if (property.type === 'RestElement') {
|
|
155
|
+
const rest = object && Object.fromEntries(Object.entries(object).filter(([key]) => !taken.includes(key)));
|
|
156
|
+
this.bind(property.argument, rest ? { ok: true, value: rest } : value.ok ? UNRESOLVED : value, names, scopes);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const key = this.propertyKey(property.key, property.computed, scopes);
|
|
160
|
+
const name = key.ok && isKey(key.value) ? String(key.value) : null;
|
|
161
|
+
if (name !== null)
|
|
162
|
+
taken.push(name);
|
|
163
|
+
const part = object && name !== null ? { ok: true, value: object[name] } : value.ok ? UNRESOLVED : value;
|
|
164
|
+
this.bind(property.value, part, names, scopes);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
88
170
|
/** `scopes` lists the scope nodes around the expression, outermost first. */
|
|
89
171
|
resolve(node, scopes) {
|
|
90
172
|
const expression = unwrap(node);
|
|
@@ -105,7 +187,7 @@ export class Values {
|
|
|
105
187
|
return { ok: true, value: text };
|
|
106
188
|
}
|
|
107
189
|
case 'BinaryExpression': {
|
|
108
|
-
if (expression.operator
|
|
190
|
+
if (!BINARY.has(expression.operator))
|
|
109
191
|
return UNRESOLVED;
|
|
110
192
|
const left = this.resolve(expression.left, scopes);
|
|
111
193
|
if (!left.ok)
|
|
@@ -115,13 +197,27 @@ export class Values {
|
|
|
115
197
|
return right;
|
|
116
198
|
if (!isPrimitive(left.value) || !isPrimitive(right.value))
|
|
117
199
|
return UNRESOLVED;
|
|
118
|
-
|
|
119
|
-
return { ok: true, value: left.value + right.value };
|
|
120
|
-
}
|
|
121
|
-
return typeof left.value === 'string' || typeof right.value === 'string'
|
|
122
|
-
? { ok: true, value: String(left.value) + String(right.value) }
|
|
123
|
-
: UNRESOLVED;
|
|
200
|
+
return binary(expression.operator, left.value, right.value);
|
|
124
201
|
}
|
|
202
|
+
case 'LogicalExpression': {
|
|
203
|
+
const left = this.resolve(expression.left, scopes);
|
|
204
|
+
if (!left.ok)
|
|
205
|
+
return left;
|
|
206
|
+
const useLeft = expression.operator === '&&'
|
|
207
|
+
? !left.value
|
|
208
|
+
: expression.operator === '||'
|
|
209
|
+
? Boolean(left.value)
|
|
210
|
+
: left.value !== null && left.value !== undefined;
|
|
211
|
+
return useLeft ? left : this.resolve(expression.right, scopes);
|
|
212
|
+
}
|
|
213
|
+
case 'ConditionalExpression': {
|
|
214
|
+
const test = this.resolve(expression.test, scopes);
|
|
215
|
+
if (!test.ok)
|
|
216
|
+
return test;
|
|
217
|
+
return this.resolve(test.value ? expression.consequent : expression.alternate, scopes);
|
|
218
|
+
}
|
|
219
|
+
case 'CallExpression':
|
|
220
|
+
return this.call(expression, scopes);
|
|
125
221
|
case 'UnaryExpression': {
|
|
126
222
|
if (expression.operator !== '!' && expression.operator !== '-')
|
|
127
223
|
return UNRESOLVED;
|
|
@@ -181,6 +277,9 @@ export class Values {
|
|
|
181
277
|
case 'Identifier':
|
|
182
278
|
return this.resolveName(expression.name, scopes);
|
|
183
279
|
case 'MemberExpression': {
|
|
280
|
+
const namespace = this.namespaceMember(expression, scopes);
|
|
281
|
+
if (namespace)
|
|
282
|
+
return namespace;
|
|
184
283
|
const object = this.resolve(expression.object, scopes);
|
|
185
284
|
if (!object.ok)
|
|
186
285
|
return object;
|
|
@@ -190,6 +289,8 @@ export class Values {
|
|
|
190
289
|
if (!key.ok)
|
|
191
290
|
return key;
|
|
192
291
|
const container = object.value;
|
|
292
|
+
if (typeof container === 'string' && key.value === 'length')
|
|
293
|
+
return { ok: true, value: container.length };
|
|
193
294
|
if (typeof container !== 'object' || container === null || !isKey(key.value))
|
|
194
295
|
return UNRESOLVED;
|
|
195
296
|
const name = String(key.value);
|
|
@@ -213,11 +314,26 @@ export class Values {
|
|
|
213
314
|
const binding = this.lookup(name, scopes)?.binding;
|
|
214
315
|
return binding?.kind === 'const' ? { init: binding.init, scopes: binding.scopes } : null;
|
|
215
316
|
}
|
|
317
|
+
/** The name a module exports that a name imported from it stands for, `default` for a default import; null otherwise. */
|
|
318
|
+
importedName(name, scopes) {
|
|
319
|
+
const binding = this.lookup(name, scopes)?.binding;
|
|
320
|
+
return binding?.kind === 'import' ? binding.imported : null;
|
|
321
|
+
}
|
|
216
322
|
/** The module a name is imported from where it is used; null when it isn't an import. */
|
|
217
323
|
importSource(name, scopes) {
|
|
218
324
|
const binding = this.lookup(name, scopes)?.binding;
|
|
219
325
|
return binding?.kind === 'import' ? binding.source : null;
|
|
220
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Whether a name refers to a parameter or loop variable bound to a value where it is used: an argument of the helper
|
|
329
|
+
* call being walked, or a value of the case or pass of an expansion.
|
|
330
|
+
*/
|
|
331
|
+
isBound(name, scopes) {
|
|
332
|
+
const binding = this.lookup(name, scopes)?.binding;
|
|
333
|
+
if (binding?.kind !== 'other')
|
|
334
|
+
return false;
|
|
335
|
+
return this.calls.findLast((item) => item.fn === binding.declaration)?.parameters.has(name) ?? false;
|
|
336
|
+
}
|
|
221
337
|
/** Whether a name refers to a function or class declared in the file. */
|
|
222
338
|
isFunction(name, scopes) {
|
|
223
339
|
return this.lookup(name, scopes)?.binding.kind === 'function';
|
|
@@ -229,6 +345,143 @@ export class Values {
|
|
|
229
345
|
return { ok: true, value: key.name };
|
|
230
346
|
return this.resolve(key, scopes);
|
|
231
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* The calls titles are often built with, all of them pure: `Object.keys()`, `Object.values()`, `Object.entries()`,
|
|
350
|
+
* `JSON.stringify()` with one argument, `String()`, and a string's `toUpperCase()`, `toLowerCase()` and `trim()` and
|
|
351
|
+
* an array's `join()`.
|
|
352
|
+
*/
|
|
353
|
+
call(expression, scopes) {
|
|
354
|
+
const callee = unwrap(expression.callee);
|
|
355
|
+
if (expression.optional || expression.arguments.some((item) => item.type === 'SpreadElement'))
|
|
356
|
+
return UNRESOLVED;
|
|
357
|
+
const args = expression.arguments;
|
|
358
|
+
if (callee.type === 'Identifier' &&
|
|
359
|
+
callee.name === 'String' &&
|
|
360
|
+
args.length === 1 &&
|
|
361
|
+
this.isGlobal('String', scopes)) {
|
|
362
|
+
const value = this.resolve(args[0], scopes);
|
|
363
|
+
return value.ok && isPrimitive(value.value)
|
|
364
|
+
? { ok: true, value: String(value.value) }
|
|
365
|
+
: value.ok
|
|
366
|
+
? UNRESOLVED
|
|
367
|
+
: value;
|
|
368
|
+
}
|
|
369
|
+
if (callee.type !== 'MemberExpression' || callee.computed)
|
|
370
|
+
return UNRESOLVED;
|
|
371
|
+
const method = propertyName(callee);
|
|
372
|
+
const target = unwrap(callee.object);
|
|
373
|
+
if (target.type === 'Identifier' && (target.name === 'Object' || target.name === 'JSON')) {
|
|
374
|
+
if (!this.isGlobal(target.name, scopes) || args.length !== 1)
|
|
375
|
+
return UNRESOLVED;
|
|
376
|
+
const value = this.resolve(args[0], scopes);
|
|
377
|
+
if (!value.ok)
|
|
378
|
+
return value;
|
|
379
|
+
if (target.name === 'JSON')
|
|
380
|
+
return method === 'stringify' ? { ok: true, value: JSON.stringify(value.value) } : UNRESOLVED;
|
|
381
|
+
if (!isPlainObject(value.value) && !Array.isArray(value.value))
|
|
382
|
+
return UNRESOLVED;
|
|
383
|
+
if (method === 'keys')
|
|
384
|
+
return { ok: true, value: Object.keys(value.value) };
|
|
385
|
+
if (method === 'values')
|
|
386
|
+
return { ok: true, value: Object.values(value.value) };
|
|
387
|
+
return method === 'entries' ? { ok: true, value: Object.entries(value.value) } : UNRESOLVED;
|
|
388
|
+
}
|
|
389
|
+
const object = this.resolve(callee.object, scopes);
|
|
390
|
+
if (!object.ok)
|
|
391
|
+
return object;
|
|
392
|
+
const text = object.value;
|
|
393
|
+
if (typeof text === 'string' && args.length === 0 && STRING_METHODS.has(method)) {
|
|
394
|
+
return {
|
|
395
|
+
ok: true,
|
|
396
|
+
value: method === 'trim' ? text.trim() : method === 'toUpperCase' ? text.toUpperCase() : text.toLowerCase(),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
if (!Array.isArray(text) || method !== 'join' || args.length > 1 || !text.every(isPrimitive))
|
|
400
|
+
return UNRESOLVED;
|
|
401
|
+
const separator = args.length === 0 ? { ok: true, value: ',' } : this.resolve(args[0], scopes);
|
|
402
|
+
if (!separator.ok)
|
|
403
|
+
return separator;
|
|
404
|
+
return typeof separator.value === 'string' ? { ok: true, value: text.join(separator.value) } : UNRESOLVED;
|
|
405
|
+
}
|
|
406
|
+
/** Whether a name refers to a global, as no scope around the use declares it. */
|
|
407
|
+
isGlobal(name, scopes) {
|
|
408
|
+
return this.lookup(name, scopes) === null;
|
|
409
|
+
}
|
|
410
|
+
/** `data.name` where `data` is `import * as data`: what the module exports as `name`; null for any other member. */
|
|
411
|
+
namespaceMember(expression, scopes) {
|
|
412
|
+
const object = unwrap(expression.object);
|
|
413
|
+
if (object.type !== 'Identifier')
|
|
414
|
+
return null;
|
|
415
|
+
const binding = this.lookup(object.name, scopes)?.binding;
|
|
416
|
+
if (binding?.kind !== 'import' || binding.imported !== null)
|
|
417
|
+
return null;
|
|
418
|
+
const key = expression.computed
|
|
419
|
+
? this.resolve(expression.property, scopes)
|
|
420
|
+
: { ok: true, value: propertyName(expression) };
|
|
421
|
+
if (!key.ok || !isKey(key.value))
|
|
422
|
+
return { ok: false, importedFrom: binding.source };
|
|
423
|
+
return this.imported(binding.source, String(key.value));
|
|
424
|
+
}
|
|
425
|
+
imported(source, name) {
|
|
426
|
+
const resolved = name !== null && this.imports ? this.imports(source, name) : UNRESOLVED;
|
|
427
|
+
return resolved.ok ? resolved : { ok: false, importedFrom: source };
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* How many items a list has when not all its values are known, as `[NaN, Infinity]`: a list written out, its spreads
|
|
431
|
+
* included, held in a constant the module doesn't change, or the keys, values or entries of an object written out;
|
|
432
|
+
* null otherwise. `seen` stops at a constant defined through itself.
|
|
433
|
+
*/
|
|
434
|
+
lengthOf(node, scopes, seen = new Set()) {
|
|
435
|
+
const resolved = this.resolve(node, scopes);
|
|
436
|
+
if (resolved.ok)
|
|
437
|
+
return Array.isArray(resolved.value) ? resolved.value.length : null;
|
|
438
|
+
const expression = unwrap(node);
|
|
439
|
+
if (expression.type === 'Identifier') {
|
|
440
|
+
const binding = this.lookup(expression.name, scopes)?.binding;
|
|
441
|
+
if (binding?.kind !== 'const' || seen.has(expression.name))
|
|
442
|
+
return null;
|
|
443
|
+
if (this.changedAtLoad(binding.scopes[0]).has(expression.name))
|
|
444
|
+
return null;
|
|
445
|
+
return this.lengthOf(binding.init, binding.scopes, new Set([...seen, expression.name]));
|
|
446
|
+
}
|
|
447
|
+
if (expression.type === 'CallExpression' && isObjectListing(expression)) {
|
|
448
|
+
const target = expression.arguments[0];
|
|
449
|
+
return this.keyCount(target, scopes, seen);
|
|
450
|
+
}
|
|
451
|
+
if (expression.type !== 'ArrayExpression')
|
|
452
|
+
return null;
|
|
453
|
+
let length = 0;
|
|
454
|
+
for (const element of expression.elements) {
|
|
455
|
+
const inner = element?.type === 'SpreadElement' ? this.lengthOf(element.argument, scopes, seen) : 1;
|
|
456
|
+
if (inner === null)
|
|
457
|
+
return null;
|
|
458
|
+
length += inner;
|
|
459
|
+
}
|
|
460
|
+
return length;
|
|
461
|
+
}
|
|
462
|
+
/** How many keys an object written out has, held in a constant the module doesn't change; null otherwise. */
|
|
463
|
+
keyCount(node, scopes, seen) {
|
|
464
|
+
const expression = unwrap(node);
|
|
465
|
+
if (expression.type === 'Identifier') {
|
|
466
|
+
const binding = this.lookup(expression.name, scopes)?.binding;
|
|
467
|
+
if (binding?.kind !== 'const' || seen.has(expression.name))
|
|
468
|
+
return null;
|
|
469
|
+
if (this.changedAtLoad(binding.scopes[0]).has(expression.name))
|
|
470
|
+
return null;
|
|
471
|
+
return this.keyCount(binding.init, binding.scopes, new Set([...seen, expression.name]));
|
|
472
|
+
}
|
|
473
|
+
if (expression.type !== 'ObjectExpression')
|
|
474
|
+
return null;
|
|
475
|
+
const keys = new Set();
|
|
476
|
+
for (const property of expression.properties) {
|
|
477
|
+
const key = property.type === 'Property' ? propertyKey(property) : null;
|
|
478
|
+
if (key === null)
|
|
479
|
+
return null;
|
|
480
|
+
keys.add(key);
|
|
481
|
+
}
|
|
482
|
+
return keys.size;
|
|
483
|
+
}
|
|
484
|
+
/** What a name refers to where it is used; `scopes` lists the scope nodes around the use, outermost first. */
|
|
232
485
|
resolveName(name, scopes) {
|
|
233
486
|
const found = this.lookup(name, scopes);
|
|
234
487
|
if (!found)
|
|
@@ -236,11 +489,15 @@ export class Values {
|
|
|
236
489
|
const { binding } = found;
|
|
237
490
|
switch (binding.kind) {
|
|
238
491
|
case 'const':
|
|
492
|
+
if (this.changedAtLoad(binding.scopes[0]).has(name))
|
|
493
|
+
return UNRESOLVED;
|
|
239
494
|
return this.resolveConstant(binding.init, binding.scopes);
|
|
240
495
|
case 'enum':
|
|
241
496
|
return { ok: true, value: this.enumMembers(binding.node, binding.scopes) };
|
|
242
497
|
case 'import':
|
|
243
|
-
|
|
498
|
+
if (this.changedAtLoad(scopes[0]).has(name))
|
|
499
|
+
return { ok: false, importedFrom: binding.source };
|
|
500
|
+
return this.imported(binding.source, binding.imported);
|
|
244
501
|
case 'function':
|
|
245
502
|
return UNRESOLVED;
|
|
246
503
|
case 'other':
|
|
@@ -252,6 +509,15 @@ export class Values {
|
|
|
252
509
|
const call = this.calls.findLast((item) => item.fn === fn);
|
|
253
510
|
return call?.parameters.get(name)?.() ?? UNRESOLVED;
|
|
254
511
|
}
|
|
512
|
+
/** Names the module changes while it loads, as in `WATCHED.push('MSFT')`, whose declared value isn't the one used. */
|
|
513
|
+
changedAtLoad(program) {
|
|
514
|
+
let names = this.changed.get(program);
|
|
515
|
+
if (!names) {
|
|
516
|
+
names = changedNames(program);
|
|
517
|
+
this.changed.set(program, names);
|
|
518
|
+
}
|
|
519
|
+
return names;
|
|
520
|
+
}
|
|
255
521
|
resolveConstant(init, scopes) {
|
|
256
522
|
const cache = this.calls.at(-1)?.cache ?? this.cache;
|
|
257
523
|
const cached = cache.get(init);
|
|
@@ -275,6 +541,9 @@ export class Values {
|
|
|
275
541
|
: { ok: true, value: next };
|
|
276
542
|
if (name.ok && isKey(name.value) && resolved.ok && isKey(resolved.value)) {
|
|
277
543
|
members[String(name.value)] = resolved.value;
|
|
544
|
+
// TypeScript maps a numeric member's value back to its name too.
|
|
545
|
+
if (typeof resolved.value === 'number')
|
|
546
|
+
members[String(resolved.value)] = String(name.value);
|
|
278
547
|
next = typeof resolved.value === 'number' ? resolved.value + 1 : null;
|
|
279
548
|
}
|
|
280
549
|
else {
|
|
@@ -300,6 +569,197 @@ export class Values {
|
|
|
300
569
|
return bindings;
|
|
301
570
|
}
|
|
302
571
|
}
|
|
572
|
+
const BINARY = new Set(['+', '-', '*', '/', '%', '===', '!==', '<', '<=', '>', '>=']);
|
|
573
|
+
const ARITHMETIC = new Set(['-', '*', '/', '%']);
|
|
574
|
+
const STRING_METHODS = new Set(['toUpperCase', 'toLowerCase', 'trim']);
|
|
575
|
+
/**
|
|
576
|
+
* A binary operator on two primitives, as JavaScript works it out; arithmetic only on numbers, as in
|
|
577
|
+
* `30 * 1000`, and comparisons only between values of one type.
|
|
578
|
+
*/
|
|
579
|
+
function binary(operator, left, right) {
|
|
580
|
+
if (operator === '===')
|
|
581
|
+
return { ok: true, value: left === right };
|
|
582
|
+
if (operator === '!==')
|
|
583
|
+
return { ok: true, value: left !== right };
|
|
584
|
+
if (operator === '+') {
|
|
585
|
+
if (typeof left === 'number' && typeof right === 'number')
|
|
586
|
+
return { ok: true, value: left + right };
|
|
587
|
+
return typeof left === 'string' || typeof right === 'string'
|
|
588
|
+
? { ok: true, value: String(left) + String(right) }
|
|
589
|
+
: UNRESOLVED;
|
|
590
|
+
}
|
|
591
|
+
const numbers = typeof left === 'number' && typeof right === 'number';
|
|
592
|
+
if (numbers && ARITHMETIC.has(operator)) {
|
|
593
|
+
const value = operator === '-'
|
|
594
|
+
? left - right
|
|
595
|
+
: operator === '*'
|
|
596
|
+
? left * right
|
|
597
|
+
: operator === '/'
|
|
598
|
+
? left / right
|
|
599
|
+
: left % right;
|
|
600
|
+
// Transforms fold `-NaN` differently, so the runner may see either sign; no real table holds NaN.
|
|
601
|
+
return Number.isNaN(value) ? UNRESOLVED : { ok: true, value };
|
|
602
|
+
}
|
|
603
|
+
const comparable = numbers || (typeof left === 'string' && typeof right === 'string');
|
|
604
|
+
if (!comparable || ARITHMETIC.has(operator))
|
|
605
|
+
return UNRESOLVED;
|
|
606
|
+
const [a, b] = [left, right];
|
|
607
|
+
const value = operator === '<' ? a < b : operator === '<=' ? a <= b : operator === '>' ? a > b : a >= b;
|
|
608
|
+
return { ok: true, value };
|
|
609
|
+
}
|
|
610
|
+
const MUTATING_METHODS = new Set([
|
|
611
|
+
'push',
|
|
612
|
+
'pop',
|
|
613
|
+
'shift',
|
|
614
|
+
'unshift',
|
|
615
|
+
'splice',
|
|
616
|
+
'sort',
|
|
617
|
+
'reverse',
|
|
618
|
+
'fill',
|
|
619
|
+
'copyWithin',
|
|
620
|
+
]);
|
|
621
|
+
const OBJECT_MUTATORS = new Set(['assign', 'defineProperty', 'defineProperties', 'setPrototypeOf']);
|
|
622
|
+
/** The identifier a member chain such as `ROUTING.venues[0]` starts from; null for any other expression. */
|
|
623
|
+
function rootName(node) {
|
|
624
|
+
let current = unwrap(node);
|
|
625
|
+
while (current.type === 'MemberExpression')
|
|
626
|
+
current = unwrap(current.object);
|
|
627
|
+
return current.type === 'Identifier' ? current.name : null;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Names assigned to or changed anywhere in `body`, its functions included: `x = ...`, `x++`, `[x] = ...`,
|
|
631
|
+
* `for (x of ...)`, `x.push(...)`.
|
|
632
|
+
*/
|
|
633
|
+
function assignedNames(body) {
|
|
634
|
+
const names = new Set();
|
|
635
|
+
const targets = (target) => {
|
|
636
|
+
switch (target?.type) {
|
|
637
|
+
case 'Identifier':
|
|
638
|
+
names.add(target.name);
|
|
639
|
+
break;
|
|
640
|
+
case 'ArrayPattern':
|
|
641
|
+
for (const element of target.elements)
|
|
642
|
+
targets(element);
|
|
643
|
+
break;
|
|
644
|
+
case 'ObjectPattern':
|
|
645
|
+
for (const property of target.properties)
|
|
646
|
+
targets(property.type === 'RestElement' ? property : property.value);
|
|
647
|
+
break;
|
|
648
|
+
case 'AssignmentPattern':
|
|
649
|
+
targets(target.left);
|
|
650
|
+
break;
|
|
651
|
+
case 'RestElement':
|
|
652
|
+
targets(target.argument);
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
const walk = (node) => {
|
|
657
|
+
if (node.type === 'AssignmentExpression')
|
|
658
|
+
targets(node.left);
|
|
659
|
+
if (node.type === 'UpdateExpression')
|
|
660
|
+
targets(node.argument);
|
|
661
|
+
if (node.type === 'ForOfStatement' || node.type === 'ForInStatement')
|
|
662
|
+
targets(node.left);
|
|
663
|
+
// A parameter whose value the function changes, as in `list.push(...)`, no longer holds the argument's value.
|
|
664
|
+
const changed = mutatedName(node);
|
|
665
|
+
if (changed !== null)
|
|
666
|
+
names.add(changed);
|
|
667
|
+
for (const child of childNodes(node))
|
|
668
|
+
walk(child);
|
|
669
|
+
};
|
|
670
|
+
walk(body);
|
|
671
|
+
return names;
|
|
672
|
+
}
|
|
673
|
+
/** Calls whose callbacks run after the runner has collected the tests: tests, hooks and steps. */
|
|
674
|
+
const RUNS_LATER = new Set([
|
|
675
|
+
'test',
|
|
676
|
+
'it',
|
|
677
|
+
'bench',
|
|
678
|
+
'beforeEach',
|
|
679
|
+
'afterEach',
|
|
680
|
+
'beforeAll',
|
|
681
|
+
'afterAll',
|
|
682
|
+
'aroundEach',
|
|
683
|
+
'aroundAll',
|
|
684
|
+
]);
|
|
685
|
+
/** Calls whose callbacks run while the runner collects the tests, also when they hang off `test`. */
|
|
686
|
+
const RUNS_NOW = new Set(['describe', 'suite', 'context']);
|
|
687
|
+
/** The names of a call's callee, as in `test.describe.serial`: the identifier it starts from and each property. */
|
|
688
|
+
function calleeNames(callee) {
|
|
689
|
+
const names = [];
|
|
690
|
+
let current = unwrap(callee);
|
|
691
|
+
while (current.type === 'MemberExpression' || current.type === 'CallExpression') {
|
|
692
|
+
if (current.type === 'CallExpression') {
|
|
693
|
+
current = unwrap(current.callee);
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
names.push(propertyName(current) ?? '');
|
|
697
|
+
current = unwrap(current.object);
|
|
698
|
+
}
|
|
699
|
+
if (current.type === 'Identifier')
|
|
700
|
+
names.push(current.name);
|
|
701
|
+
return names;
|
|
702
|
+
}
|
|
703
|
+
/** The name a mutation changes the value of, as in `WATCHED.push('MSFT')` or `ROUTING.market = 'NYSE'`; or null. */
|
|
704
|
+
function mutatedName(node) {
|
|
705
|
+
switch (node.type) {
|
|
706
|
+
case 'AssignmentExpression':
|
|
707
|
+
return node.left.type === 'MemberExpression' ? rootName(node.left) : null;
|
|
708
|
+
case 'UpdateExpression':
|
|
709
|
+
return node.argument.type === 'MemberExpression' ? rootName(node.argument) : null;
|
|
710
|
+
case 'UnaryExpression':
|
|
711
|
+
return node.operator === 'delete' ? rootName(node.argument) : null;
|
|
712
|
+
case 'CallExpression': {
|
|
713
|
+
const callee = unwrap(node.callee);
|
|
714
|
+
if (callee.type !== 'MemberExpression')
|
|
715
|
+
return null;
|
|
716
|
+
const method = propertyName(callee) ?? '';
|
|
717
|
+
if (MUTATING_METHODS.has(method))
|
|
718
|
+
return rootName(callee.object);
|
|
719
|
+
const target = node.arguments.at(0);
|
|
720
|
+
return OBJECT_MUTATORS.has(method) && rootName(callee.object) === 'Object' && target ? rootName(target) : null;
|
|
721
|
+
}
|
|
722
|
+
default:
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Names whose value the module changes while it loads, as describe callbacks and helpers run then: a member assigned
|
|
728
|
+
* or deleted, a mutating method such as `push()`, or `Object.assign()`, also through an alias such as
|
|
729
|
+
* `const all = SIDES`. The callbacks of tests, hooks and steps run after the runner has read titles and tables, so
|
|
730
|
+
* what they change doesn't count.
|
|
731
|
+
*/
|
|
732
|
+
function changedNames(program) {
|
|
733
|
+
const names = new Set();
|
|
734
|
+
const aliases = new Map();
|
|
735
|
+
const walk = (node, later) => {
|
|
736
|
+
if (!later) {
|
|
737
|
+
const name = mutatedName(node);
|
|
738
|
+
if (name !== null)
|
|
739
|
+
names.add(name);
|
|
740
|
+
}
|
|
741
|
+
if (node.type === 'VariableDeclarator' && node.id.type === 'Identifier' && node.init?.type === 'Identifier') {
|
|
742
|
+
aliases.set(node.id.name, node.init.name);
|
|
743
|
+
}
|
|
744
|
+
if (node.type === 'CallExpression') {
|
|
745
|
+
const callee = calleeNames(node.callee);
|
|
746
|
+
const runsLater = !later && callee.some((name) => RUNS_LATER.has(name)) && !callee.some((name) => RUNS_NOW.has(name));
|
|
747
|
+
walk(node.callee, later);
|
|
748
|
+
for (const argument of node.arguments)
|
|
749
|
+
walk(argument, later || (runsLater && isFunctionNode(argument)));
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
for (const child of childNodes(node))
|
|
753
|
+
walk(child, later);
|
|
754
|
+
};
|
|
755
|
+
walk(program, false);
|
|
756
|
+
for (const name of [...names]) {
|
|
757
|
+
for (let target = aliases.get(name); target !== undefined && !names.has(target); target = aliases.get(target)) {
|
|
758
|
+
names.add(target);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return names;
|
|
762
|
+
}
|
|
303
763
|
function isPlainObject(value) {
|
|
304
764
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
305
765
|
}
|
|
@@ -316,7 +776,33 @@ function collectBindings(scope, scopes) {
|
|
|
316
776
|
if (statement.declare)
|
|
317
777
|
break;
|
|
318
778
|
for (const declarator of statement.declarations) {
|
|
319
|
-
|
|
779
|
+
const required = declarator.init ? localRequire(declarator.init) : null;
|
|
780
|
+
if (required !== null && declarator.id.type === 'Identifier') {
|
|
781
|
+
// `const symbols = require('./symbols')` stands for what the module exports, as a default import does.
|
|
782
|
+
bindings.set(declarator.id.name, {
|
|
783
|
+
kind: 'import',
|
|
784
|
+
source: required,
|
|
785
|
+
imported: 'default',
|
|
786
|
+
declaration: declarator,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
else if (required !== null && declarator.id.type === 'ObjectPattern') {
|
|
790
|
+
for (const property of declarator.id.properties) {
|
|
791
|
+
const key = property.type === 'Property' ? propertyKey(property) : null;
|
|
792
|
+
if (key !== null && property.type === 'Property' && property.value.type === 'Identifier') {
|
|
793
|
+
bindings.set(property.value.name, {
|
|
794
|
+
kind: 'import',
|
|
795
|
+
source: required,
|
|
796
|
+
imported: key,
|
|
797
|
+
declaration: declarator,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
other(bindingNames(property.type === 'Property' ? property.value : property), declarator);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
else if (statement.kind === 'const' && declarator.id.type === 'Identifier' && declarator.init) {
|
|
320
806
|
bindings.set(declarator.id.name, { kind: 'const', init: declarator.init, scopes, declaration: declarator });
|
|
321
807
|
}
|
|
322
808
|
else {
|
|
@@ -337,6 +823,13 @@ function collectBindings(scope, scopes) {
|
|
|
337
823
|
bindings.set(specifier.local.name, {
|
|
338
824
|
kind: 'import',
|
|
339
825
|
source: statement.source.value,
|
|
826
|
+
imported: specifier.type === 'ImportSpecifier'
|
|
827
|
+
? specifier.imported.type === 'Identifier'
|
|
828
|
+
? specifier.imported.name
|
|
829
|
+
: specifier.imported.value
|
|
830
|
+
: specifier.type === 'ImportDefaultSpecifier'
|
|
831
|
+
? 'default'
|
|
832
|
+
: null,
|
|
340
833
|
declaration: specifier,
|
|
341
834
|
});
|
|
342
835
|
}
|
|
@@ -376,3 +869,22 @@ function collectBindings(scope, scopes) {
|
|
|
376
869
|
}
|
|
377
870
|
return bindings;
|
|
378
871
|
}
|
|
872
|
+
/** `Object.keys(x)`, `Object.values(x)` or `Object.entries(x)`, which list as many items as `x` has keys. */
|
|
873
|
+
function isObjectListing(call) {
|
|
874
|
+
const callee = unwrap(call.callee);
|
|
875
|
+
return (callee.type === 'MemberExpression' &&
|
|
876
|
+
callee.object.type === 'Identifier' &&
|
|
877
|
+
callee.object.name === 'Object' &&
|
|
878
|
+
['keys', 'values', 'entries'].includes(propertyName(callee)) &&
|
|
879
|
+
call.arguments.length === 1 &&
|
|
880
|
+
call.arguments[0].type !== 'SpreadElement');
|
|
881
|
+
}
|
|
882
|
+
/** The local file `require('./symbols')` loads, as an import of it; null for a package or any other expression. */
|
|
883
|
+
function localRequire(expression) {
|
|
884
|
+
const node = unwrap(expression);
|
|
885
|
+
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier' || node.callee.name !== 'require')
|
|
886
|
+
return null;
|
|
887
|
+
const [argument] = node.arguments;
|
|
888
|
+
const isLocal = argument.type === 'Literal' && typeof argument.value === 'string' && /^\.\.?\//.test(argument.value);
|
|
889
|
+
return isLocal ? argument.value : null;
|
|
890
|
+
}
|