Skip to content

Commit a244c17

Browse files
committed
Various tweaks
1 parent 82118fb commit a244c17

59 files changed

Lines changed: 1620 additions & 402 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

create-ava-rule.js

Lines changed: 129 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,129 @@
1+
/* eslint-disable eslint-plugin/prefer-message-ids, eslint-plugin/prefer-object-rule, eslint-plugin/require-meta-docs-description, eslint-plugin/require-meta-docs-recommended, eslint-plugin/require-meta-schema, eslint-plugin/require-meta-type */
12
import enhance from 'enhance-visitors';
2-
import {getTestModifiers, unwrapTypeExpression} from './util.js';
3+
import {findVariable} from '@eslint-community/eslint-utils';
4+
import {hasComputedTestModifier, unwrapTypeExpression} from './util.js';
35

4-
export default () => {
6+
const trackedAliasModifiers = new Set([
7+
'after',
8+
'afterEach',
9+
'always',
10+
'before',
11+
'beforeEach',
12+
'failing',
13+
'macro',
14+
'only',
15+
'serial',
16+
'skip',
17+
'todo',
18+
]);
19+
20+
const createAvaRule = sourceCode => {
521
let isTestFile = false;
622
let currentTestNode;
7-
const testIdentifiers = new Set();
23+
const testBindings = new WeakMap();
24+
// Cache per-reference resolution so repeated helper calls on the same node stay cheap.
25+
const trackedModifiersCache = new WeakMap();
26+
const testFunctionCallCache = new WeakMap();
27+
const testModifierNamesCache = new WeakMap();
28+
29+
function getTrackedModifiersFromVariable(variable, node) {
30+
const currentReference = variable.references.find(reference => reference.identifier === node);
31+
const currentVariableScope = currentReference?.from.variableScope;
32+
const isReassignedBeforeUse = variable.references.some(reference => reference.isWrite()
33+
&& !reference.init
34+
&& reference.identifier.range[0] < node.range[0]
35+
&& reference.from.variableScope === currentVariableScope);
36+
if (isReassignedBeforeUse) {
37+
return undefined;
38+
}
39+
40+
for (const definition of variable.defs) {
41+
const modifiers = testBindings.get(definition.name);
42+
if (modifiers !== undefined) {
43+
return modifiers;
44+
}
45+
}
46+
}
47+
48+
function getTrackedModifiers(node) {
49+
if (node.type !== 'Identifier') {
50+
return undefined;
51+
}
52+
53+
if (trackedModifiersCache.has(node)) {
54+
return trackedModifiersCache.get(node);
55+
}
56+
57+
const variable = findVariable(sourceCode.getScope(node), node);
58+
const modifiers = variable
59+
? getTrackedModifiersFromVariable(variable, node)
60+
: testBindings.get(node);
61+
trackedModifiersCache.set(node, modifiers);
62+
return modifiers;
63+
}
64+
65+
function trackTestBinding(binding, modifiers = []) {
66+
// Bindings, not names, preserve alias semantics without leaking across shadowed scopes.
67+
testBindings.set(binding, modifiers);
68+
}
869

970
function isTestFunctionCall(node) {
10-
if (node.type === 'Identifier') {
11-
return testIdentifiers.has(node.name);
71+
if (testFunctionCallCache.has(node)) {
72+
return testFunctionCallCache.get(node);
1273
}
1374

14-
if (node.type === 'MemberExpression') {
15-
return isTestFunctionCall(node.object);
75+
let isTestFunction = false;
76+
77+
switch (node.type) {
78+
case 'Identifier': {
79+
isTestFunction = getTrackedModifiers(node) !== undefined;
80+
break;
81+
}
82+
83+
case 'MemberExpression': {
84+
isTestFunction = isTestFunctionCall(unwrapTypeExpression(node.object));
85+
break;
86+
}
87+
88+
default: {
89+
break;
90+
}
1691
}
1792

18-
return false;
93+
testFunctionCallCache.set(node, isTestFunction);
94+
return isTestFunction;
1995
}
2096

2197
function getTestModifierNames(node) {
22-
return getTestModifiers(node).map(property => property.name);
98+
if (testModifierNamesCache.has(node)) {
99+
return testModifierNamesCache.get(node);
100+
}
101+
102+
let modifierNames = [];
103+
104+
switch (node.type) {
105+
case 'CallExpression': {
106+
modifierNames = getTestModifierNames(node.callee);
107+
break;
108+
}
109+
110+
case 'Identifier': {
111+
modifierNames = getTrackedModifiers(node) ?? [];
112+
break;
113+
}
114+
115+
case 'MemberExpression': {
116+
modifierNames = [...getTestModifierNames(unwrapTypeExpression(node.object)), node.property.name];
117+
break;
118+
}
119+
120+
default: {
121+
break;
122+
}
123+
}
124+
125+
testModifierNamesCache.set(node, modifierNames);
126+
return modifierNames;
23127
}
24128

25129
/* eslint quote-props: [2, "as-needed"] */
@@ -36,19 +140,28 @@ export default () => {
36140

37141
if (specifier.type === 'ImportDefaultSpecifier') {
38142
isTestFile = true;
39-
testIdentifiers.add(specifier.local.name);
143+
trackTestBinding(specifier.local);
40144
} else if (specifier.type === 'ImportSpecifier' && specifier.imported.name === 'serial') {
41145
isTestFile = true;
42-
testIdentifiers.add(specifier.local.name);
146+
trackTestBinding(specifier.local, ['serial']);
43147
}
44148
}
45149
},
46150
VariableDeclarator(node) {
47151
const init = unwrapTypeExpression(node.init);
48152

49-
// Track re-assignment from a test identifier (e.g., `const test = anyTest as TestFn<Context>`)
50-
if (init?.type === 'Identifier' && testIdentifiers.has(init.name) && node.id.type === 'Identifier') {
51-
testIdentifiers.add(node.id.name);
153+
// Track aliases of test identifiers:
154+
// - Re-assignment: `const test = anyTest as TestFn<Context>`
155+
// - Member access: `const serial = test.serial`
156+
// Mutable aliases are allowed, but later writes stop them from being treated as AVA bindings.
157+
if (
158+
init
159+
&& node.id.type === 'Identifier'
160+
&& isTestFunctionCall(init)
161+
&& !hasComputedTestModifier(init)
162+
&& getTestModifierNames(init).every(modifier => trackedAliasModifiers.has(modifier))
163+
) {
164+
trackTestBinding(node.id, getTestModifierNames(init));
52165
}
53166
},
54167
CallExpression(node) {
@@ -65,7 +178,6 @@ export default () => {
65178
},
66179
'Program:exit'() {
67180
isTestFile = false;
68-
testIdentifiers.clear();
69181
},
70182
};
71183

@@ -85,3 +197,5 @@ export default () => {
85197
merge: customHandlers => enhance.mergeVisitors([predefinedRules, customHandlers]),
86198
};
87199
};
200+
201+
export default createAvaRule;

rules/assertion-arguments.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ function isString(node) {
227227
}
228228

229229
const create = context => {
230-
const ava = createAvaRule();
230+
const ava = createAvaRule(context.sourceCode);
231231
const options = context.options[0];
232232
const enforcesMessage = Boolean(options.message);
233233
const shouldHaveMessage = options.message !== 'never';
@@ -346,6 +346,8 @@ const create = context => {
346346
} else if (!/^(?:[A-Z][a-z\d]*)*Error$/.test(lastArgument.name)) {
347347
return;
348348
}
349+
} else if (lastArgument.type === 'MemberExpression' || lastArgument.type === 'ChainExpression' || lastArgument.type === 'CallExpression') {
350+
return; // Cannot statically determine the type (e.g. `error.message`, `getMessage()`)
349351
}
350352

351353
if (!isString(lastArgument)) {

rules/failing-test-url.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const MESSAGE_ID = 'failing-test-url';
77
const urlPattern = /https?:\/\/\S+/;
88

99
const create = context => {
10-
const ava = createAvaRule();
10+
const ava = createAvaRule(context.sourceCode);
1111

1212
return ava.merge({
1313
CallExpression: visitIf([

rules/hooks-order.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const buildMessage = (name, orders, visited) => {
2727
};
2828

2929
const create = context => {
30-
const ava = createAvaRule();
30+
const ava = createAvaRule(context.sourceCode);
3131

3232
const orders = buildOrders([
3333
'before',
@@ -52,6 +52,10 @@ const create = context => {
5252
return;
5353
}
5454

55+
if (ava.hasTestModifier('macro')) {
56+
return;
57+
}
58+
5559
const name = util.getHookName(node) ?? 'test';
5660

5761
visited[name] = node;

rules/max-asserts.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const MESSAGE_ID = 'max-asserts';
77
const notAssertionMethods = new Set(['plan', 'end']);
88

99
const create = context => {
10-
const ava = createAvaRule();
10+
const ava = createAvaRule(context.sourceCode);
1111
const {max: maxAssertions} = context.options[0];
1212
let assertionCount = 0;
1313
let nodeToReport;

rules/no-async-fn-without-await.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ const MESSAGE_ID = 'no-async-fn-without-await';
66
const MESSAGE_ID_SUGGESTION = 'no-async-fn-without-await-suggestion';
77

88
const create = context => {
9-
const ava = createAvaRule();
9+
const ava = createAvaRule(context.sourceCode);
10+
const {sourceCode} = context;
1011
let testUsed = false;
1112
let asyncTest;
1213
let nestedFunctionDepth = 0;
@@ -36,8 +37,8 @@ const create = context => {
3637
ava.isInTestFile,
3738
ava.isTestNode,
3839
])(node => {
39-
asyncTest = (isAsync(node.arguments[0]) && node.arguments[0])
40-
|| (isAsync(node.arguments[1]) && node.arguments[1]);
40+
const implementationArgument = util.getExecutableTestImplementation(node, sourceCode);
41+
asyncTest = isAsync(implementationArgument) && implementationArgument;
4142
}),
4243
':function': enterFunction,
4344
':function:exit': exitFunction,

rules/no-commented-tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const MESSAGE_ID = 'no-commented-tests';
66
const commentedTestPattern = /^\s*\*?\s*(?:test|serial)\s*(?:\.\s*\w+\s*)*\(/;
77

88
const create = context => {
9-
const ava = createAvaRule();
9+
const ava = createAvaRule(context.sourceCode);
1010

1111
return ava.merge({
1212
'Program:exit'() {

rules/no-conditional-assertion.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ function shouldTrackConditionalAncestor(node, child) {
269269
}
270270

271271
const create = context => {
272-
const ava = createAvaRule();
272+
const ava = createAvaRule(context.sourceCode);
273273

274274
return ava.merge({
275275
CallExpression: visitIf([ava.isInTestFile, ava.isInTestNode])(node => {

rules/no-duplicate-hooks.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import util from '../util.js';
55
const MESSAGE_ID = 'no-duplicate-hooks';
66

77
const create = context => {
8-
const ava = createAvaRule();
8+
const ava = createAvaRule(context.sourceCode);
99
const seen = new Set();
1010

1111
return ava.merge({

rules/no-duplicate-modifiers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const MESSAGE_ID = 'no-duplicate-modifiers';
77
const sortByName = (a, b) => a.name.localeCompare(b.name);
88

99
const create = context => {
10-
const ava = createAvaRule();
10+
const ava = createAvaRule(context.sourceCode);
1111

1212
return ava.merge({
1313
CallExpression: visitIf([

0 commit comments

Comments
 (0)