Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added regexp/no-obscure-range rule #122

Merged
merged 14 commits into from
Apr 16, 2021
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/no-invisible-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invisible-character.html) | disallow invisible raw character | :star::wrench: |
| [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 | |
| [regexp/no-legacy-features](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-legacy-features.html) | disallow legacy RegExp features | |
| [regexp/no-obscure-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-obscure-range.html) | disallow obscure character ranges | |
| [regexp/no-octal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-octal.html) | disallow octal escape sequence | :star: |
| [regexp/no-trivially-nested-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-trivially-nested-assertion.html) | disallow trivially nested assertions | :wrench: |
| [regexp/no-unused-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-unused-capturing-group.html) | disallow unused capturing group | |
Expand Down
3 changes: 2 additions & 1 deletion docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ module.exports = {
{ text: "Introduction", link: "/" },
{ text: "User Guide", link: "/user-guide/" },
{ text: "Rules", link: "/rules/" },
{ text: "Settings", link: "/settings/" },
{ text: "Playground", link: "/playground/" },
],

Expand Down Expand Up @@ -93,7 +94,7 @@ module.exports = {
]
: []),
],
"/": ["/", "/user-guide/", "/rules/", "/playground/"],
"/": ["/", "/user-guide/", "/rules/", "/settings/", "/playground/"],
},
},
}
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ See [User Guide](./user-guide/README.md).

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

## :gear: Settings

See [Settings](./settings/README.md).

## :lock: License

See the [LICENSE](LICENSE) file for license rights and limitations (MIT).
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/no-invisible-character](./no-invisible-character.md) | disallow invisible raw character | :star::wrench: |
| [regexp/no-lazy-ends](./no-lazy-ends.md) | disallow lazy quantifiers at the end of an expression | |
| [regexp/no-legacy-features](./no-legacy-features.md) | disallow legacy RegExp features | |
| [regexp/no-obscure-range](./no-obscure-range.md) | disallow obscure character ranges | |
| [regexp/no-octal](./no-octal.md) | disallow octal escape sequence | :star: |
| [regexp/no-trivially-nested-assertion](./no-trivially-nested-assertion.md) | disallow trivially nested assertions | :wrench: |
| [regexp/no-unused-capturing-group](./no-unused-capturing-group.md) | disallow unused capturing group | |
Expand Down
126 changes: 126 additions & 0 deletions docs/rules/no-obscure-range.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
pageClass: "rule-details"
sidebarDepth: 0
title: "regexp/no-obscure-range"
description: "disallow obscure character ranges"
---
# regexp/no-obscure-range

> disallow obscure character ranges

- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>

## :book: Rule Details

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.

<eslint-code-block>

```js
/* eslint regexp/no-obscure-range: "error" */

/* ✓ GOOD */
var foo = /[a-z]/;
var foo = /[J-O]/;
var foo = /[1-9]/;
var foo = /[\x00-\x40]/;
var foo = /[\0-\uFFFF]/;
var foo = /[\0-\u{10FFFF}]/u;
var foo = /[\1-\5]/;
var foo = /[\cA-\cZ]/;

/* ✗ BAD */
var foo = /[A-\x43]/;
var foo = /[\41-\x45]/;
var foo = /[!-$]/;
var foo = /[😀-😄]/u;
```

</eslint-code-block>

## :wrench: Options


```json5
{
"regexp/no-obscure-range": ["error",
{
"allowed": "alphanumeric" // or "all" or [...]
}
]
}
```

This option can be used to override the [allowedCharacterRanges] setting.

It allows all values that the [allowedCharacterRanges] setting allows.

[allowedCharacterRanges]: ../settings/README.md#allowedCharacterRanges

### `"allowed": "alphanumeric"`

<eslint-code-block fix>

```js
/* eslint regexp/no-obscure-range: ["error", { "allowed": "alphanumeric" }] */

/* ✓ GOOD */
var foo = /[a-z]/;
var foo = /[J-O]/;
var foo = /[1-9]/;

/* ✗ BAD */
var foo = /[A-\x43]/;
var foo = /[\41-\x45]/;
var foo = /[!-$]/;
var foo = /[😀-😄]/u;
```

</eslint-code-block>

### `"allowed": "all"`

<eslint-code-block fix>

```js
/* eslint regexp/no-obscure-range: ["error", { "allowed": "all" }] */

/* ✓ GOOD */
var foo = /[a-z]/;
var foo = /[J-O]/;
var foo = /[1-9]/;
var foo = /[!-$]/;
var foo = /[😀-😄]/u;

/* ✗ BAD */
var foo = /[A-\x43]/;
var foo = /[\41-\x45]/;
```

</eslint-code-block>

### `"allowed": [ "alphanumeric", "😀-😏" ]`

<eslint-code-block fix>

```js
/* eslint regexp/no-obscure-range: ["error", { "allowed": [ "alphanumeric", "😀-😏" ] }] */

/* ✓ GOOD */
var foo = /[a-z]/;
var foo = /[J-O]/;
var foo = /[1-9]/;
var foo = /[😀-😄]/u;

/* ✗ BAD */
var foo = /[A-\x43]/;
var foo = /[\41-\x45]/;
var foo = /[!-$]/;
```

</eslint-code-block>

## :mag: Implementation

- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-obscure-range.ts)
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-obscure-range.ts)
15 changes: 7 additions & 8 deletions docs/rules/prefer-range.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,13 @@ var foo = /[a-cd-f]/
}
```

- `target` ... Specify the range of characters you want to check with this rule.
- `"alphanumeric"` ... Check only alphanumeric characters (`0-9`,`a-z` and `A-Z`). This is the default.
- `"all"` ... Check all characters. Use `"all"`, if you want to focus on regular expression optimization.
- `[...]` (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.
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)
You can also use `"alphanumeric"`.

### `"target": "alphanumeric"` (Default)
This option can be used to override the [allowedCharacterRanges] setting.

It allows all values that the [allowedCharacterRanges] setting allows.

[allowedCharacterRanges]: ../settings/README.md#allowedCharacterRanges

### `"target": "alphanumeric"`

<eslint-code-block fix>

Expand Down
75 changes: 75 additions & 0 deletions docs/settings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Settings

[Shared settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings) are a way to configure multiple rules at once.

## :book: Usage

All settings for this plugin use the `regexp` namespace.

Example **.eslintrc.js**:

```js
module.exports = {
..., // rules, plugins, etc.

settings: {
// all settings for this plugin have to be in the `regexp` namespace
regexp: {
// define settings here, such as:
// allowedCharacterRanges: 'all'
}
}
}
```

## :gear: Available settings

### `allowedCharacterRanges`

Defines a set of allowed character ranges. Rules will only allow, create, and fix character ranges defined here.

#### Values

The following values are allowed:

- `"alphanumeric"`

This will allow only alphanumeric ranges (`0-9`, `A-Z`, and `a-z`). Only ASCII character are included.

- `"all"`

This will allow only all ranges (roughly equivalent to `"\x00-\uFFFF"`).

- `"<min>-<max>"`

A custom range that allows all character from `<min>` to `<max>`. Both `<min>` and `<max>` have to be single Unicode code points.

E.g. `"A-Z"` (U+0041 - U+005A), `"а-я"` (U+0430 - U+044F), `"😀-😏"` (U+1F600 - U+1F60F).

- A non-empty array of the string values mentioned above. All ranges of the array items will be allowed.

#### Default

If the setting isn't defined, its value defaults to `"alphanumeric"`.

#### Example

```js
module.exports = {
..., // rules, plugins, etc.
settings: {
regexp: {
// allow alphanumeric and cyrillic ranges
allowedCharacterRanges: ['alphanumeric', 'а-я', 'А-Я']
}
}
}
```

#### Affected rules

- [regexp/no-obscure-range]
- [regexp/prefer-range]

[regexp/no-obscure-range]: ../rules/no-obscure-range.md
[regexp/prefer-range]: ../rules/prefer-range.md
4 changes: 3 additions & 1 deletion docs/user-guide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ module.exports = {

This plugin provides one config:

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

See [the rule list](../rules/README.md) to get the `rules` that this plugin provides.

Some rules also support [shared settings](../settings/README.md).
106 changes: 106 additions & 0 deletions lib/rules/no-obscure-range.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { Expression } from "estree"
import {
getAllowedCharRanges,
inRange,
getAllowedCharValueSchema,
} from "../utils/char-ranges"
import type { RegExpVisitor } from "regexpp/visitor"
import {
createRule,
defineRegexpVisitor,
getRegexpLocation,
isControlEscape,
isEscapeSequence,
isHexadecimalEscape,
isOctalEscape,
} from "../utils"

export default createRule("no-obscure-range", {
meta: {
docs: {
description: "disallow obscure character ranges",
// TODO Switch to recommended in the major version.
// recommended: true,
recommended: false,
},
schema: [
{
type: "object",
properties: {
allowed: getAllowedCharValueSchema(),
},
additionalProperties: false,
},
],
messages: {
unexpected:
"Unexpected obscure character range. The characters of '{{range}}' ({{unicode}}) are not obvious.",
},
type: "suggestion", // "problem",
},
create(context) {
const allowedRanges = getAllowedCharRanges(
context.options[0]?.allowed,
context,
)
const sourceCode = context.getSourceCode()

/**
* Create visitor
* @param node
*/
function createVisitor(node: Expression): RegExpVisitor.Handlers {
return {
onCharacterClassRangeEnter(rNode) {
const { min, max } = rNode

if (min.value === max.value) {
// we don't deal with that
return
}

if (isControlEscape(min.raw) && isControlEscape(max.raw)) {
// both min and max are control escapes
return
}
if (isOctalEscape(min.raw) && isOctalEscape(max.raw)) {
// both min and max are either octal
return
}
if (
(isHexadecimalEscape(min.raw) || min.value === 0) &&
isHexadecimalEscape(max.raw)
) {
// both min and max are hexadecimal (with a small exception for \0)
return
}

if (
!isEscapeSequence(min.raw) &&
!isEscapeSequence(max.raw) &&
inRange(allowedRanges, min.value, max.value)
) {
return
}

const uMin = `U+${min.value.toString(16).padStart(4, "0")}`
const uMax = `U+${max.value.toString(16).padStart(4, "0")}`

context.report({
node,
loc: getRegexpLocation(sourceCode, node, rNode),
messageId: "unexpected",
data: {
range: rNode.raw,
unicode: `${uMin} - ${uMax}`,
},
})
},
}
}

return defineRegexpVisitor(context, {
createVisitor,
})
},
})
Loading