test-inventory 0.2.1 → 0.4.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.
@@ -9,12 +9,43 @@ export interface ModuleImport {
9
9
  node: Node;
10
10
  }
11
11
  export declare function exportName(name: ModuleExportName): string;
12
+ /** What a module is followed for: a test object, or a function that returns one, as in `defineBrokerTest()`. */
13
+ export type Exported = 'test object' | 'function';
12
14
  /** Names a module's test objects are known by, used to check what a fixtures file exports. */
13
15
  export interface TestObjects {
14
16
  isTestObject(expression: Expression): boolean;
15
17
  isTestName(name: string): boolean;
16
- /** Whether re-exporting `imported` from `source` exports a test object. */
17
- reexports(source: string, imported: string): boolean;
18
+ /** Whether a function the module declares or imports under `name` returns a test object. */
19
+ returnsTestObject(name: string): boolean;
20
+ /** Whether re-exporting `imported` from `source` exports a test object, or a function that returns one. */
21
+ reexports(source: string, imported: string, exported: Exported): boolean;
22
+ }
23
+ /**
24
+ * The functions a module declares at its top level, by name, to tell whether one returns a test object: every value it
25
+ * returns is one, as `return base.extend({...})` is. An imported function is followed to its module. The function a
26
+ * module exports as its default, named or not, is also known as `default`.
27
+ */
28
+ export declare class ModuleFunctions {
29
+ private readonly functions;
30
+ /** The functions being read, to end a function that returns what it returns itself. */
31
+ private readonly reading;
32
+ /** Whether each function returns a test object, read once however many calls and functions lead to it. */
33
+ private readonly read;
34
+ /** The values of variables the bindings left unbound, by name, which a call may make test objects. */
35
+ private readonly values;
36
+ /** Whether each of those holds a test object; null while that is being worked out, which ends a loop. */
37
+ private readonly built;
38
+ private readonly imported;
39
+ private readonly follow;
40
+ constructor(program: Program, imported: Map<string, ModuleImport>, follow: FollowImport);
41
+ /** Keeps the variables the bindings didn't bind without following calls, to be worked out when asked. */
42
+ keep(declarators: Iterable<VariableDeclarator>): void;
43
+ /**
44
+ * Whether a variable holds a test object built by a call, as `const test = defineBrokerTest()`. It is worked out
45
+ * only when asked, as following the call reads other modules.
46
+ */
47
+ holdsTestObject(name: string, isTestObject: (expression: Expression) => boolean): boolean;
48
+ returnsTestObject(name: string, isTestObject: (expression: Expression) => boolean): boolean;
18
49
  }
19
50
  export declare function topLevelDeclarators(program: Program): VariableDeclarator[];
20
51
  /** All variable declarators in the file, at any depth, such as `const { it } = import.meta.vitest` inside an `if`. */
@@ -25,12 +56,12 @@ export declare function moduleImports(program: Program, isFramework: (source: st
25
56
  export declare function requiredModule(expression: Expression): string | null;
26
57
  /** Applies `bind` to each declarator until no more names are bound, so declaration order doesn't matter. */
27
58
  export declare function bindAll(declarators: VariableDeclarator[], bind: (declarator: VariableDeclarator) => boolean): void;
28
- /** Whether a module exports `name` as a test object. */
29
- export declare function exportsTestObject(program: Program, name: string, objects: TestObjects): boolean;
59
+ /** Whether a module exports `name` as a test object, or as a function that returns one. */
60
+ export declare function exportsTestObject(program: Program, name: string, objects: TestObjects, exported?: Exported): boolean;
30
61
  /** Whether a module imports, re-exports or requires any of the given sources. */
31
62
  export declare function importsFrom(program: Program, matches: (source: string) => boolean): boolean;
32
63
  /** Whether a name a module imports from another module is a test object there, which follows that module in turn. */
33
- export type FollowImport = (source: string, imported: string) => boolean;
64
+ export type FollowImport = (source: string, imported: string, exported?: Exported) => boolean;
34
65
  /** How a framework recognizes the modules and test objects of a followed file. */
35
66
  export interface Framework {
36
67
  name: string;
@@ -46,7 +77,7 @@ export declare function parseModule(module: ModuleSource): ParsedFile;
46
77
  * nothing to do with the framework, as when a call only looked like a test. Each module is followed once for a name,
47
78
  * however many imports lead to it, as fixtures files that build on several others share their bases.
48
79
  */
49
- export declare function followModule(source: string, name: string, readModule: ModuleReader | null, framework: Framework, path?: readonly string[], followed?: Map<string, Followed>): Followed;
80
+ export declare function followModule(source: string, name: string, readModule: ModuleReader | null, framework: Framework, exported?: Exported, path?: readonly string[], followed?: Map<string, Followed>): Followed;
50
81
  interface Followed {
51
82
  exported: boolean;
52
83
  reason: string | null;
@@ -1,10 +1,94 @@
1
- import { childNodes, propertyName, unwrap } from "../ast.js";
1
+ import { childNodes, isFunctionNode, propertyName, returnValues, unwrap } from "../ast.js";
2
2
  import { parseFile } from "../parse.js";
3
3
  import { snippet } from "../source.js";
4
4
  import { problem, text } from "./common.js";
5
5
  export function exportName(name) {
6
6
  return name.type === 'Identifier' ? name.name : name.value;
7
7
  }
8
+ /**
9
+ * The functions a module declares at its top level, by name, to tell whether one returns a test object: every value it
10
+ * returns is one, as `return base.extend({...})` is. An imported function is followed to its module. The function a
11
+ * module exports as its default, named or not, is also known as `default`.
12
+ */
13
+ export class ModuleFunctions {
14
+ functions;
15
+ /** The functions being read, to end a function that returns what it returns itself. */
16
+ reading = new Set();
17
+ /** Whether each function returns a test object, read once however many calls and functions lead to it. */
18
+ read = new Map();
19
+ /** The values of variables the bindings left unbound, by name, which a call may make test objects. */
20
+ values = new Map();
21
+ /** Whether each of those holds a test object; null while that is being worked out, which ends a loop. */
22
+ built = new Map();
23
+ imported;
24
+ follow;
25
+ constructor(program, imported, follow) {
26
+ this.functions = topLevelFunctions(program);
27
+ this.imported = imported;
28
+ this.follow = follow;
29
+ }
30
+ /** Keeps the variables the bindings didn't bind without following calls, to be worked out when asked. */
31
+ keep(declarators) {
32
+ for (const { id, init } of declarators) {
33
+ if (id.type === 'Identifier' && init)
34
+ this.values.set(id.name, [...(this.values.get(id.name) ?? []), init]);
35
+ }
36
+ }
37
+ /**
38
+ * Whether a variable holds a test object built by a call, as `const test = defineBrokerTest()`. It is worked out
39
+ * only when asked, as following the call reads other modules.
40
+ */
41
+ holdsTestObject(name, isTestObject) {
42
+ const values = this.values.get(name);
43
+ if (!values)
44
+ return false;
45
+ if (!this.built.has(name)) {
46
+ this.built.set(name, null);
47
+ this.built.set(name, values.some(isTestObject));
48
+ }
49
+ return this.built.get(name) === true;
50
+ }
51
+ returnsTestObject(name, isTestObject) {
52
+ const fn = this.functions.get(name);
53
+ if (!fn) {
54
+ const from = this.imported.get(name);
55
+ return from !== undefined && this.follow(from.source, from.imported, 'function');
56
+ }
57
+ if (this.reading.has(fn))
58
+ return false;
59
+ if (!this.read.has(fn)) {
60
+ this.reading.add(fn);
61
+ this.read.set(fn, returnValues(fn)?.every(({ expression }) => isTestObject(expression)) ?? false);
62
+ this.reading.delete(fn);
63
+ }
64
+ return this.read.get(fn) === true;
65
+ }
66
+ }
67
+ /** The functions a module declares at its top level, by name; the one it exports as its default is also `default`. */
68
+ function topLevelFunctions(program) {
69
+ const functions = new Map();
70
+ for (const statement of program.body) {
71
+ if (statement.type === 'ExportDefaultDeclaration') {
72
+ const value = statement.declaration;
73
+ if (value.type === 'FunctionDeclaration' || isFunctionNode(value))
74
+ functions.set('default', value);
75
+ }
76
+ const declaration = statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
77
+ ? statement.declaration
78
+ : statement;
79
+ if (declaration?.type === 'FunctionDeclaration' && declaration.id) {
80
+ functions.set(declaration.id.name, declaration);
81
+ }
82
+ else if (declaration?.type === 'VariableDeclaration') {
83
+ for (const { id, init } of declaration.declarations) {
84
+ const value = init ? unwrap(init) : null;
85
+ if (id.type === 'Identifier' && isFunctionNode(value))
86
+ functions.set(id.name, value);
87
+ }
88
+ }
89
+ }
90
+ return functions;
91
+ }
8
92
  export function topLevelDeclarators(program) {
9
93
  const declarators = [];
10
94
  for (const statement of program.body) {
@@ -79,22 +163,28 @@ export function bindAll(declarators, bind) {
79
163
  }
80
164
  }
81
165
  }
82
- /** Whether a module exports `name` as a test object. */
83
- export function exportsTestObject(program, name, objects) {
166
+ /** Whether a module exports `name` as a test object, or as a function that returns one. */
167
+ export function exportsTestObject(program, name, objects, exported = 'test object') {
84
168
  const own = ownExport(program, name);
169
+ const local = (localName) => exported === 'function' ? objects.returnsTestObject(localName) : objects.isTestName(localName);
85
170
  switch (own?.kind) {
86
- case 'default':
87
- return objects.isTestObject(own.value);
171
+ case 'default': {
172
+ if (exported === 'test object')
173
+ return objects.isTestObject(own.value);
174
+ // `export default defineBrokerTest`, or a function written there, which the module's functions know as `default`.
175
+ const value = unwrap(own.value);
176
+ return objects.returnsTestObject(value.type === 'Identifier' ? value.name : 'default');
177
+ }
88
178
  case 'variable':
89
- return objects.isTestName(name);
179
+ return local(name);
90
180
  case 'function':
91
- return false;
181
+ return exported === 'function' && objects.returnsTestObject(name);
92
182
  case 'specifier':
93
- return own.source === null ? objects.isTestName(own.local) : objects.reexports(own.source, own.local);
183
+ return own.source === null ? local(own.local) : objects.reexports(own.source, own.local, exported);
94
184
  }
95
185
  return program.body.some((statement) => {
96
186
  const source = starReexport(statement);
97
- return source !== null && objects.reexports(source, name);
187
+ return source !== null && objects.reexports(source, name, exported);
98
188
  });
99
189
  }
100
190
  /**
@@ -135,35 +225,44 @@ function starReexport(statement) {
135
225
  * The module and name a module's export comes from when it re-exports it, as in `export { test } from './base'`, or
136
226
  * builds it from something imported, directly or through its own variables, as in `const test = base.extend({...})`.
137
227
  */
138
- function importOrigin(program, name, isFramework) {
228
+ function importOrigin(program, name, isFramework, exported) {
139
229
  const imported = moduleImports(program, isFramework);
140
230
  const own = ownExport(program, name);
141
231
  if (own === null) {
142
232
  for (const statement of program.body) {
143
233
  const source = starReexport(statement);
144
234
  if (source !== null && !isFramework(source))
145
- return { source, imported: name, how: 're-exports' };
235
+ return { source, imported: name, how: 're-exports', exported };
146
236
  }
147
237
  }
148
238
  else if (own.kind === 'specifier') {
149
239
  // `export { test } from './fixtures'`, or `import { test } from './fixtures'` and then `export { test }`.
150
240
  const from = own.source === null ? imported.get(own.local) : { source: own.source, imported: own.local };
151
- if (from && !isFramework(from.source))
152
- return { source: from.source, imported: from.imported, how: 're-exports' };
241
+ if (from && !isFramework(from.source)) {
242
+ return { source: from.source, imported: from.imported, how: 're-exports', exported };
243
+ }
153
244
  }
245
+ if (exported === 'function')
246
+ return null;
154
247
  const values = new Map();
155
248
  for (const declarator of topLevelDeclarators(program)) {
156
249
  if (declarator.id.type === 'Identifier' && declarator.init)
157
250
  values.set(declarator.id.name, declarator.init);
158
251
  }
252
+ const functions = topLevelFunctions(program);
159
253
  const seen = new Set();
160
- // The import a local name is, or is built from: the object `extend` is called on, the test objects a call such as
161
- // `mergeTests(a, b)` combines, either side of `?:`, or another local name. Values only passed along, such as a
162
- // fixtures object in `base.extend(fixtures)`, are not what the test object is built on.
254
+ const from = (local, kind) => {
255
+ const found = imported.get(local);
256
+ return found ? { source: found.source, imported: found.imported, exported: kind } : null;
257
+ };
258
+ // The import a local name is, or is built from: the object `extend` is called on, an imported function that builds
259
+ // it, what a function the module writes returns, the test objects a call such as `mergeTests(a, b)` combines,
260
+ // either side of `?:`, or another local name. Values only passed along, such as a fixtures object in
261
+ // `base.extend(fixtures)`, are not what the test object is built on.
163
262
  const builtFrom = (local) => {
164
263
  const value = values.get(local);
165
264
  if (imported.has(local) || !value || seen.has(local))
166
- return imported.get(local) ?? null;
265
+ return from(local, 'test object');
167
266
  seen.add(local);
168
267
  return builtOn(value);
169
268
  };
@@ -180,6 +279,17 @@ function importOrigin(program, name, isFramework) {
180
279
  const callee = unwrap(expression.callee);
181
280
  if (callee.type === 'MemberExpression' && propertyName(callee) === 'extend')
182
281
  return builtOn(callee.object);
282
+ if (callee.type === 'Identifier' && imported.has(callee.name))
283
+ return from(callee.name, 'function');
284
+ const fn = callee.type === 'Identifier' && !seen.has(callee.name) ? functions.get(callee.name) : undefined;
285
+ if (fn && callee.type === 'Identifier') {
286
+ seen.add(callee.name);
287
+ for (const { expression: value } of returnValues(fn) ?? []) {
288
+ const found = builtOn(value);
289
+ if (found)
290
+ return found;
291
+ }
292
+ }
183
293
  for (const argument of expression.arguments) {
184
294
  const found = argument.type === 'SpreadElement' ? null : builtOn(argument);
185
295
  if (found)
@@ -198,9 +308,11 @@ function importOrigin(program, name, isFramework) {
198
308
  for (const local of [name, ...locals]) {
199
309
  const found = values.has(local) ? builtFrom(local) : null;
200
310
  if (found)
201
- return { source: found.source, imported: found.imported, how: 'builds' };
311
+ return { ...found, how: 'builds' };
202
312
  }
203
- return null;
313
+ // `export default defineBrokerTest()`, built where it is exported.
314
+ const found = own?.kind === 'default' ? builtOn(own.value) : null;
315
+ return found && { ...found, how: 'builds' };
204
316
  }
205
317
  /** Whether a module imports, re-exports or requires any of the given sources. */
206
318
  export function importsFrom(program, matches) {
@@ -232,39 +344,61 @@ const MAX_DEPTH = 16;
232
344
  * nothing to do with the framework, as when a call only looked like a test. Each module is followed once for a name,
233
345
  * however many imports lead to it, as fixtures files that build on several others share their bases.
234
346
  */
235
- export function followModule(source, name, readModule, framework, path = [], followed = new Map()) {
347
+ export function followModule(source, name, readModule, framework, exported = 'test object', path = [], followed = new Map()) {
236
348
  // Without a reader, as in scanSource, nothing can be read.
237
349
  const module = readModule ? readModule(source) : { reason: 'could not be read' };
238
350
  if ('reason' in module)
239
351
  return { exported: false, reason: module.reason };
240
- if (path.includes(module.relativeFilePath))
352
+ // A module may be followed again for another name, as a fixtures file that a function it imports builds on.
353
+ const key = `${module.relativeFilePath}\0${exported}\0${name}`;
354
+ if (path.includes(key))
241
355
  return { exported: false, reason: 'imports it back, in a cycle' };
242
356
  if (path.length >= MAX_DEPTH)
243
357
  return { exported: false, reason: `is more than ${MAX_DEPTH} modules deep` };
244
- const key = `${module.relativeFilePath}\0${name}`;
245
358
  let result = followed.get(key);
246
359
  if (!result) {
247
- result = followExports(module, name, framework, [...path, module.relativeFilePath], followed);
360
+ result = followExports(module, name, framework, exported, [...path, key], followed);
248
361
  followed.set(key, result);
249
362
  }
250
363
  return result;
251
364
  }
252
- /** Whether a module exports `name` as a test object; `within` is the chain of modules followed to it, itself last. */
253
- function followExports(module, name, framework, within, followed) {
254
- const { program, error } = parseModule(module);
365
+ /**
366
+ * Whether a module exports `name` as a test object; `within` is the chain of modules and names followed to it, itself
367
+ * last.
368
+ */
369
+ function followExports(module, name, framework, exported, within, followed) {
370
+ const { program, error, source } = parseModule(module);
255
371
  if (error)
256
372
  return { exported: false, reason: 'could not be parsed' };
257
- const followFrom = (from, imported) => followModule(from, imported, module.readModule, framework, within, followed);
258
- const objects = framework.testObjects(program, (from, imported) => followFrom(from, imported).exported);
259
- if (exportsTestObject(program, name, objects))
373
+ const followFrom = (from, imported, kind) => followModule(from, imported, module.readModule, framework, kind, within, followed);
374
+ const objects = framework.testObjects(program, (from, imported, kind = 'test object') => followFrom(from, imported, kind).exported);
375
+ if (exportsTestObject(program, name, objects, exported))
260
376
  return { exported: true, reason: null };
261
- const unrecognized = `does not export a ${framework.name} test object that could be recognized`;
262
- const origin = importOrigin(program, name, framework.isSource);
377
+ const unrecognized = (kind) => `does not export ${kind === 'function' ? 'a function that returns a test object' : `a ${framework.name} test object`} that could be recognized`;
378
+ const origin = importOrigin(program, name, framework.isSource, exported);
263
379
  if (origin) {
264
- const end = followFrom(origin.source, origin.imported).reason ?? unrecognized;
380
+ const end = followFrom(origin.source, origin.imported, origin.exported).reason ?? unrecognized(origin.exported);
265
381
  return { exported: false, reason: `${origin.how} it from '${origin.source}', which ${end}` };
266
382
  }
267
- return { exported: false, reason: importsFrom(program, framework.isSource) ? unrecognized : null };
383
+ if (importsFrom(program, framework.isSource))
384
+ return { exported: false, reason: unrecognized(exported) };
385
+ // A module that doesn't import the framework may still build the test object with a function, as in
386
+ // `export const test = defineBrokerTest()`; one the scan can't follow is worth an error, not silence.
387
+ const call = exported === 'test object' ? creatingCall(program, name, source) : null;
388
+ return { exported: false, reason: call ? `builds it with ${call}, which is not followed` : null };
389
+ }
390
+ /**
391
+ * The call that creates what the module exports as `name`, a variable or its default export, as `defineBrokerTest()`;
392
+ * null for other values.
393
+ */
394
+ function creatingCall(program, name, source) {
395
+ const own = ownExport(program, name);
396
+ const local = own?.kind === 'variable' ? name : own?.kind === 'specifier' && own.source === null ? own.local : null;
397
+ const declarator = topLevelDeclarators(program).find((item) => item.id.type === 'Identifier' && item.id.name === local);
398
+ const written = own?.kind === 'default' ? own.value : declarator?.init;
399
+ const init = written ? unwrap(written) : null;
400
+ const value = init?.type === 'AwaitExpression' ? unwrap(init.argument) : init;
401
+ return value?.type === 'CallExpression' ? `${snippet(source.slice(value.callee.start, value.callee.end))}()` : null;
268
402
  }
269
403
  /** Follows the import of `name` in a test file, with the diagnostic to report when it isn't a test object. */
270
404
  export function followImport(name, imported, readModule, file, framework) {
@@ -1,6 +1,6 @@
1
1
  import type { Argument, CallExpression, Expression, Node, StringLiteral, TemplateLiteral, VariableDeclarator } from 'oxc-parser';
2
2
  import { type FunctionNode } from '../ast.ts';
3
- import type { Condition, Problem, Returns, Span, Title } from '../model.ts';
3
+ import type { ApiCall, CallContext, Condition, Problem, Returns, Span, Title } from '../model.ts';
4
4
  import { type SourceText } from '../source.ts';
5
5
  import type { DiagnosticCode } from '../types.ts';
6
6
  import { type Resolved, type Values } from '../values.ts';
@@ -61,6 +61,11 @@ export declare function returnedFunction<T>(returns: Returns, shared: (a: T | nu
61
61
  * so worth following to see if the name is a custom test object.
62
62
  */
63
63
  export declare function looksLikeDeclaration(args: readonly Argument[]): boolean;
64
+ /**
65
+ * A call the runner refuses. Shaped like a declaration outside any test or hook, it is also declared, without what
66
+ * makes it fail, so that its tests, and those of a describe, are listed as not loaded rather than missing.
67
+ */
68
+ export declare function refused(call: CallExpression, refusal: Problem, context: CallContext, declare: () => ApiCall): ApiCall;
64
69
  /**
65
70
  * Whether a call shaped like a test, through an import that can't be followed, is worth an error: the name reads like
66
71
  * a test function, or a member such as `.describe` follows it. Helpers such as `join('root', file)` and
@@ -107,6 +112,10 @@ export interface Options {
107
112
  export declare function readOptions(file: FileContext, scopes: readonly Node[], node: Argument): Options;
108
113
  /** Warnings for the parts of an options object that could not be worked out, with what is missing as a result. */
109
114
  export declare function unresolvedOptions(file: FileContext, options: Options, result: string): Problem[];
115
+ /** The names a function takes from the object pattern of one of its parameters, in source order. */
116
+ export declare function destructuredNames(fn: FunctionNode | null, index: number | null): string[];
117
+ /** A whole number, such as a timeout in milliseconds; null, with a warning, when it can't be worked out. */
118
+ export declare function readCount(file: FileContext, name: string, option: OptionValue, problems: Problem[]): number | null;
110
119
  /** A warning for one option whose value could not be worked out. */
111
120
  export declare function unresolvedOption(file: FileContext, name: string, node: Node, importedFrom: string | null, result: string): Problem;
112
121
  export interface Strings {
@@ -1,4 +1,4 @@
1
- import { isFunctionNode, isStringLiteral, propertyName, unwrap } from "../ast.js";
1
+ import { isFunctionNode, isStringLiteral, propertyKey, propertyName, unwrap } from "../ast.js";
2
2
  import { snippet } from "../source.js";
3
3
  import { isKey } from "../values.js";
4
4
  export function readChain(callee) {
@@ -93,11 +93,22 @@ export function returnedTestObject(file, call) {
93
93
  * `shared` combines two; null when it may return something else.
94
94
  */
95
95
  export function returnedFunction(returns, shared) {
96
- if (!returns.values)
96
+ const { values } = returns;
97
+ // A function that returns what it returns itself, through others or not, ends the lookup.
98
+ if (!values || reading.has(returns))
97
99
  return null;
98
- const found = returns.values.map(({ expression, scopes }) => returns.adapter.testFunction(expression, scopes));
99
- return found.slice(1).reduce(shared, found[0]);
100
+ if (!read.has(returns)) {
101
+ reading.add(returns);
102
+ const found = values.map(({ expression, scopes }) => returns.adapter.testFunction(expression, scopes, returns.returnsOf));
103
+ reading.delete(returns);
104
+ read.set(returns, found.slice(1).reduce(shared, found[0]));
105
+ }
106
+ return read.get(returns);
100
107
  }
108
+ /** The functions whose values are being read, each one returning what the next returns. */
109
+ const reading = new Set();
110
+ /** What each function returns, read once however many calls and functions use it. */
111
+ const read = new WeakMap();
101
112
  /** A title written as a string, a template, or text joined to one, as in `'cycles through ' + venue`. */
102
113
  function isTitleShaped(node) {
103
114
  if (node?.type === 'BinaryExpression' && node.operator === '+') {
@@ -119,6 +130,17 @@ export function looksLikeDeclaration(args) {
119
130
  (args.length === 3 && isFunctionNode(args[1]));
120
131
  return isTitleShaped(args.at(0)) && args.length >= 2 && args.length <= 3 && body;
121
132
  }
133
+ /**
134
+ * A call the runner refuses. Shaped like a declaration outside any test or hook, it is also declared, without what
135
+ * makes it fail, so that its tests, and those of a describe, are listed as not loaded rather than missing.
136
+ */
137
+ export function refused(call, refusal, context, declare) {
138
+ const declared = context.inBody || !looksLikeDeclaration(call.arguments) ? null : declare();
139
+ // A declaration refused for a second reason as well, as for its options, is reported for the first.
140
+ const found = declared?.kind === 'invalid' ? declared.declaration : declared;
141
+ const declaration = found?.kind === 'test' || found?.kind === 'describe' ? found : undefined;
142
+ return { kind: 'invalid', problem: refusal, declaration };
143
+ }
122
144
  /**
123
145
  * Whether a call shaped like a test, through an import that can't be followed, is worth an error: the name reads like
124
146
  * a test function, or a member such as `.describe` follows it. Helpers such as `join('root', file)` and
@@ -253,6 +275,27 @@ export function unresolvedOptions(file, options, result) {
253
275
  return problem('unresolved-option', node, `Options object ${source}${origin(importedFrom)} could not be worked out; ${result}.`, source);
254
276
  });
255
277
  }
278
+ /** The names a function takes from the object pattern of one of its parameters, in source order. */
279
+ export function destructuredNames(fn, index) {
280
+ const parameter = index === null ? undefined : fn?.params[index];
281
+ if (parameter?.type !== 'ObjectPattern')
282
+ return [];
283
+ const names = parameter.properties.map((property) => (property.type === 'Property' ? propertyKey(property) : null));
284
+ return names.filter((name) => name !== null);
285
+ }
286
+ /** A whole number, such as a timeout in milliseconds; null, with a warning, when it can't be worked out. */
287
+ export function readCount(file, name, option, problems) {
288
+ const { node, resolved } = option;
289
+ if (!resolved.ok) {
290
+ problems.push(unresolvedOption(file, name, node, resolved.importedFrom, 'it is not applied'));
291
+ return null;
292
+ }
293
+ if (Number.isInteger(resolved.value) && resolved.value >= 0)
294
+ return resolved.value;
295
+ const source = snippet(text(file, node));
296
+ problems.push(problem('unresolved-option', node, `Option ${name}: ${source} is not a whole number; it is not applied.`, source));
297
+ return null;
298
+ }
256
299
  /** A warning for one option whose value could not be worked out. */
257
300
  export function unresolvedOption(file, name, node, importedFrom, result) {
258
301
  const source = snippet(text(file, node));
@@ -28,7 +28,7 @@ export declare class PlaywrightAdapter implements Adapter {
28
28
  * When the function can't be found, a call named like a test object is read as one on trust, with a warning.
29
29
  */
30
30
  private returned;
31
- testFunction(expression: Expression, scopes: readonly Node[]): string[] | null;
31
+ testFunction(expression: Expression, scopes: readonly Node[], returnsOf: CallContext['returnsOf']): string[] | null;
32
32
  /** Whether a local variable is read as a test object it could be; see `localTestObject`. */
33
33
  private assumeLocal;
34
34
  private looksLikeTest;
@@ -49,11 +49,16 @@ export declare class PlaywrightAdapter implements Adapter {
49
49
  private follow;
50
50
  /** Whether an argument is, or works out to, a string, as a title is. Spread arguments are handled before. */
51
51
  private isTitle;
52
+ /** A chain Playwright refuses; one that starts like a declaration also declares a plain test or describe. */
52
53
  private invalid;
53
54
  private testCall;
55
+ /** `test.step(title, body)` in a test body; a title that can't be worked out is kept as source text. */
56
+ private step;
54
57
  private modifier;
55
58
  private describeCall;
56
59
  private configure;
60
+ /** `test.setTimeout(ms)` or `test.slow(condition?, description?)`, which apply where they are called. */
61
+ private runSetting;
57
62
  /** `test.info().annotations.push(...)` inside a test body. */
58
63
  private annotationsPush;
59
64
  private option;