-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathprefer-regexp-test.ts
158 lines (153 loc) · 6.28 KB
/
prefer-regexp-test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import type * as ES from "estree"
import { createRule } from "../utils"
import {
getParent,
isKnownMethodCall,
getStaticValue,
} from "../utils/ast-utils"
import { createTypeTracker } from "../utils/type-tracker"
import {
hasSideEffect,
isOpeningParenToken,
} from "@eslint-community/eslint-utils"
// Inspired by https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-regexp-test.md
export default createRule("prefer-regexp-test", {
meta: {
docs: {
description:
"enforce that `RegExp#test` is used instead of `String#match` and `RegExp#exec`",
category: "Best Practices",
recommended: false,
},
fixable: "code",
schema: [],
messages: {
disallow:
"Use the `RegExp#test()` method instead of `{{target}}`, if you need a boolean.",
},
type: "suggestion", // "problem",
},
create(context) {
const sourceCode = context.sourceCode
const typeTracer = createTypeTracker(context)
return {
CallExpression(node: ES.CallExpression) {
if (!isKnownMethodCall(node, { match: 1, exec: 1 })) {
return
}
if (!isUseBoolean(node)) {
return
}
if (node.callee.property.name === "match") {
if (!typeTracer.isString(node.callee.object)) {
return
}
const arg = node.arguments[0]
const evaluated = getStaticValue(context, arg)
let argIsRegExp = true
if (evaluated && evaluated.value instanceof RegExp) {
if (evaluated.value.flags.includes("g")) {
return
}
} else if (!typeTracer.isRegExp(arg)) {
// Not RegExp
// String.prototype.match function allows non-RegExp arguments
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match#a_non-regexp_object_as_the_parameter
argIsRegExp = false
}
const memberExpr = node.callee
context.report({
node,
messageId: "disallow",
data: { target: "String#match" },
fix(fixer) {
if (!argIsRegExp) {
// If the argument is not RegExp, it will not be autofix.
// Must use `new RegExp()` before fixing it.
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match#a_non-regexp_object_as_the_parameter
// When the regexp parameter is a string or a number, it is implicitly converted to a RegExp by using new RegExp(regexp).
return null
}
if (
node.arguments.length !== 1 ||
hasSideEffect(memberExpr, sourceCode) ||
hasSideEffect(node.arguments[0], sourceCode)
) {
return null
}
const openParen = sourceCode.getTokenAfter(
node.callee,
isOpeningParenToken,
)!
const closeParen = sourceCode.getLastToken(node)!
const stringRange = memberExpr.object.range!
const regexpRange: [number, number] = [
openParen.range[1],
closeParen.range[0],
]
const stringText = sourceCode.text.slice(
...stringRange,
)
const regexpText = sourceCode.text.slice(
...regexpRange,
)
return [
fixer.replaceTextRange(stringRange, regexpText),
fixer.replaceText(memberExpr.property, "test"),
fixer.replaceTextRange(regexpRange, stringText),
]
},
})
}
if (node.callee.property.name === "exec") {
if (!typeTracer.isRegExp(node.callee.object)) {
return
}
const execNode = node.callee.property
context.report({
node: execNode,
messageId: "disallow",
data: { target: "RegExp#exec" },
fix: (fixer) => fixer.replaceText(execNode, "test"),
})
}
},
}
},
})
/** Checks if the given node is use boolean. */
function isUseBoolean(node: ES.Expression): boolean {
const parent = getParent(node)
if (!parent) {
return false
}
if (parent.type === "UnaryExpression") {
// e.g. !expr
return parent.operator === "!"
}
if (parent.type === "CallExpression") {
// e.g. Boolean(expr)
return (
parent.callee.type === "Identifier" &&
parent.callee.name === "Boolean" &&
parent.arguments[0] === node
)
}
if (
parent.type === "IfStatement" ||
parent.type === "ConditionalExpression" ||
parent.type === "WhileStatement" ||
parent.type === "DoWhileStatement" ||
parent.type === "ForStatement"
) {
// e.g. if (expr) {}
return parent.test === node
}
if (parent.type === "LogicalExpression") {
if (parent.operator === "&&" || parent.operator === "||") {
// e.g. Boolean(expr1 || expr2)
return isUseBoolean(parent)
}
}
return false
}