test-inventory 0.7.0 → 0.9.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 (67) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/README.md +16 -10
  3. package/dist/case-titles.d.ts +22 -0
  4. package/dist/case-titles.js +184 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +5 -5
  7. package/dist/config-dependencies.js +27 -3
  8. package/dist/config-runs.d.ts +117 -3
  9. package/dist/config-runs.js +1015 -19
  10. package/dist/config.d.ts +42 -1
  11. package/dist/config.js +287 -21
  12. package/dist/dependency-hash.d.ts +13 -5
  13. package/dist/dependency-hash.js +56 -12
  14. package/dist/diagnostics.js +16 -1
  15. package/dist/extract.d.ts +2 -0
  16. package/dist/extract.js +89 -29
  17. package/dist/files.js +4 -2
  18. package/dist/filter.js +1 -0
  19. package/dist/framework-version.d.ts +13 -2
  20. package/dist/framework-version.js +207 -7
  21. package/dist/frameworks/bindings.d.ts +14 -0
  22. package/dist/frameworks/bindings.js +99 -6
  23. package/dist/frameworks/bun.d.ts +42 -0
  24. package/dist/frameworks/bun.js +272 -0
  25. package/dist/frameworks/common.d.ts +3 -1
  26. package/dist/frameworks/common.js +18 -9
  27. package/dist/frameworks/cypress.d.ts +31 -0
  28. package/dist/frameworks/cypress.js +70 -0
  29. package/dist/frameworks/jasmine.d.ts +28 -0
  30. package/dist/frameworks/jasmine.js +218 -0
  31. package/dist/frameworks/jest.d.ts +46 -0
  32. package/dist/frameworks/jest.js +442 -0
  33. package/dist/frameworks/mocha.d.ts +50 -0
  34. package/dist/frameworks/mocha.js +306 -0
  35. package/dist/frameworks/node.d.ts +30 -0
  36. package/dist/frameworks/node.js +268 -0
  37. package/dist/frameworks/playwright.d.ts +13 -0
  38. package/dist/frameworks/playwright.js +57 -0
  39. package/dist/frameworks/test-functions.d.ts +194 -0
  40. package/dist/frameworks/test-functions.js +534 -0
  41. package/dist/frameworks/vitest.d.ts +15 -62
  42. package/dist/frameworks/vitest.js +69 -389
  43. package/dist/frameworks/webdriverio.d.ts +27 -0
  44. package/dist/frameworks/webdriverio.js +37 -0
  45. package/dist/imported-values.js +15 -1
  46. package/dist/model.d.ts +9 -2
  47. package/dist/node-scripts.d.ts +6 -0
  48. package/dist/node-scripts.js +85 -0
  49. package/dist/options.d.ts +16 -3
  50. package/dist/options.js +22 -3
  51. package/dist/parse.js +17 -2
  52. package/dist/records.d.ts +8 -2
  53. package/dist/records.js +94 -22
  54. package/dist/resolve.d.ts +21 -4
  55. package/dist/resolve.js +91 -52
  56. package/dist/scan-file.d.ts +8 -0
  57. package/dist/scan-file.js +21 -3
  58. package/dist/scan.js +62 -12
  59. package/dist/toml.d.ts +13 -0
  60. package/dist/toml.js +172 -0
  61. package/dist/types.d.ts +27 -11
  62. package/dist/values.d.ts +15 -0
  63. package/dist/values.js +120 -2
  64. package/dist/yaml.d.ts +12 -0
  65. package/dist/yaml.js +148 -0
  66. package/package.json +7 -6
  67. package/schema/v1.json +30 -8
@@ -0,0 +1,534 @@
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) => isOwnModule(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 (!isOwnModule(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.bindModule(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 && isOwnModule(dialect, requiredModule(declarator.init))) {
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) || this.dialect.importOnly ? 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
+ const name = propertyName(node) ?? '';
158
+ return this.dialect.starts.get(name) ?? null;
159
+ }
160
+ /** A name the module is bound to: its namespace, and its `test` too when the module can be called. */
161
+ bindModule(name) {
162
+ this.namespaces.add(name);
163
+ if (this.dialect.callable)
164
+ this.starts.set(name, this.dialect.callable);
165
+ }
166
+ exportedStart(name) {
167
+ const own = ownExport(this.program, name);
168
+ if (own?.kind === 'default')
169
+ return this.startOf(own.value);
170
+ const local = own?.kind === 'specifier' ? own.local : name;
171
+ return this.starts.get(local) ?? null;
172
+ }
173
+ holdsTestObject(name) {
174
+ return this.functions.holdsTestObject(name, (expression) => this.isTestObject(expression));
175
+ }
176
+ reexports(source, imported, exported) {
177
+ if (!isOwnModule(this.dialect, source))
178
+ return this.follow(source, imported, exported);
179
+ return exported === 'test object' && this.dialect.starts.has(imported);
180
+ }
181
+ /** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
182
+ importsTestObject(name) {
183
+ const from = this.imported.get(name);
184
+ return from !== undefined && this.follow(from.source, from.imported);
185
+ }
186
+ /** Whether an expression is the framework's module: a namespace import, or the module itself. */
187
+ isNamespace(expression) {
188
+ const node = unwrap(expression);
189
+ return (node.type === 'Identifier' && this.namespaces.has(node.name)) || this.dialect.isModule(node);
190
+ }
191
+ bindMember(imported, local) {
192
+ const start = this.dialect.starts.get(imported);
193
+ const hook = this.dialect.hooks.get(imported);
194
+ if (start)
195
+ this.starts.set(local, start);
196
+ else if (hook)
197
+ this.hooks.set(local, hook);
198
+ else
199
+ this.members.set(local, imported);
200
+ }
201
+ bind(declarator) {
202
+ const { id, init } = declarator;
203
+ if (!init)
204
+ return false;
205
+ if (id.type === 'Identifier' && this.isTestObject(init, false)) {
206
+ // `const later = xit` or `= globals.xit` keeps the skip that `xit` stands for.
207
+ this.starts.set(id.name, this.boundStart(init) ?? TEST);
208
+ return true;
209
+ }
210
+ if (!this.dialect.isModule(init))
211
+ return false;
212
+ if (id.type === 'Identifier') {
213
+ this.bindModule(id.name);
214
+ return true;
215
+ }
216
+ if (id.type !== 'ObjectPattern')
217
+ return false;
218
+ // `const { it, describe } = import.meta.vitest`, or `= require('@jest/globals')`.
219
+ for (const property of id.properties) {
220
+ if (property.type === 'RestElement' || property.value.type !== 'Identifier')
221
+ continue;
222
+ const key = propertyKey(property);
223
+ if (key !== null)
224
+ this.bindMember(key, property.value.name);
225
+ }
226
+ return true;
227
+ }
228
+ }
229
+ /** Whether a module is the framework's own, or one it reads as its own. */
230
+ function isOwnModule(dialect, source) {
231
+ return source !== null && (source === dialect.source || (dialect.aliases?.has(source) ?? false));
232
+ }
233
+ /**
234
+ * What Vitest's and Jest's adapters share: how a call's chain is followed to the test or describe function it starts
235
+ * from, through imports, variables, helper functions and globals.
236
+ */
237
+ export class SuiteAdapter {
238
+ problems;
239
+ fileTags = [];
240
+ fileTagSpans = [];
241
+ usesGlobals = false;
242
+ file;
243
+ bindings;
244
+ dialect;
245
+ /** Describe callbacks and the name of their first parameter, which Vitest calls with its `test`. */
246
+ suiteParameters = new Map();
247
+ readModule;
248
+ imports;
249
+ followed = new Map();
250
+ reported = new Set();
251
+ /** Variables and names of functions read as test objects on trust, each reported once. */
252
+ assumed = new Set();
253
+ aliases = new Map();
254
+ /** First members that show a name imported from a local file is used as the framework's `test`. */
255
+ testMembers;
256
+ constructor(parsed, file, readModule, dialect, testMembers) {
257
+ this.file = file;
258
+ this.readModule = readModule;
259
+ this.dialect = dialect;
260
+ this.testMembers = new Set([...testMembers, ...dialect.starts.keys()]);
261
+ // A test object the file builds from an imported one, as in `const it = base.extend({...})`, is followed quietly:
262
+ // a call through the import itself is what reports an import that can't be followed.
263
+ const followed = new Map();
264
+ this.bindings = new SuiteBindings(parsed.program, dialect, (source, imported, exported = 'test object') => {
265
+ const key = `${source}\0${exported}\0${imported}`;
266
+ if (!followed.has(key)) {
267
+ followed.set(key, followModule(source, imported, readModule, this.framework(), exported).exported);
268
+ }
269
+ return followed.get(key) === true;
270
+ });
271
+ this.problems = [...this.bindings.problems];
272
+ this.imports = moduleImports(parsed.program, (source) => isOwnModule(dialect, source) || dialect.mismatched.has(source));
273
+ }
274
+ testFunction(expression, scopes, returnsOf) {
275
+ return this.aliasOf(expression, { scopes, inBody: false, returnsOf });
276
+ }
277
+ /**
278
+ * 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
279
+ * followed is reported only outside test and hook bodies, where a call can declare tests.
280
+ */
281
+ resolve(chain, args, lookup) {
282
+ const start = this.start(chain, args, lookup);
283
+ if (!start)
284
+ return null;
285
+ let kind = start.kind;
286
+ let links = [...(start.modifiers ?? []), ...chain.links.slice(start.index)];
287
+ // A test object derived inline, as in `test.extend({ ... }).skip(...)`.
288
+ links = links.slice(links.findLastIndex((link) => this.dialect.derivations.has(link.name) && link.args !== null) + 1);
289
+ // Vitest's test object also carries `describe` and `suite`, and they carry `test` and `it`.
290
+ while (this.dialect.carriesKinds && links.length > 0 && !links[0].args) {
291
+ const carried = this.dialect.starts.get(links[0].name);
292
+ if (!carried)
293
+ break;
294
+ kind = carried.kind;
295
+ links = links.slice(1);
296
+ }
297
+ return { kind, links };
298
+ }
299
+ start(chain, args, lookup) {
300
+ const { base, links: chainLinks } = chain;
301
+ const { scopes, inBody } = lookup;
302
+ // `require('@jest/globals').test(...)`: the module itself, called in place.
303
+ if (base.type === 'CallExpression' && this.dialect.isModule(base))
304
+ return this.member(chainLinks);
305
+ if (base.type === 'CallExpression') {
306
+ const returned = this.returned(base, chainLinks, args, lookup);
307
+ return returned && { kind: returned.kind, index: 0, modifiers: returned.links };
308
+ }
309
+ if (base.type !== 'Identifier')
310
+ return null;
311
+ // A name the file doesn't declare is one of the framework's globals; a local variable or parameter with the same
312
+ // name, such as `(test) => test.result()`, is not the framework's.
313
+ const declaration = this.file.values.declarationOf(base.name, scopes);
314
+ if (declaration === null) {
315
+ const global = this.dialect.importOnly ? undefined : this.dialect.starts.get(base.name);
316
+ if (!global)
317
+ return null;
318
+ this.usesGlobals = true;
319
+ return { kind: global.kind, index: 0, modifiers: links(global.links) };
320
+ }
321
+ // A parameter's declaration is its function: `describe('market orders', (test) => { test(...) })`.
322
+ if (this.suiteParameters.get(declaration) === base.name)
323
+ return { kind: 'test', index: 0 };
324
+ const followed = this.imports.get(base.name)?.node === declaration &&
325
+ this.worthFollowing(base.name, chainLinks, args) &&
326
+ this.follow(base.name, !inBody && mayDeclareTests(base.name, chainLinks));
327
+ if (!followed && !this.bindings.declarations.has(declaration)) {
328
+ const alias = declaration.type === 'VariableDeclarator' ? this.alias(declaration, base.name, lookup) : null;
329
+ if (alias)
330
+ return { kind: alias.kind, index: 0, modifiers: alias.links };
331
+ return this.assumeLocal(declaration, base.name, chainLinks, args)
332
+ ? { kind: this.dialect.starts.get(base.name)?.kind ?? 'test', index: 0 }
333
+ : null;
334
+ }
335
+ if (this.bindings.namespaces.has(base.name))
336
+ return this.member(chainLinks);
337
+ const start = this.bindings.starts.get(base.name);
338
+ return start ? { kind: start.kind, index: 0, modifiers: links(start.links) } : null;
339
+ }
340
+ /**
341
+ * What a chain on the framework's module starts with: the member it names, or for a module that can be called, as
342
+ * `node:test`'s, the module itself, as in `test.skip(...)`.
343
+ */
344
+ member(chainLinks) {
345
+ const start = this.dialect.starts.get(chainLinks.at(0)?.name ?? '');
346
+ if (start)
347
+ return { kind: start.kind, index: 1, modifiers: links(start.links) };
348
+ const { callable } = this.dialect;
349
+ // The module's other members, as `test.mock.fn()`, are not tests; one it doesn't have is refused as a chain.
350
+ if (!callable || this.dialect.others?.has(chainLinks.at(0)?.name ?? ''))
351
+ return null;
352
+ return { kind: callable.kind, index: 0, modifiers: links(callable.links) };
353
+ }
354
+ /**
355
+ * The member a name is destructured from a test or describe function as, with that function's kind and the
356
+ * expression it is taken from: `each`, `test` and `test` in `const { each } = test`; null for any other name.
357
+ */
358
+ destructuredMember(name, lookup) {
359
+ const declaration = this.file.values.declarationOf(name, lookup.scopes);
360
+ if (declaration?.type !== 'VariableDeclarator')
361
+ return null;
362
+ const member = boundMembers(declaration.id, name)?.at(-1);
363
+ if (member === undefined)
364
+ return null;
365
+ const alias = this.alias(declaration, name, lookup);
366
+ // A variable holds a test function only through its value, so there is one.
367
+ return alias && { kind: alias.kind, member, from: declaration.init };
368
+ }
369
+ /**
370
+ * The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`, with the member `name`
371
+ * takes when it is destructured, as in `const { beforeEach } = test`. The members Vitest reads through `this`, such as
372
+ * `each` and `skipIf`, return a test function only when called, so their calls are never read through a variable.
373
+ */
374
+ alias(declarator, name, lookup) {
375
+ const destructured = boundMembers(declarator.id, name);
376
+ if (!destructured)
377
+ return null;
378
+ let alias = this.aliases.get(declarator);
379
+ if (alias === undefined) {
380
+ // Set first, so a variable defined through itself ends the lookup.
381
+ this.aliases.set(declarator, null);
382
+ alias = declarator.init ? this.aliasOf(declarator.init, lookup) : null;
383
+ this.aliases.set(declarator, alias);
384
+ }
385
+ return alias && { kind: alias.kind, links: [...alias.links, ...links(destructured)] };
386
+ }
387
+ aliasOf(expression, lookup) {
388
+ const functions = this.choices(expression, lookup)?.functions ?? [];
389
+ if (functions.length === 0)
390
+ return null;
391
+ // Only the modifiers every choice has apply.
392
+ return functions.slice(1).reduce((common, item) => shared(common, item), functions[0]);
393
+ }
394
+ /**
395
+ * The test functions a value may hold, and whether it may also hold something else, as `process.env.CI` may. A test
396
+ * function is never falsy, so `test.skip || test` and `test.skip ?? test` hold `test.skip`, while
397
+ * `process.env.CI && test.skip || test` may hold either. Both branches of `?:` have to be test functions.
398
+ */
399
+ choices(expression, lookup) {
400
+ const node = unwrap(expression);
401
+ if (node.type === 'ConditionalExpression') {
402
+ const [yes, no] = [this.choices(node.consequent, lookup), this.choices(node.alternate, lookup)];
403
+ if (!yes || !no)
404
+ return null;
405
+ return { functions: [...yes.functions, ...no.functions], mayBeOther: yes.mayBeOther || no.mayBeOther };
406
+ }
407
+ if (node.type === 'LogicalExpression') {
408
+ const [left, right] = [this.choices(node.left, lookup), this.choices(node.right, lookup)];
409
+ const isFunction = left !== null && !left.mayBeOther;
410
+ if (node.operator === '&&')
411
+ return isFunction ? right : { functions: right?.functions ?? [], mayBeOther: true };
412
+ if (isFunction)
413
+ return left;
414
+ return {
415
+ functions: [...(left?.functions ?? []), ...(right?.functions ?? [])],
416
+ mayBeOther: right?.mayBeOther ?? true,
417
+ };
418
+ }
419
+ const chain = readChain(node);
420
+ const resolved = chain ? this.resolve(chain, [], lookup) : null;
421
+ const isAlias = resolved?.links.every((link) => this.isAliasLink(resolved.kind, link));
422
+ return resolved && isAlias ? { functions: [resolved], mayBeOther: false } : null;
423
+ }
424
+ /**
425
+ * The function a call returns, as `conditionalTest({ min: 22 })` returns `it` or `it.skip`: what all the values the
426
+ * function returns have in common, as for a variable chosen by a condition. When the function can't be found, a call
427
+ * named like a test object is read as one on trust, with a warning; see `returnedTestObject`.
428
+ */
429
+ returned(call, chainLinks, args, lookup) {
430
+ const callee = unwrap(call.callee);
431
+ if (callee.type !== 'Identifier')
432
+ return null;
433
+ const returns = lookup.returnsOf(callee.name, lookup.scopes);
434
+ if (returns)
435
+ return returnedFunction(returns, shared);
436
+ if (!isTestName(callee.name) || !this.looksLikeTest(chainLinks, args))
437
+ return null;
438
+ if (!this.assumed.has(callee.name)) {
439
+ this.assumed.add(callee.name);
440
+ this.problems.push(returnedTestObject(this.file, call));
441
+ }
442
+ return { kind: this.dialect.starts.get(callee.name)?.kind ?? 'test', links: [] };
443
+ }
444
+ /** Whether a local variable is read as a test object it could be; see `localTestObject`. */
445
+ assumeLocal(declaration, name, chainLinks, args) {
446
+ if (declaration.type !== 'VariableDeclarator' || !mayHoldTestObject(declaration) || !isTestName(name))
447
+ return false;
448
+ if (!this.looksLikeTest(chainLinks, args))
449
+ return false;
450
+ if (!this.assumed.has(declaration)) {
451
+ this.assumed.add(declaration);
452
+ this.problems.push(localTestObject(this.file, name, declaration));
453
+ }
454
+ return true;
455
+ }
456
+ looksLikeTest(chainLinks, args) {
457
+ return chainLinks.length === 0 ? looksLikeDeclaration(args) : this.testMembers.has(chainLinks[0].name);
458
+ }
459
+ /**
460
+ * Whether a call through an imported name is worth following to the module: shaped like a test, or any call with a
461
+ * title and a body through a name that reads like a test function, as `it.effect('title', fn)` on a test object that
462
+ * a package extends.
463
+ */
464
+ worthFollowing(name, chainLinks, args) {
465
+ return this.looksLikeTest(chainLinks, args) || (isTestName(name) && looksLikeDeclaration(args));
466
+ }
467
+ /** The framework as followed modules are read. */
468
+ framework() {
469
+ const { dialect } = this;
470
+ return {
471
+ name: dialect.name,
472
+ isSource: (source) => isOwnModule(dialect, source),
473
+ testObjects: (program, follow) => new SuiteBindings(program, dialect, follow, true),
474
+ };
475
+ }
476
+ /** Follows the import of `name` once, and reports it once when it can't be followed and `report` is set. */
477
+ follow(name, report) {
478
+ const imported = this.imports.get(name);
479
+ let result = this.followed.get(name);
480
+ if (!result) {
481
+ result = followImport(name, imported, this.readModule, this.file, this.framework());
482
+ this.followed.set(name, result);
483
+ if (result.exported) {
484
+ const start = result.start ?? this.dialect.starts.get(imported.imported) ?? TEST;
485
+ this.bindings.starts.set(name, start);
486
+ this.bindings.declarations.add(imported.node);
487
+ }
488
+ }
489
+ if (report && result.problem && !this.reported.has(name)) {
490
+ this.reported.add(name);
491
+ this.problems.push(result.problem);
492
+ }
493
+ return result.exported;
494
+ }
495
+ /** Hooks called directly, such as `beforeEach(...)` imported from the framework or used as a global. */
496
+ hookCall(call, context) {
497
+ const callee = unwrap(call.callee);
498
+ // A hook on the module, as `test.beforeEach()` on `node:test`'s default export.
499
+ if (callee.type === 'MemberExpression' && this.bindings.isNamespace(callee.object)) {
500
+ const hook = this.dialect.hooks.get(propertyName(callee) ?? '');
501
+ return hook ? this.hook(call, hook, context) : null;
502
+ }
503
+ if (callee.type !== 'Identifier')
504
+ return null;
505
+ const declaration = this.file.values.declarationOf(callee.name, context.scopes);
506
+ const hook = declaration === null
507
+ ? this.dialect.hooks.get(callee.name)
508
+ : this.bindings.declarations.has(declaration) && this.bindings.hooks.get(callee.name);
509
+ return hook ? this.hook(call, hook, context) : null;
510
+ }
511
+ /** The number of items of a table, also when not all its values are known; see `Values.lengthOf`. */
512
+ arrayLength(node, context) {
513
+ if (node === undefined || node.type === 'SpreadElement')
514
+ return null;
515
+ return this.file.values.lengthOf(node, context.scopes);
516
+ }
517
+ }
518
+ /** The names a module declares or imports at its top level, destructured ones too. */
519
+ function topLevelNames(program) {
520
+ const names = new Set();
521
+ for (const declarator of topLevelDeclarators(program))
522
+ for (const name of bindingNames(declarator.id))
523
+ names.add(name);
524
+ for (const statement of program.body) {
525
+ const declaration = statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement;
526
+ if ((declaration?.type === 'FunctionDeclaration' || declaration?.type === 'ClassDeclaration') && declaration.id) {
527
+ names.add(declaration.id.name);
528
+ }
529
+ if (statement.type === 'ImportDeclaration')
530
+ for (const specifier of statement.specifiers)
531
+ names.add(specifier.local.name);
532
+ }
533
+ return names;
534
+ }
@@ -1,71 +1,26 @@
1
- import type { CallExpression, Expression, Node } from 'oxc-parser';
2
- import type { Adapter, ApiCall, CallContext, ModuleReader, Problem, Span } from '../model.ts';
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 = 'test' | 'describe';
6
- /** A test or describe function and the links after it, as in `test.skipIf(isCI)`. */
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
- constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
9
+ /** The length a value in a case title is shortened to, as the config sets it. */
10
+ private readonly titleTruncate;
11
+ constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null, titleTruncate?: number);
12
+ protected isAliasLink(kind: Kind, link: Link): boolean;
29
13
  classify(call: CallExpression, context: CallContext): ApiCall | null;
30
- testFunction(expression: Expression, scopes: readonly Node[], returnsOf: CallContext['returnsOf']): TestFunction | null;
31
14
  /** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
32
15
  private benchCall;
33
- private chainCall;
34
16
  /**
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.
17
+ * A call of a member Vitest calls on its test or describe function, destructured from it, as `each` in
18
+ * `const { each } = test`: the member reads the function it is called on, which a plain call doesn't give it, so the
19
+ * call throws while the file loads. Which members do that depends on the version.
37
20
  */
38
- private resolve;
39
- private start;
40
- /**
41
- * The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`, with the member `name`
42
- * takes when it is destructured, as in `const { beforeEach } = test`. The members Vitest reads through `this`, such as
43
- * `each` and `skipIf`, return a test function only when called, so their calls are never read through a variable.
44
- */
45
- private alias;
46
- private aliasOf;
47
- /**
48
- * The function a call returns, as `conditionalTest({ min: 22 })` returns `it` or `it.skip`: what all the values the
49
- * function returns have in common, as for a variable chosen by a condition. When the function can't be found, a call
50
- * named like a test object is read as one on trust, with a warning; see `returnedTestObject`.
51
- */
52
- private returned;
53
- /** Whether a local variable is read as a test object it could be; see `localTestObject`. */
54
- private assumeLocal;
55
- private looksLikeTest;
56
- /**
57
- * Whether a call through an imported name is worth following to the module: shaped like a test, or any call with a
58
- * title and a body through a name that reads like a test function, as `it.effect('title', fn)` on a test object that
59
- * a package extends.
60
- */
61
- private worthFollowing;
62
- /** Vitest as followed modules are read, with the rules of the scanned version. */
63
- private framework;
64
- /** Follows the import of `name` once, and reports it once when it can't be followed and `report` is set. */
65
- private follow;
66
- /** Hooks called directly, such as `beforeEach(...)` imported from Vitest or used as a global. */
67
- private hookCall;
68
- private hook;
21
+ private unboundCall;
22
+ private chainCall;
23
+ protected hook(call: CallExpression, name: keyof HookCounts, context: CallContext): ApiCall;
69
24
  private contextSkip;
70
25
  /**
71
26
  * `annotate(message, type?)` from the test context (3.2), as an annotation of type `notice` when no type is given; the
@@ -89,7 +44,5 @@ export declare class VitestAdapter implements Adapter {
89
44
  * gives one object per row, keyed by the names in its first line.
90
45
  */
91
46
  private tableCases;
92
- private arrayLength;
93
47
  private tagsUsed;
94
48
  }
95
- export {};