@@ -15,6 +15,12 @@ const MATCHERS_TO_COMBINE = [
1515 'toHaveBeenCalledWith' ,
1616] as const
1717
18+ const MOCK_CALL_RESET_METHODS = [
19+ 'mockClear' ,
20+ 'mockReset' ,
21+ 'mockRestore' ,
22+ ] as const
23+
1824type CombinedMatcher = ( typeof MATCHERS_TO_COMBINE ) [ number ]
1925
2026type MatcherReference = {
@@ -26,13 +32,12 @@ const hasMatchersToCombine = (target: string): target is CombinedMatcher =>
2632 MATCHERS_TO_COMBINE . some ( ( matcher ) => matcher === target )
2733
2834const getExpectText = (
29- expression : TSESTree . CallExpression ,
35+ callee : TSESTree . Expression ,
3036 source : Readonly < SourceCode > ,
3137) => {
32- if ( expression . callee . type !== AST_NODE_TYPES . MemberExpression ) return null
38+ if ( callee . type !== AST_NODE_TYPES . MemberExpression ) return null
3339
34- const { range } = expression . callee . object
35- return source . text . slice ( range [ 0 ] , range [ 1 ] )
40+ return source . getText ( callee . object )
3641}
3742
3843const getArgumentsText = (
@@ -59,6 +64,73 @@ const getMatcherName = (vitestFnCall: ReturnType<typeof parseVitestFnCall>) => {
5964 return validExpectCall ? getAccessorValue ( validExpectCall . matcher ) : null
6065}
6166
67+ const getExpectArgText = ( { callee } : TSESTree . CallExpression ) => {
68+ if ( callee . type !== AST_NODE_TYPES . MemberExpression ) return null
69+ const { object } = callee
70+ if ( object . type !== AST_NODE_TYPES . CallExpression ) return null
71+
72+ const [ firstArgument ] = object . arguments
73+ if ( firstArgument . type !== AST_NODE_TYPES . Identifier ) return null
74+
75+ return firstArgument . name
76+ }
77+
78+ const getSharedExpectArgText = (
79+ firstCallExpression : TSESTree . CallExpression ,
80+ secondCallExpression : TSESTree . CallExpression ,
81+ ) => {
82+ const firstArgText = getExpectArgText ( firstCallExpression )
83+ if ( ! firstArgText ) return null
84+ const secondArgText = getExpectArgText ( secondCallExpression )
85+ if ( firstArgText !== secondArgText ) return null
86+
87+ return firstArgText
88+ }
89+
90+ const isTargetMockResetCall = (
91+ statement : TSESTree . Statement ,
92+ expectArgText : string ,
93+ minLine : number ,
94+ maxLine : number ,
95+ ) => {
96+ if ( statement . type !== AST_NODE_TYPES . ExpressionStatement ) return false
97+ if ( statement . expression . type !== AST_NODE_TYPES . CallExpression ) return false
98+
99+ const statementLine = statement . loc . start . line
100+ if ( statementLine <= minLine || statementLine >= maxLine ) return false
101+
102+ const { callee } = statement . expression
103+ if ( callee . type !== AST_NODE_TYPES . MemberExpression ) return false
104+
105+ const { object, property } = callee
106+ if ( object . type !== AST_NODE_TYPES . Identifier ) return false
107+ if ( object . name !== expectArgText ) return false
108+ if ( property . type !== AST_NODE_TYPES . Identifier ) return false
109+
110+ return MOCK_CALL_RESET_METHODS . some ( ( method ) => method === property . name )
111+ }
112+
113+ const hasMockResetBetween = (
114+ body : TSESTree . Statement [ ] ,
115+ firstCallExpression : TSESTree . CallExpression ,
116+ secondCallExpression : TSESTree . CallExpression ,
117+ ) : boolean => {
118+ const firstLine = firstCallExpression . loc . start . line
119+ const secondLine = secondCallExpression . loc . start . line
120+ const [ minLine , maxLine ] =
121+ firstLine < secondLine ? [ firstLine , secondLine ] : [ secondLine , firstLine ]
122+
123+ const expectArgText = getSharedExpectArgText (
124+ firstCallExpression ,
125+ secondCallExpression ,
126+ )
127+ if ( ! expectArgText ) return false
128+
129+ return body . some ( ( statement ) =>
130+ isTargetMockResetCall ( statement , expectArgText , minLine , maxLine ) ,
131+ )
132+ }
133+
62134const getMemberProperty = ( expression : TSESTree . CallExpression ) =>
63135 expression . callee . type === AST_NODE_TYPES . MemberExpression
64136 ? expression . callee . property
@@ -102,7 +174,7 @@ export default createEslintRule<Options, MESSAGE_IDS>({
102174 const matcherName = getMatcherName (
103175 parseVitestFnCall ( callExpression , context ) ,
104176 )
105- const expectedText = getExpectText ( callExpression , sourceCode )
177+ const expectedText = getExpectText ( callExpression . callee , sourceCode )
106178 if ( ! matcherName || ! hasMatchersToCombine ( matcherName ) || ! expectedText )
107179 continue
108180
@@ -141,6 +213,11 @@ export default createEslintRule<Options, MESSAGE_IDS>({
141213 const { callExpression : secondCallExpression , matcherName } =
142214 secondMatcherReference
143215
216+ if (
217+ hasMockResetBetween ( body , firstCallExpression , secondCallExpression )
218+ )
219+ continue
220+
144221 context . report ( {
145222 messageId : 'preferCalledExactlyOnceWith' ,
146223 node : targetNode ,
0 commit comments