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/extract.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { childNodes, isFunctionNode, isImportMetaVitest, propertyName, returnValues, unwrap, } from "./ast.js";
|
|
2
|
-
import { exportName } from "./frameworks/bindings.js";
|
|
2
|
+
import { commonJsExport, exportName } from "./frameworks/bindings.js";
|
|
3
3
|
import { problem, readCount } from "./frameworks/common.js";
|
|
4
4
|
import { snippet, splitLines } from "./source.js";
|
|
5
5
|
import { isScopeNode } from "./values.js";
|
|
@@ -11,6 +11,11 @@ const MAX_DEPTH = 16;
|
|
|
11
11
|
* of six levels are a million.
|
|
12
12
|
*/
|
|
13
13
|
const MAX_CALLS = 10_000;
|
|
14
|
+
/**
|
|
15
|
+
* How many loop passes and describe cases `expandCases` walks one by one in a file, far more than real suites expand;
|
|
16
|
+
* past it, loops and describe tables are one record, so that a long loop over test data can't use up the walk.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_EXPANDED = 1000;
|
|
14
19
|
/** Whether a function is called where it is written, so that its tests are registered there. */
|
|
15
20
|
function calledWhereWritten(call, written) {
|
|
16
21
|
return (call.scope === written.scope &&
|
|
@@ -20,6 +25,70 @@ function calledWhereWritten(call, written) {
|
|
|
20
25
|
!call.inBody);
|
|
21
26
|
}
|
|
22
27
|
const LOOP_METHODS = new Set(['forEach', 'map']);
|
|
28
|
+
/**
|
|
29
|
+
* Whether a statement leaves on every path through it, with `return` or `throw`, or with `break` or `continue` aimed
|
|
30
|
+
* at a loop or label outside it, so that what follows it in its block doesn't run. `labels` are those declared inside.
|
|
31
|
+
*/
|
|
32
|
+
function alwaysLeaves(statement, labels = new Set()) {
|
|
33
|
+
switch (statement.type) {
|
|
34
|
+
case 'ReturnStatement':
|
|
35
|
+
case 'ThrowStatement':
|
|
36
|
+
return true;
|
|
37
|
+
case 'BreakStatement':
|
|
38
|
+
case 'ContinueStatement':
|
|
39
|
+
return statement.label === null || !labels.has(statement.label.name);
|
|
40
|
+
case 'BlockStatement':
|
|
41
|
+
return statement.body.some((item) => alwaysLeaves(item, labels));
|
|
42
|
+
case 'IfStatement':
|
|
43
|
+
return (statement.alternate !== null &&
|
|
44
|
+
alwaysLeaves(statement.consequent, labels) &&
|
|
45
|
+
alwaysLeaves(statement.alternate, labels));
|
|
46
|
+
case 'LabeledStatement':
|
|
47
|
+
return alwaysLeaves(statement.body, new Set([...labels, statement.label.name]));
|
|
48
|
+
case 'TryStatement':
|
|
49
|
+
return ((statement.finalizer !== null && alwaysLeaves(statement.finalizer, labels)) ||
|
|
50
|
+
(alwaysLeaves(statement.block, labels) &&
|
|
51
|
+
(statement.handler === null || alwaysLeaves(statement.handler.body, labels))));
|
|
52
|
+
default:
|
|
53
|
+
// A loop or switch takes the `break` and `continue` inside it, and may run its body not at all.
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The names a condition reads, as in `side === 'buy'` or `!strategy.enabled`; null when it has a function, which the
|
|
59
|
+
* scan doesn't look into.
|
|
60
|
+
*/
|
|
61
|
+
function referencedNames(node) {
|
|
62
|
+
if (isFunctionNode(node))
|
|
63
|
+
return null;
|
|
64
|
+
if (node.type === 'Identifier')
|
|
65
|
+
return [node.name];
|
|
66
|
+
const parts = node.type === 'MemberExpression' && !node.computed ? [node.object] : childNodes(node);
|
|
67
|
+
const names = [];
|
|
68
|
+
for (const part of parts) {
|
|
69
|
+
const found = referencedNames(part);
|
|
70
|
+
if (!found)
|
|
71
|
+
return null;
|
|
72
|
+
names.push(...found);
|
|
73
|
+
}
|
|
74
|
+
return names;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether a loop body can end a pass before its end or stop the loop, with `break`, `continue`, `return` or `throw`,
|
|
78
|
+
* so that a test in it may not be registered on every pass. Functions inside run later, and don't count.
|
|
79
|
+
*/
|
|
80
|
+
function leavesEarly(body) {
|
|
81
|
+
if (body.type === 'FunctionDeclaration' || isFunctionNode(body))
|
|
82
|
+
return false;
|
|
83
|
+
switch (body.type) {
|
|
84
|
+
case 'BreakStatement':
|
|
85
|
+
case 'ContinueStatement':
|
|
86
|
+
case 'ReturnStatement':
|
|
87
|
+
case 'ThrowStatement':
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return childNodes(body).some(leavesEarly);
|
|
91
|
+
}
|
|
23
92
|
/**
|
|
24
93
|
* What `test.describe.configure()` sets on the describe or file it is called in: the last mode, timeout and retries
|
|
25
94
|
* win, locks add up.
|
|
@@ -119,13 +188,17 @@ class Walker {
|
|
|
119
188
|
callStack = [];
|
|
120
189
|
/** Helper calls walked in the file so far, up to MAX_CALLS. */
|
|
121
190
|
callsWalked = 0;
|
|
191
|
+
/** Loop passes and describe cases walked one by one so far, up to MAX_EXPANDED. */
|
|
192
|
+
expandedPasses = 0;
|
|
122
193
|
/** Problems already reported, since a function walked at several calls finds the same ones each time. */
|
|
123
194
|
reported = new Set();
|
|
124
195
|
/** What the functions named in calls such as `onNode(22)('title', fn)` return, by the declaration of each name. */
|
|
125
196
|
returns = new Map();
|
|
126
197
|
/** For each file's program, the scopes around each function in it. */
|
|
127
198
|
scopeChains = new Map();
|
|
128
|
-
|
|
199
|
+
options;
|
|
200
|
+
constructor(main, openModule, options) {
|
|
201
|
+
this.options = options;
|
|
129
202
|
this.main = main;
|
|
130
203
|
this.frame = main;
|
|
131
204
|
this.openModule = openModule;
|
|
@@ -155,6 +228,11 @@ class Walker {
|
|
|
155
228
|
testContext: null,
|
|
156
229
|
guards: [],
|
|
157
230
|
isInLoop: false,
|
|
231
|
+
loops: [],
|
|
232
|
+
loopConditions: [],
|
|
233
|
+
caseIndex: null,
|
|
234
|
+
isExpanding: false,
|
|
235
|
+
expandedAt: null,
|
|
158
236
|
isInCondition: false,
|
|
159
237
|
isInFunction: false,
|
|
160
238
|
helper: null,
|
|
@@ -216,6 +294,13 @@ class Walker {
|
|
|
216
294
|
break;
|
|
217
295
|
const condition = this.text(node.test);
|
|
218
296
|
this.visit(node.test, context);
|
|
297
|
+
const known = this.known(node.test, context);
|
|
298
|
+
if (known) {
|
|
299
|
+
const taken = known.value ? node.consequent : node.alternate;
|
|
300
|
+
if (taken)
|
|
301
|
+
this.visit(taken, context);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
219
304
|
this.visit(node.consequent, this.guarded(context, condition));
|
|
220
305
|
if (node.alternate)
|
|
221
306
|
this.visit(node.alternate, this.guarded(context, `!(${condition})`));
|
|
@@ -224,6 +309,11 @@ class Walker {
|
|
|
224
309
|
case 'ConditionalExpression': {
|
|
225
310
|
const condition = this.text(node.test);
|
|
226
311
|
this.visit(node.test, context);
|
|
312
|
+
const known = this.known(node.test, context);
|
|
313
|
+
if (known) {
|
|
314
|
+
this.visit(known.value ? node.consequent : node.alternate, context);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
227
317
|
this.visit(node.consequent, this.guarded(context, condition));
|
|
228
318
|
this.visit(node.alternate, this.guarded(context, `!(${condition})`));
|
|
229
319
|
return;
|
|
@@ -232,6 +322,18 @@ class Walker {
|
|
|
232
322
|
const left = this.text(node.left);
|
|
233
323
|
const guard = node.operator === '&&' ? left : node.operator === '||' ? `!(${left})` : `(${left}) == null`;
|
|
234
324
|
this.visit(node.left, context);
|
|
325
|
+
const known = this.known(node.left, context);
|
|
326
|
+
if (known) {
|
|
327
|
+
const { value } = known;
|
|
328
|
+
const right = node.operator === '&&'
|
|
329
|
+
? Boolean(value)
|
|
330
|
+
: node.operator === '||'
|
|
331
|
+
? !value
|
|
332
|
+
: value === null || value === undefined;
|
|
333
|
+
if (right)
|
|
334
|
+
this.visit(node.right, context);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
235
337
|
this.visit(node.right, this.guarded(context, guard));
|
|
236
338
|
return;
|
|
237
339
|
}
|
|
@@ -255,12 +357,30 @@ class Walker {
|
|
|
255
357
|
}
|
|
256
358
|
return;
|
|
257
359
|
}
|
|
258
|
-
case '
|
|
360
|
+
case 'BlockStatement':
|
|
361
|
+
this.visitStatements(node.body, context);
|
|
362
|
+
return;
|
|
259
363
|
case 'ForInStatement':
|
|
260
|
-
case 'ForOfStatement':
|
|
364
|
+
case 'ForOfStatement': {
|
|
365
|
+
const leaves = leavesEarly(node.body);
|
|
366
|
+
const values = leaves ? null : this.loopValues(node);
|
|
367
|
+
const expands = values ? this.expands(node, values, context) : 'no';
|
|
368
|
+
if (values && expands === 'yes') {
|
|
369
|
+
this.expandLoop(node, values, context);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// For `for...in`, the keys are what `Object.keys()` lists.
|
|
373
|
+
const listed = node.type === 'ForOfStatement' && !leaves ? node.right : null;
|
|
374
|
+
const count = values?.length ?? (listed ? this.frame.values.lengthOf(listed, this.scopes.slice(0, -1)) : null);
|
|
375
|
+
this.walkUnexpanded(node, expands === 'limit', () => {
|
|
376
|
+
this.visitChildren(node, this.inLoop(context, count));
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
case 'ForStatement':
|
|
261
381
|
case 'WhileStatement':
|
|
262
382
|
case 'DoWhileStatement':
|
|
263
|
-
this.visitChildren(node,
|
|
383
|
+
this.visitChildren(node, this.inLoop(context, null));
|
|
264
384
|
return;
|
|
265
385
|
case 'FunctionDeclaration':
|
|
266
386
|
case 'FunctionExpression':
|
|
@@ -297,6 +417,47 @@ class Walker {
|
|
|
297
417
|
return ((fn.type === 'FunctionDeclaration' && fn.id !== null) ||
|
|
298
418
|
(parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && parent.init === fn));
|
|
299
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* With `expandCases`, the value of a condition outside test and hook bodies when the scan can work it out, as with
|
|
422
|
+
* the values of a case or pass; null otherwise, and then both branches are walked under the condition.
|
|
423
|
+
*/
|
|
424
|
+
known(test, context) {
|
|
425
|
+
if (!this.options.expandCases || context.test || context.hookScope)
|
|
426
|
+
return null;
|
|
427
|
+
// Only the values of the case, pass or helper call decide: a constant of the module may be changed in ways the scan
|
|
428
|
+
// doesn't see, and taking a branch away would drop a test the runner registers.
|
|
429
|
+
const names = referencedNames(test);
|
|
430
|
+
if (!names?.every((name) => this.frame.values.isBound(name, this.scopes)))
|
|
431
|
+
return null;
|
|
432
|
+
const resolved = this.frame.values.resolve(test, this.scopes);
|
|
433
|
+
return resolved.ok ? { value: resolved.value } : null;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Walks the statements of a block. After an `if` whose branch always leaves the function or loop, as in
|
|
437
|
+
* `if (!strategy.enabled) return;`, what follows runs only when that branch isn't taken, so it is under the opposite
|
|
438
|
+
* condition; a branch taken for certain, or both branches leaving, ends the walk. A branch that leaves on only some
|
|
439
|
+
* of its paths leaves what follows as it is.
|
|
440
|
+
*/
|
|
441
|
+
visitStatements(statements, context) {
|
|
442
|
+
let current = context;
|
|
443
|
+
for (const statement of statements) {
|
|
444
|
+
this.visit(statement, current);
|
|
445
|
+
if (statement.type !== 'IfStatement' || isImportMetaVitest(statement.test))
|
|
446
|
+
continue;
|
|
447
|
+
const thenLeaves = alwaysLeaves(statement.consequent);
|
|
448
|
+
const elseLeaves = statement.alternate !== null && alwaysLeaves(statement.alternate);
|
|
449
|
+
if (!thenLeaves && !elseLeaves)
|
|
450
|
+
continue;
|
|
451
|
+
const known = this.known(statement.test, current);
|
|
452
|
+
const taken = known && (known.value ? thenLeaves : elseLeaves);
|
|
453
|
+
if (taken || (thenLeaves && elseLeaves))
|
|
454
|
+
return;
|
|
455
|
+
if (known)
|
|
456
|
+
continue;
|
|
457
|
+
const condition = this.text(statement.test);
|
|
458
|
+
current = this.guarded(current, thenLeaves ? `!(${condition})` : condition);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
300
461
|
placement(context) {
|
|
301
462
|
return {
|
|
302
463
|
scope: context.scope,
|
|
@@ -327,15 +488,110 @@ class Walker {
|
|
|
327
488
|
text(node) {
|
|
328
489
|
return snippet(this.frame.parsed.source.slice(node.start, node.end));
|
|
329
490
|
}
|
|
491
|
+
/** `pass` is the pass of an expanded loop, and where the loop starts. */
|
|
492
|
+
inLoop(context, passes, pass = null) {
|
|
493
|
+
return {
|
|
494
|
+
...context,
|
|
495
|
+
isInLoop: true,
|
|
496
|
+
loops: [...context.loops, passes],
|
|
497
|
+
loopConditions: [...context.loopConditions, context.isInCondition || pass !== null],
|
|
498
|
+
caseIndex: pass?.index ?? context.caseIndex,
|
|
499
|
+
isExpanding: context.isExpanding || pass !== null,
|
|
500
|
+
expandedAt: this.expandedAt(context, pass?.at ?? null),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Where what an expansion registers is ordered: the outermost expansion in the scanned file. In a helper walked at a
|
|
505
|
+
* call, the call already orders what it registers, and the helper may be in another file.
|
|
506
|
+
*/
|
|
507
|
+
expandedAt(context, at) {
|
|
508
|
+
return context.expandedAt ?? (context.registeredBy === null ? at : null);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Whether to walk a loop or describe table once per value: with `expandCases`, outside test and hook bodies, which
|
|
512
|
+
* the runner walks once for each test, for a loop that declares its variable with `let` or `const`. An expansion
|
|
513
|
+
* outside any other starts only while the file has expanded fewer than MAX_EXPANDED passes and cases, so that a long
|
|
514
|
+
* loop over test data can't use up the walk; inside one, loops expand up to ten times that, so that an expansion
|
|
515
|
+
* is whole. `limit` means the limit is why it doesn't.
|
|
516
|
+
*/
|
|
517
|
+
expands(loop, values, context) {
|
|
518
|
+
if (!this.options.expandCases || context.test || context.hookScope)
|
|
519
|
+
return 'no';
|
|
520
|
+
// Vitest runs describe callbacks after the loop, when a `var` holds its last value.
|
|
521
|
+
if (loop && !(loop.left.type === 'VariableDeclaration' && loop.left.kind !== 'var'))
|
|
522
|
+
return 'no';
|
|
523
|
+
const limit = context.isExpanding ? 10 * MAX_EXPANDED : MAX_EXPANDED;
|
|
524
|
+
if (this.expandedPasses + values.length > limit)
|
|
525
|
+
return 'limit';
|
|
526
|
+
this.expandedPasses += values.length;
|
|
527
|
+
return 'yes';
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Walks a loop as one record, and says why it isn't expanded when it was asked to be and registers tests or
|
|
531
|
+
* describes.
|
|
532
|
+
*/
|
|
533
|
+
walkUnexpanded(node, limited, walk) {
|
|
534
|
+
const before = this.tests.length + this.suites.length;
|
|
535
|
+
walk();
|
|
536
|
+
if (!limited || this.tests.length + this.suites.length === before)
|
|
537
|
+
return;
|
|
538
|
+
this.report(problem('cases-not-expanded', node, `The passes of the loop are listed as one record, since expanding it would take the file past ${MAX_EXPANDED.toLocaleString('en-US')} loop passes and describe cases.`));
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Walks the body of `for...of` or `for...in` once per value, with the loop variable bound to it, as the runner runs
|
|
542
|
+
* it; false when the loop doesn't declare its variable, as in `for (symbol of SYMBOLS)`.
|
|
543
|
+
*/
|
|
544
|
+
expandLoop(loop, values, context) {
|
|
545
|
+
const declarator = loop.left.declarations[0];
|
|
546
|
+
this.visit(loop.right, context);
|
|
547
|
+
for (const [index, value] of values.entries()) {
|
|
548
|
+
this.frame.values.withLoopValue(declarator, declarator.id, { ok: true, value }, loop.body, this.scopes, () => {
|
|
549
|
+
this.visit(loop.body, this.inLoop(context, 1, { index, at: loop.start }));
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* What `for...of` or `for...in` goes over, when the scan can work it out: an array's elements, or the keys of any
|
|
556
|
+
* value, as `for...in` takes them; null when the value is unknown or `for...of` can't go over it.
|
|
557
|
+
*/
|
|
558
|
+
loopValues(loop) {
|
|
559
|
+
// The loop's own scope declares the loop variable, which the value it goes over can't refer to.
|
|
560
|
+
const resolved = this.frame.values.resolve(loop.right, this.scopes.slice(0, -1));
|
|
561
|
+
if (!resolved.ok)
|
|
562
|
+
return null;
|
|
563
|
+
if (loop.type === 'ForInStatement')
|
|
564
|
+
return Object.keys(Object(resolved.value ?? {}));
|
|
565
|
+
return Array.isArray(resolved.value) ? resolved.value : null;
|
|
566
|
+
}
|
|
330
567
|
/** `symbols.forEach((symbol) => test(...))` declares its tests in a loop. */
|
|
331
568
|
loopCall(call, context) {
|
|
332
569
|
const callee = unwrap(call.callee);
|
|
333
570
|
if (callee.type !== 'MemberExpression' || !LOOP_METHODS.has(propertyName(callee) ?? ''))
|
|
334
571
|
return false;
|
|
335
572
|
this.visit(call.callee, context);
|
|
573
|
+
const resolved = this.frame.values.resolve(callee.object, this.scopes);
|
|
574
|
+
const list = resolved.ok && Array.isArray(resolved.value) ? resolved.value : null;
|
|
336
575
|
for (const arg of call.arguments) {
|
|
337
576
|
if (arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression') {
|
|
338
|
-
|
|
577
|
+
const leaves = leavesEarly(arg.body);
|
|
578
|
+
const values = leaves ? null : list;
|
|
579
|
+
const expands = values ? this.expands(null, values, context) : 'no';
|
|
580
|
+
if (values && expands === 'yes') {
|
|
581
|
+
// The callback gets each element, its index and the array.
|
|
582
|
+
for (const [index, value] of values.entries()) {
|
|
583
|
+
const args = [value, index, values].map((item) => ({ ok: true, value: item }));
|
|
584
|
+
this.frame.values.withArguments(arg, args, this.scopes, () => {
|
|
585
|
+
this.visitCallback(arg, this.inLoop(context, 1, { index, at: call.start }));
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
const count = values?.length ?? (leaves ? null : this.frame.values.lengthOf(callee.object, this.scopes));
|
|
591
|
+
this.walkUnexpanded(call, expands === 'limit', () => {
|
|
592
|
+
this.visitCallback(arg, this.inLoop(context, count));
|
|
593
|
+
});
|
|
594
|
+
}
|
|
339
595
|
}
|
|
340
596
|
else {
|
|
341
597
|
this.visit(arg, context);
|
|
@@ -441,9 +697,11 @@ class Walker {
|
|
|
441
697
|
context.test?.problems.push(...api.problems.map((item) => this.here(item)));
|
|
442
698
|
return true;
|
|
443
699
|
case 'invalid': {
|
|
444
|
-
|
|
700
|
+
// Code that runs after the file loads, or only when a condition holds, can't be sure to stop it loading.
|
|
701
|
+
const afterLoad = context.test !== null || context.hookScope !== null || context.helper !== null || context.isInCondition;
|
|
445
702
|
this.report(afterLoad ? { ...api.problem, afterLoad } : api.problem);
|
|
446
|
-
|
|
703
|
+
// Under a condition, the call either never runs or fails the file, so it declares nothing either way.
|
|
704
|
+
return api.declaration && !context.isInCondition ? this.handle(call, api.declaration, context) : true;
|
|
447
705
|
}
|
|
448
706
|
case 'other':
|
|
449
707
|
return false;
|
|
@@ -542,11 +800,10 @@ class Walker {
|
|
|
542
800
|
}
|
|
543
801
|
/** The function a name imported from a local module stands for, in the file that writes it; null otherwise. */
|
|
544
802
|
importedFunction(name, scopes, from = this.frame) {
|
|
545
|
-
const declaration = from.values.declarationOf(name, scopes);
|
|
546
803
|
const source = from.values.importSource(name, scopes);
|
|
547
804
|
if (source === null)
|
|
548
805
|
return null;
|
|
549
|
-
const imported =
|
|
806
|
+
const imported = from.values.importedName(name, scopes);
|
|
550
807
|
const frame = this.openFrom(from, source);
|
|
551
808
|
return frame ? this.exportedFunction(frame, imported, new Set(), 0) : null;
|
|
552
809
|
}
|
|
@@ -570,14 +827,18 @@ class Walker {
|
|
|
570
827
|
return null;
|
|
571
828
|
searched.add(key);
|
|
572
829
|
const { program } = frame.parsed;
|
|
573
|
-
const local = (localName) => {
|
|
574
|
-
const fn = this.functionIn(frame, localName, [program]);
|
|
575
|
-
return fn ? { fn, frame } : null;
|
|
576
|
-
};
|
|
577
830
|
const from = (source, exported) => {
|
|
578
831
|
const module = this.openFrom(frame, source);
|
|
579
832
|
return module ? this.exportedFunction(module, exported, searched, depth + 1) : null;
|
|
580
833
|
};
|
|
834
|
+
const local = (localName) => {
|
|
835
|
+
const fn = this.functionIn(frame, localName, [program]);
|
|
836
|
+
if (fn)
|
|
837
|
+
return { fn, frame };
|
|
838
|
+
// A name the module imports and exports again, as `import helper from './a'; export { helper }`.
|
|
839
|
+
const source = frame.values.importSource(localName, [program]);
|
|
840
|
+
return source === null ? null : from(source, frame.values.importedName(localName, [program]));
|
|
841
|
+
};
|
|
581
842
|
for (const statement of program.body) {
|
|
582
843
|
if (statement.type === 'ExportDefaultDeclaration' && name === 'default') {
|
|
583
844
|
const declaration = statement.declaration;
|
|
@@ -606,7 +867,12 @@ class Walker {
|
|
|
606
867
|
return statement.source ? from(statement.source.value, localName) : local(localName);
|
|
607
868
|
}
|
|
608
869
|
}
|
|
609
|
-
|
|
870
|
+
// A CommonJS module: `module.exports = { registerSettlement }` or `exports.registerSettlement = function () {}`.
|
|
871
|
+
const commonJs = commonJsExport(program, name);
|
|
872
|
+
const value = commonJs ? unwrap(commonJs) : null;
|
|
873
|
+
if (value?.type === 'Identifier')
|
|
874
|
+
return local(value.name);
|
|
875
|
+
return isFunctionNode(value) ? { fn: value, frame } : null;
|
|
610
876
|
}
|
|
611
877
|
/**
|
|
612
878
|
* Walks a function at one of its calls from the file or a describe, as the runner runs it there: with the scopes
|
|
@@ -695,6 +961,18 @@ class Walker {
|
|
|
695
961
|
* called instead.
|
|
696
962
|
*/
|
|
697
963
|
runSetting(call, api, context) {
|
|
964
|
+
if (api.fileWide) {
|
|
965
|
+
for (const item of api.problems)
|
|
966
|
+
this.report(item);
|
|
967
|
+
// Called by a function or while tests run, it changes only the tests that run after it.
|
|
968
|
+
if (context.declaredIn !== null || context.test || context.hookScope)
|
|
969
|
+
return;
|
|
970
|
+
const { settings, problems } = this.guardedRun(call, api.settings, api.argument, context.guards);
|
|
971
|
+
for (const item of problems)
|
|
972
|
+
this.report(item);
|
|
973
|
+
mergeRun(this.fileScope.details.run, settings);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
698
976
|
if (context.declaredIn !== null && !context.test) {
|
|
699
977
|
for (const item of api.problems)
|
|
700
978
|
this.report(item);
|
|
@@ -733,11 +1011,18 @@ class Walker {
|
|
|
733
1011
|
conditions: own.map((condition) => joinConditions([...guards, ...(condition === null ? [] : [condition])])),
|
|
734
1012
|
};
|
|
735
1013
|
}
|
|
736
|
-
|
|
737
|
-
const message = `Option
|
|
1014
|
+
const unapplied = (key) => {
|
|
1015
|
+
const message = `Option ${key}: ${argument} is set only when ${guard}; it is not applied.`;
|
|
738
1016
|
problems.push(problem('unresolved-option', call, message, snippet(this.text(call))));
|
|
1017
|
+
};
|
|
1018
|
+
if (settings.timeout !== undefined && guard !== null) {
|
|
1019
|
+
unapplied('timeout');
|
|
739
1020
|
delete settings.timeout;
|
|
740
1021
|
}
|
|
1022
|
+
if (settings.retry !== undefined && guard !== null) {
|
|
1023
|
+
unapplied('retry');
|
|
1024
|
+
delete settings.retry;
|
|
1025
|
+
}
|
|
741
1026
|
return { settings, problems };
|
|
742
1027
|
}
|
|
743
1028
|
/** Run settings a helper sets when a test body or a `beforeEach` calls it, under the conditions around the call. */
|
|
@@ -925,6 +1210,46 @@ class Walker {
|
|
|
925
1210
|
declareTest(call, declaration, context) {
|
|
926
1211
|
if (context.quiet || this.nested(call, context))
|
|
927
1212
|
return;
|
|
1213
|
+
for (const item of this.cases(declaration, call, context))
|
|
1214
|
+
this.declareCase(call, item, context);
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* With `expandCases`, a declaration for each case of its table, with the case's title and the arguments its callback
|
|
1218
|
+
* gets; otherwise, or when the scan can't give each case its title, the declaration alone, with a problem that says
|
|
1219
|
+
* why.
|
|
1220
|
+
*/
|
|
1221
|
+
cases(declaration, call, context) {
|
|
1222
|
+
const single = (reason) => {
|
|
1223
|
+
if (reason === null)
|
|
1224
|
+
return [{ declaration, index: null, args: null }];
|
|
1225
|
+
const item = problem('cases-not-expanded', call, `The cases of the table are listed as one record, since ${reason}.`);
|
|
1226
|
+
return [{ declaration: { ...declaration, problems: [...declaration.problems, item] }, index: null, args: null }];
|
|
1227
|
+
};
|
|
1228
|
+
if (!this.options.expandCases || !declaration.isParameterized || declaration.caseCount === null)
|
|
1229
|
+
return single(null);
|
|
1230
|
+
if (!declaration.cases)
|
|
1231
|
+
return single("the scan can't work out every value in it");
|
|
1232
|
+
const title = declaration.title;
|
|
1233
|
+
if (!title || title.isDynamic)
|
|
1234
|
+
return single("the scan can't work out the title");
|
|
1235
|
+
const titles = declaration.cases.map((item) => item.title);
|
|
1236
|
+
if (titles.some((text) => text === null)) {
|
|
1237
|
+
return single("the title shows a value the scan doesn't format the way the runner does");
|
|
1238
|
+
}
|
|
1239
|
+
if (declaration.kind === 'describe' && this.expands(null, declaration.cases, context) === 'limit') {
|
|
1240
|
+
return single(`expanding it would take the file past ${MAX_EXPANDED.toLocaleString('en-US')} loop passes and describe cases`);
|
|
1241
|
+
}
|
|
1242
|
+
return declaration.cases.map((item, index) => ({
|
|
1243
|
+
declaration: {
|
|
1244
|
+
...declaration,
|
|
1245
|
+
title: { text: titles[index], withoutTags: titles[index], isDynamic: false },
|
|
1246
|
+
caseCount: 1,
|
|
1247
|
+
},
|
|
1248
|
+
index,
|
|
1249
|
+
args: item.args.map((value) => ({ ok: true, value })),
|
|
1250
|
+
}));
|
|
1251
|
+
}
|
|
1252
|
+
declareCase(call, { declaration, index, args }, context) {
|
|
928
1253
|
const statement = this.statement(call);
|
|
929
1254
|
const empty = emptyTitle(declaration, call);
|
|
930
1255
|
const test = {
|
|
@@ -935,12 +1260,16 @@ class Walker {
|
|
|
935
1260
|
declaredAt: this.declaredAt(call),
|
|
936
1261
|
frame: this.frame,
|
|
937
1262
|
registeredBy: context.registeredBy,
|
|
938
|
-
order:
|
|
1263
|
+
order: context.expandedAt ?? context.registeredBy?.start ?? call.start,
|
|
939
1264
|
states: [...declaration.states],
|
|
940
1265
|
annotations: [...declaration.details.annotations],
|
|
941
1266
|
isInLoop: context.isInLoop,
|
|
1267
|
+
// A condition inside a loop decides on each pass whether the test is registered.
|
|
1268
|
+
passes: context.loops,
|
|
1269
|
+
loops: context.loops.map((passes, index) => context.isInCondition && !context.loopConditions[index] ? null : passes),
|
|
942
1270
|
isInCondition: context.isInCondition,
|
|
943
1271
|
isInFunction: context.isInFunction,
|
|
1272
|
+
caseIndex: index ?? context.caseIndex,
|
|
944
1273
|
comments: this.commentsAbove(statement.start),
|
|
945
1274
|
run: {},
|
|
946
1275
|
expectCount: 0,
|
|
@@ -958,21 +1287,34 @@ class Walker {
|
|
|
958
1287
|
if (body) {
|
|
959
1288
|
if (!declaration.fn)
|
|
960
1289
|
this.walkedAtCall.add(body);
|
|
961
|
-
this.
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1290
|
+
this.withCase(body, args, () => {
|
|
1291
|
+
this.visitCallback(body, {
|
|
1292
|
+
...context,
|
|
1293
|
+
test,
|
|
1294
|
+
hookScope: null,
|
|
1295
|
+
hookName: null,
|
|
1296
|
+
stepDepth: 0,
|
|
1297
|
+
testContext: declaration.testContext,
|
|
1298
|
+
guards: [],
|
|
1299
|
+
helper: null,
|
|
1300
|
+
});
|
|
970
1301
|
});
|
|
971
1302
|
}
|
|
972
1303
|
}
|
|
1304
|
+
/** Runs `walk` with a case's arguments bound to the parameters of its callback, as the runner calls it. */
|
|
1305
|
+
withCase(fn, args, walk) {
|
|
1306
|
+
if (args)
|
|
1307
|
+
this.frame.values.withArguments(fn, args, this.scopes, walk);
|
|
1308
|
+
else
|
|
1309
|
+
walk();
|
|
1310
|
+
}
|
|
973
1311
|
declareSuite(call, declaration, context) {
|
|
974
1312
|
if (context.quiet || this.nested(call, context))
|
|
975
1313
|
return;
|
|
1314
|
+
for (const item of this.cases(declaration, call, context))
|
|
1315
|
+
this.declareSuiteCase(call, item, context);
|
|
1316
|
+
}
|
|
1317
|
+
declareSuiteCase(call, { declaration, index, args }, context) {
|
|
976
1318
|
for (const item of [...declaration.problems, ...emptyTitle(declaration, call)])
|
|
977
1319
|
this.report(item);
|
|
978
1320
|
const scope = {
|
|
@@ -997,26 +1339,34 @@ class Walker {
|
|
|
997
1339
|
declaredAt: this.declaredAt(call),
|
|
998
1340
|
frame: this.frame,
|
|
999
1341
|
registeredBy: context.registeredBy,
|
|
1000
|
-
order:
|
|
1342
|
+
order: context.expandedAt ?? context.registeredBy?.start ?? call.start,
|
|
1001
1343
|
cuts: [],
|
|
1002
1344
|
isInCondition: context.isInCondition,
|
|
1003
1345
|
isInFunction: context.isInFunction,
|
|
1346
|
+
caseIndex: index ?? context.caseIndex,
|
|
1004
1347
|
};
|
|
1005
1348
|
scope.suite = suite;
|
|
1006
1349
|
this.suites.push(suite);
|
|
1007
1350
|
this.cut(context.scope, call);
|
|
1008
1351
|
this.suiteFrames.set(suite, this.frame);
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1352
|
+
const { fn } = declaration;
|
|
1353
|
+
if (fn) {
|
|
1354
|
+
this.withCase(fn, args, () => {
|
|
1355
|
+
this.visitCallback(fn, {
|
|
1356
|
+
...context,
|
|
1357
|
+
scope,
|
|
1358
|
+
test: null,
|
|
1359
|
+
hookScope: null,
|
|
1360
|
+
hookName: null,
|
|
1361
|
+
stepDepth: 0,
|
|
1362
|
+
testContext: null,
|
|
1363
|
+
guards: [],
|
|
1364
|
+
helper: null,
|
|
1365
|
+
// The pass of a loop around the describe is the describe's, not that of each test in it.
|
|
1366
|
+
caseIndex: null,
|
|
1367
|
+
isExpanding: context.isExpanding || index !== null,
|
|
1368
|
+
expandedAt: this.expandedAt(context, index === null ? null : call.start),
|
|
1369
|
+
});
|
|
1020
1370
|
});
|
|
1021
1371
|
}
|
|
1022
1372
|
}
|
|
@@ -1051,6 +1401,6 @@ class Walker {
|
|
|
1051
1401
|
}
|
|
1052
1402
|
}
|
|
1053
1403
|
/** Walks the scanned file; `openModule` reads the modules of imported helpers that the file calls. */
|
|
1054
|
-
export function extract(main, openModule) {
|
|
1055
|
-
return new Walker(main, openModule).run();
|
|
1404
|
+
export function extract(main, openModule, options = { expandCases: false, version: null }) {
|
|
1405
|
+
return new Walker(main, openModule, options).run();
|
|
1056
1406
|
}
|
package/dist/files.js
CHANGED
|
@@ -82,8 +82,9 @@ export async function findFiles(root, patterns, ignore, framework, platform = pr
|
|
|
82
82
|
const absolutePath = path.resolve(root, match);
|
|
83
83
|
const realPath = realpathSync(absolutePath);
|
|
84
84
|
const linked = realPath !== path.join(realRoot, match);
|
|
85
|
-
if (linked && framework
|
|
86
|
-
|
|
85
|
+
if (linked && framework !== 'vitest') {
|
|
86
|
+
const name = framework === 'jest' ? 'Jest' : 'Playwright';
|
|
87
|
+
diagnostics.push(fileDiagnostic('symlink-ignored', match, `The file is reached through a symbolic link, which ${name} does not follow.`));
|
|
87
88
|
continue;
|
|
88
89
|
}
|
|
89
90
|
candidates.push({ absolutePath, relativeFilePath: match, realPath, linked });
|
package/dist/filter.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Framework } from './types.ts';
|
|
2
|
+
/** Each framework's name in messages. */
|
|
3
|
+
export declare const FRAMEWORK_NAMES: Record<Framework, string>;
|
|
2
4
|
/** Oldest supported version of each framework. */
|
|
3
5
|
export declare const OLDEST: Record<Framework, [number, number]>;
|
|
4
6
|
/** Where the framework version came from. */
|
|
@@ -11,6 +13,15 @@ export interface DetectedVersion {
|
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* The framework version from the installed package, then a lockfile, then package.json, each looked for in the root
|
|
14
|
-
* and the folders above it. A version range, as in `^4.0.0`, is used only when nothing else is found.
|
|
16
|
+
* and the folders above it. A version range, as in `^4.0.0`, is used only when nothing else is found. When none of
|
|
17
|
+
* those has the framework, as in a monorepo whose root leaves it to a package such as `code/`, the shallowest package
|
|
18
|
+
* below the root that declares it and holds one of the scanned `files` is read the same way, from its own folder up.
|
|
15
19
|
*/
|
|
16
|
-
export declare function detectFrameworkVersion(root: string, framework: Framework): DetectedVersion | null;
|
|
20
|
+
export declare function detectFrameworkVersion(root: string, framework: Framework, files: readonly string[]): DetectedVersion | null;
|
|
21
|
+
/**
|
|
22
|
+
* The runner a project uses, for a scan without `framework`: the one whose package a package.json in the root or a
|
|
23
|
+
* folder below it, outside `ignore`, lists, or whose config is there. Only when none is found there do the package.json files above the
|
|
24
|
+
* root count, as a monorepo may declare its runner once at its top. Throws an `OptionsError` when the project uses
|
|
25
|
+
* none of them or several, naming what it found.
|
|
26
|
+
*/
|
|
27
|
+
export declare function detectFramework(root: string, ignore: readonly string[]): Framework;
|