Skip to content

Commit fe6eced

Browse files
Added regexp/no-obscure-range rule (#122)
* Added `regexp/no-obscure-range` rule * Added docs * Added `regexp/no-obscure-range` rule * Added docs * Updated recommended * Updated docs * Support setting * Added test * Renamed setting * Fixed false negative * Updated docs
1 parent a483ecb commit fe6eced

File tree

16 files changed

+654
-87
lines changed

16 files changed

+654
-87
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
9797
| [regexp/no-invisible-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invisible-character.html) | disallow invisible raw character | :star::wrench: |
9898
| [regexp/no-lazy-ends](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-lazy-ends.html) | disallow lazy quantifiers at the end of an expression | |
9999
| [regexp/no-legacy-features](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-legacy-features.html) | disallow legacy RegExp features | |
100+
| [regexp/no-obscure-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-obscure-range.html) | disallow obscure character ranges | |
100101
| [regexp/no-octal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-octal.html) | disallow octal escape sequence | :star: |
101102
| [regexp/no-potentially-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-potentially-useless-backreference.html) | disallow backreferences that reference a group that might not be matched | |
102103
| [regexp/no-trivially-nested-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-trivially-nested-assertion.html) | disallow trivially nested assertions | :wrench: |

docs/.vuepress/config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ module.exports = {
4444
{ text: "Introduction", link: "/" },
4545
{ text: "User Guide", link: "/user-guide/" },
4646
{ text: "Rules", link: "/rules/" },
47+
{ text: "Settings", link: "/settings/" },
4748
{ text: "Playground", link: "/playground/" },
4849
],
4950

@@ -93,7 +94,7 @@ module.exports = {
9394
]
9495
: []),
9596
],
96-
"/": ["/", "/user-guide/", "/rules/", "/playground/"],
97+
"/": ["/", "/user-guide/", "/rules/", "/settings/", "/playground/"],
9798
},
9899
},
99100
}

docs/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ See [User Guide](./user-guide/README.md).
3030

3131
See [Available Rules](./rules/README.md).
3232

33+
## :gear: Settings
34+
35+
See [Settings](./settings/README.md).
36+
3337
## :lock: License
3438

3539
See the [LICENSE](LICENSE) file for license rights and limitations (MIT).

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
2525
| [regexp/no-invisible-character](./no-invisible-character.md) | disallow invisible raw character | :star::wrench: |
2626
| [regexp/no-lazy-ends](./no-lazy-ends.md) | disallow lazy quantifiers at the end of an expression | |
2727
| [regexp/no-legacy-features](./no-legacy-features.md) | disallow legacy RegExp features | |
28+
| [regexp/no-obscure-range](./no-obscure-range.md) | disallow obscure character ranges | |
2829
| [regexp/no-octal](./no-octal.md) | disallow octal escape sequence | :star: |
2930
| [regexp/no-potentially-useless-backreference](./no-potentially-useless-backreference.md) | disallow backreferences that reference a group that might not be matched | |
3031
| [regexp/no-trivially-nested-assertion](./no-trivially-nested-assertion.md) | disallow trivially nested assertions | :wrench: |

docs/rules/no-obscure-range.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/no-obscure-range"
5+
description: "disallow obscure character ranges"
6+
---
7+
# regexp/no-obscure-range
8+
9+
> disallow obscure character ranges
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+
The character range operator (the `-` inside character classes) can easily be misused (mostly unintentionally) to construct non-obvious character class. This rule will disallow all non-obvious uses of the character range operator.
16+
17+
<eslint-code-block>
18+
19+
```js
20+
/* eslint regexp/no-obscure-range: "error" */
21+
22+
/* ✓ GOOD */
23+
var foo = /[a-z]/;
24+
var foo = /[J-O]/;
25+
var foo = /[1-9]/;
26+
var foo = /[\x00-\x40]/;
27+
var foo = /[\0-\uFFFF]/;
28+
var foo = /[\0-\u{10FFFF}]/u;
29+
var foo = /[\1-\5]/;
30+
var foo = /[\cA-\cZ]/;
31+
32+
/* ✗ BAD */
33+
var foo = /[A-\x43]/;
34+
var foo = /[\41-\x45]/;
35+
var foo = /[!-$]/;
36+
var foo = /[😀-😄]/u;
37+
```
38+
39+
</eslint-code-block>
40+
41+
## :wrench: Options
42+
43+
44+
```json5
45+
{
46+
"regexp/no-obscure-range": ["error",
47+
{
48+
"allowed": "alphanumeric" // or "all" or [...]
49+
}
50+
]
51+
}
52+
```
53+
54+
This option can be used to override the [allowedCharacterRanges] setting.
55+
56+
It allows all values that the [allowedCharacterRanges] setting allows.
57+
58+
[allowedCharacterRanges]: ../settings/README.md#allowedCharacterRanges
59+
60+
### `"allowed": "alphanumeric"`
61+
62+
<eslint-code-block fix>
63+
64+
```js
65+
/* eslint regexp/no-obscure-range: ["error", { "allowed": "alphanumeric" }] */
66+
67+
/* ✓ GOOD */
68+
var foo = /[a-z]/;
69+
var foo = /[J-O]/;
70+
var foo = /[1-9]/;
71+
72+
/* ✗ BAD */
73+
var foo = /[A-\x43]/;
74+
var foo = /[\41-\x45]/;
75+
var foo = /[!-$]/;
76+
var foo = /[😀-😄]/u;
77+
```
78+
79+
</eslint-code-block>
80+
81+
### `"allowed": "all"`
82+
83+
<eslint-code-block fix>
84+
85+
```js
86+
/* eslint regexp/no-obscure-range: ["error", { "allowed": "all" }] */
87+
88+
/* ✓ GOOD */
89+
var foo = /[a-z]/;
90+
var foo = /[J-O]/;
91+
var foo = /[1-9]/;
92+
var foo = /[!-$]/;
93+
var foo = /[😀-😄]/u;
94+
95+
/* ✗ BAD */
96+
var foo = /[A-\x43]/;
97+
var foo = /[\41-\x45]/;
98+
```
99+
100+
</eslint-code-block>
101+
102+
### `"allowed": [ "alphanumeric", "😀-😏" ]`
103+
104+
<eslint-code-block fix>
105+
106+
```js
107+
/* eslint regexp/no-obscure-range: ["error", { "allowed": [ "alphanumeric", "😀-😏" ] }] */
108+
109+
/* ✓ GOOD */
110+
var foo = /[a-z]/;
111+
var foo = /[J-O]/;
112+
var foo = /[1-9]/;
113+
var foo = /[😀-😄]/u;
114+
115+
/* ✗ BAD */
116+
var foo = /[A-\x43]/;
117+
var foo = /[\41-\x45]/;
118+
var foo = /[!-$]/;
119+
```
120+
121+
</eslint-code-block>
122+
123+
## :mag: Implementation
124+
125+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-obscure-range.ts)
126+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-obscure-range.ts)

docs/rules/prefer-range.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,13 @@ var foo = /[a-cd-f]/
4343
}
4444
```
4545

46-
- `target` ... Specify the range of characters you want to check with this rule.
47-
- `"alphanumeric"` ... Check only alphanumeric characters (`0-9`,`a-z` and `A-Z`). This is the default.
48-
- `"all"` ... Check all characters. Use `"all"`, if you want to focus on regular expression optimization.
49-
- `[...]` (Array) ... Specify as an array of character ranges. List the character ranges that your team is familiar with in this option, and replace redundant contiguous characters with ranges.
50-
Specify the range as a three-character string in which the from and to characters are connected with a hyphen (`-`) using. e.g. `"!-/"` (U+0021 - U+002F), `"😀-😏"` (U+1F600 - U+1F60F)
51-
You can also use `"alphanumeric"`.
52-
53-
### `"target": "alphanumeric"` (Default)
46+
This option can be used to override the [allowedCharacterRanges] setting.
47+
48+
It allows all values that the [allowedCharacterRanges] setting allows.
49+
50+
[allowedCharacterRanges]: ../settings/README.md#allowedCharacterRanges
51+
52+
### `"target": "alphanumeric"`
5453

5554
<eslint-code-block fix>
5655

docs/settings/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Settings
2+
3+
[Shared settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings) are a way to configure multiple rules at once.
4+
5+
## :book: Usage
6+
7+
All settings for this plugin use the `regexp` namespace.
8+
9+
Example **.eslintrc.js**:
10+
11+
```js
12+
module.exports = {
13+
..., // rules, plugins, etc.
14+
15+
settings: {
16+
// all settings for this plugin have to be in the `regexp` namespace
17+
regexp: {
18+
// define settings here, such as:
19+
// allowedCharacterRanges: 'all'
20+
}
21+
}
22+
}
23+
```
24+
25+
## :gear: Available settings
26+
27+
### `allowedCharacterRanges`
28+
29+
Defines a set of allowed character ranges. Rules will only allow, create, and fix character ranges defined here.
30+
31+
#### Values
32+
33+
The following values are allowed:
34+
35+
- `"alphanumeric"`
36+
37+
This will allow only alphanumeric ranges (`0-9`, `A-Z`, and `a-z`). Only ASCII character are included.
38+
39+
- `"all"`
40+
41+
This will allow only all ranges (roughly equivalent to `"\x00-\uFFFF"`).
42+
43+
- `"<min>-<max>"`
44+
45+
A custom range that allows all character from `<min>` to `<max>`. Both `<min>` and `<max>` have to be single Unicode code points.
46+
47+
E.g. `"A-Z"` (U+0041 - U+005A), `"а-я"` (U+0430 - U+044F), `"😀-😏"` (U+1F600 - U+1F60F).
48+
49+
- A non-empty array of the string values mentioned above. All ranges of the array items will be allowed.
50+
51+
#### Default
52+
53+
If the setting isn't defined, its value defaults to `"alphanumeric"`.
54+
55+
#### Example
56+
57+
```js
58+
module.exports = {
59+
..., // rules, plugins, etc.
60+
settings: {
61+
regexp: {
62+
// allow alphanumeric and cyrillic ranges
63+
allowedCharacterRanges: ['alphanumeric', 'а-я', 'А-Я']
64+
}
65+
}
66+
}
67+
```
68+
69+
#### Affected rules
70+
71+
- [regexp/no-obscure-range]
72+
- [regexp/prefer-range]
73+
74+
[regexp/no-obscure-range]: ../rules/no-obscure-range.md
75+
[regexp/prefer-range]: ../rules/prefer-range.md

docs/user-guide/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ module.exports = {
3737

3838
This plugin provides one config:
3939

40-
- `plugin:regexp/recommended` ... This is the recommended configuration for this plugin.
40+
- `plugin:regexp/recommended` ... This is the recommended configuration for this plugin.
4141
See [lib/configs/recommended.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/recommended.ts) for details.
4242

4343
See [the rule list](../rules/README.md) to get the `rules` that this plugin provides.
44+
45+
Some rules also support [shared settings](../settings/README.md).

lib/rules/no-obscure-range.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import type { Expression } from "estree"
2+
import {
3+
getAllowedCharRanges,
4+
inRange,
5+
getAllowedCharValueSchema,
6+
} from "../utils/char-ranges"
7+
import type { RegExpVisitor } from "regexpp/visitor"
8+
import {
9+
createRule,
10+
defineRegexpVisitor,
11+
getRegexpLocation,
12+
isControlEscape,
13+
isEscapeSequence,
14+
isHexadecimalEscape,
15+
isOctalEscape,
16+
} from "../utils"
17+
18+
export default createRule("no-obscure-range", {
19+
meta: {
20+
docs: {
21+
description: "disallow obscure character ranges",
22+
// TODO Switch to recommended in the major version.
23+
// recommended: true,
24+
recommended: false,
25+
},
26+
schema: [
27+
{
28+
type: "object",
29+
properties: {
30+
allowed: getAllowedCharValueSchema(),
31+
},
32+
additionalProperties: false,
33+
},
34+
],
35+
messages: {
36+
unexpected:
37+
"Unexpected obscure character range. The characters of '{{range}}' ({{unicode}}) are not obvious.",
38+
},
39+
type: "suggestion", // "problem",
40+
},
41+
create(context) {
42+
const allowedRanges = getAllowedCharRanges(
43+
context.options[0]?.allowed,
44+
context,
45+
)
46+
const sourceCode = context.getSourceCode()
47+
48+
/**
49+
* Create visitor
50+
* @param node
51+
*/
52+
function createVisitor(node: Expression): RegExpVisitor.Handlers {
53+
return {
54+
onCharacterClassRangeEnter(rNode) {
55+
const { min, max } = rNode
56+
57+
if (min.value === max.value) {
58+
// we don't deal with that
59+
return
60+
}
61+
62+
if (isControlEscape(min.raw) && isControlEscape(max.raw)) {
63+
// both min and max are control escapes
64+
return
65+
}
66+
if (isOctalEscape(min.raw) && isOctalEscape(max.raw)) {
67+
// both min and max are either octal
68+
return
69+
}
70+
if (
71+
(isHexadecimalEscape(min.raw) || min.value === 0) &&
72+
isHexadecimalEscape(max.raw)
73+
) {
74+
// both min and max are hexadecimal (with a small exception for \0)
75+
return
76+
}
77+
78+
if (
79+
!isEscapeSequence(min.raw) &&
80+
!isEscapeSequence(max.raw) &&
81+
inRange(allowedRanges, min.value, max.value)
82+
) {
83+
return
84+
}
85+
86+
const uMin = `U+${min.value.toString(16).padStart(4, "0")}`
87+
const uMax = `U+${max.value.toString(16).padStart(4, "0")}`
88+
89+
context.report({
90+
node,
91+
loc: getRegexpLocation(sourceCode, node, rNode),
92+
messageId: "unexpected",
93+
data: {
94+
range: rNode.raw,
95+
unicode: `${uMin} - ${uMax}`,
96+
},
97+
})
98+
},
99+
}
100+
}
101+
102+
return defineRegexpVisitor(context, {
103+
createVisitor,
104+
})
105+
},
106+
})

0 commit comments

Comments
 (0)