11import { createEslintRule , getAccessorValue } from '../utils'
22import { parseVitestFnCall } from '../utils/parse-vitest-fn-call'
3+ import { getScope } from '../utils/scope'
34import { AST_NODE_TYPES , TSESTree } from '@typescript-eslint/utils'
45export const RULE_NAME = 'require-test-timeout'
56export type MESSAGE_ID = 'missingTimeout'
@@ -18,6 +19,116 @@ export default createEslintRule<Options, MESSAGE_ID>({
1819 schema : [ ] ,
1920 } ,
2021 create ( context ) {
22+ function resolveConstTimeout (
23+ node : TSESTree . Node | undefined ,
24+ propName = 'timeout' ,
25+ ) : number | undefined {
26+ if ( ! node ) return undefined
27+
28+ if (
29+ node . type === AST_NODE_TYPES . Literal &&
30+ typeof node . value === 'number'
31+ )
32+ return node . value
33+
34+ // handle unary negative/positive numbers like -1
35+ if (
36+ node . type === AST_NODE_TYPES . UnaryExpression &&
37+ ( node . operator === '-' || node . operator === '+' ) &&
38+ node . argument . type === AST_NODE_TYPES . Literal &&
39+ typeof node . argument . value === 'number'
40+ ) {
41+ return node . operator === '-'
42+ ? - node . argument . value
43+ : node . argument . value
44+ }
45+
46+ if ( node . type === AST_NODE_TYPES . ObjectExpression ) {
47+ for ( const prop of node . properties ) {
48+ if ( prop . type !== AST_NODE_TYPES . Property ) continue
49+
50+ const key = prop . key
51+ if (
52+ ( key . type === AST_NODE_TYPES . Identifier && key . name === propName ) ||
53+ ( key . type === AST_NODE_TYPES . Literal && key . value === propName )
54+ ) {
55+ if (
56+ prop . value . type === AST_NODE_TYPES . Literal &&
57+ typeof prop . value . value === 'number'
58+ ) {
59+ return prop . value . value
60+ }
61+
62+ // if the value is an identifier, try to resolve that identifier to a const
63+ if ( prop . value . type === AST_NODE_TYPES . Identifier ) {
64+ const nested = resolveConstTimeout ( prop . value , propName )
65+ if ( nested !== undefined ) return nested
66+ }
67+
68+ // explicitly present but non-numeric -> signal invalid
69+ return Number . NaN
70+ }
71+ }
72+
73+ return undefined
74+ }
75+
76+ if ( node . type === AST_NODE_TYPES . Identifier ) {
77+ const scope = getScope ( context , node )
78+ const variable = scope . set . get ( node . name )
79+ if ( ! variable || ! variable . defs || variable . defs . length === 0 )
80+ return undefined
81+
82+ for ( const def of variable . defs ) {
83+ if ( def . type !== 'Variable' ) continue
84+
85+ // only accept `const` bindings
86+ const parent = def . parent
87+ if ( ! isVariableDeclaration ( parent ) || parent . kind !== 'const' )
88+ continue
89+
90+ const declaratorNode = def . node
91+ if ( ! isVariableDeclarator ( declaratorNode ) || ! declaratorNode . init )
92+ continue
93+
94+ const init = declaratorNode . init
95+ if (
96+ init . type === AST_NODE_TYPES . Literal &&
97+ typeof init . value === 'number'
98+ )
99+ return init . value
100+
101+ if ( init . type === AST_NODE_TYPES . ObjectExpression ) {
102+ for ( const p of init . properties ) {
103+ if ( p . type !== AST_NODE_TYPES . Property ) continue
104+
105+ const key = p . key
106+ if (
107+ ( key . type === AST_NODE_TYPES . Identifier &&
108+ key . name === propName ) ||
109+ ( key . type === AST_NODE_TYPES . Literal && key . value === propName )
110+ ) {
111+ if (
112+ p . value . type === AST_NODE_TYPES . Literal &&
113+ typeof p . value . value === 'number'
114+ )
115+ return p . value . value
116+
117+ if ( p . value . type === AST_NODE_TYPES . Identifier ) {
118+ const nested = resolveConstTimeout ( p . value , propName )
119+ if ( nested !== undefined ) return nested
120+ }
121+
122+ return Number . NaN
123+ }
124+ }
125+ }
126+ }
127+ }
128+
129+ return undefined
130+ }
131+
21132 /**
22133 * Track positions (character offsets) of vi.setConfig({ testTimeout })
23134 * calls so we only exempt tests that appear _after_ the call. We use the
@@ -44,19 +155,25 @@ export default createEslintRule<Options, MESSAGE_ID>({
44155
45156 // Only accept a numeric literal >= 0 for testTimeout
46157 if (
47- ( ( key . type === AST_NODE_TYPES . Identifier &&
158+ ( key . type === AST_NODE_TYPES . Identifier &&
48159 key . name === 'testTimeout' ) ||
49- ( key . type === AST_NODE_TYPES . Literal &&
50- key . value === 'testTimeout' ) ) &&
51- prop . value . type === AST_NODE_TYPES . Literal &&
52- typeof prop . value . value === 'number' &&
53- prop . value . value >= 0
160+ ( key . type === AST_NODE_TYPES . Literal &&
161+ key . value === 'testTimeout' )
54162 ) {
55- const endOffset = node . range ? node . range [ 1 ] : 0
56-
57- setConfigPositions . push ( endOffset )
163+ const resolved = resolveConstTimeout (
164+ prop . value ,
165+ 'testTimeout' ,
166+ )
58167
59- break
168+ if (
169+ resolved !== undefined &&
170+ ! Number . isNaN ( resolved ) &&
171+ resolved >= 0
172+ ) {
173+ const endOffset = node . range ? node . range [ 1 ] : 0
174+ setConfigPositions . push ( endOffset )
175+ break
176+ }
60177 }
61178 }
62179 }
@@ -113,6 +230,24 @@ export default createEslintRule<Options, MESSAGE_ID>({
113230 }
114231 }
115232
233+ // identifier that resolves to a const numeric or const object with `timeout`
234+ if ( a . type === AST_NODE_TYPES . Identifier ) {
235+ const resolved = resolveConstTimeout ( a , 'timeout' )
236+ if ( resolved !== undefined ) {
237+ if ( Number . isNaN ( resolved ) ) {
238+ context . report ( { node, messageId : 'missingTimeout' } )
239+ return
240+ }
241+
242+ if ( resolved >= 0 ) {
243+ foundNumericTimeout = true
244+ } else {
245+ context . report ( { node, messageId : 'missingTimeout' } )
246+ return
247+ }
248+ }
249+ }
250+
116251 // object literal with timeout property
117252 if ( a . type === AST_NODE_TYPES . ObjectExpression ) {
118253 for ( const prop of a . properties ) {
@@ -125,17 +260,20 @@ export default createEslintRule<Options, MESSAGE_ID>({
125260 key . name === 'timeout' ) ||
126261 ( key . type === AST_NODE_TYPES . Literal && key . value === 'timeout' )
127262 ) {
128- if (
129- prop . value . type === AST_NODE_TYPES . Literal &&
130- typeof prop . value . value === 'number' &&
131- prop . value . value >= 0
132- ) {
133- foundObjectTimeout = true
134- } else {
135- // any explicitly provided non-numeric or negative timeout is invalid
136- context . report ( { node, messageId : 'missingTimeout' } )
137-
138- return
263+ const resolved = resolveConstTimeout ( prop . value , 'timeout' )
264+
265+ if ( resolved !== undefined ) {
266+ if ( Number . isNaN ( resolved ) ) {
267+ context . report ( { node, messageId : 'missingTimeout' } )
268+ return
269+ }
270+
271+ if ( resolved >= 0 ) {
272+ foundObjectTimeout = true
273+ } else {
274+ context . report ( { node, messageId : 'missingTimeout' } )
275+ return
276+ }
139277 }
140278 }
141279 }
@@ -149,3 +287,15 @@ export default createEslintRule<Options, MESSAGE_ID>({
149287 }
150288 } ,
151289} )
290+
291+ function isVariableDeclaration (
292+ node : TSESTree . Node | null | undefined ,
293+ ) : node is TSESTree . VariableDeclaration {
294+ return ! ! node && node . type === AST_NODE_TYPES . VariableDeclaration
295+ }
296+
297+ function isVariableDeclarator (
298+ node : TSESTree . Node | null | undefined ,
299+ ) : node is TSESTree . VariableDeclarator {
300+ return ! ! node && node . type === AST_NODE_TYPES . VariableDeclarator
301+ }
0 commit comments