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
|
@@ -1,67 +1,18 @@
|
|
|
1
|
-
import type { CallExpression
|
|
2
|
-
import type {
|
|
1
|
+
import type { CallExpression } from 'oxc-parser';
|
|
2
|
+
import type { ApiCall, CallContext, ModuleReader } from '../model.ts';
|
|
3
3
|
import type { ParsedFile } from '../parse.ts';
|
|
4
|
+
import type { HookCounts } from '../types.ts';
|
|
4
5
|
import { type FileContext, type Link } from './common.ts';
|
|
5
|
-
type Kind
|
|
6
|
-
|
|
7
|
-
interface TestFunction {
|
|
8
|
-
kind: Kind;
|
|
9
|
-
links: Link[];
|
|
10
|
-
}
|
|
11
|
-
export declare class VitestAdapter implements Adapter {
|
|
12
|
-
readonly problems: Problem[];
|
|
13
|
-
readonly fileTags: string[];
|
|
14
|
-
readonly fileTagSpans: Span[];
|
|
15
|
-
usesGlobals: boolean;
|
|
16
|
-
private readonly file;
|
|
17
|
-
private readonly readModule;
|
|
18
|
-
private readonly bindings;
|
|
19
|
-
private readonly imports;
|
|
20
|
-
private readonly followed;
|
|
21
|
-
private readonly reported;
|
|
22
|
-
/** Variables and names of functions read as test objects on trust, each reported once. */
|
|
23
|
-
private readonly assumed;
|
|
24
|
-
private readonly aliases;
|
|
25
|
-
/** Describe callbacks and the name of their first parameter, which Vitest calls with its `test`. */
|
|
26
|
-
private readonly suiteParameters;
|
|
6
|
+
import { SuiteAdapter, type Kind } from './test-functions.ts';
|
|
7
|
+
export declare class VitestAdapter extends SuiteAdapter {
|
|
27
8
|
private warnedAboutTags;
|
|
28
9
|
constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
|
|
10
|
+
protected isAliasLink(kind: Kind, link: Link): boolean;
|
|
29
11
|
classify(call: CallExpression, context: CallContext): ApiCall | null;
|
|
30
|
-
testFunction(expression: Expression, scopes: readonly Node[], returnsOf: CallContext['returnsOf']): TestFunction | null;
|
|
31
12
|
/** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
|
|
32
13
|
private benchCall;
|
|
33
14
|
private chainCall;
|
|
34
|
-
|
|
35
|
-
* The function a chain calls and the links after it, or null when it isn't one of Vitest's. An import that can't be
|
|
36
|
-
* followed is reported only outside test and hook bodies, where a call can declare tests.
|
|
37
|
-
*/
|
|
38
|
-
private resolve;
|
|
39
|
-
private start;
|
|
40
|
-
/** The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`. */
|
|
41
|
-
private alias;
|
|
42
|
-
private aliasOf;
|
|
43
|
-
/**
|
|
44
|
-
* The function a call returns, as `conditionalTest({ min: 22 })` returns `it` or `it.skip`: what all the values the
|
|
45
|
-
* function returns have in common, as for a variable chosen by a condition. When the function can't be found, a call
|
|
46
|
-
* named like a test object is read as one on trust, with a warning; see `returnedTestObject`.
|
|
47
|
-
*/
|
|
48
|
-
private returned;
|
|
49
|
-
/** Whether a local variable is read as a test object it could be; see `localTestObject`. */
|
|
50
|
-
private assumeLocal;
|
|
51
|
-
private looksLikeTest;
|
|
52
|
-
/**
|
|
53
|
-
* Whether a call through an imported name is worth following to the module: shaped like a test, or any call with a
|
|
54
|
-
* title and a body through a name that reads like a test function, as `it.effect('title', fn)` on a test object that
|
|
55
|
-
* a package extends.
|
|
56
|
-
*/
|
|
57
|
-
private worthFollowing;
|
|
58
|
-
/** Vitest as followed modules are read, with the rules of the scanned version. */
|
|
59
|
-
private framework;
|
|
60
|
-
/** Follows the import of `name` once, and reports it once when it can't be followed and `report` is set. */
|
|
61
|
-
private follow;
|
|
62
|
-
/** Hooks called directly, such as `beforeEach(...)` imported from Vitest or used as a global. */
|
|
63
|
-
private hookCall;
|
|
64
|
-
private hook;
|
|
15
|
+
protected hook(call: CallExpression, name: keyof HookCounts, context: CallContext): ApiCall;
|
|
65
16
|
private contextSkip;
|
|
66
17
|
/**
|
|
67
18
|
* `annotate(message, type?)` from the test context (3.2), as an annotation of type `notice` when no type is given; the
|
|
@@ -79,7 +30,11 @@ export declare class VitestAdapter implements Adapter {
|
|
|
79
30
|
private retry;
|
|
80
31
|
private meta;
|
|
81
32
|
private caseCount;
|
|
82
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The cases of a table whose values the scan can work out, as Vitest passes them: `.each` spreads a case that is an
|
|
35
|
+
* array into the callback's arguments when every case is one, and `.for` passes each case whole. A tagged template
|
|
36
|
+
* gives one object per row, keyed by the names in its first line.
|
|
37
|
+
*/
|
|
38
|
+
private tableCases;
|
|
83
39
|
private tagsUsed;
|
|
84
40
|
}
|
|
85
|
-
export {};
|
|
@@ -1,26 +1,8 @@
|
|
|
1
1
|
import { childNodes, isFunctionNode, isImportMetaVitest, propertyKey, propertyName, unwrap, } from "../ast.js";
|
|
2
2
|
import { snippet } from "../source.js";
|
|
3
|
-
import {
|
|
4
|
-
import { atLeast, hookFunction,
|
|
5
|
-
|
|
6
|
-
function shared(a, b) {
|
|
7
|
-
if (!a || a.kind !== b?.kind)
|
|
8
|
-
return null;
|
|
9
|
-
const same = (link, index) => {
|
|
10
|
-
const other = b.links.at(index);
|
|
11
|
-
return other?.name === link.name && !link.args && !link.template && !other.args && !other.template;
|
|
12
|
-
};
|
|
13
|
-
const differs = a.links.findIndex((link, index) => !same(link, index));
|
|
14
|
-
return { kind: a.kind, links: differs === -1 ? a.links : a.links.slice(0, differs) };
|
|
15
|
-
}
|
|
16
|
-
// Maps rather than object literals, so names such as `constructor` don't match inherited properties.
|
|
17
|
-
const KINDS = new Map([
|
|
18
|
-
['test', 'test'],
|
|
19
|
-
['it', 'test'],
|
|
20
|
-
['describe', 'describe'],
|
|
21
|
-
['suite', 'describe'],
|
|
22
|
-
]);
|
|
23
|
-
const MISMATCHED_SOURCES = new Set(['@playwright/test', 'playwright/test', '@jest/globals']);
|
|
3
|
+
import { vitestCaseTitle } from "../case-titles.js";
|
|
4
|
+
import { atLeast, hookFunction, problem, readChain, readCondition, destructuredNames, takesWhole, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, origin, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
|
|
5
|
+
import { SuiteAdapter } from "./test-functions.js";
|
|
24
6
|
const MODIFIERS = {
|
|
25
7
|
test: new Set(['concurrent', 'sequential', 'only', 'skip', 'todo', 'fails']),
|
|
26
8
|
describe: new Set(['concurrent', 'sequential', 'shuffle', 'skip', 'only', 'todo']),
|
|
@@ -44,7 +26,25 @@ function isHook(name) {
|
|
|
44
26
|
/** First members of chains that never declare a test, even when Vitest refuses the rest of the chain. */
|
|
45
27
|
const NOT_DECLARING = new Set([...HOOKS.keys(), ...DERIVATIONS]);
|
|
46
28
|
/** First members that show a name imported from a local file is used as Vitest's `test`. */
|
|
47
|
-
const TEST_MEMBERS =
|
|
29
|
+
const TEST_MEMBERS = [...MODIFIERS.test, ...MODIFIERS.describe, ...TABLES, ...CONDITIONS];
|
|
30
|
+
const VITEST = {
|
|
31
|
+
name: 'Vitest',
|
|
32
|
+
source: 'vitest',
|
|
33
|
+
mismatched: new Set(['@playwright/test', 'playwright/test', '@jest/globals']),
|
|
34
|
+
starts: new Map([
|
|
35
|
+
['test', { kind: 'test', links: [] }],
|
|
36
|
+
['it', { kind: 'test', links: [] }],
|
|
37
|
+
['describe', { kind: 'describe', links: [] }],
|
|
38
|
+
['suite', { kind: 'describe', links: [] }],
|
|
39
|
+
]),
|
|
40
|
+
hooks: new Set(HOOKS.keys()),
|
|
41
|
+
derivations: DERIVATIONS,
|
|
42
|
+
modifiers: new Set([...MODIFIERS.test, ...MODIFIERS.describe]),
|
|
43
|
+
carriesKinds: true,
|
|
44
|
+
isModule: isImportMetaVitest,
|
|
45
|
+
defaultImport: "Vitest has no default export; import { test, describe } from 'vitest' instead.",
|
|
46
|
+
required: 'Vitest cannot be loaded with require(); tests that use it are missing.',
|
|
47
|
+
};
|
|
48
48
|
/** Calls Vitest moves to the top of the file before running it: `vi.mock`, `vi.unmock` and `vi.hoisted`. */
|
|
49
49
|
const HOISTED_OBJECTS = new Set(['vi', 'vitest']);
|
|
50
50
|
const HOISTED_METHODS = new Set(['mock', 'unmock', 'hoisted']);
|
|
@@ -112,186 +112,16 @@ function fixturesOf(fn, index, version) {
|
|
|
112
112
|
return !since || !atLeast(version, ...since);
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
|
-
|
|
116
|
-
class VitestBindings {
|
|
117
|
-
kinds = new Map();
|
|
118
|
-
hooks = new Map();
|
|
119
|
-
namespaces = new Set();
|
|
120
|
-
/** Names `bench` is imported as from Vitest 5, which no longer exports it. */
|
|
121
|
-
benches = new Set();
|
|
122
|
-
/** The import specifiers and declarators that bind those names, to tell them from local names that shadow them. */
|
|
123
|
-
declarations = new Set();
|
|
124
|
-
problems = [];
|
|
125
|
-
/** Names imported from other modules, which may be test objects there. */
|
|
126
|
-
imported;
|
|
127
|
-
follow;
|
|
128
|
-
/** The module's functions and the variables they may build test objects in; see `ModuleFunctions`. */
|
|
129
|
-
functions;
|
|
130
|
-
constructor(program, version, follow) {
|
|
131
|
-
this.imported = moduleImports(program, (source) => source === 'vitest' || MISMATCHED_SOURCES.has(source));
|
|
132
|
-
this.follow = follow;
|
|
133
|
-
this.functions = new ModuleFunctions(program, this.imported, follow);
|
|
134
|
-
for (const statement of program.body) {
|
|
135
|
-
if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type')
|
|
136
|
-
continue;
|
|
137
|
-
const source = statement.source.value;
|
|
138
|
-
if (MISMATCHED_SOURCES.has(source)) {
|
|
139
|
-
this.problems.push(problem('framework-mismatch', statement, `The file imports '${source}', but the scan is for Vitest.`));
|
|
140
|
-
}
|
|
141
|
-
if (source !== 'vitest')
|
|
142
|
-
continue;
|
|
143
|
-
for (const specifier of statement.specifiers) {
|
|
144
|
-
this.declarations.add(specifier);
|
|
145
|
-
if (specifier.type === 'ImportNamespaceSpecifier') {
|
|
146
|
-
this.namespaces.add(specifier.local.name);
|
|
147
|
-
}
|
|
148
|
-
else if (specifier.type === 'ImportDefaultSpecifier') {
|
|
149
|
-
this.problems.push(problem('invalid-import', specifier, "Vitest has no default export; import { test, describe } from 'vitest' instead."));
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
this.bindImported(exportName(specifier.imported), specifier.local.name, version);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const declarators = allDeclarators(program);
|
|
157
|
-
for (const declarator of declarators) {
|
|
158
|
-
if (declarator.init && requiredModule(declarator.init) === 'vitest') {
|
|
159
|
-
this.problems.push(problem('invalid-import', declarator, 'Vitest cannot be loaded with require(); tests that use it are missing.'));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
bindAll(declarators, (declarator) => {
|
|
163
|
-
const bound = this.bind(declarator);
|
|
164
|
-
if (bound)
|
|
165
|
-
this.declarations.add(declarator);
|
|
166
|
-
return bound;
|
|
167
|
-
});
|
|
168
|
-
this.functions.keep(declarators.filter((declarator) => !this.declarations.has(declarator)));
|
|
169
|
-
}
|
|
170
|
-
isTestName(name) {
|
|
171
|
-
return this.kinds.has(name) || this.importsTestObject(name) || this.holdsTestObject(name);
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* `followsCalls` is cleared where a call is not followed to what it returns: while the names are bound, so that only
|
|
175
|
-
* a value asked about is followed, as following reads other modules, and in the scanned file, whose calls the walk
|
|
176
|
-
* follows in their scopes.
|
|
177
|
-
*/
|
|
178
|
-
isTestObject(expression, followsCalls = true) {
|
|
179
|
-
const node = unwrap(expression);
|
|
180
|
-
if (node.type === 'Identifier') {
|
|
181
|
-
const { name } = node;
|
|
182
|
-
return (this.kinds.get(name) === 'test' || this.importsTestObject(name) || (followsCalls && this.holdsTestObject(name)));
|
|
183
|
-
}
|
|
184
|
-
if (node.type === 'MemberExpression') {
|
|
185
|
-
return KINDS.get(propertyName(node) ?? '') === 'test' && this.isNamespace(node.object);
|
|
186
|
-
}
|
|
187
|
-
// `process.env.CI ? vitestTest : skippedTest` is a test object either way.
|
|
188
|
-
if (node.type === 'ConditionalExpression') {
|
|
189
|
-
return this.isTestObject(node.consequent, followsCalls) || this.isTestObject(node.alternate, followsCalls);
|
|
190
|
-
}
|
|
191
|
-
if (node.type !== 'CallExpression')
|
|
192
|
-
return false;
|
|
193
|
-
const callee = unwrap(node.callee);
|
|
194
|
-
// `defineBrokerTest()`, a function that returns a test object.
|
|
195
|
-
if (callee.type === 'Identifier')
|
|
196
|
-
return followsCalls && this.returnsTestObject(callee.name);
|
|
197
|
-
return (callee.type === 'MemberExpression' &&
|
|
198
|
-
DERIVATIONS.has(propertyName(callee) ?? '') &&
|
|
199
|
-
this.isTestObject(callee.object, followsCalls));
|
|
200
|
-
}
|
|
201
|
-
returnsTestObject(name) {
|
|
202
|
-
return this.functions.returnsTestObject(name, (expression) => this.isTestObject(expression));
|
|
203
|
-
}
|
|
204
|
-
holdsTestObject(name) {
|
|
205
|
-
return this.functions.holdsTestObject(name, (expression) => this.isTestObject(expression));
|
|
206
|
-
}
|
|
207
|
-
reexports(source, imported, exported) {
|
|
208
|
-
if (source !== 'vitest')
|
|
209
|
-
return this.follow(source, imported, exported);
|
|
210
|
-
return exported === 'test object' && KINDS.has(imported);
|
|
211
|
-
}
|
|
212
|
-
/** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
|
|
213
|
-
importsTestObject(name) {
|
|
214
|
-
const from = this.imported.get(name);
|
|
215
|
-
return from !== undefined && this.follow(from.source, from.imported);
|
|
216
|
-
}
|
|
217
|
-
isNamespace(expression) {
|
|
218
|
-
const node = unwrap(expression);
|
|
219
|
-
return node.type === 'Identifier' && this.namespaces.has(node.name);
|
|
220
|
-
}
|
|
221
|
-
bindImported(imported, local, version) {
|
|
222
|
-
const kind = KINDS.get(imported);
|
|
223
|
-
if (kind) {
|
|
224
|
-
this.kinds.set(local, kind);
|
|
225
|
-
}
|
|
226
|
-
else if (isHook(imported)) {
|
|
227
|
-
this.hooks.set(local, imported);
|
|
228
|
-
}
|
|
229
|
-
else if (imported === 'bench' && atLeast(version, 5, 0)) {
|
|
230
|
-
this.benches.add(local);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
bind(declarator) {
|
|
234
|
-
const { id, init } = declarator;
|
|
235
|
-
if (!init)
|
|
236
|
-
return false;
|
|
237
|
-
if (id.type === 'Identifier' && this.isTestObject(init, false)) {
|
|
238
|
-
this.kinds.set(id.name, 'test');
|
|
239
|
-
return true;
|
|
240
|
-
}
|
|
241
|
-
if (id.type !== 'ObjectPattern' || !isImportMetaVitest(init))
|
|
242
|
-
return false;
|
|
243
|
-
// In-source tests: const { it, describe } = import.meta.vitest
|
|
244
|
-
for (const property of id.properties) {
|
|
245
|
-
if (property.type === 'RestElement' || property.value.type !== 'Identifier')
|
|
246
|
-
continue;
|
|
247
|
-
const key = propertyKey(property);
|
|
248
|
-
const kind = KINDS.get(key ?? '');
|
|
249
|
-
if (kind)
|
|
250
|
-
this.kinds.set(property.value.name, kind);
|
|
251
|
-
if (key !== null && isHook(key))
|
|
252
|
-
this.hooks.set(property.value.name, key);
|
|
253
|
-
}
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
export class VitestAdapter {
|
|
258
|
-
problems;
|
|
259
|
-
fileTags = [];
|
|
260
|
-
fileTagSpans = [];
|
|
261
|
-
usesGlobals = false;
|
|
262
|
-
file;
|
|
263
|
-
readModule;
|
|
264
|
-
bindings;
|
|
265
|
-
imports;
|
|
266
|
-
followed = new Map();
|
|
267
|
-
reported = new Set();
|
|
268
|
-
/** Variables and names of functions read as test objects on trust, each reported once. */
|
|
269
|
-
assumed = new Set();
|
|
270
|
-
aliases = new Map();
|
|
271
|
-
/** Describe callbacks and the name of their first parameter, which Vitest calls with its `test`. */
|
|
272
|
-
suiteParameters = new Map();
|
|
115
|
+
export class VitestAdapter extends SuiteAdapter {
|
|
273
116
|
warnedAboutTags = false;
|
|
274
117
|
constructor(parsed, file, readModule) {
|
|
275
|
-
|
|
276
|
-
this.readModule = readModule;
|
|
277
|
-
// A test object the file builds from an imported one, as in `const it = base.extend({...})`, is followed quietly:
|
|
278
|
-
// a call through the import itself is what reports an import that can't be followed.
|
|
279
|
-
const followed = new Map();
|
|
280
|
-
this.bindings = new VitestBindings(parsed.program, file.version, (source, imported, exported = 'test object') => {
|
|
281
|
-
const key = `${source}\0${exported}\0${imported}`;
|
|
282
|
-
if (!followed.has(key)) {
|
|
283
|
-
followed.set(key, followModule(source, imported, readModule, this.framework(), exported).exported);
|
|
284
|
-
}
|
|
285
|
-
return followed.get(key) === true;
|
|
286
|
-
});
|
|
287
|
-
this.problems = [...this.bindings.problems];
|
|
118
|
+
super(parsed, file, readModule, VITEST, TEST_MEMBERS);
|
|
288
119
|
if (atLeast(file.version, 5, 0)) {
|
|
289
120
|
for (const call of nestedHoistedCalls(parsed.program)) {
|
|
290
121
|
const callee = snippet(text(this.file, call.callee));
|
|
291
122
|
this.problems.push(problem('removed-api', call, `Vitest 5 fails a file that calls ${callee}(...) anywhere but its top level, because the call is hoisted there.`, callee));
|
|
292
123
|
}
|
|
293
124
|
}
|
|
294
|
-
this.imports = moduleImports(parsed.program, (source) => source === 'vitest' || MISMATCHED_SOURCES.has(source));
|
|
295
125
|
if (atLeast(file.version, 4, 1)) {
|
|
296
126
|
// Vitest's own pattern, over the whole text as Vitest reads it, so the same tags come out.
|
|
297
127
|
for (const match of parsed.source.text.matchAll(/(\/\/|\*)\s*@module-tag\s+([\w\-/]+)\b/g)) {
|
|
@@ -302,19 +132,21 @@ export class VitestAdapter {
|
|
|
302
132
|
}
|
|
303
133
|
}
|
|
304
134
|
}
|
|
135
|
+
isAliasLink(kind, link) {
|
|
136
|
+
return MODIFIERS[kind].has(link.name) || TABLES.has(link.name) || CONDITIONS.has(link.name);
|
|
137
|
+
}
|
|
305
138
|
classify(call, context) {
|
|
306
139
|
return (this.benchCall(call, context) ??
|
|
307
140
|
this.hookCall(call, context) ??
|
|
308
141
|
this.contextSkip(call, context) ??
|
|
309
142
|
this.chainCall(call, context));
|
|
310
143
|
}
|
|
311
|
-
testFunction(expression, scopes, returnsOf) {
|
|
312
|
-
return this.aliasOf(expression, { scopes, inBody: false, returnsOf });
|
|
313
|
-
}
|
|
314
144
|
/** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
|
|
315
145
|
benchCall(call, context) {
|
|
316
146
|
const base = readChain(call.callee)?.base;
|
|
317
|
-
if (base?.type !== 'Identifier' ||
|
|
147
|
+
if (base?.type !== 'Identifier' || this.bindings.members.get(base.name) !== 'bench')
|
|
148
|
+
return null;
|
|
149
|
+
if (!atLeast(this.file.version, 5, 0))
|
|
318
150
|
return null;
|
|
319
151
|
const declaration = this.file.values.declarationOf(base.name, context.scopes);
|
|
320
152
|
if (declaration === null || !this.bindings.declarations.has(declaration))
|
|
@@ -358,168 +190,6 @@ export class VitestAdapter {
|
|
|
358
190
|
// Which of its parts the chain means is not known, so it declares a plain test or describe.
|
|
359
191
|
return refused(call, refusal, context, () => this.declaration(call, kind, [], context));
|
|
360
192
|
}
|
|
361
|
-
/**
|
|
362
|
-
* The function a chain calls and the links after it, or null when it isn't one of Vitest's. An import that can't be
|
|
363
|
-
* followed is reported only outside test and hook bodies, where a call can declare tests.
|
|
364
|
-
*/
|
|
365
|
-
resolve(chain, args, lookup) {
|
|
366
|
-
const start = this.start(chain, args, lookup);
|
|
367
|
-
if (!start)
|
|
368
|
-
return null;
|
|
369
|
-
let kind = start.kind;
|
|
370
|
-
let links = [...(start.modifiers ?? []), ...chain.links.slice(start.index)];
|
|
371
|
-
// A test object derived inline, as in `test.extend({ ... }).skip(...)`.
|
|
372
|
-
links = links.slice(links.findLastIndex((link) => DERIVATIONS.has(link.name) && link.args !== null) + 1);
|
|
373
|
-
// Vitest's test object also carries `describe` and `suite`, and they carry `test` and `it`.
|
|
374
|
-
while (links.length > 0 && KINDS.has(links[0].name) && !links[0].args) {
|
|
375
|
-
kind = KINDS.get(links[0].name);
|
|
376
|
-
links = links.slice(1);
|
|
377
|
-
}
|
|
378
|
-
return { kind, links };
|
|
379
|
-
}
|
|
380
|
-
start(chain, args, lookup) {
|
|
381
|
-
const { base, links } = chain;
|
|
382
|
-
const { scopes, inBody } = lookup;
|
|
383
|
-
if (base.type === 'CallExpression') {
|
|
384
|
-
const returned = this.returned(base, links, args, lookup);
|
|
385
|
-
return returned && { kind: returned.kind, index: 0, modifiers: returned.links };
|
|
386
|
-
}
|
|
387
|
-
if (base.type !== 'Identifier')
|
|
388
|
-
return null;
|
|
389
|
-
// A name the file doesn't declare is one of Vitest's globals; a local variable or parameter with the same name,
|
|
390
|
-
// such as `(test) => test.result()`, is not Vitest's.
|
|
391
|
-
const declaration = this.file.values.declarationOf(base.name, scopes);
|
|
392
|
-
if (declaration === null) {
|
|
393
|
-
const global = KINDS.get(base.name);
|
|
394
|
-
if (global)
|
|
395
|
-
this.usesGlobals = true;
|
|
396
|
-
return global ? { kind: global, index: 0 } : null;
|
|
397
|
-
}
|
|
398
|
-
// A parameter's declaration is its function: `describe('market orders', (test) => { test(...) })`.
|
|
399
|
-
if (this.suiteParameters.get(declaration) === base.name)
|
|
400
|
-
return { kind: 'test', index: 0 };
|
|
401
|
-
const followed = this.imports.get(base.name)?.node === declaration &&
|
|
402
|
-
this.worthFollowing(base.name, links, args) &&
|
|
403
|
-
this.follow(base.name, !inBody && mayDeclareTests(base.name, links));
|
|
404
|
-
if (!followed && !this.bindings.declarations.has(declaration)) {
|
|
405
|
-
const alias = declaration.type === 'VariableDeclarator' ? this.alias(declaration, lookup) : null;
|
|
406
|
-
if (alias)
|
|
407
|
-
return { kind: alias.kind, index: 0, modifiers: alias.links };
|
|
408
|
-
return this.assumeLocal(declaration, base.name, links, args)
|
|
409
|
-
? { kind: KINDS.get(base.name) ?? 'test', index: 0 }
|
|
410
|
-
: null;
|
|
411
|
-
}
|
|
412
|
-
if (this.bindings.namespaces.has(base.name)) {
|
|
413
|
-
const kind = KINDS.get(links.at(0)?.name ?? '');
|
|
414
|
-
return kind ? { kind, index: 1 } : null;
|
|
415
|
-
}
|
|
416
|
-
const kind = this.bindings.kinds.get(base.name);
|
|
417
|
-
return kind ? { kind, index: 0 } : null;
|
|
418
|
-
}
|
|
419
|
-
/** The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`. */
|
|
420
|
-
alias(declarator, lookup) {
|
|
421
|
-
let alias = this.aliases.get(declarator);
|
|
422
|
-
if (alias === undefined) {
|
|
423
|
-
// Set first, so a variable defined through itself ends the lookup.
|
|
424
|
-
this.aliases.set(declarator, null);
|
|
425
|
-
alias = declarator.init ? this.aliasOf(declarator.init, lookup) : null;
|
|
426
|
-
this.aliases.set(declarator, alias);
|
|
427
|
-
}
|
|
428
|
-
return alias;
|
|
429
|
-
}
|
|
430
|
-
aliasOf(expression, lookup) {
|
|
431
|
-
const node = unwrap(expression);
|
|
432
|
-
if (node.type === 'ConditionalExpression') {
|
|
433
|
-
// `isCI ? describe.concurrent : describe`: only the modifiers both branches have apply.
|
|
434
|
-
return shared(this.aliasOf(node.consequent, lookup), this.aliasOf(node.alternate, lookup));
|
|
435
|
-
}
|
|
436
|
-
const chain = readChain(node);
|
|
437
|
-
const resolved = chain ? this.resolve(chain, [], lookup) : null;
|
|
438
|
-
const allowed = (link) => resolved !== null &&
|
|
439
|
-
(MODIFIERS[resolved.kind].has(link.name) || TABLES.has(link.name) || CONDITIONS.has(link.name));
|
|
440
|
-
return resolved?.links.every(allowed) ? resolved : null;
|
|
441
|
-
}
|
|
442
|
-
/**
|
|
443
|
-
* The function a call returns, as `conditionalTest({ min: 22 })` returns `it` or `it.skip`: what all the values the
|
|
444
|
-
* function returns have in common, as for a variable chosen by a condition. When the function can't be found, a call
|
|
445
|
-
* named like a test object is read as one on trust, with a warning; see `returnedTestObject`.
|
|
446
|
-
*/
|
|
447
|
-
returned(call, links, args, lookup) {
|
|
448
|
-
const callee = unwrap(call.callee);
|
|
449
|
-
if (callee.type !== 'Identifier')
|
|
450
|
-
return null;
|
|
451
|
-
const returns = lookup.returnsOf(callee.name, lookup.scopes);
|
|
452
|
-
if (returns)
|
|
453
|
-
return returnedFunction(returns, shared);
|
|
454
|
-
if (!isTestName(callee.name) || !this.looksLikeTest(links, args))
|
|
455
|
-
return null;
|
|
456
|
-
if (!this.assumed.has(callee.name)) {
|
|
457
|
-
this.assumed.add(callee.name);
|
|
458
|
-
this.problems.push(returnedTestObject(this.file, call));
|
|
459
|
-
}
|
|
460
|
-
return { kind: KINDS.get(callee.name) ?? 'test', links: [] };
|
|
461
|
-
}
|
|
462
|
-
/** Whether a local variable is read as a test object it could be; see `localTestObject`. */
|
|
463
|
-
assumeLocal(declaration, name, links, args) {
|
|
464
|
-
if (declaration.type !== 'VariableDeclarator' || !mayHoldTestObject(declaration) || !isTestName(name))
|
|
465
|
-
return false;
|
|
466
|
-
if (!this.looksLikeTest(links, args))
|
|
467
|
-
return false;
|
|
468
|
-
if (!this.assumed.has(declaration)) {
|
|
469
|
-
this.assumed.add(declaration);
|
|
470
|
-
this.problems.push(localTestObject(this.file, name, declaration));
|
|
471
|
-
}
|
|
472
|
-
return true;
|
|
473
|
-
}
|
|
474
|
-
looksLikeTest(links, args) {
|
|
475
|
-
return links.length === 0 ? looksLikeDeclaration(args) : TEST_MEMBERS.has(links[0].name);
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Whether a call through an imported name is worth following to the module: shaped like a test, or any call with a
|
|
479
|
-
* title and a body through a name that reads like a test function, as `it.effect('title', fn)` on a test object that
|
|
480
|
-
* a package extends.
|
|
481
|
-
*/
|
|
482
|
-
worthFollowing(name, links, args) {
|
|
483
|
-
return this.looksLikeTest(links, args) || (isTestName(name) && looksLikeDeclaration(args));
|
|
484
|
-
}
|
|
485
|
-
/** Vitest as followed modules are read, with the rules of the scanned version. */
|
|
486
|
-
framework() {
|
|
487
|
-
const version = this.file.version;
|
|
488
|
-
return {
|
|
489
|
-
name: 'Vitest',
|
|
490
|
-
isSource: (source) => source === 'vitest',
|
|
491
|
-
testObjects: (program, follow) => new VitestBindings(program, version, follow),
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
/** Follows the import of `name` once, and reports it once when it can't be followed and `report` is set. */
|
|
495
|
-
follow(name, report) {
|
|
496
|
-
const imported = this.imports.get(name);
|
|
497
|
-
let result = this.followed.get(name);
|
|
498
|
-
if (!result) {
|
|
499
|
-
result = followImport(name, imported, this.readModule, this.file, this.framework());
|
|
500
|
-
this.followed.set(name, result);
|
|
501
|
-
if (result.exported) {
|
|
502
|
-
this.bindings.kinds.set(name, KINDS.get(imported.imported) ?? 'test');
|
|
503
|
-
this.bindings.declarations.add(imported.node);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
if (report && result.problem && !this.reported.has(name)) {
|
|
507
|
-
this.reported.add(name);
|
|
508
|
-
this.problems.push(result.problem);
|
|
509
|
-
}
|
|
510
|
-
return result.exported;
|
|
511
|
-
}
|
|
512
|
-
/** Hooks called directly, such as `beforeEach(...)` imported from Vitest or used as a global. */
|
|
513
|
-
hookCall(call, context) {
|
|
514
|
-
const callee = unwrap(call.callee);
|
|
515
|
-
if (callee.type !== 'Identifier')
|
|
516
|
-
return null;
|
|
517
|
-
const declaration = this.file.values.declarationOf(callee.name, context.scopes);
|
|
518
|
-
const hook = declaration === null
|
|
519
|
-
? isHook(callee.name) && callee.name
|
|
520
|
-
: this.bindings.declarations.has(declaration) && this.bindings.hooks.get(callee.name);
|
|
521
|
-
return hook ? this.hook(call, hook, context) : null;
|
|
522
|
-
}
|
|
523
193
|
hook(call, name, context) {
|
|
524
194
|
const fn = hookFunction(this.file, context.scopes, call.arguments);
|
|
525
195
|
const index = HOOKS.get(name);
|
|
@@ -695,6 +365,7 @@ export class VitestAdapter {
|
|
|
695
365
|
hashed: fn ?? namedFunction(this.file, context.scopes, body),
|
|
696
366
|
isParameterized: table !== null,
|
|
697
367
|
caseCount: table ? this.caseCount(call, table, context, problems) : null,
|
|
368
|
+
cases: table ? this.tableCases(table, title, context) : null,
|
|
698
369
|
problems,
|
|
699
370
|
};
|
|
700
371
|
let declared;
|
|
@@ -887,19 +558,43 @@ export class VitestAdapter {
|
|
|
887
558
|
}
|
|
888
559
|
return count;
|
|
889
560
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
561
|
+
/**
|
|
562
|
+
* The cases of a table whose values the scan can work out, as Vitest passes them: `.each` spreads a case that is an
|
|
563
|
+
* array into the callback's arguments when every case is one, and `.for` passes each case whole. A tagged template
|
|
564
|
+
* gives one object per row, keyed by the names in its first line.
|
|
565
|
+
*/
|
|
566
|
+
tableCases(table, title, context) {
|
|
567
|
+
let cases;
|
|
568
|
+
if (table.template) {
|
|
569
|
+
const values = table.template.expressions.map((item) => this.file.values.resolve(item, context.scopes));
|
|
570
|
+
if (!values.every((item) => item.ok))
|
|
571
|
+
return null;
|
|
572
|
+
const header = templateHeader(table.template);
|
|
573
|
+
// Without values, Vitest passes the template's strings as the table.
|
|
574
|
+
cases =
|
|
575
|
+
values.length === 0
|
|
576
|
+
? table.template.quasis.map((quasi) => quasi.value.cooked ?? undefined)
|
|
577
|
+
: Array.from({ length: Math.floor(values.length / header.length) }, (_, row) => Object.fromEntries(header.map((name, column) => [name, values[row * header.length + column].value])));
|
|
901
578
|
}
|
|
902
|
-
|
|
579
|
+
else {
|
|
580
|
+
const first = table.args.at(0);
|
|
581
|
+
const resolved = first && first.type !== 'SpreadElement' ? this.file.values.resolve(first, context.scopes) : null;
|
|
582
|
+
if (!resolved?.ok || !Array.isArray(resolved.value))
|
|
583
|
+
return null;
|
|
584
|
+
cases = resolved.value;
|
|
585
|
+
}
|
|
586
|
+
const caseTitle = (items, index) => title.isDynamic ? null : vitestCaseTitle(title.text, items, index, this.file.version);
|
|
587
|
+
if (table.name === 'for') {
|
|
588
|
+
return cases.map((item, index) => ({
|
|
589
|
+
title: caseTitle(item === null || item === undefined ? [] : Array.isArray(item) ? item : [item], index),
|
|
590
|
+
args: [item],
|
|
591
|
+
}));
|
|
592
|
+
}
|
|
593
|
+
const arraysOnly = cases.every((item) => Array.isArray(item));
|
|
594
|
+
return cases.map((item, index) => {
|
|
595
|
+
const items = Array.isArray(item) ? item : [item];
|
|
596
|
+
return { title: caseTitle(items, index), args: arraysOnly ? items : [item] };
|
|
597
|
+
});
|
|
903
598
|
}
|
|
904
599
|
tagsUsed(node) {
|
|
905
600
|
if (this.warnedAboutTags)
|
|
@@ -908,13 +603,24 @@ export class VitestAdapter {
|
|
|
908
603
|
this.problems.push(problem('config-may-change-state', node, "The file uses Vitest tags, and tags defined in the Vitest config can skip tests or change how they run; the scan can't work out the config's tags."));
|
|
909
604
|
}
|
|
910
605
|
}
|
|
911
|
-
/**
|
|
606
|
+
/**
|
|
607
|
+
* The column names of a tagged-template table such as ``each`side | qty ${'buy'} | ${100}` ``, as Vitest's
|
|
608
|
+
* `formatTemplateString` reads them: the text of the template with its spaces taken out, its first line split at `|`.
|
|
609
|
+
* Outer pipes, as in `| side | qty |`, give empty names too.
|
|
610
|
+
*/
|
|
611
|
+
function templateHeader(template) {
|
|
612
|
+
// A string with an invalid escape, as in `\xZZ`, has no cooked value, which joins as nothing.
|
|
613
|
+
const strings = template.quasis.map((quasi) => quasi.value.cooked ?? '');
|
|
614
|
+
return strings.join('').trim().replace(/ /g, '').split('\n')[0].split('|');
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Rows of a tagged-template table: as many as the values fill, an incomplete last row left out, as Vitest does. A
|
|
618
|
+
* template without values isn't split, so its text is the one case.
|
|
619
|
+
*/
|
|
912
620
|
function templateRows(template) {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
return null;
|
|
917
|
-
return template.expressions.length / columns;
|
|
621
|
+
if (template.expressions.length === 0)
|
|
622
|
+
return 1;
|
|
623
|
+
return Math.floor(template.expressions.length / templateHeader(template).length);
|
|
918
624
|
}
|
|
919
625
|
function isMetaValue(value) {
|
|
920
626
|
return value !== undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ModuleReader } from './model.ts';
|
|
2
|
+
import { type ImportReader } from './values.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Works out the values project modules export, for the files of one scan: each module is parsed once and each of its
|
|
5
|
+
* exports worked out once, however many test files import it. Installed packages are never read, since the reader
|
|
6
|
+
* only gives project files.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ExportedValues {
|
|
9
|
+
private readonly modules;
|
|
10
|
+
private readonly done;
|
|
11
|
+
/** Results of the current lookup, also those that `done` can't keep, so that each module is read once in it. */
|
|
12
|
+
private readonly current;
|
|
13
|
+
private readonly pending;
|
|
14
|
+
/** How many times a lookup was cut short by a cycle or the depth limit. */
|
|
15
|
+
private cutShort;
|
|
16
|
+
/** What a file that reads its imports with `readModule` gets for `import { name } from source`. */
|
|
17
|
+
reader(readModule: ModuleReader | null): ImportReader | null;
|
|
18
|
+
/** What `module` exports under `name`; null when it doesn't export it. */
|
|
19
|
+
private exported;
|
|
20
|
+
private read;
|
|
21
|
+
private open;
|
|
22
|
+
}
|