|
8 | 8 | findTopMostCallExpression, |
9 | 9 | getAccessorValue, |
10 | 10 | isIdentifier, |
| 11 | + isSupportedAccessor, |
11 | 12 | parseJestFnCall, |
12 | 13 | } from './utils'; |
13 | 14 |
|
@@ -73,9 +74,116 @@ export default createRule<Options, MessageIds>({ |
73 | 74 | return {}; |
74 | 75 | } |
75 | 76 |
|
| 77 | + /** |
| 78 | + * Checks if a MemberExpression is an argument to a `jest.mocked()` call. |
| 79 | + * This handles cases like `jest.mocked(service.method)` where `service.method` |
| 80 | + * should not be flagged as an unbound method. |
| 81 | + */ |
| 82 | + const isArgumentToJestMocked = ( |
| 83 | + node: TSESTree.MemberExpression, |
| 84 | + ): boolean => { |
| 85 | + // Check if the immediate parent is a CallExpression |
| 86 | + if (node.parent?.type !== AST_NODE_TYPES.CallExpression) { |
| 87 | + return false; |
| 88 | + } |
| 89 | + |
| 90 | + const parentCall = node.parent; |
| 91 | + |
| 92 | + // Check if this node is an argument to the call |
| 93 | + if (!parentCall.arguments.some(arg => arg === node)) { |
| 94 | + return false; |
| 95 | + } |
| 96 | + |
| 97 | + // Check if the call is jest.mocked() by examining the callee |
| 98 | + if ( |
| 99 | + parentCall.callee.type === AST_NODE_TYPES.MemberExpression && |
| 100 | + isSupportedAccessor(parentCall.callee.object) && |
| 101 | + isSupportedAccessor(parentCall.callee.property) |
| 102 | + ) { |
| 103 | + const objectName = getAccessorValue(parentCall.callee.object); |
| 104 | + const propertyName = getAccessorValue(parentCall.callee.property); |
| 105 | + |
| 106 | + if (objectName === 'jest' && propertyName === 'mocked') { |
| 107 | + return true; |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + // Also try using parseJestFnCall as a fallback |
| 112 | + const jestFnCall = parseJestFnCall( |
| 113 | + findTopMostCallExpression(parentCall), |
| 114 | + context, |
| 115 | + ); |
| 116 | + |
| 117 | + return ( |
| 118 | + jestFnCall?.type === 'jest' && |
| 119 | + jestFnCall.members.length >= 1 && |
| 120 | + isIdentifier(jestFnCall.members[0], 'mocked') |
| 121 | + ); |
| 122 | + }; |
| 123 | + |
| 124 | + /** |
| 125 | + * Checks if a MemberExpression is accessing `.mock` property on a |
| 126 | + * `jest.mocked()` result. This handles cases like: |
| 127 | + * - `jest.mocked(service.method).mock.calls[0][0]` |
| 128 | + * - `jest.mocked(service.method).mock.calls` |
| 129 | + */ |
| 130 | + const isAccessingMockProperty = ( |
| 131 | + node: TSESTree.MemberExpression, |
| 132 | + ): boolean => { |
| 133 | + // Check if we're accessing `.mock` property |
| 134 | + if (!isSupportedAccessor(node.property)) { |
| 135 | + return false; |
| 136 | + } |
| 137 | + |
| 138 | + const propertyName = getAccessorValue(node.property); |
| 139 | + |
| 140 | + if (propertyName !== 'mock') { |
| 141 | + return false; |
| 142 | + } |
| 143 | + |
| 144 | + // Traverse up the chain to find if the object is a jest.mocked() call |
| 145 | + let current: TSESTree.Node = node.object; |
| 146 | + |
| 147 | + while (current) { |
| 148 | + if (current.type === AST_NODE_TYPES.CallExpression) { |
| 149 | + const jestFnCall = parseJestFnCall( |
| 150 | + findTopMostCallExpression(current), |
| 151 | + context, |
| 152 | + ); |
| 153 | + |
| 154 | + if ( |
| 155 | + jestFnCall?.type === 'jest' && |
| 156 | + jestFnCall.members.length >= 1 && |
| 157 | + isIdentifier(jestFnCall.members[0], 'mocked') |
| 158 | + ) { |
| 159 | + return true; |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + // Continue traversing up if the current node is part of a member chain |
| 164 | + if (current.type === AST_NODE_TYPES.MemberExpression) { |
| 165 | + current = current.object; |
| 166 | + } else { |
| 167 | + break; |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + return false; |
| 172 | + }; |
| 173 | + |
76 | 174 | return { |
77 | 175 | ...baseSelectors, |
78 | 176 | MemberExpression(node: TSESTree.MemberExpression): void { |
| 177 | + // Check if this MemberExpression is an argument to jest.mocked() |
| 178 | + if (isArgumentToJestMocked(node)) { |
| 179 | + return; |
| 180 | + } |
| 181 | + |
| 182 | + // Check if accessing .mock property on jest.mocked() result |
| 183 | + if (isAccessingMockProperty(node)) { |
| 184 | + return; |
| 185 | + } |
| 186 | + |
79 | 187 | if (node.parent?.type === AST_NODE_TYPES.CallExpression) { |
80 | 188 | const jestFnCall = parseJestFnCall( |
81 | 189 | findTopMostCallExpression(node.parent), |
|
0 commit comments