Skip to content

Commit 85d3795

Browse files
authored
Add regexp/no-legacy-features rule (#60)
1 parent d03408c commit 85d3795

File tree

6 files changed

+290
-0
lines changed

6 files changed

+290
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
9393
| [regexp/no-empty-lookarounds-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-lookarounds-assertion.html) | disallow empty lookahead assertion or empty lookbehind assertion | :star: |
9494
| [regexp/no-escape-backspace](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-escape-backspace.html) | disallow escape backspace (`[\b]`) | :star: |
9595
| [regexp/no-invisible-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invisible-character.html) | disallow invisible raw character | :star::wrench: |
96+
| [regexp/no-legacy-features](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-legacy-features.html) | disallow invisible raw character | |
9697
| [regexp/no-octal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-octal.html) | disallow octal escape sequence | :star: |
9798
| [regexp/no-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-backreference.html) | disallow useless backreferences in regular expressions | |
9899
| [regexp/no-useless-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-character-class.html) | disallow character class with one character | :wrench: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
2121
| [regexp/no-empty-lookarounds-assertion](./no-empty-lookarounds-assertion.md) | disallow empty lookahead assertion or empty lookbehind assertion | :star: |
2222
| [regexp/no-escape-backspace](./no-escape-backspace.md) | disallow escape backspace (`[\b]`) | :star: |
2323
| [regexp/no-invisible-character](./no-invisible-character.md) | disallow invisible raw character | :star::wrench: |
24+
| [regexp/no-legacy-features](./no-legacy-features.md) | disallow invisible raw character | |
2425
| [regexp/no-octal](./no-octal.md) | disallow octal escape sequence | :star: |
2526
| [regexp/no-useless-backreference](./no-useless-backreference.md) | disallow useless backreferences in regular expressions | |
2627
| [regexp/no-useless-character-class](./no-useless-character-class.md) | disallow character class with one character | :wrench: |

docs/rules/no-legacy-features.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/no-legacy-features"
5+
description: "disallow invisible raw character"
6+
---
7+
# regexp/no-legacy-features
8+
9+
> disallow invisible raw character
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
13+
## :book: Rule Details
14+
15+
This rule disallow legacy RegExp features.
16+
17+
<eslint-code-block>
18+
19+
```js
20+
/* eslint regexp/no-legacy-features: "error" */
21+
22+
/* ✗ BAD */
23+
RegExp.input
24+
RegExp.$_
25+
RegExp.lastMatch
26+
RegExp["$&"]
27+
RegExp.lastParen
28+
RegExp["$+"]
29+
RegExp.leftContext
30+
RegExp["$`"]
31+
RegExp.rightContext
32+
RegExp["$'"]
33+
RegExp.$1
34+
RegExp.$2
35+
RegExp.$3
36+
RegExp.$4
37+
RegExp.$5
38+
RegExp.$6
39+
RegExp.$7
40+
RegExp.$8
41+
RegExp.$9
42+
43+
const regexObj = new RegExp('foo', 'gi');
44+
regexObj.compile('new foo', 'g');
45+
```
46+
47+
</eslint-code-block>
48+
49+
## :wrench: Options
50+
51+
```json
52+
{
53+
"regexp/no-legacy-features": ["error", {
54+
"staticProperties": [
55+
"input", "$_",
56+
"lastMatch", "$&",
57+
"lastParen", "$+",
58+
"leftContext", "$`",
59+
"rightContext", "$'",
60+
"$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9"
61+
],
62+
"prototypeMethods": ["compile"]
63+
}]
64+
}
65+
```
66+
67+
- `staticProperties` ... An array of legacy static properties to forbid.
68+
- `prototypeMethods` ... An array of legacy prototype methods to forbid.
69+
70+
## :books: Further reading
71+
72+
- [Legacy RegExp features in JavaScript](https://github.com/tc39/proposal-regexp-legacy-features/)
73+
74+
## :mag: Implementation
75+
76+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-legacy-features.ts)
77+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-legacy-features.ts)

lib/rules/no-legacy-features.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import type { MemberExpression } from "estree"
2+
import type { TYPES } from "eslint-utils"
3+
import { READ, ReferenceTracker } from "eslint-utils"
4+
import { createRule } from "../utils"
5+
import { createTypeTracker } from "../utils/type-tracker"
6+
7+
type StaticProperty =
8+
| "input"
9+
| "$_"
10+
| "lastMatch"
11+
| "$&"
12+
| "lastParen"
13+
| "$+"
14+
| "leftContext"
15+
| "$`"
16+
| "rightContext"
17+
| "$'"
18+
| "$1"
19+
| "$2"
20+
| "$3"
21+
| "$4"
22+
| "$5"
23+
| "$6"
24+
| "$7"
25+
| "$8"
26+
| "$9"
27+
type PrototypeMethod = "compile"
28+
const STATIC_PROPERTIES: StaticProperty[] = [
29+
"input",
30+
"$_",
31+
"lastMatch",
32+
"$&",
33+
"lastParen",
34+
"$+",
35+
"leftContext",
36+
"$`",
37+
"rightContext",
38+
"$'",
39+
"$1",
40+
"$2",
41+
"$3",
42+
"$4",
43+
"$5",
44+
"$6",
45+
"$7",
46+
"$8",
47+
"$9",
48+
]
49+
50+
const PROTOTYPE_METHODS: PrototypeMethod[] = ["compile"]
51+
52+
export default createRule("no-legacy-features", {
53+
meta: {
54+
docs: {
55+
description: "disallow legacy RegExp features",
56+
// TODO In the major version
57+
// recommended: true,
58+
recommended: false,
59+
},
60+
schema: [
61+
{
62+
type: "object",
63+
properties: {
64+
staticProperties: {
65+
type: "array",
66+
items: { enum: STATIC_PROPERTIES },
67+
uniqueItems: true,
68+
},
69+
prototypeMethods: {
70+
type: "array",
71+
items: { enum: PROTOTYPE_METHODS },
72+
uniqueItems: true,
73+
},
74+
},
75+
additionalProperties: false,
76+
},
77+
],
78+
messages: {
79+
forbiddenStaticProperty: "'{{name}}' static property is forbidden.",
80+
forbiddenPrototypeMethods:
81+
"RegExp.prototype.{{name}} method is forbidden.",
82+
},
83+
type: "suggestion", // "problem",
84+
},
85+
create(context) {
86+
const staticProperties: StaticProperty[] =
87+
context.options[0]?.staticProperties ?? STATIC_PROPERTIES
88+
const prototypeMethods: PrototypeMethod[] =
89+
context.options[0]?.prototypeMethods ?? PROTOTYPE_METHODS
90+
const typeTracer = createTypeTracker(context)
91+
92+
return {
93+
...(staticProperties.length
94+
? {
95+
Program() {
96+
const scope = context.getScope()
97+
const tracker = new ReferenceTracker(scope)
98+
99+
const regexpTraceMap: TYPES.TraceMap = {}
100+
for (const sp of staticProperties) {
101+
regexpTraceMap[sp] = { [READ]: true }
102+
}
103+
for (const {
104+
node,
105+
path,
106+
} of tracker.iterateGlobalReferences({
107+
RegExp: regexpTraceMap,
108+
})) {
109+
context.report({
110+
node,
111+
messageId: "forbiddenStaticProperty",
112+
data: { name: path.join(".") },
113+
})
114+
}
115+
},
116+
}
117+
: {}),
118+
...(prototypeMethods.length
119+
? {
120+
MemberExpression(node: MemberExpression) {
121+
if (
122+
node.computed ||
123+
node.property.type !== "Identifier" ||
124+
!(prototypeMethods as string[]).includes(
125+
node.property.name,
126+
) ||
127+
node.object.type === "Super"
128+
) {
129+
return
130+
}
131+
if (typeTracer.isRegExp(node.object)) {
132+
context.report({
133+
node,
134+
messageId: "forbiddenPrototypeMethods",
135+
data: { name: node.property.name },
136+
})
137+
}
138+
},
139+
}
140+
: {}),
141+
}
142+
},
143+
})

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import noEmptyGroup from "../rules/no-empty-group"
99
import noEmptyLookaroundsAssertion from "../rules/no-empty-lookarounds-assertion"
1010
import noEscapeBackspace from "../rules/no-escape-backspace"
1111
import noInvisibleCharacter from "../rules/no-invisible-character"
12+
import noLegacyFeatures from "../rules/no-legacy-features"
1213
import noOctal from "../rules/no-octal"
1314
import noUselessBackreference from "../rules/no-useless-backreference"
1415
import noUselessCharacterClass from "../rules/no-useless-character-class"
@@ -43,6 +44,7 @@ export const rules = [
4344
noEmptyLookaroundsAssertion,
4445
noEscapeBackspace,
4546
noInvisibleCharacter,
47+
noLegacyFeatures,
4648
noOctal,
4749
noUselessBackreference,
4850
noUselessCharacterClass,

tests/lib/rules/no-legacy-features.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../lib/rules/no-legacy-features"
3+
4+
const tester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 2020,
7+
sourceType: "module",
8+
},
9+
})
10+
11+
const STATIC_PROPERTIES: string[] = [
12+
"input",
13+
"$_",
14+
"lastMatch",
15+
"$&",
16+
"lastParen",
17+
"$+",
18+
"leftContext",
19+
"$`",
20+
"rightContext",
21+
"$'",
22+
"$1",
23+
"$2",
24+
"$3",
25+
"$4",
26+
"$5",
27+
"$6",
28+
"$7",
29+
"$8",
30+
"$9",
31+
]
32+
const PROTOTYPE_METHODS: string[] = ["compile"]
33+
34+
tester.run("no-legacy-features", rule as any, {
35+
valid: [`RegExp`, `new RegExp()`, `RegExp.unknown`],
36+
invalid: [
37+
...STATIC_PROPERTIES.map((sp) => {
38+
return {
39+
code: `RegExp["${sp}"]`,
40+
errors: [`'RegExp.${sp}' static property is forbidden.`],
41+
}
42+
}),
43+
...STATIC_PROPERTIES.filter((sp) => /^[\w$]+$/u.test(sp)).map((sp) => {
44+
return {
45+
code: `RegExp.${sp}`,
46+
errors: [`'RegExp.${sp}' static property is forbidden.`],
47+
}
48+
}),
49+
...PROTOTYPE_METHODS.map((pm) => {
50+
return {
51+
code: `
52+
const regexObj = new RegExp('foo', 'gi');
53+
regexObj.${pm}`,
54+
errors: ["RegExp.prototype.compile method is forbidden."],
55+
}
56+
}),
57+
...PROTOTYPE_METHODS.map((pm) => {
58+
return {
59+
code: `
60+
const regexObj = /foo/;
61+
regexObj.${pm}('new foo', 'g')`,
62+
errors: ["RegExp.prototype.compile method is forbidden."],
63+
}
64+
}),
65+
],
66+
})

0 commit comments

Comments
 (0)