Skip to content

Commit d894de6

Browse files
authored
fix(prefer-to-have-count): Support variable references (#292)
1 parent f7c9214 commit d894de6

File tree

4 files changed

+166
-77
lines changed

4 files changed

+166
-77
lines changed

src/rules/prefer-to-have-count.test.ts

+77
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,83 @@ runRuleTester('prefer-to-have-count', rule, {
7272
},
7373
},
7474
},
75+
{
76+
code: `
77+
const filesCount = await files.count();
78+
expect(filesCount).toBe(1)
79+
`,
80+
errors: [
81+
{ column: 28, endColumn: 32, line: 3, messageId: 'useToHaveCount' },
82+
],
83+
output: `
84+
const filesCount = files;
85+
await expect(filesCount).toHaveCount(1)
86+
`,
87+
},
88+
{
89+
code: `
90+
const filesCount = await files.count();
91+
const unrelatedConst = unrelated;
92+
expect(filesCount).toBe(1)
93+
`,
94+
errors: [
95+
{ column: 28, endColumn: 32, line: 4, messageId: 'useToHaveCount' },
96+
],
97+
output: `
98+
const filesCount = files;
99+
const unrelatedConst = unrelated;
100+
await expect(filesCount).toHaveCount(1)
101+
`,
102+
},
103+
{
104+
code: `
105+
let filesCount = 3;
106+
filesCount = await files.count();
107+
expect(filesCount).toBe(1);
108+
`,
109+
errors: [
110+
{ column: 28, endColumn: 32, line: 4, messageId: 'useToHaveCount' },
111+
],
112+
output: `
113+
let filesCount = 3;
114+
filesCount = files;
115+
await expect(filesCount).toHaveCount(1);
116+
`,
117+
},
118+
{
119+
code: `
120+
let filesCount = 3;
121+
filesCount = await files.count();
122+
let unrelatedVar = unrelated;
123+
expect(filesCount).toBe(1);
124+
`,
125+
errors: [
126+
{ column: 28, endColumn: 32, line: 5, messageId: 'useToHaveCount' },
127+
],
128+
output: `
129+
let filesCount = 3;
130+
filesCount = files;
131+
let unrelatedVar = unrelated;
132+
await expect(filesCount).toHaveCount(1);
133+
`,
134+
},
135+
{
136+
code: `
137+
let filesCount = 3;
138+
filesCount = await files.count();
139+
expect(filesCount).toBe(1);
140+
filesCount = 0;
141+
`,
142+
errors: [
143+
{ column: 28, endColumn: 32, line: 4, messageId: 'useToHaveCount' },
144+
],
145+
output: `
146+
let filesCount = 3;
147+
filesCount = files;
148+
await expect(filesCount).toHaveCount(1);
149+
filesCount = 0;
150+
`,
151+
},
75152
],
76153
valid: [
77154
{ code: 'await expect(files).toHaveCount(1)' },

src/rules/prefer-to-have-count.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { equalityMatchers, isPropertyAccessor } from '../utils/ast'
1+
import { dereference, equalityMatchers, isPropertyAccessor } from '../utils/ast'
22
import { createRule } from '../utils/createRule'
33
import { replaceAccessorFixer } from '../utils/fixer'
44
import { parseFnCall } from '../utils/parseFnCall'
@@ -15,7 +15,7 @@ export default createRule({
1515
return
1616
}
1717

18-
const [argument] = call.args
18+
const argument = dereference(context, call.args[0])
1919
if (
2020
argument?.type !== 'AwaitExpression' ||
2121
argument.argument.type !== 'CallExpression' ||

src/rules/prefer-web-first-assertions.ts

+1-74
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import { Rule } from 'eslint'
2-
import ESTree, { AssignmentExpression } from 'estree'
31
import {
2+
dereference,
43
findParent,
54
getRawValue,
65
getStringValue,
76
isBooleanLiteral,
87
} from '../utils/ast'
98
import { createRule } from '../utils/createRule'
109
import { parseFnCall } from '../utils/parseFnCall'
11-
import { TypedNodeWithParent } from '../utils/types'
1210

1311
type MethodConfig = {
1412
inverse?: string
@@ -60,77 +58,6 @@ const supportedMatchers = new Set([
6058
'toBeFalsy',
6159
])
6260

63-
const isVariableDeclarator = (
64-
node: ESTree.Node,
65-
): node is TypedNodeWithParent<'VariableDeclarator'> =>
66-
node.type === 'VariableDeclarator'
67-
68-
const isAssignmentExpression = (
69-
node: ESTree.Node,
70-
): node is TypedNodeWithParent<'AssignmentExpression'> =>
71-
node.type === 'AssignmentExpression'
72-
73-
/**
74-
* Given a Node and an assignment expression, finds out if the assignment
75-
* expression happens before the node identifier (based on their range
76-
* properties) and if the assignment expression left side is of the same name as
77-
* the name of the given node.
78-
*
79-
* @param node The node we are comparing the assignment expression to.
80-
* @param assignment The assignment that will be verified to see if its left
81-
* operand is the same as the node.name and if it happens before it.
82-
* @returns True if the assignment left hand operator belongs to the node and
83-
* occurs before it, false otherwise. If either the node or the assignment
84-
* expression doesn't contain a range array, this will also return false
85-
* because their relative positions cannot be calculated.
86-
*/
87-
function isNodeLastAssignment(
88-
node: ESTree.Identifier,
89-
assignment: AssignmentExpression,
90-
) {
91-
if (node.range && assignment.range && node.range[0] < assignment.range[1]) {
92-
return false
93-
}
94-
95-
return (
96-
assignment.left.type === 'Identifier' && assignment.left.name === node.name
97-
)
98-
}
99-
100-
/**
101-
* If the expect call argument is a variable reference, finds the variable
102-
* initializer or last variable assignment.
103-
*
104-
* If a variable is assigned after initialization we have to look for the last
105-
* time it was assigned because it could have been changed multiple times. We
106-
* then use its right hand assignment operator as the dereferenced node.
107-
*/
108-
function dereference(context: Rule.RuleContext, node: ESTree.Node | undefined) {
109-
if (node?.type !== 'Identifier') {
110-
return node
111-
}
112-
113-
const scope = context.sourceCode.getScope(node)
114-
const parents = scope.references
115-
.map((ref) => ref.identifier as Rule.Node)
116-
.map((ident) => ident.parent)
117-
118-
// Look for any variable declarators in the scope references that match the
119-
// dereferenced node variable name
120-
const decl = parents
121-
.filter(isVariableDeclarator)
122-
.find((p) => p.id.type === 'Identifier' && p.id.name === node.name)
123-
124-
// Look for any variable assignments in the scope references and pick the last
125-
// one that matches the dereferenced node variable name
126-
const expr = parents
127-
.filter(isAssignmentExpression)
128-
.reverse()
129-
.find((assignment) => isNodeLastAssignment(node, assignment))
130-
131-
return expr?.right ?? decl?.init
132-
}
133-
13461
export default createRule({
13562
create(context) {
13663
return {

src/utils/ast.ts

+86-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Rule } from 'eslint'
2-
import ESTree from 'estree'
2+
import ESTree, { AssignmentExpression } from 'estree'
33
import { isSupportedAccessor } from './parseFnCall'
44
import { NodeWithParent, TypedNodeWithParent } from './types'
55

@@ -158,3 +158,88 @@ export function getNodeName(node: ESTree.Node): string | null {
158158

159159
return null
160160
}
161+
162+
const isVariableDeclarator = (
163+
node: ESTree.Node,
164+
): node is TypedNodeWithParent<'VariableDeclarator'> =>
165+
node.type === 'VariableDeclarator'
166+
167+
const isAssignmentExpression = (
168+
node: ESTree.Node,
169+
): node is TypedNodeWithParent<'AssignmentExpression'> =>
170+
node.type === 'AssignmentExpression'
171+
172+
/**
173+
* Given a Node and an assignment expression, finds out if the assignment
174+
* expression happens before the node identifier (based on their range
175+
* properties) and if the assignment expression left side is of the same name as
176+
* the name of the given node.
177+
*
178+
* @param node The node we are comparing the assignment expression to.
179+
* @param assignment The assignment that will be verified to see if its left
180+
* operand is the same as the node.name and if it happens before it.
181+
* @returns True if the assignment left hand operator belongs to the node and
182+
* occurs before it, false otherwise. If either the node or the assignment
183+
* expression doesn't contain a range array, this will also return false
184+
* because their relative positions cannot be calculated.
185+
*/
186+
function isNodeLastAssignment(
187+
node: ESTree.Identifier,
188+
assignment: AssignmentExpression,
189+
) {
190+
if (node.range && assignment.range && node.range[0] < assignment.range[1]) {
191+
return false
192+
}
193+
194+
return (
195+
assignment.left.type === 'Identifier' && assignment.left.name === node.name
196+
)
197+
}
198+
199+
/**
200+
* If the node argument is a variable reference, finds the variable initializer
201+
* or last variable assignment and returns the assigned value.
202+
*
203+
* If a variable is assigned after initialization we have to look for the last
204+
* time it was assigned because it could have been changed multiple times. We
205+
* then use its right hand assignment operator as the dereferenced node.
206+
*
207+
* @example <caption>Dereference a `const` initialized node:</caption>
208+
* // returns 1
209+
* const variable = 1
210+
* console.log(variable) // dereferenced value of the 'variable' node is 1
211+
*
212+
* @example <caption>Dereference a `let` re-assigned node:</caption>
213+
* // returns 1
214+
* let variable = 0
215+
* variable = 1
216+
* console.log(variable) // dereferenced value of the 'variable' node is 1
217+
*/
218+
export function dereference(
219+
context: Rule.RuleContext,
220+
node: ESTree.Node | undefined,
221+
) {
222+
if (node?.type !== 'Identifier') {
223+
return node
224+
}
225+
226+
const scope = context.sourceCode.getScope(node)
227+
const parents = scope.references
228+
.map((ref) => ref.identifier as Rule.Node)
229+
.map((ident) => ident.parent)
230+
231+
// Look for any variable declarators in the scope references that match the
232+
// dereferenced node variable name
233+
const decl = parents
234+
.filter(isVariableDeclarator)
235+
.find((p) => p.id.type === 'Identifier' && p.id.name === node.name)
236+
237+
// Look for any variable assignments in the scope references and pick the last
238+
// one that matches the dereferenced node variable name
239+
const expr = parents
240+
.filter(isAssignmentExpression)
241+
.reverse()
242+
.find((assignment) => isNodeLastAssignment(node, assignment))
243+
244+
return expr?.right ?? decl?.init
245+
}

0 commit comments

Comments
 (0)