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.
Files changed (49) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +4 -3
  3. package/dist/case-titles.d.ts +27 -0
  4. package/dist/case-titles.js +329 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +6 -5
  7. package/dist/config-dependencies.js +8 -3
  8. package/dist/config-runs.d.ts +22 -0
  9. package/dist/config-runs.js +263 -3
  10. package/dist/config.d.ts +18 -1
  11. package/dist/config.js +160 -14
  12. package/dist/dependency-hash.d.ts +13 -5
  13. package/dist/dependency-hash.js +56 -12
  14. package/dist/diagnostics.js +3 -0
  15. package/dist/extract.d.ts +19 -1
  16. package/dist/extract.js +391 -41
  17. package/dist/files.js +3 -2
  18. package/dist/filter.js +1 -0
  19. package/dist/framework-version.d.ts +13 -2
  20. package/dist/framework-version.js +64 -5
  21. package/dist/frameworks/bindings.d.ts +14 -0
  22. package/dist/frameworks/bindings.js +99 -6
  23. package/dist/frameworks/common.d.ts +6 -0
  24. package/dist/frameworks/common.js +27 -0
  25. package/dist/frameworks/jest.d.ts +46 -0
  26. package/dist/frameworks/jest.js +442 -0
  27. package/dist/frameworks/playwright.d.ts +3 -1
  28. package/dist/frameworks/playwright.js +14 -5
  29. package/dist/frameworks/test-functions.d.ts +169 -0
  30. package/dist/frameworks/test-functions.js +492 -0
  31. package/dist/frameworks/vitest.d.ts +13 -58
  32. package/dist/frameworks/vitest.js +84 -378
  33. package/dist/imported-values.d.ts +22 -0
  34. package/dist/imported-values.js +149 -0
  35. package/dist/model.d.ts +11 -0
  36. package/dist/options.d.ts +16 -1
  37. package/dist/options.js +9 -2
  38. package/dist/parse.js +17 -2
  39. package/dist/records.js +57 -15
  40. package/dist/resolve.js +39 -36
  41. package/dist/scan-file.d.ts +5 -0
  42. package/dist/scan-file.js +15 -8
  43. package/dist/scan-source.js +3 -1
  44. package/dist/scan.js +31 -10
  45. package/dist/types.d.ts +24 -6
  46. package/dist/values.d.ts +49 -1
  47. package/dist/values.js +531 -19
  48. package/package.json +1 -1
  49. package/schema/v1.json +33 -4
@@ -0,0 +1,169 @@
1
+ import type { Argument, CallExpression, Expression, Node, Program } from 'oxc-parser';
2
+ import type { Adapter, ApiCall, CallContext, ModuleReader, Problem, Span } from '../model.ts';
3
+ import type { ParsedFile } from '../parse.ts';
4
+ import type { HookCounts } from '../types.ts';
5
+ import { type Exported, type FollowImport, type TestObjects } from './bindings.ts';
6
+ import { type Chain, type FileContext, type Link } from './common.ts';
7
+ export type Kind = 'test' | 'describe';
8
+ /** A test or describe function and the links after it, as in `test.skipIf(isCI)`. */
9
+ export interface TestFunction {
10
+ kind: Kind;
11
+ links: Link[];
12
+ }
13
+ /** A name that starts a chain: the function it is, and the links it stands for, as Jest's `fit` stands for `it.only`. */
14
+ export interface Start {
15
+ kind: Kind;
16
+ links: readonly string[];
17
+ }
18
+ /**
19
+ * How a framework with `describe` and `test` functions, imported or used as globals, names them: Vitest and Jest.
20
+ */
21
+ export interface Dialect {
22
+ /** The framework's name in messages. */
23
+ name: string;
24
+ /** The module the functions are imported from. */
25
+ source: string;
26
+ /** Modules of other frameworks, which a file of this one doesn't import. */
27
+ mismatched: ReadonlySet<string>;
28
+ /** The names that start a chain, as exported and as globals; a map, so `constructor` matches nothing inherited. */
29
+ starts: ReadonlyMap<string, Start>;
30
+ hooks: ReadonlySet<string>;
31
+ /** Members that build a test object from another, such as Vitest's `extend`. */
32
+ derivations: ReadonlySet<string>;
33
+ /** Members that give a test function a mode, as `skip` and `only` do, which a variable can hold it with. */
34
+ modifiers: ReadonlySet<string>;
35
+ /** Whether the test function carries the others, as Vitest's `test.describe` and `describe.test` do. */
36
+ carriesKinds: boolean;
37
+ /** Whether an expression is the framework's module, as `import.meta.vitest` or `require('@jest/globals')` are. */
38
+ isModule: (expression: Expression) => boolean;
39
+ /** Why a default import of the module fails; null when it gives the module. */
40
+ defaultImport: string | null;
41
+ /** Why `require()` of the module fails; null when the module can be required. */
42
+ required: string | null;
43
+ }
44
+ /** What a test function chosen at run time, as in `isCI ? it.skip : it`, is either way: only the links both have. */
45
+ export declare function shared(a: TestFunction | null, b: TestFunction | null): TestFunction | null;
46
+ /** The names a file binds to the framework's test functions, hooks, other exports and module. */
47
+ export declare class SuiteBindings implements TestObjects {
48
+ readonly starts: Map<string, Start>;
49
+ readonly hooks: Map<string, keyof HookCounts>;
50
+ readonly namespaces: Set<string>;
51
+ /** Other names imported from the framework, by their exported name, such as Vitest's `bench` or Jest's `jest`. */
52
+ readonly members: Map<string, string>;
53
+ /** The import specifiers and declarators that bind those names, to tell them from local names that shadow them. */
54
+ readonly declarations: Set<Node>;
55
+ readonly problems: Problem[];
56
+ private readonly dialect;
57
+ /** Names imported from other modules, which may be test objects there. */
58
+ private readonly imported;
59
+ private readonly follow;
60
+ /** The module's functions and the variables they may build test objects in; see `ModuleFunctions`. */
61
+ private readonly functions;
62
+ private readonly program;
63
+ /** Names the module declares or imports at its top level, which hide the framework's globals. */
64
+ private readonly declared;
65
+ /** Whether the module is one a scanned file imports from, where no walk reads its globals and chains. */
66
+ private readonly isFollowed;
67
+ constructor(program: Program, dialect: Dialect, follow: FollowImport, isFollowed?: boolean);
68
+ isTestName(name: string): boolean;
69
+ /**
70
+ * `followsCalls` is cleared where a call is not followed to what it returns: while the names are bound, so that only
71
+ * a value asked about is followed, as following reads other modules, and in the scanned file, whose calls the walk
72
+ * follows in their scopes.
73
+ */
74
+ isTestObject(expression: Expression, followsCalls?: boolean): boolean;
75
+ returnsTestObject(name: string): boolean;
76
+ /**
77
+ * What a name or chain stands for: one bound from the framework, a global the module doesn't hide, a member of the
78
+ * module, or one of those with modifiers, as `it.skip`; null otherwise.
79
+ */
80
+ startOf(expression: Expression): Start | null;
81
+ /**
82
+ * What a value stands for when the file binds it: in a scanned file, a name bound from the framework or a member of
83
+ * its module, as the walk reads globals and chains itself; in a followed module, also globals and chains.
84
+ */
85
+ private boundStart;
86
+ exportedStart(name: string): Start | null;
87
+ private holdsTestObject;
88
+ reexports(source: string, imported: string, exported: Exported): boolean;
89
+ /** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
90
+ private importsTestObject;
91
+ /** Whether an expression is the framework's module: a namespace import, or the module itself. */
92
+ isNamespace(expression: Expression): boolean;
93
+ private bindMember;
94
+ private bind;
95
+ }
96
+ /** Where a chain's names are looked up. */
97
+ export type Lookup = Pick<CallContext, 'scopes' | 'inBody' | 'returnsOf'>;
98
+ /**
99
+ * What Vitest's and Jest's adapters share: how a call's chain is followed to the test or describe function it starts
100
+ * from, through imports, variables, helper functions and globals.
101
+ */
102
+ export declare abstract class SuiteAdapter implements Adapter {
103
+ readonly problems: Problem[];
104
+ readonly fileTags: string[];
105
+ readonly fileTagSpans: Span[];
106
+ usesGlobals: boolean;
107
+ protected readonly file: FileContext;
108
+ protected readonly bindings: SuiteBindings;
109
+ protected readonly dialect: Dialect;
110
+ /** Describe callbacks and the name of their first parameter, which Vitest calls with its `test`. */
111
+ protected readonly suiteParameters: Map<Node, string>;
112
+ private readonly readModule;
113
+ private readonly imports;
114
+ private readonly followed;
115
+ private readonly reported;
116
+ /** Variables and names of functions read as test objects on trust, each reported once. */
117
+ private readonly assumed;
118
+ private readonly aliases;
119
+ /** First members that show a name imported from a local file is used as the framework's `test`. */
120
+ private readonly testMembers;
121
+ constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null, dialect: Dialect, testMembers: Iterable<string>);
122
+ abstract classify(call: CallExpression, context: CallContext): ApiCall | null;
123
+ /** Whether a variable that holds a test function may hold this link after it, as `const skipOnCi = test.skipIf(isCI)`. */
124
+ protected abstract isAliasLink(kind: Kind, link: Link): boolean;
125
+ protected abstract hook(call: CallExpression, name: keyof HookCounts, context: CallContext): ApiCall;
126
+ testFunction(expression: Expression, scopes: readonly Node[], returnsOf: CallContext['returnsOf']): TestFunction | null;
127
+ /**
128
+ * The function a chain calls and the links after it, or null when it isn't the framework's. An import that can't be
129
+ * followed is reported only outside test and hook bodies, where a call can declare tests.
130
+ */
131
+ protected resolve(chain: Chain, args: readonly Argument[], lookup: Lookup): TestFunction | null;
132
+ private start;
133
+ /**
134
+ * The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`, with the member `name`
135
+ * takes when it is destructured, as in `const { beforeEach } = test`. The members Vitest reads through `this`, such as
136
+ * `each` and `skipIf`, return a test function only when called, so their calls are never read through a variable.
137
+ */
138
+ private alias;
139
+ private aliasOf;
140
+ /**
141
+ * The test functions a value may hold, and whether it may also hold something else, as `process.env.CI` may. A test
142
+ * function is never falsy, so `test.skip || test` and `test.skip ?? test` hold `test.skip`, while
143
+ * `process.env.CI && test.skip || test` may hold either. Both branches of `?:` have to be test functions.
144
+ */
145
+ private choices;
146
+ /**
147
+ * The function a call returns, as `conditionalTest({ min: 22 })` returns `it` or `it.skip`: what all the values the
148
+ * function returns have in common, as for a variable chosen by a condition. When the function can't be found, a call
149
+ * named like a test object is read as one on trust, with a warning; see `returnedTestObject`.
150
+ */
151
+ private returned;
152
+ /** Whether a local variable is read as a test object it could be; see `localTestObject`. */
153
+ private assumeLocal;
154
+ private looksLikeTest;
155
+ /**
156
+ * Whether a call through an imported name is worth following to the module: shaped like a test, or any call with a
157
+ * title and a body through a name that reads like a test function, as `it.effect('title', fn)` on a test object that
158
+ * a package extends.
159
+ */
160
+ private worthFollowing;
161
+ /** The framework as followed modules are read. */
162
+ private framework;
163
+ /** Follows the import of `name` once, and reports it once when it can't be followed and `report` is set. */
164
+ private follow;
165
+ /** Hooks called directly, such as `beforeEach(...)` imported from the framework or used as a global. */
166
+ protected hookCall(call: CallExpression, context: CallContext): ApiCall | null;
167
+ /** The number of items of a table, also when not all its values are known; see `Values.lengthOf`. */
168
+ protected arrayLength(node: Argument | undefined, context: CallContext): number | null;
169
+ }
@@ -0,0 +1,492 @@
1
+ import { propertyKey, propertyName, unwrap } from "../ast.js";
2
+ import { bindingNames } from "../values.js";
3
+ import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, ownExport, requiredModule, topLevelDeclarators, } from "./bindings.js";
4
+ import { boundMembers, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, problem, readChain, returnedFunction, returnedTestObject, } from "./common.js";
5
+ /** What a test function chosen at run time, as in `isCI ? it.skip : it`, is either way: only the links both have. */
6
+ export 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
+ function links(names) {
17
+ return names.map((name) => ({ name, args: null, template: null }));
18
+ }
19
+ const TEST = { kind: 'test', links: [] };
20
+ /** The names a file binds to the framework's test functions, hooks, other exports and module. */
21
+ export class SuiteBindings {
22
+ starts = new Map();
23
+ hooks = new Map();
24
+ namespaces = new Set();
25
+ /** Other names imported from the framework, by their exported name, such as Vitest's `bench` or Jest's `jest`. */
26
+ members = new Map();
27
+ /** The import specifiers and declarators that bind those names, to tell them from local names that shadow them. */
28
+ declarations = new Set();
29
+ problems = [];
30
+ dialect;
31
+ /** Names imported from other modules, which may be test objects there. */
32
+ imported;
33
+ follow;
34
+ /** The module's functions and the variables they may build test objects in; see `ModuleFunctions`. */
35
+ functions;
36
+ program;
37
+ /** Names the module declares or imports at its top level, which hide the framework's globals. */
38
+ declared;
39
+ /** Whether the module is one a scanned file imports from, where no walk reads its globals and chains. */
40
+ isFollowed;
41
+ constructor(program, dialect, follow, isFollowed = false) {
42
+ this.dialect = dialect;
43
+ this.program = program;
44
+ this.isFollowed = isFollowed;
45
+ this.declared = topLevelNames(program);
46
+ this.imported = moduleImports(program, (source) => source === dialect.source || dialect.mismatched.has(source));
47
+ this.follow = follow;
48
+ this.functions = new ModuleFunctions(program, this.imported, follow);
49
+ for (const statement of program.body) {
50
+ if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type')
51
+ continue;
52
+ const source = statement.source.value;
53
+ if (dialect.mismatched.has(source)) {
54
+ this.problems.push(problem('framework-mismatch', statement, `The file imports '${source}', but the scan is for ${dialect.name}.`));
55
+ }
56
+ if (source !== dialect.source)
57
+ continue;
58
+ for (const specifier of statement.specifiers) {
59
+ this.declarations.add(specifier);
60
+ if (specifier.type === 'ImportNamespaceSpecifier') {
61
+ this.namespaces.add(specifier.local.name);
62
+ }
63
+ else if (specifier.type === 'ImportDefaultSpecifier') {
64
+ if (dialect.defaultImport === null)
65
+ this.namespaces.add(specifier.local.name);
66
+ else
67
+ this.problems.push(problem('invalid-import', specifier, dialect.defaultImport));
68
+ }
69
+ else {
70
+ this.bindMember(exportName(specifier.imported), specifier.local.name);
71
+ }
72
+ }
73
+ }
74
+ const declarators = allDeclarators(program);
75
+ if (dialect.required !== null) {
76
+ for (const declarator of declarators) {
77
+ if (declarator.init && requiredModule(declarator.init) === dialect.source) {
78
+ this.problems.push(problem('invalid-import', declarator, dialect.required));
79
+ }
80
+ }
81
+ }
82
+ bindAll(declarators, (declarator) => {
83
+ const bound = this.bind(declarator);
84
+ if (bound)
85
+ this.declarations.add(declarator);
86
+ return bound;
87
+ });
88
+ this.functions.keep(declarators.filter((declarator) => !this.declarations.has(declarator)));
89
+ }
90
+ isTestName(name) {
91
+ return this.starts.has(name) || this.importsTestObject(name) || this.holdsTestObject(name);
92
+ }
93
+ /**
94
+ * `followsCalls` is cleared where a call is not followed to what it returns: while the names are bound, so that only
95
+ * a value asked about is followed, as following reads other modules, and in the scanned file, whose calls the walk
96
+ * follows in their scopes.
97
+ */
98
+ isTestObject(expression, followsCalls = true) {
99
+ const node = unwrap(expression);
100
+ if (node.type === 'Identifier') {
101
+ const { name } = node;
102
+ return (this.boundStart(node)?.kind === 'test' ||
103
+ this.importsTestObject(name) ||
104
+ (followsCalls && this.holdsTestObject(name)));
105
+ }
106
+ if (node.type === 'MemberExpression')
107
+ return this.boundStart(node)?.kind === 'test';
108
+ // `process.env.CI ? vitestTest : skippedTest` is a test object either way.
109
+ if (node.type === 'ConditionalExpression') {
110
+ return this.isTestObject(node.consequent, followsCalls) || this.isTestObject(node.alternate, followsCalls);
111
+ }
112
+ if (node.type !== 'CallExpression')
113
+ return false;
114
+ const callee = unwrap(node.callee);
115
+ // `defineBrokerTest()`, a function that returns a test object.
116
+ if (callee.type === 'Identifier')
117
+ return followsCalls && this.returnsTestObject(callee.name);
118
+ return (callee.type === 'MemberExpression' &&
119
+ this.dialect.derivations.has(propertyName(callee) ?? '') &&
120
+ this.isTestObject(callee.object, followsCalls));
121
+ }
122
+ returnsTestObject(name) {
123
+ return this.functions.returnsTestObject(name, (expression) => this.isTestObject(expression));
124
+ }
125
+ /**
126
+ * What a name or chain stands for: one bound from the framework, a global the module doesn't hide, a member of the
127
+ * module, or one of those with modifiers, as `it.skip`; null otherwise.
128
+ */
129
+ startOf(expression) {
130
+ const node = unwrap(expression);
131
+ if (node.type === 'Identifier') {
132
+ const global = this.declared.has(node.name) ? undefined : this.dialect.starts.get(node.name);
133
+ return this.starts.get(node.name) ?? global ?? null;
134
+ }
135
+ if (node.type !== 'MemberExpression')
136
+ return null;
137
+ const name = propertyName(node);
138
+ if (name === null)
139
+ return null;
140
+ if (this.isNamespace(node.object))
141
+ return this.dialect.starts.get(name) ?? null;
142
+ const base = this.startOf(node.object);
143
+ return base && this.dialect.modifiers.has(name) ? { kind: base.kind, links: [...base.links, name] } : null;
144
+ }
145
+ /**
146
+ * What a value stands for when the file binds it: in a scanned file, a name bound from the framework or a member of
147
+ * its module, as the walk reads globals and chains itself; in a followed module, also globals and chains.
148
+ */
149
+ boundStart(expression) {
150
+ if (this.isFollowed)
151
+ return this.startOf(expression);
152
+ const node = unwrap(expression);
153
+ if (node.type === 'Identifier')
154
+ return this.starts.get(node.name) ?? null;
155
+ if (node.type !== 'MemberExpression' || !this.isNamespace(node.object))
156
+ return null;
157
+ return this.dialect.starts.get(propertyName(node) ?? '') ?? null;
158
+ }
159
+ exportedStart(name) {
160
+ const own = ownExport(this.program, name);
161
+ if (own?.kind === 'default')
162
+ return this.startOf(own.value);
163
+ const local = own?.kind === 'specifier' ? own.local : name;
164
+ return this.starts.get(local) ?? null;
165
+ }
166
+ holdsTestObject(name) {
167
+ return this.functions.holdsTestObject(name, (expression) => this.isTestObject(expression));
168
+ }
169
+ reexports(source, imported, exported) {
170
+ if (source !== this.dialect.source)
171
+ return this.follow(source, imported, exported);
172
+ return exported === 'test object' && this.dialect.starts.has(imported);
173
+ }
174
+ /** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
175
+ importsTestObject(name) {
176
+ const from = this.imported.get(name);
177
+ return from !== undefined && this.follow(from.source, from.imported);
178
+ }
179
+ /** Whether an expression is the framework's module: a namespace import, or the module itself. */
180
+ isNamespace(expression) {
181
+ const node = unwrap(expression);
182
+ return (node.type === 'Identifier' && this.namespaces.has(node.name)) || this.dialect.isModule(node);
183
+ }
184
+ bindMember(imported, local) {
185
+ const start = this.dialect.starts.get(imported);
186
+ if (start)
187
+ this.starts.set(local, start);
188
+ else if (this.dialect.hooks.has(imported))
189
+ this.hooks.set(local, imported);
190
+ else
191
+ this.members.set(local, imported);
192
+ }
193
+ bind(declarator) {
194
+ const { id, init } = declarator;
195
+ if (!init)
196
+ return false;
197
+ if (id.type === 'Identifier' && this.isTestObject(init, false)) {
198
+ // `const later = xit` or `= globals.xit` keeps the skip that `xit` stands for.
199
+ this.starts.set(id.name, this.boundStart(init) ?? TEST);
200
+ return true;
201
+ }
202
+ if (!this.dialect.isModule(init))
203
+ return false;
204
+ if (id.type === 'Identifier') {
205
+ this.namespaces.add(id.name);
206
+ return true;
207
+ }
208
+ if (id.type !== 'ObjectPattern')
209
+ return false;
210
+ // `const { it, describe } = import.meta.vitest`, or `= require('@jest/globals')`.
211
+ for (const property of id.properties) {
212
+ if (property.type === 'RestElement' || property.value.type !== 'Identifier')
213
+ continue;
214
+ const key = propertyKey(property);
215
+ if (key !== null)
216
+ this.bindMember(key, property.value.name);
217
+ }
218
+ return true;
219
+ }
220
+ }
221
+ /**
222
+ * What Vitest's and Jest's adapters share: how a call's chain is followed to the test or describe function it starts
223
+ * from, through imports, variables, helper functions and globals.
224
+ */
225
+ export class SuiteAdapter {
226
+ problems;
227
+ fileTags = [];
228
+ fileTagSpans = [];
229
+ usesGlobals = false;
230
+ file;
231
+ bindings;
232
+ dialect;
233
+ /** Describe callbacks and the name of their first parameter, which Vitest calls with its `test`. */
234
+ suiteParameters = new Map();
235
+ readModule;
236
+ imports;
237
+ followed = new Map();
238
+ reported = new Set();
239
+ /** Variables and names of functions read as test objects on trust, each reported once. */
240
+ assumed = new Set();
241
+ aliases = new Map();
242
+ /** First members that show a name imported from a local file is used as the framework's `test`. */
243
+ testMembers;
244
+ constructor(parsed, file, readModule, dialect, testMembers) {
245
+ this.file = file;
246
+ this.readModule = readModule;
247
+ this.dialect = dialect;
248
+ this.testMembers = new Set([...testMembers, ...dialect.starts.keys()]);
249
+ // A test object the file builds from an imported one, as in `const it = base.extend({...})`, is followed quietly:
250
+ // a call through the import itself is what reports an import that can't be followed.
251
+ const followed = new Map();
252
+ this.bindings = new SuiteBindings(parsed.program, dialect, (source, imported, exported = 'test object') => {
253
+ const key = `${source}\0${exported}\0${imported}`;
254
+ if (!followed.has(key)) {
255
+ followed.set(key, followModule(source, imported, readModule, this.framework(), exported).exported);
256
+ }
257
+ return followed.get(key) === true;
258
+ });
259
+ this.problems = [...this.bindings.problems];
260
+ this.imports = moduleImports(parsed.program, (source) => source === dialect.source || dialect.mismatched.has(source));
261
+ }
262
+ testFunction(expression, scopes, returnsOf) {
263
+ return this.aliasOf(expression, { scopes, inBody: false, returnsOf });
264
+ }
265
+ /**
266
+ * The function a chain calls and the links after it, or null when it isn't the framework's. An import that can't be
267
+ * followed is reported only outside test and hook bodies, where a call can declare tests.
268
+ */
269
+ resolve(chain, args, lookup) {
270
+ const start = this.start(chain, args, lookup);
271
+ if (!start)
272
+ return null;
273
+ let kind = start.kind;
274
+ let links = [...(start.modifiers ?? []), ...chain.links.slice(start.index)];
275
+ // A test object derived inline, as in `test.extend({ ... }).skip(...)`.
276
+ links = links.slice(links.findLastIndex((link) => this.dialect.derivations.has(link.name) && link.args !== null) + 1);
277
+ // Vitest's test object also carries `describe` and `suite`, and they carry `test` and `it`.
278
+ while (this.dialect.carriesKinds && links.length > 0 && !links[0].args) {
279
+ const carried = this.dialect.starts.get(links[0].name);
280
+ if (!carried)
281
+ break;
282
+ kind = carried.kind;
283
+ links = links.slice(1);
284
+ }
285
+ return { kind, links };
286
+ }
287
+ start(chain, args, lookup) {
288
+ const { base, links: chainLinks } = chain;
289
+ const { scopes, inBody } = lookup;
290
+ // `require('@jest/globals').test(...)`: the module itself, called in place.
291
+ if (base.type === 'CallExpression' && this.dialect.isModule(base)) {
292
+ const start = this.dialect.starts.get(chainLinks.at(0)?.name ?? '');
293
+ return start ? { kind: start.kind, index: 1, modifiers: links(start.links) } : null;
294
+ }
295
+ if (base.type === 'CallExpression') {
296
+ const returned = this.returned(base, chainLinks, args, lookup);
297
+ return returned && { kind: returned.kind, index: 0, modifiers: returned.links };
298
+ }
299
+ if (base.type !== 'Identifier')
300
+ return null;
301
+ // A name the file doesn't declare is one of the framework's globals; a local variable or parameter with the same
302
+ // name, such as `(test) => test.result()`, is not the framework's.
303
+ const declaration = this.file.values.declarationOf(base.name, scopes);
304
+ if (declaration === null) {
305
+ const global = this.dialect.starts.get(base.name);
306
+ if (!global)
307
+ return null;
308
+ this.usesGlobals = true;
309
+ return { kind: global.kind, index: 0, modifiers: links(global.links) };
310
+ }
311
+ // A parameter's declaration is its function: `describe('market orders', (test) => { test(...) })`.
312
+ if (this.suiteParameters.get(declaration) === base.name)
313
+ return { kind: 'test', index: 0 };
314
+ const followed = this.imports.get(base.name)?.node === declaration &&
315
+ this.worthFollowing(base.name, chainLinks, args) &&
316
+ this.follow(base.name, !inBody && mayDeclareTests(base.name, chainLinks));
317
+ if (!followed && !this.bindings.declarations.has(declaration)) {
318
+ const alias = declaration.type === 'VariableDeclarator' ? this.alias(declaration, base.name, lookup) : null;
319
+ if (alias)
320
+ return { kind: alias.kind, index: 0, modifiers: alias.links };
321
+ return this.assumeLocal(declaration, base.name, chainLinks, args)
322
+ ? { kind: this.dialect.starts.get(base.name)?.kind ?? 'test', index: 0 }
323
+ : null;
324
+ }
325
+ if (this.bindings.namespaces.has(base.name)) {
326
+ const start = this.dialect.starts.get(chainLinks.at(0)?.name ?? '');
327
+ return start ? { kind: start.kind, index: 1, modifiers: links(start.links) } : null;
328
+ }
329
+ const start = this.bindings.starts.get(base.name);
330
+ return start ? { kind: start.kind, index: 0, modifiers: links(start.links) } : null;
331
+ }
332
+ /**
333
+ * The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`, with the member `name`
334
+ * takes when it is destructured, as in `const { beforeEach } = test`. The members Vitest reads through `this`, such as
335
+ * `each` and `skipIf`, return a test function only when called, so their calls are never read through a variable.
336
+ */
337
+ alias(declarator, name, lookup) {
338
+ const destructured = boundMembers(declarator.id, name);
339
+ if (!destructured)
340
+ return null;
341
+ let alias = this.aliases.get(declarator);
342
+ if (alias === undefined) {
343
+ // Set first, so a variable defined through itself ends the lookup.
344
+ this.aliases.set(declarator, null);
345
+ alias = declarator.init ? this.aliasOf(declarator.init, lookup) : null;
346
+ this.aliases.set(declarator, alias);
347
+ }
348
+ return alias && { kind: alias.kind, links: [...alias.links, ...links(destructured)] };
349
+ }
350
+ aliasOf(expression, lookup) {
351
+ const functions = this.choices(expression, lookup)?.functions ?? [];
352
+ if (functions.length === 0)
353
+ return null;
354
+ // Only the modifiers every choice has apply.
355
+ return functions.slice(1).reduce((common, item) => shared(common, item), functions[0]);
356
+ }
357
+ /**
358
+ * The test functions a value may hold, and whether it may also hold something else, as `process.env.CI` may. A test
359
+ * function is never falsy, so `test.skip || test` and `test.skip ?? test` hold `test.skip`, while
360
+ * `process.env.CI && test.skip || test` may hold either. Both branches of `?:` have to be test functions.
361
+ */
362
+ choices(expression, lookup) {
363
+ const node = unwrap(expression);
364
+ if (node.type === 'ConditionalExpression') {
365
+ const [yes, no] = [this.choices(node.consequent, lookup), this.choices(node.alternate, lookup)];
366
+ if (!yes || !no)
367
+ return null;
368
+ return { functions: [...yes.functions, ...no.functions], mayBeOther: yes.mayBeOther || no.mayBeOther };
369
+ }
370
+ if (node.type === 'LogicalExpression') {
371
+ const [left, right] = [this.choices(node.left, lookup), this.choices(node.right, lookup)];
372
+ const isFunction = left !== null && !left.mayBeOther;
373
+ if (node.operator === '&&')
374
+ return isFunction ? right : { functions: right?.functions ?? [], mayBeOther: true };
375
+ if (isFunction)
376
+ return left;
377
+ return {
378
+ functions: [...(left?.functions ?? []), ...(right?.functions ?? [])],
379
+ mayBeOther: right?.mayBeOther ?? true,
380
+ };
381
+ }
382
+ const chain = readChain(node);
383
+ const resolved = chain ? this.resolve(chain, [], lookup) : null;
384
+ const isAlias = resolved?.links.every((link) => this.isAliasLink(resolved.kind, link));
385
+ return resolved && isAlias ? { functions: [resolved], mayBeOther: false } : null;
386
+ }
387
+ /**
388
+ * The function a call returns, as `conditionalTest({ min: 22 })` returns `it` or `it.skip`: what all the values the
389
+ * function returns have in common, as for a variable chosen by a condition. When the function can't be found, a call
390
+ * named like a test object is read as one on trust, with a warning; see `returnedTestObject`.
391
+ */
392
+ returned(call, chainLinks, args, lookup) {
393
+ const callee = unwrap(call.callee);
394
+ if (callee.type !== 'Identifier')
395
+ return null;
396
+ const returns = lookup.returnsOf(callee.name, lookup.scopes);
397
+ if (returns)
398
+ return returnedFunction(returns, shared);
399
+ if (!isTestName(callee.name) || !this.looksLikeTest(chainLinks, args))
400
+ return null;
401
+ if (!this.assumed.has(callee.name)) {
402
+ this.assumed.add(callee.name);
403
+ this.problems.push(returnedTestObject(this.file, call));
404
+ }
405
+ return { kind: this.dialect.starts.get(callee.name)?.kind ?? 'test', links: [] };
406
+ }
407
+ /** Whether a local variable is read as a test object it could be; see `localTestObject`. */
408
+ assumeLocal(declaration, name, chainLinks, args) {
409
+ if (declaration.type !== 'VariableDeclarator' || !mayHoldTestObject(declaration) || !isTestName(name))
410
+ return false;
411
+ if (!this.looksLikeTest(chainLinks, args))
412
+ return false;
413
+ if (!this.assumed.has(declaration)) {
414
+ this.assumed.add(declaration);
415
+ this.problems.push(localTestObject(this.file, name, declaration));
416
+ }
417
+ return true;
418
+ }
419
+ looksLikeTest(chainLinks, args) {
420
+ return chainLinks.length === 0 ? looksLikeDeclaration(args) : this.testMembers.has(chainLinks[0].name);
421
+ }
422
+ /**
423
+ * Whether a call through an imported name is worth following to the module: shaped like a test, or any call with a
424
+ * title and a body through a name that reads like a test function, as `it.effect('title', fn)` on a test object that
425
+ * a package extends.
426
+ */
427
+ worthFollowing(name, chainLinks, args) {
428
+ return this.looksLikeTest(chainLinks, args) || (isTestName(name) && looksLikeDeclaration(args));
429
+ }
430
+ /** The framework as followed modules are read. */
431
+ framework() {
432
+ const { dialect } = this;
433
+ return {
434
+ name: dialect.name,
435
+ isSource: (source) => source === dialect.source,
436
+ testObjects: (program, follow) => new SuiteBindings(program, dialect, follow, true),
437
+ };
438
+ }
439
+ /** Follows the import of `name` once, and reports it once when it can't be followed and `report` is set. */
440
+ follow(name, report) {
441
+ const imported = this.imports.get(name);
442
+ let result = this.followed.get(name);
443
+ if (!result) {
444
+ result = followImport(name, imported, this.readModule, this.file, this.framework());
445
+ this.followed.set(name, result);
446
+ if (result.exported) {
447
+ const start = result.start ?? this.dialect.starts.get(imported.imported) ?? TEST;
448
+ this.bindings.starts.set(name, start);
449
+ this.bindings.declarations.add(imported.node);
450
+ }
451
+ }
452
+ if (report && result.problem && !this.reported.has(name)) {
453
+ this.reported.add(name);
454
+ this.problems.push(result.problem);
455
+ }
456
+ return result.exported;
457
+ }
458
+ /** Hooks called directly, such as `beforeEach(...)` imported from the framework or used as a global. */
459
+ hookCall(call, context) {
460
+ const callee = unwrap(call.callee);
461
+ if (callee.type !== 'Identifier')
462
+ return null;
463
+ const declaration = this.file.values.declarationOf(callee.name, context.scopes);
464
+ const hook = declaration === null
465
+ ? this.dialect.hooks.has(callee.name) && callee.name
466
+ : this.bindings.declarations.has(declaration) && this.bindings.hooks.get(callee.name);
467
+ return hook ? this.hook(call, hook, context) : null;
468
+ }
469
+ /** The number of items of a table, also when not all its values are known; see `Values.lengthOf`. */
470
+ arrayLength(node, context) {
471
+ if (node === undefined || node.type === 'SpreadElement')
472
+ return null;
473
+ return this.file.values.lengthOf(node, context.scopes);
474
+ }
475
+ }
476
+ /** The names a module declares or imports at its top level, destructured ones too. */
477
+ function topLevelNames(program) {
478
+ const names = new Set();
479
+ for (const declarator of topLevelDeclarators(program))
480
+ for (const name of bindingNames(declarator.id))
481
+ names.add(name);
482
+ for (const statement of program.body) {
483
+ const declaration = statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement;
484
+ if ((declaration?.type === 'FunctionDeclaration' || declaration?.type === 'ClassDeclaration') && declaration.id) {
485
+ names.add(declaration.id.name);
486
+ }
487
+ if (statement.type === 'ImportDeclaration')
488
+ for (const specifier of statement.specifiers)
489
+ names.add(specifier.local.name);
490
+ }
491
+ return names;
492
+ }