Skip to content

Commit 8bf592c

Browse files
DMartensota-meshi
andauthored
feat: support explicit comparisons to null for prefer-regexp-test (#839)
* feat: support explicit comparisons to null for prefer-regexp-test * Create polite-berries-kiss.md --------- Co-authored-by: Yosuke Ota <[email protected]>
1 parent 0d14f05 commit 8bf592c

File tree

5 files changed

+152
-1
lines changed

5 files changed

+152
-1
lines changed

.changeset/polite-berries-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-regexp": minor
3+
---
4+
5+
feat: support explicit comparisons to null for prefer-regexp-test

lib/rules/prefer-regexp-test.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { SourceCode, Rule } from "eslint"
12
import type * as ES from "estree"
23
import { createRule } from "../utils"
34
import {
@@ -95,10 +96,19 @@ export default createRule("prefer-regexp-test", {
9596
const regexpText = sourceCode.text.slice(
9697
...regexpRange,
9798
)
99+
const convertedComparison =
100+
node.parent.type === "BinaryExpression" &&
101+
isComparisonToNull(node.parent)
102+
? convertComparison(
103+
node.parent,
104+
sourceCode,
105+
)(fixer)
106+
: [] // Do nothing
98107
return [
99108
fixer.replaceTextRange(stringRange, regexpText),
100109
fixer.replaceText(memberExpr.property, "test"),
101110
fixer.replaceTextRange(regexpRange, stringText),
111+
...convertedComparison,
102112
]
103113
},
104114
})
@@ -112,7 +122,19 @@ export default createRule("prefer-regexp-test", {
112122
node: execNode,
113123
messageId: "disallow",
114124
data: { target: "RegExp#exec" },
115-
fix: (fixer) => fixer.replaceText(execNode, "test"),
125+
*fix(fixer) {
126+
yield fixer.replaceText(execNode, "test")
127+
128+
if (
129+
node.parent.type === "BinaryExpression" &&
130+
isComparisonToNull(node.parent)
131+
) {
132+
yield* convertComparison(
133+
node.parent,
134+
sourceCode,
135+
)(fixer)
136+
}
137+
},
116138
})
117139
}
118140
},
@@ -148,11 +170,46 @@ function isUseBoolean(node: ES.Expression): boolean {
148170
// e.g. if (expr) {}
149171
return parent.test === node
150172
}
173+
if (parent.type === "BinaryExpression") {
174+
// e.g. expr !== null
175+
return isComparisonToNull(parent)
176+
}
151177
if (parent.type === "LogicalExpression") {
152178
if (parent.operator === "&&" || parent.operator === "||") {
153179
// e.g. Boolean(expr1 || expr2)
154180
return isUseBoolean(parent)
155181
}
156182
}
183+
157184
return false
158185
}
186+
187+
function isComparisonToNull(binary: ES.BinaryExpression): boolean {
188+
return (
189+
(binary.operator === "===" || binary.operator === "!==") &&
190+
binary.right.type === "Literal" &&
191+
binary.right.value === null
192+
)
193+
}
194+
195+
function convertComparison(
196+
comparison: ES.BinaryExpression,
197+
sourceCode: SourceCode,
198+
): (fixer: Rule.RuleFixer) => Rule.Fix[] {
199+
return function removeComparisonFixer(fixer: Rule.RuleFixer) {
200+
const operator = sourceCode.getTokenBefore(
201+
comparison.right,
202+
({ value }) => value === comparison.operator,
203+
)!
204+
const beforeOperator = sourceCode.getTokenBefore(operator, {
205+
includeComments: true,
206+
})!
207+
208+
return [
209+
fixer.removeRange([beforeOperator.range![1], comparison.range![1]]),
210+
...(comparison.operator === "==="
211+
? [fixer.insertTextBefore(comparison.left, "!")]
212+
: []),
213+
]
214+
}
215+
}

lib/utils/ast-utils/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ export function findFunction(
222222
export type KnownMethodCall = CallExpression & {
223223
callee: MemberExpression & { object: Expression; property: Identifier }
224224
arguments: Expression[]
225+
parent: Node
225226
}
226227
/**
227228
* Checks whether given node is expected method call

tests/lib/rules/__snapshots__/prefer-regexp-test.ts.eslintsnap

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,67 @@ Output:
202202
[1] Use the `RegExp#test()` method instead of `RegExp#exec`, if you need a boolean.
203203
[2] Use the `RegExp#test()` method instead of `String#match`, if you need a boolean.
204204
---
205+
206+
207+
Test: prefer-regexp-test >> invalid
208+
Code:
209+
1 |
210+
2 | const text = 'something';
211+
3 | const pattern = /thing/;
212+
4 | const a = pattern.exec(test) === null;
213+
| ^~~~ [1]
214+
5 | const b = pattern.exec(test) !== null;
215+
| ^~~~ [2]
216+
6 | const c = text.match(pattern) === null;
217+
| ^~~~~~~~~~~~~~~~~~~ [3]
218+
7 | const d = text.match(pattern) !== null;
219+
| ^~~~~~~~~~~~~~~~~~~ [4]
220+
8 |
221+
222+
Output:
223+
1 |
224+
2 | const text = 'something';
225+
3 | const pattern = /thing/;
226+
4 | const a = !pattern.test(test);
227+
5 | const b = pattern.test(test);
228+
6 | const c = !pattern.test(text);
229+
7 | const d = pattern.test(text);
230+
8 |
231+
232+
[1] Use the `RegExp#test()` method instead of `RegExp#exec`, if you need a boolean.
233+
[2] Use the `RegExp#test()` method instead of `RegExp#exec`, if you need a boolean.
234+
[3] Use the `RegExp#test()` method instead of `String#match`, if you need a boolean.
235+
[4] Use the `RegExp#test()` method instead of `String#match`, if you need a boolean.
236+
---
237+
238+
239+
Test: prefer-regexp-test >> invalid
240+
Code:
241+
1 |
242+
2 | const text = 'something';
243+
3 | const pattern = /thing/;
244+
4 | const a = pattern.exec(test)=== null;
245+
| ^~~~ [1]
246+
5 | const b = pattern.exec(test) /* Comment */ !== null;
247+
| ^~~~ [2]
248+
6 | const c = text.match(pattern) === /** Comment */ null;
249+
| ^~~~~~~~~~~~~~~~~~~ [3]
250+
7 | const d = (text.match(pattern)) !== null;
251+
| ^~~~~~~~~~~~~~~~~~~ [4]
252+
8 |
253+
254+
Output:
255+
1 |
256+
2 | const text = 'something';
257+
3 | const pattern = /thing/;
258+
4 | const a = !pattern.test(test);
259+
5 | const b = pattern.test(test) /* Comment */;
260+
6 | const c = !pattern.test(text);
261+
7 | const d = (pattern.test(text));
262+
8 |
263+
264+
[1] Use the `RegExp#test()` method instead of `RegExp#exec`, if you need a boolean.
265+
[2] Use the `RegExp#test()` method instead of `RegExp#exec`, if you need a boolean.
266+
[3] Use the `RegExp#test()` method instead of `String#match`, if you need a boolean.
267+
[4] Use the `RegExp#test()` method instead of `String#match`, if you need a boolean.
268+
---

tests/lib/rules/prefer-regexp-test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ tester.run("prefer-regexp-test", rule as any, {
3838
const pattern = /thin[[g]]/v;
3939
if (pattern.test(text)) {}
4040
`,
41+
`
42+
const text = 'something';
43+
const pattern = /thing/;
44+
const a = pattern.exec(test) instanceof null;
45+
const b = pattern.exec(test) === maybeNull;
46+
const c = pattern.exec(test).groups !== undefined;
47+
const d = text.match(pattern) != null;
48+
`,
4149
],
4250
invalid: [
4351
`
@@ -98,5 +106,21 @@ tester.run("prefer-regexp-test", rule as any, {
98106
if (pattern.exec(text)) {}
99107
if (text.match(pattern)) {}
100108
`,
109+
`
110+
const text = 'something';
111+
const pattern = /thing/;
112+
const a = pattern.exec(test) === null;
113+
const b = pattern.exec(test) !== null;
114+
const c = text.match(pattern) === null;
115+
const d = text.match(pattern) !== null;
116+
`,
117+
`
118+
const text = 'something';
119+
const pattern = /thing/;
120+
const a = pattern.exec(test)=== null;
121+
const b = pattern.exec(test) /* Comment */ !== null;
122+
const c = text.match(pattern) === /** Comment */ null;
123+
const d = (text.match(pattern)) !== null;
124+
`,
101125
],
102126
})

0 commit comments

Comments
 (0)