Skip to content

Commit 6cd600d

Browse files
authored
fix: produce valid code when when fixing properties accessed with square brackets (#1131)
* fix(prefer-todo): support fixing indexed property access * fix(prefer-to-be): support fixing indexed property access * fix(no-alias-methods): support fixing indexed property access * fix(prefer-strict-equal): support fixing indexed property access * refactor: use helper for wrapping text in quotes * refactor(prefer-todo): reduce code a little
1 parent 379ceb3 commit 6cd600d

9 files changed

+114
-16
lines changed

src/rules/__tests__/no-alias-methods.test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -231,5 +231,20 @@ ruleTester.run('no-alias-methods', rule, {
231231
},
232232
],
233233
},
234+
{
235+
code: 'expect(a).not["toThrowError"]()',
236+
output: "expect(a).not['toThrow']()",
237+
errors: [
238+
{
239+
messageId: 'replaceAlias',
240+
data: {
241+
alias: 'toThrowError',
242+
canonical: 'toThrow',
243+
},
244+
column: 15,
245+
line: 1,
246+
},
247+
],
248+
},
234249
],
235250
});

src/rules/__tests__/prefer-strict-equal.test.ts

+16
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,21 @@ ruleTester.run('prefer-strict-equal', rule, {
2626
},
2727
],
2828
},
29+
{
30+
code: 'expect(something)["toEqual"](somethingElse);',
31+
errors: [
32+
{
33+
messageId: 'useToStrictEqual',
34+
column: 19,
35+
line: 1,
36+
suggestions: [
37+
{
38+
messageId: 'suggestReplaceWithStrictEqual',
39+
output: "expect(something)['toStrictEqual'](somethingElse);",
40+
},
41+
],
42+
},
43+
],
44+
},
2945
],
3046
});

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

+35
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ ruleTester.run('prefer-to-be', rule, {
4545
output: 'expect(value).toBe(`my string`);',
4646
errors: [{ messageId: 'useToBe', column: 15, line: 1 }],
4747
},
48+
{
49+
code: 'expect(value)["toEqual"](`my string`);',
50+
output: "expect(value)['toBe'](`my string`);",
51+
errors: [{ messageId: 'useToBe', column: 15, line: 1 }],
52+
},
4853
{
4954
code: 'expect(value).toStrictEqual(`my ${string}`);',
5055
output: 'expect(value).toBe(`my ${string}`);',
@@ -55,6 +60,16 @@ ruleTester.run('prefer-to-be', rule, {
5560
output: 'expect(loadMessage()).resolves.toBe("hello world");',
5661
errors: [{ messageId: 'useToBe', column: 32, line: 1 }],
5762
},
63+
{
64+
code: 'expect(loadMessage()).resolves["toStrictEqual"]("hello world");',
65+
output: 'expect(loadMessage()).resolves[\'toBe\']("hello world");',
66+
errors: [{ messageId: 'useToBe', column: 32, line: 1 }],
67+
},
68+
{
69+
code: 'expect(loadMessage())["resolves"].toStrictEqual("hello world");',
70+
output: 'expect(loadMessage())["resolves"].toBe("hello world");',
71+
errors: [{ messageId: 'useToBe', column: 35, line: 1 }],
72+
},
5873
{
5974
code: 'expect(loadMessage()).resolves.toStrictEqual(false);',
6075
output: 'expect(loadMessage()).resolves.toBe(false);',
@@ -103,6 +118,16 @@ ruleTester.run('prefer-to-be: null', rule, {
103118
output: 'expect("a string").not.toBeNull();',
104119
errors: [{ messageId: 'useToBeNull', column: 24, line: 1 }],
105120
},
121+
{
122+
code: 'expect("a string").not["toBe"](null);',
123+
output: 'expect("a string").not[\'toBeNull\']();',
124+
errors: [{ messageId: 'useToBeNull', column: 24, line: 1 }],
125+
},
126+
{
127+
code: 'expect("a string")["not"]["toBe"](null);',
128+
output: 'expect("a string")["not"][\'toBeNull\']();',
129+
errors: [{ messageId: 'useToBeNull', column: 27, line: 1 }],
130+
},
106131
{
107132
code: 'expect("a string").not.toEqual(null);',
108133
output: 'expect("a string").not.toBeNull();',
@@ -156,6 +181,11 @@ ruleTester.run('prefer-to-be: undefined', rule, {
156181
output: 'expect("a string").rejects.toBeDefined();',
157182
errors: [{ messageId: 'useToBeDefined', column: 32, line: 1 }],
158183
},
184+
{
185+
code: 'expect("a string").rejects.not["toBe"](undefined);',
186+
output: 'expect("a string").rejects[\'toBeDefined\']();',
187+
errors: [{ messageId: 'useToBeDefined', column: 32, line: 1 }],
188+
},
159189
{
160190
code: 'expect("a string").not.toEqual(undefined);',
161191
output: 'expect("a string").toBeDefined();',
@@ -208,6 +238,11 @@ ruleTester.run('prefer-to-be: NaN', rule, {
208238
output: 'expect("a string").rejects.not.toBeNaN();',
209239
errors: [{ messageId: 'useToBeNaN', column: 32, line: 1 }],
210240
},
241+
{
242+
code: 'expect("a string")["rejects"].not.toBe(NaN);',
243+
output: 'expect("a string")["rejects"].not.toBeNaN();',
244+
errors: [{ messageId: 'useToBeNaN', column: 35, line: 1 }],
245+
},
211246
{
212247
code: 'expect("a string").not.toEqual(NaN);',
213248
output: 'expect("a string").not.toBeNaN();',

src/rules/__tests__/prefer-todo.test.ts

+10
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,15 @@ ruleTester.run('prefer-todo', rule, {
5858
output: 'test.todo("i need to write this test");',
5959
errors: [{ messageId: 'emptyTest' }],
6060
},
61+
{
62+
code: `test["skip"]("i need to write this test", function() {});`,
63+
output: 'test[\'todo\']("i need to write this test");',
64+
errors: [{ messageId: 'emptyTest' }],
65+
},
66+
{
67+
code: `test[\`skip\`]("i need to write this test", function() {});`,
68+
output: 'test[\'todo\']("i need to write this test");',
69+
errors: [{ messageId: 'emptyTest' }],
70+
},
6171
],
6272
});

src/rules/no-alias-methods.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { createRule, isExpectCall, parseExpectCall } from './utils';
1+
import {
2+
createRule,
3+
isExpectCall,
4+
parseExpectCall,
5+
replaceAccessorFixer,
6+
} from './utils';
27

38
export default createRule({
49
name: __filename,
@@ -56,7 +61,9 @@ export default createRule({
5661
canonical,
5762
},
5863
node: matcher.node.property,
59-
fix: fixer => [fixer.replaceText(matcher.node.property, canonical)],
64+
fix: fixer => [
65+
replaceAccessorFixer(fixer, matcher.node.property, canonical),
66+
],
6067
});
6168
}
6269
},

src/rules/prefer-strict-equal.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
isExpectCall,
55
isParsedEqualityMatcherCall,
66
parseExpectCall,
7+
replaceAccessorFixer,
78
} from './utils';
89

910
export default createRule({
@@ -44,7 +45,8 @@ export default createRule({
4445
{
4546
messageId: 'suggestReplaceWithStrictEqual',
4647
fix: fixer => [
47-
fixer.replaceText(
48+
replaceAccessorFixer(
49+
fixer,
4850
matcher.node.property,
4951
EqualityMatcher.toStrictEqual,
5052
),

src/rules/prefer-to-be.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
isIdentifier,
1313
isParsedEqualityMatcherCall,
1414
parseExpectCall,
15+
replaceAccessorFixer,
1516
} from './utils';
1617

1718
const isNullLiteral = (node: TSESTree.Node): node is TSESTree.NullLiteral =>
@@ -70,7 +71,7 @@ const reportPreferToBe = (
7071
messageId: `useToBe${whatToBe}`,
7172
fix(fixer) {
7273
const fixes = [
73-
fixer.replaceText(matcher.node.property, `toBe${whatToBe}`),
74+
replaceAccessorFixer(fixer, matcher.node.property, `toBe${whatToBe}`),
7475
];
7576

7677
if (matcher.arguments?.length && whatToBe !== '') {

src/rules/prefer-todo.ts

+7-12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
isFunction,
88
isStringNode,
99
parseJestFnCall,
10+
replaceAccessorFixer,
1011
} from './utils';
1112

1213
function isEmptyFunction(node: TSESTree.CallExpressionArgument) {
@@ -23,20 +24,14 @@ function createTodoFixer(
2324
jestFnCall: ParsedJestFnCall,
2425
fixer: TSESLint.RuleFixer,
2526
) {
26-
const fixes = [
27-
fixer.replaceText(jestFnCall.head.node, `${jestFnCall.head.local}.todo`),
28-
];
29-
3027
if (jestFnCall.members.length) {
31-
fixes.unshift(
32-
fixer.removeRange([
33-
jestFnCall.head.node.range[1],
34-
jestFnCall.members[0].range[1],
35-
]),
36-
);
28+
return replaceAccessorFixer(fixer, jestFnCall.members[0], 'todo');
3729
}
3830

39-
return fixes;
31+
return fixer.replaceText(
32+
jestFnCall.head.node,
33+
`${jestFnCall.head.local}.todo`,
34+
);
4035
}
4136

4237
const isTargetedTestCase = (jestFnCall: ParsedJestFnCall): boolean => {
@@ -91,7 +86,7 @@ export default createRule({
9186
node,
9287
fix: fixer => [
9388
fixer.removeRange([title.range[1], callback.range[1]]),
94-
...createTodoFixer(jestFnCall, fixer),
89+
createTodoFixer(jestFnCall, fixer),
9590
],
9691
});
9792
}

src/rules/utils/misc.ts

+17
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,20 @@ export const getTestCallExpressionsFromDeclaredVariables = (
137137
[],
138138
);
139139
};
140+
141+
/**
142+
* Replaces an accessor node with the given `text`, surrounding it in quotes if required.
143+
*
144+
* This ensures that fixes produce valid code when replacing both dot-based and
145+
* bracket-based property accessors.
146+
*/
147+
export const replaceAccessorFixer = (
148+
fixer: TSESLint.RuleFixer,
149+
node: AccessorNode,
150+
text: string,
151+
) => {
152+
return fixer.replaceText(
153+
node,
154+
node.type === AST_NODE_TYPES.Identifier ? text : `'${text}'`,
155+
);
156+
};

0 commit comments

Comments
 (0)