test-inventory 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/files.d.ts CHANGED
@@ -12,7 +12,7 @@ export interface FoundFile {
12
12
  * left out for Playwright, which doesn't follow them, and read once for Vitest, which does, from the file's own path
13
13
  * when it matches too.
14
14
  */
15
- export declare function findFiles(root: string, patterns: string[], ignore: string[], framework: Framework): Promise<{
15
+ export declare function findFiles(root: string, patterns: string[], ignore: string[], framework: Framework, platform?: NodeJS.Platform): Promise<{
16
16
  files: FoundFile[];
17
17
  diagnostics: Diagnostic[];
18
18
  }>;
package/dist/files.js CHANGED
@@ -43,12 +43,14 @@ function recordingFs(failures) {
43
43
  * left out for Playwright, which doesn't follow them, and read once for Vitest, which does, from the file's own path
44
44
  * when it matches too.
45
45
  */
46
- export async function findFiles(root, patterns, ignore, framework) {
46
+ export async function findFiles(root, patterns, ignore, framework, platform = process.platform) {
47
47
  const failures = [];
48
48
  // Only files match; a folder named like a test file, such as a screenshot folder `checkout.spec.ts/`, is not expanded.
49
- const matches = await glob(patterns.map((pattern) => normalizePattern(pattern)), {
49
+ // As in both runners, names that start with a dot match too, as in `.storybook/`.
50
+ const matches = await glob(patterns.map((pattern) => normalizePattern(pattern, platform)), {
50
51
  cwd: root,
51
- ignore: ['**/node_modules/**', ...ignore.map((pattern) => normalizePattern(pattern))],
52
+ dot: true,
53
+ ignore: ['**/node_modules/**', '**/.git/**', ...ignore.map((pattern) => normalizePattern(pattern, platform))],
52
54
  onlyFiles: true,
53
55
  expandDirectories: false,
54
56
  fs: recordingFs(failures),
@@ -1,5 +1,6 @@
1
1
  import type { Expression, ModuleExportName, Node, Program, VariableDeclarator } from 'oxc-parser';
2
- import type { ModuleReader, Problem } from '../model.ts';
2
+ import type { ModuleReader, ModuleSource, Problem } from '../model.ts';
3
+ import { type ParsedFile } from '../parse.ts';
3
4
  import { type FileContext } from './common.ts';
4
5
  /** A name imported from a module other than the framework, which may be a custom `test` object in a fixtures file. */
5
6
  export interface ModuleImport {
@@ -26,26 +27,33 @@ export declare function requiredModule(expression: Expression): string | null;
26
27
  export declare function bindAll(declarators: VariableDeclarator[], bind: (declarator: VariableDeclarator) => boolean): void;
27
28
  /** Whether a module exports `name` as a test object. */
28
29
  export declare function exportsTestObject(program: Program, name: string, objects: TestObjects): boolean;
29
- /** Whether a module re-exports `name` from another module, which the scan does not follow. */
30
- export declare function reexportsFromModule(program: Program, name: string, isFramework: (source: string) => boolean): boolean;
31
- /**
32
- * Whether a module exports `name` as a value built from something imported from another module, directly or through
33
- * its own variables, as in `const test = baseTest.extend({...})` and then `export const browserTest = test`.
34
- */
35
- export declare function buildsFromModule(program: Program, name: string, isFramework: (source: string) => boolean): boolean;
36
- /** Whether a module imports or requires any of the given sources. */
30
+ /** Whether a module imports, re-exports or requires any of the given sources. */
37
31
  export declare function importsFrom(program: Program, matches: (source: string) => boolean): boolean;
32
+ /** 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;
38
34
  /** How a framework recognizes the modules and test objects of a followed file. */
39
35
  export interface Framework {
40
36
  name: string;
41
37
  isSource: (source: string) => boolean;
42
- testObjects: (program: Program) => TestObjects;
38
+ /** The test objects of a followed module, which uses `follow` for the names it imports from other modules. */
39
+ testObjects: (program: Program, follow: FollowImport) => TestObjects;
43
40
  }
41
+ /** Shared with the walk of imported helpers, so that a module both read and walked is parsed once. */
42
+ export declare function parseModule(module: ModuleSource): ParsedFile;
44
43
  /**
45
- * Follows one import to see whether the module exports a test object. Only one level is followed. A module that has
46
- * nothing to do with the framework is not reported: the call only looked like a test.
44
+ * Whether `name` from `source` is a test object, following the module through the modules it re-exports from or builds
45
+ * on, as deep as it takes. When it isn't, the reason says where the chain ends; it is null for a module that has
46
+ * nothing to do with the framework, as when a call only looked like a test. Each module is followed once for a name,
47
+ * however many imports lead to it, as fixtures files that build on several others share their bases.
47
48
  */
49
+ export declare function followModule(source: string, name: string, readModule: ModuleReader | null, framework: Framework, path?: readonly string[], followed?: Map<string, Followed>): Followed;
50
+ interface Followed {
51
+ exported: boolean;
52
+ reason: string | null;
53
+ }
54
+ /** Follows the import of `name` in a test file, with the diagnostic to report when it isn't a test object. */
48
55
  export declare function followImport(name: string, imported: ModuleImport, readModule: ModuleReader | null, file: FileContext, framework: Framework): {
49
56
  exported: boolean;
50
57
  problem: Problem | null;
51
58
  };
59
+ export {};
@@ -81,95 +81,142 @@ export function bindAll(declarators, bind) {
81
81
  }
82
82
  /** Whether a module exports `name` as a test object. */
83
83
  export function exportsTestObject(program, name, objects) {
84
+ const own = ownExport(program, name);
85
+ switch (own?.kind) {
86
+ case 'default':
87
+ return objects.isTestObject(own.value);
88
+ case 'variable':
89
+ return objects.isTestName(name);
90
+ case 'function':
91
+ return false;
92
+ case 'specifier':
93
+ return own.source === null ? objects.isTestName(own.local) : objects.reexports(own.source, own.local);
94
+ }
95
+ return program.body.some((statement) => {
96
+ const source = starReexport(statement);
97
+ return source !== null && objects.reexports(source, name);
98
+ });
99
+ }
100
+ /**
101
+ * The module's own export of `name`; null when it has none and only `export *` may pass the name on. An own export
102
+ * wins over those, as in JavaScript.
103
+ */
104
+ function ownExport(program, name) {
84
105
  for (const statement of program.body) {
85
- switch (statement.type) {
86
- case 'ExportNamedDeclaration':
87
- if (statement.declaration?.type === 'VariableDeclaration') {
88
- for (const declarator of statement.declaration.declarations) {
89
- if (declarator.id.type === 'Identifier' && declarator.id.name === name) {
90
- return objects.isTestName(name);
91
- }
92
- }
93
- }
94
- for (const specifier of statement.specifiers) {
95
- if (exportName(specifier.exported) !== name)
96
- continue;
97
- const local = exportName(specifier.local);
98
- return statement.source ? objects.reexports(statement.source.value, local) : objects.isTestName(local);
99
- }
100
- break;
101
- case 'ExportDefaultDeclaration':
102
- if (name === 'default')
103
- return objects.isTestObject(statement.declaration);
104
- break;
105
- case 'ExportAllDeclaration':
106
- if (!statement.exported && objects.reexports(statement.source.value, name))
107
- return true;
108
- break;
109
- case 'ExpressionStatement': {
110
- const source = commonJsReexport(statement);
111
- if (source !== null && objects.reexports(source, name))
112
- return true;
113
- break;
114
- }
106
+ if (statement.type === 'ExportDefaultDeclaration' && name === 'default') {
107
+ return { kind: 'default', value: statement.declaration };
108
+ }
109
+ if (statement.type !== 'ExportNamedDeclaration')
110
+ continue;
111
+ const { declaration } = statement;
112
+ if (declaration?.type === 'VariableDeclaration') {
113
+ const declared = declaration.declarations.some((item) => item.id.type === 'Identifier' && item.id.name === name);
114
+ if (declared)
115
+ return { kind: 'variable' };
116
+ }
117
+ const named = declaration?.type === 'FunctionDeclaration' || declaration?.type === 'ClassDeclaration';
118
+ if (named && declaration.id?.name === name)
119
+ return { kind: 'function' };
120
+ for (const specifier of statement.specifiers) {
121
+ if (exportName(specifier.exported) !== name)
122
+ continue;
123
+ return { kind: 'specifier', local: exportName(specifier.local), source: statement.source?.value ?? null };
115
124
  }
116
125
  }
117
- return false;
126
+ return null;
118
127
  }
119
- /** Whether a module re-exports `name` from another module, which the scan does not follow. */
120
- export function reexportsFromModule(program, name, isFramework) {
121
- const imported = moduleImports(program, isFramework);
122
- return program.body.some((statement) => {
123
- const required = commonJsReexport(statement);
124
- if (required !== null)
125
- return !isFramework(required);
126
- if (statement.type === 'ExportAllDeclaration')
127
- return !statement.exported && !isFramework(statement.source.value);
128
- if (statement.type !== 'ExportNamedDeclaration')
129
- return false;
130
- // `export { test } from './fixtures'`, or `import { test } from './fixtures'` and then `export { test }`.
131
- const fromModule = (local) => statement.source ? !isFramework(statement.source.value) : imported.has(local);
132
- return statement.specifiers.some((specifier) => exportName(specifier.exported) === name && fromModule(exportName(specifier.local)));
133
- });
128
+ /** The module `export * from 'module'` or `module.exports = require('module')` passes every name on from; or null. */
129
+ function starReexport(statement) {
130
+ if (statement.type === 'ExportAllDeclaration')
131
+ return statement.exported ? null : statement.source.value;
132
+ return commonJsReexport(statement);
134
133
  }
135
134
  /**
136
- * Whether a module exports `name` as a value built from something imported from another module, directly or through
137
- * its own variables, as in `const test = baseTest.extend({...})` and then `export const browserTest = test`.
135
+ * The module and name a module's export comes from when it re-exports it, as in `export { test } from './base'`, or
136
+ * builds it from something imported, directly or through its own variables, as in `const test = base.extend({...})`.
138
137
  */
139
- export function buildsFromModule(program, name, isFramework) {
138
+ function importOrigin(program, name, isFramework) {
140
139
  const imported = moduleImports(program, isFramework);
140
+ const own = ownExport(program, name);
141
+ if (own === null) {
142
+ for (const statement of program.body) {
143
+ const source = starReexport(statement);
144
+ if (source !== null && !isFramework(source))
145
+ return { source, imported: name, how: 're-exports' };
146
+ }
147
+ }
148
+ else if (own.kind === 'specifier') {
149
+ // `export { test } from './fixtures'`, or `import { test } from './fixtures'` and then `export { test }`.
150
+ 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' };
153
+ }
141
154
  const values = new Map();
142
155
  for (const declarator of topLevelDeclarators(program)) {
143
156
  if (declarator.id.type === 'Identifier' && declarator.init)
144
157
  values.set(declarator.id.name, declarator.init);
145
158
  }
146
159
  const seen = new Set();
147
- const builtFromImport = (local) => {
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.
163
+ const builtFrom = (local) => {
148
164
  const value = values.get(local);
149
- if (!value || seen.has(local))
150
- return false;
165
+ if (imported.has(local) || !value || seen.has(local))
166
+ return imported.get(local) ?? null;
151
167
  seen.add(local);
152
- return usesImport(value);
168
+ return builtOn(value);
169
+ };
170
+ const builtOn = (node) => {
171
+ const expression = unwrap(node);
172
+ switch (expression.type) {
173
+ case 'Identifier':
174
+ return builtFrom(expression.name);
175
+ case 'MemberExpression':
176
+ return builtOn(expression.object);
177
+ case 'ConditionalExpression':
178
+ return builtOn(expression.consequent) ?? builtOn(expression.alternate);
179
+ case 'CallExpression': {
180
+ const callee = unwrap(expression.callee);
181
+ if (callee.type === 'MemberExpression' && propertyName(callee) === 'extend')
182
+ return builtOn(callee.object);
183
+ for (const argument of expression.arguments) {
184
+ const found = argument.type === 'SpreadElement' ? null : builtOn(argument);
185
+ if (found)
186
+ return found;
187
+ }
188
+ return null;
189
+ }
190
+ default:
191
+ return null;
192
+ }
153
193
  };
154
- const usesImport = (node) => (node.type === 'Identifier' && (imported.has(node.name) || builtFromImport(node.name))) ||
155
- childNodes(node).some(usesImport);
156
194
  // `export { test as browserTest }` exports a variable under another name.
157
195
  const locals = program.body.flatMap((statement) => statement.type === 'ExportNamedDeclaration' && !statement.source
158
196
  ? statement.specifiers.filter((item) => exportName(item.exported) === name).map((item) => exportName(item.local))
159
197
  : []);
160
- return [name, ...locals].some(builtFromImport);
198
+ for (const local of [name, ...locals]) {
199
+ const found = values.has(local) ? builtFrom(local) : null;
200
+ if (found)
201
+ return { source: found.source, imported: found.imported, how: 'builds' };
202
+ }
203
+ return null;
161
204
  }
162
- /** Whether a module imports or requires any of the given sources. */
205
+ /** Whether a module imports, re-exports or requires any of the given sources. */
163
206
  export function importsFrom(program, matches) {
164
207
  const requires = (node) => {
165
208
  const source = node.type === 'CallExpression' ? requiredModule(node) : null;
166
209
  return (source !== null && matches(source)) || childNodes(node).some(requires);
167
210
  };
168
- return program.body.some((statement) => (statement.type === 'ImportDeclaration' && matches(statement.source.value)) || requires(statement));
211
+ return program.body.some((statement) => ((statement.type === 'ImportDeclaration' || statement.type === 'ExportAllDeclaration') &&
212
+ matches(statement.source.value)) ||
213
+ (statement.type === 'ExportNamedDeclaration' && statement.source !== null && matches(statement.source.value)) ||
214
+ requires(statement));
169
215
  }
170
216
  /** Parsed modules, kept while their source is, so that a fixtures file many tests import is parsed once. */
171
217
  const parsedModules = new WeakMap();
172
- function parseModule(module) {
218
+ /** Shared with the walk of imported helpers, so that a module both read and walked is parsed once. */
219
+ export function parseModule(module) {
173
220
  let parsed = parsedModules.get(module);
174
221
  if (!parsed) {
175
222
  parsed = parseFile(module.relativeFilePath, module.code);
@@ -177,31 +224,53 @@ function parseModule(module) {
177
224
  }
178
225
  return parsed;
179
226
  }
227
+ /** How many modules deep an import is followed, far more than real fixture chains need. */
228
+ const MAX_DEPTH = 16;
180
229
  /**
181
- * Follows one import to see whether the module exports a test object. Only one level is followed. A module that has
182
- * nothing to do with the framework is not reported: the call only looked like a test.
230
+ * Whether `name` from `source` is a test object, following the module through the modules it re-exports from or builds
231
+ * on, as deep as it takes. When it isn't, the reason says where the chain ends; it is null for a module that has
232
+ * nothing to do with the framework, as when a call only looked like a test. Each module is followed once for a name,
233
+ * however many imports lead to it, as fixtures files that build on several others share their bases.
183
234
  */
184
- export function followImport(name, imported, readModule, file, framework) {
185
- const unresolved = (reason) => problem('unresolved-test-import', imported.node, `'${name}' is imported from '${imported.source}', which ${reason}; tests that use it are missing.`, snippet(text(file, imported.node)));
235
+ export function followModule(source, name, readModule, framework, path = [], followed = new Map()) {
186
236
  // Without a reader, as in scanSource, nothing can be read.
187
- const module = readModule ? readModule(imported.source) : { reason: 'could not be read' };
237
+ const module = readModule ? readModule(source) : { reason: 'could not be read' };
188
238
  if ('reason' in module)
189
- return { exported: false, problem: unresolved(module.reason) };
239
+ return { exported: false, reason: module.reason };
240
+ if (path.includes(module.relativeFilePath))
241
+ return { exported: false, reason: 'imports it back, in a cycle' };
242
+ if (path.length >= MAX_DEPTH)
243
+ return { exported: false, reason: `is more than ${MAX_DEPTH} modules deep` };
244
+ const key = `${module.relativeFilePath}\0${name}`;
245
+ let result = followed.get(key);
246
+ if (!result) {
247
+ result = followExports(module, name, framework, [...path, module.relativeFilePath], followed);
248
+ followed.set(key, result);
249
+ }
250
+ return result;
251
+ }
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) {
190
254
  const { program, error } = parseModule(module);
191
255
  if (error)
192
- return { exported: false, problem: unresolved('could not be parsed') };
193
- if (exportsTestObject(program, imported.imported, framework.testObjects(program))) {
194
- return { exported: true, problem: null };
256
+ 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))
260
+ 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);
263
+ if (origin) {
264
+ const end = followFrom(origin.source, origin.imported).reason ?? unrecognized;
265
+ return { exported: false, reason: `${origin.how} it from '${origin.source}', which ${end}` };
195
266
  }
196
- let reason = null;
197
- if (reexportsFromModule(program, imported.imported, framework.isSource)) {
198
- reason = 're-exports it from another module, which is not followed';
199
- }
200
- else if (buildsFromModule(program, imported.imported, framework.isSource)) {
201
- reason = 'builds it from another module, which is not followed';
202
- }
203
- else if (importsFrom(program, framework.isSource)) {
204
- reason = `does not export a ${framework.name} test object that could be recognized`;
205
- }
206
- return { exported: false, problem: reason ? unresolved(reason) : null };
267
+ return { exported: false, reason: importsFrom(program, framework.isSource) ? unrecognized : null };
268
+ }
269
+ /** Follows the import of `name` in a test file, with the diagnostic to report when it isn't a test object. */
270
+ export function followImport(name, imported, readModule, file, framework) {
271
+ const { exported, reason } = followModule(imported.source, imported.imported, readModule, framework);
272
+ const problemFound = reason
273
+ ? problem('unresolved-test-import', imported.node, `'${name}' is imported from '${imported.source}', which ${reason}; tests that use it are missing.`, snippet(text(file, imported.node)))
274
+ : null;
275
+ return { exported, problem: problemFound };
207
276
  }
@@ -1,6 +1,6 @@
1
- import type { Argument, Expression, Node, StringLiteral, TemplateLiteral, VariableDeclarator } from 'oxc-parser';
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, Span, Title } from '../model.ts';
3
+ import type { 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';
@@ -46,6 +46,16 @@ export declare function mayHoldTestObject(declarator: VariableDeclarator): decla
46
46
  * called like one. The call that creates it can't be followed, so its calls are read as tests on trust.
47
47
  */
48
48
  export declare function localTestObject(file: FileContext, name: string, declarator: CreatedDeclarator): Problem;
49
+ /**
50
+ * A warning for a call shaped like a test on what a function named like a test object returns, as in
51
+ * `conditionalTest({ min: 22 })('title', fn)`, when the function can't be found. What it returns is read on trust.
52
+ */
53
+ export declare function returnedTestObject(file: FileContext, call: CallExpression): Problem;
54
+ /**
55
+ * The test or describe function a function returns, in the adapter's terms: what all its values have in common, as
56
+ * `shared` combines two; null when it may return something else.
57
+ */
58
+ export declare function returnedFunction<T>(returns: Returns, shared: (a: T | null, b: T | null) => T | null): T | null;
49
59
  /**
50
60
  * Whether a call on a name imported from a local file is shaped like a test declaration, `name('title', fn)`, and
51
61
  * so worth following to see if the name is a custom test object.
@@ -80,6 +80,24 @@ export function localTestObject(file, name, declarator) {
80
80
  const created = snippet(text(file, declarator.init));
81
81
  return problem('local-test-object', declarator, `'${name}' is created by ${created}, which is not followed; calls shaped like tests are read as tests.`, created);
82
82
  }
83
+ /**
84
+ * A warning for a call shaped like a test on what a function named like a test object returns, as in
85
+ * `conditionalTest({ min: 22 })('title', fn)`, when the function can't be found. What it returns is read on trust.
86
+ */
87
+ export function returnedTestObject(file, call) {
88
+ const created = snippet(text(file, call));
89
+ return problem('local-test-object', call, `What ${created} returns is not followed; calls on it shaped like tests are read as tests.`, created);
90
+ }
91
+ /**
92
+ * The test or describe function a function returns, in the adapter's terms: what all its values have in common, as
93
+ * `shared` combines two; null when it may return something else.
94
+ */
95
+ export function returnedFunction(returns, shared) {
96
+ if (!returns.values)
97
+ 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
+ }
83
101
  /** A title written as a string, a template, or text joined to one, as in `'cycles through ' + venue`. */
84
102
  function isTitleShaped(node) {
85
103
  if (node?.type === 'BinaryExpression' && node.operator === '+') {
@@ -93,8 +111,12 @@ function isTitleShaped(node) {
93
111
  */
94
112
  export function looksLikeDeclaration(args) {
95
113
  const last = args.at(-1);
96
- // The body is the last argument: a function, or a function passed by name.
97
- const body = isFunctionNode(last) || last?.type === 'Identifier' || last?.type === 'MemberExpression';
114
+ // The body is the last argument, a function or a function passed by name, or a function before a timeout, as in
115
+ // Vitest's `test('title', fn, 30_000)`.
116
+ const body = isFunctionNode(last) ||
117
+ last?.type === 'Identifier' ||
118
+ last?.type === 'MemberExpression' ||
119
+ (args.length === 3 && isFunctionNode(args[1]));
98
120
  return isTitleShaped(args.at(0)) && args.length >= 2 && args.length <= 3 && body;
99
121
  }
100
122
  /**
@@ -1,4 +1,4 @@
1
- import type { CallExpression, Program } from 'oxc-parser';
1
+ import type { CallExpression, Expression, Node, Program } from 'oxc-parser';
2
2
  import type { Adapter, ApiCall, CallContext, ModuleReader, Problem } from '../model.ts';
3
3
  import { type FileContext } from './common.ts';
4
4
  export declare class PlaywrightAdapter implements Adapter {
@@ -11,6 +11,7 @@ export declare class PlaywrightAdapter implements Adapter {
11
11
  private readonly imports;
12
12
  private readonly followed;
13
13
  private readonly reported;
14
+ /** Variables and names of functions read as test objects on trust, each reported once. */
14
15
  private readonly assumed;
15
16
  private readonly aliases;
16
17
  constructor(program: Program, file: FileContext, readModule: ModuleReader | null);
@@ -20,12 +21,30 @@ export declare class PlaywrightAdapter implements Adapter {
20
21
  * can't be followed is reported only outside test and hook bodies, where a call can declare tests.
21
22
  */
22
23
  private testLinks;
24
+ private linksFromTest;
25
+ /**
26
+ * The members of `test` a call returns: none for a test object, as `conditionalTest({ min: 22 })` may return, or
27
+ * those of a describe function. As for a variable chosen by a condition, only the members all its values have apply.
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
+ */
30
+ private returned;
31
+ testFunction(expression: Expression, scopes: readonly Node[]): string[] | null;
23
32
  /** Whether a local variable is read as a test object it could be; see `localTestObject`. */
24
33
  private assumeLocal;
25
34
  private looksLikeTest;
26
- /** The members of `test` a variable holds a describe function from, as in `const serialDescribe = test.describe.serial`. */
27
- private describeAlias;
28
- private describeMembers;
35
+ /**
36
+ * Whether a call through an imported name is worth following to the module: shaped like a test, or any call with a
37
+ * title and a body through a name that reads like a test function, as `it.effect('title', fn)` on a test object that
38
+ * a package extends.
39
+ */
40
+ private worthFollowing;
41
+ /**
42
+ * The members of `test` a variable holds: those of a describe function, as in `const serialDescribe =
43
+ * test.describe.serial`, or none for a test object a function returns.
44
+ */
45
+ private alias;
46
+ /** The members of `test` a value holds: none for a test object, or those of a describe function. */
47
+ private members;
29
48
  /** Follows the import of `name` once, and reports it once when it can't be followed and `report` is set. */
30
49
  private follow;
31
50
  /** Whether an argument is, or works out to, a string, as a title is. Spread arguments are handled before. */