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 */
12import 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 ;
0 commit comments