Skip to content

Commit 73984a7

Browse files
authored
fix(prefer-to-contain): support square bracket accessors (#1009)
* fix(prefer-to-contain): support square bracket accessors * refactor(prefer-to-contain): simplify fixer * refactor(prefer-to-contain): simplify fixer further * refactor(prefer-to-contain): simplify fixer even further * chore(prefer-to-contain): add comments * refactor(prefer-to-contain): swap order of fixers to read better
1 parent 902a70d commit 73984a7

File tree

2 files changed

+53
-128
lines changed

2 files changed

+53
-128
lines changed

src/rules/__tests__/prefer-to-contain.test.ts

+30-6
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,36 @@ ruleTester.run('prefer-to-contain', rule, {
3232
output: 'expect(a).toContain(b);',
3333
errors: [{ messageId: 'useToContain', column: 23, line: 1 }],
3434
},
35-
// todo: support this, as it's counted by isSupportedAccessor
36-
// {
37-
// code: "expect(a['includes'](b)).toEqual(true);",
38-
// errors: [{ messageId: 'useToContain', column: 23, line: 1 }],
39-
// output: 'expect(a).toContain(b);',
40-
// },
35+
{
36+
code: "expect(a['includes'](b)).toEqual(true);",
37+
output: 'expect(a).toContain(b);',
38+
errors: [{ messageId: 'useToContain', column: 26, line: 1 }],
39+
},
40+
{
41+
code: "expect(a['includes'](b))['toEqual'](true);",
42+
output: 'expect(a).toContain(b);',
43+
errors: [{ messageId: 'useToContain', column: 26, line: 1 }],
44+
},
45+
{
46+
code: "expect(a['includes'](b)).toEqual(false);",
47+
output: 'expect(a).not.toContain(b);',
48+
errors: [{ messageId: 'useToContain', column: 26, line: 1 }],
49+
},
50+
{
51+
code: "expect(a['includes'](b)).not.toEqual(false);",
52+
output: 'expect(a).toContain(b);',
53+
errors: [{ messageId: 'useToContain', column: 26, line: 1 }],
54+
},
55+
{
56+
code: "expect(a['includes'](b))['not'].toEqual(false);",
57+
output: 'expect(a).toContain(b);',
58+
errors: [{ messageId: 'useToContain', column: 26, line: 1 }],
59+
},
60+
{
61+
code: "expect(a['includes'](b))['not']['toEqual'](false);",
62+
output: 'expect(a).toContain(b);',
63+
errors: [{ messageId: 'useToContain', column: 26, line: 1 }],
64+
},
4165
{
4266
code: 'expect(a.includes(b)).toEqual(false);',
4367
output: 'expect(a).not.toContain(b);',

src/rules/prefer-to-contain.ts

+23-122
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import {
22
AST_NODE_TYPES,
3-
TSESLint,
43
TSESTree,
54
} from '@typescript-eslint/experimental-utils';
65
import {
76
CallExpressionWithSingleArgument,
87
KnownCallExpression,
98
MaybeTypeCast,
109
ModifierName,
11-
NotNegatableParsedModifier,
1210
ParsedEqualityMatcherCall,
1311
ParsedExpectMatcher,
1412
createRule,
@@ -57,103 +55,15 @@ type FixableIncludesCallExpression = KnownCallExpression<'includes'> &
5755
* @param {CallExpression} node
5856
*
5957
* @return {node is FixableIncludesCallExpression}
60-
*
61-
* @todo support `['includes']()` syntax (remove last property.type check to begin)
62-
* @todo break out into `isMethodCall<Name extends string>(node: TSESTree.Node, method: Name)` util-fn
6358
*/
6459
const isFixableIncludesCallExpression = (
6560
node: TSESTree.Node,
6661
): node is FixableIncludesCallExpression =>
6762
node.type === AST_NODE_TYPES.CallExpression &&
6863
node.callee.type === AST_NODE_TYPES.MemberExpression &&
6964
isSupportedAccessor(node.callee.property, 'includes') &&
70-
node.callee.property.type === AST_NODE_TYPES.Identifier &&
7165
hasOnlyOneArgument(node);
7266

73-
const buildToContainFuncExpectation = (negated: boolean) =>
74-
negated ? `${ModifierName.not}.toContain` : 'toContain';
75-
76-
/**
77-
* Finds the first `.` character token between the `object` & `property` of the given `member` expression.
78-
*
79-
* @param {TSESTree.MemberExpression} member
80-
* @param {SourceCode} sourceCode
81-
*
82-
* @return {Token | null}
83-
*/
84-
const findPropertyDotToken = (
85-
member: TSESTree.MemberExpression,
86-
sourceCode: TSESLint.SourceCode,
87-
) =>
88-
sourceCode.getFirstTokenBetween(
89-
member.object,
90-
member.property,
91-
token => token.value === '.',
92-
);
93-
94-
const getNegationFixes = (
95-
node: FixableIncludesCallExpression,
96-
modifier: NotNegatableParsedModifier,
97-
matcher: ParsedBooleanEqualityMatcherCall,
98-
sourceCode: TSESLint.SourceCode,
99-
fixer: TSESLint.RuleFixer,
100-
fileName: string,
101-
) => {
102-
const [containArg] = node.arguments;
103-
const negationPropertyDot = findPropertyDotToken(modifier.node, sourceCode);
104-
105-
const toContainFunc = buildToContainFuncExpectation(
106-
followTypeAssertionChain(matcher.arguments[0]).value,
107-
);
108-
109-
/* istanbul ignore if */
110-
if (negationPropertyDot === null) {
111-
throw new Error(
112-
`Unexpected null when attempting to fix ${fileName} - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`,
113-
);
114-
}
115-
116-
return [
117-
fixer.remove(negationPropertyDot),
118-
fixer.remove(modifier.node.property),
119-
fixer.replaceText(matcher.node.property, toContainFunc),
120-
fixer.replaceText(matcher.arguments[0], sourceCode.getText(containArg)),
121-
];
122-
};
123-
124-
const getCommonFixes = (
125-
node: FixableIncludesCallExpression,
126-
sourceCode: TSESLint.SourceCode,
127-
fileName: string,
128-
): Array<TSESTree.Node | TSESTree.Token> => {
129-
const [containArg] = node.arguments;
130-
const includesCallee = node.callee;
131-
132-
const propertyDot = findPropertyDotToken(includesCallee, sourceCode);
133-
134-
const closingParenthesis = sourceCode.getTokenAfter(containArg);
135-
const openParenthesis = sourceCode.getTokenBefore(containArg);
136-
137-
/* istanbul ignore if */
138-
if (
139-
propertyDot === null ||
140-
closingParenthesis === null ||
141-
openParenthesis === null
142-
) {
143-
throw new Error(
144-
`Unexpected null when attempting to fix ${fileName} - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`,
145-
);
146-
}
147-
148-
return [
149-
containArg,
150-
includesCallee.property,
151-
propertyDot,
152-
closingParenthesis,
153-
openParenthesis,
154-
];
155-
};
156-
15767
// expect(array.includes(<value>)[not.]{toBe,toEqual}(<boolean>)
15868
export default createRule({
15969
name: __filename,
@@ -181,6 +91,7 @@ export default createRule({
18191
const {
18292
expect: {
18393
arguments: [includesCall],
94+
range: [, expectCallEnd],
18495
},
18596
matcher,
18697
modifier,
@@ -199,42 +110,32 @@ export default createRule({
199110
context.report({
200111
fix(fixer) {
201112
const sourceCode = context.getSourceCode();
202-
const fileName = context.getFilename();
203-
204-
const fixArr = getCommonFixes(
205-
includesCall,
206-
sourceCode,
207-
fileName,
208-
).map(target => fixer.remove(target));
209113

210-
if (modifier) {
211-
return getNegationFixes(
212-
includesCall,
213-
modifier,
214-
matcher,
215-
sourceCode,
216-
fixer,
217-
fileName,
218-
).concat(fixArr);
219-
}
220-
221-
const toContainFunc = buildToContainFuncExpectation(
222-
!followTypeAssertionChain(matcher.arguments[0]).value,
223-
);
224-
225-
const [containArg] = includesCall.arguments;
226-
227-
fixArr.push(
228-
fixer.replaceText(matcher.node.property, toContainFunc),
229-
);
230-
fixArr.push(
114+
// we need to negate the expectation if the current expected
115+
// value is itself negated by the "not" modifier
116+
const addNotModifier =
117+
followTypeAssertionChain(matcher.arguments[0]).value ===
118+
!!modifier;
119+
120+
return [
121+
// remove the "includes" call entirely
122+
fixer.removeRange([
123+
includesCall.callee.property.range[0] - 1,
124+
includesCall.range[1],
125+
]),
126+
// replace the current matcher with "toContain", adding "not" if needed
127+
fixer.replaceTextRange(
128+
[expectCallEnd, matcher.node.range[1]],
129+
addNotModifier
130+
? `.${ModifierName.not}.toContain`
131+
: '.toContain',
132+
),
133+
// replace the matcher argument with the value from the "includes"
231134
fixer.replaceText(
232135
matcher.arguments[0],
233-
sourceCode.getText(containArg),
136+
sourceCode.getText(includesCall.arguments[0]),
234137
),
235-
);
236-
237-
return fixArr;
138+
];
238139
},
239140
messageId: 'useToContain',
240141
node: (modifier || matcher).node.property,

0 commit comments

Comments
 (0)