diff --git a/README.md b/README.md
index c339112f4..5e3f89efb 100644
--- a/README.md
+++ b/README.md
@@ -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 | |
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index 4a58ddafc..b69405584 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -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/" },
],
@@ -93,7 +94,7 @@ module.exports = {
]
: []),
],
- "/": ["/", "/user-guide/", "/rules/", "/playground/"],
+ "/": ["/", "/user-guide/", "/rules/", "/settings/", "/playground/"],
},
},
}
diff --git a/docs/README.md b/docs/README.md
index c5b27b630..da122d3d6 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -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).
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 3b8a93179..7262c6496 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -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 | |
diff --git a/docs/rules/no-obscure-range.md b/docs/rules/no-obscure-range.md
new file mode 100644
index 000000000..3c0a3647c
--- /dev/null
+++ b/docs/rules/no-obscure-range.md
@@ -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: ***This rule has not been released yet.***
+
+## :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.
+
+
+
+```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;
+```
+
+
+
+## :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"`
+
+
+
+```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;
+```
+
+
+
+### `"allowed": "all"`
+
+
+
+```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]/;
+```
+
+
+
+### `"allowed": [ "alphanumeric", "😀-😏" ]`
+
+
+
+```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 = /[!-$]/;
+```
+
+
+
+## :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)
diff --git a/docs/rules/prefer-range.md b/docs/rules/prefer-range.md
index f1344097a..faeb202b3 100644
--- a/docs/rules/prefer-range.md
+++ b/docs/rules/prefer-range.md
@@ -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"`
diff --git a/docs/settings/README.md b/docs/settings/README.md
new file mode 100644
index 000000000..285b9a545
--- /dev/null
+++ b/docs/settings/README.md
@@ -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"`).
+
+- `"-"`
+
+ A custom range that allows all character from `` to ``. Both `` and `` 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
diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md
index 4eb6438b9..cdc0b675b 100644
--- a/docs/user-guide/README.md
+++ b/docs/user-guide/README.md
@@ -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).
diff --git a/lib/rules/no-obscure-range.ts b/lib/rules/no-obscure-range.ts
new file mode 100644
index 000000000..9905c25d6
--- /dev/null
+++ b/lib/rules/no-obscure-range.ts
@@ -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,
+ })
+ },
+})
diff --git a/lib/rules/no-octal.ts b/lib/rules/no-octal.ts
index 6568c65ca..726becb46 100644
--- a/lib/rules/no-octal.ts
+++ b/lib/rules/no-octal.ts
@@ -5,6 +5,7 @@ import {
defineRegexpVisitor,
fixReplaceNode,
getRegexpLocation,
+ isOctalEscape,
} from "../utils"
export default createRule("no-octal", {
@@ -35,7 +36,7 @@ export default createRule("no-octal", {
// \0 looks like a octal escape but is allowed
return
}
- if (!/^\\[0-7]+$/.test(cNode.raw)) {
+ if (!isOctalEscape(cNode.raw)) {
// not an octal escape
return
}
diff --git a/lib/rules/prefer-range.ts b/lib/rules/prefer-range.ts
index bd5e0cc94..812d9321e 100644
--- a/lib/rules/prefer-range.ts
+++ b/lib/rules/prefer-range.ts
@@ -1,48 +1,12 @@
import type { Expression } from "estree"
import type { RegExpVisitor } from "regexpp/visitor"
import type { Character, CharacterClassRange } from "regexpp/ast"
+import { createRule, defineRegexpVisitor, getRegexpRange } from "../utils"
import {
- createRule,
- defineRegexpVisitor,
- getRegexpRange,
- isDigit,
- isLetter,
-} from "../utils"
-
-const reOptionRange = /^([\ud800-\udbff][\udc00-\udfff]|[^\ud800-\udfff])-([\ud800-\udbff][\udc00-\udfff]|[^\ud800-\udfff])$/
-
-/**
- * Parse option
- */
-function parseOption(
- option:
- | undefined
- | {
- target?: "all" | "alphanumeric" | string[]
- },
-): (cp: number) => boolean {
- const target = option?.target ?? ["alphanumeric"]
- if (typeof target === "string") {
- return parseOption({ target: [target] })
- }
- const predicates: ((cp: number) => boolean)[] = []
- for (const t of target) {
- if (t === "all") {
- return () => true
- }
- if (t === "alphanumeric") {
- predicates.push((cp) => isDigit(cp) || isLetter(cp))
- }
- const res = reOptionRange.exec(t)
- if (!res) {
- continue
- }
- const from = res[1].codePointAt(0)!
- const to = res[2].codePointAt(0)!
- predicates.push((cp) => from <= cp && cp <= to)
- }
- return (cp) => predicates.some((p) => p(cp))
-}
+ getAllowedCharRanges,
+ getAllowedCharValueSchema,
+ inRange,
+} from "../utils/char-ranges"
export default createRule("prefer-range", {
meta: {
@@ -57,32 +21,7 @@ export default createRule("prefer-range", {
{
type: "object",
properties: {
- target: {
- anyOf: [
- { enum: ["all", "alphanumeric"] },
- {
- type: "array",
- items: [{ enum: ["all", "alphanumeric"] }],
- minItems: 1,
- additionalItems: false,
- },
- {
- type: "array",
- items: {
- anyOf: [
- { const: "alphanumeric" },
- {
- type: "string",
- pattern: reOptionRange.source,
- },
- ],
- },
- uniqueItems: true,
- minItems: 1,
- additionalItems: false,
- },
- ],
- },
+ target: getAllowedCharValueSchema(),
},
additionalProperties: false,
},
@@ -94,7 +33,10 @@ export default createRule("prefer-range", {
type: "suggestion", // "problem",
},
create(context) {
- const isTarget = parseOption(context.options[0])
+ const allowedRanges = getAllowedCharRanges(
+ context.options[0]?.target,
+ context,
+ )
const sourceCode = context.getSourceCode()
type CharacterGroup = {
@@ -141,21 +83,23 @@ export default createRule("prefer-range", {
for (const element of ccNode.elements) {
let data: { min: Character; max: Character }
if (element.type === "Character") {
- if (!isTarget(element.value)) {
+ if (inRange(allowedRanges, element.value)) {
+ data = { min: element, max: element }
+ } else {
continue
}
- data = { min: element, max: element }
} else if (element.type === "CharacterClassRange") {
if (
- !isTarget(element.min.value) &&
- !isTarget(element.max.value)
+ inRange(
+ allowedRanges,
+ element.min.value,
+ element.max.value,
+ )
) {
+ data = { min: element.min, max: element.max }
+ } else {
continue
}
- data = {
- min: element.min,
- max: element.max,
- }
} else {
continue
}
diff --git a/lib/utils/char-ranges.ts b/lib/utils/char-ranges.ts
new file mode 100644
index 000000000..4b5df8fde
--- /dev/null
+++ b/lib/utils/char-ranges.ts
@@ -0,0 +1,122 @@
+import type { Rule } from "eslint"
+import {
+ CP_CAPITAL_A,
+ CP_CAPITAL_Z,
+ CP_DIGIT_NINE,
+ CP_DIGIT_ZERO,
+ CP_SMALL_A,
+ CP_SMALL_Z,
+} from "./unicode"
+import type { JSONSchema4 } from "json-schema"
+
+export interface CharRange {
+ readonly min: number
+ readonly max: number
+}
+
+const ALL_RANGES: readonly CharRange[] = [{ min: 0, max: 0x10ffff }]
+const ALPHANUMERIC_RANGES: readonly CharRange[] = [
+ // digits 0-9
+ { min: CP_DIGIT_ZERO, max: CP_DIGIT_NINE },
+ // Latin A-Z
+ { min: CP_CAPITAL_A, max: CP_CAPITAL_Z },
+ // Latin a-z
+ { min: CP_SMALL_A, max: CP_SMALL_Z },
+]
+
+/**
+ * Returns all character ranges allowed by the user.
+ */
+export function getAllowedCharRanges(
+ allowedByRuleOption: string | readonly string[] | undefined,
+ context: Rule.RuleContext,
+): readonly CharRange[] {
+ let target: string | readonly string[] | undefined =
+ allowedByRuleOption || context.settings.regexp?.allowedCharacterRanges
+
+ if (!target) {
+ // defaults to "alphanumeric"
+ return ALPHANUMERIC_RANGES
+ }
+
+ if (typeof target === "string") {
+ target = [target]
+ }
+
+ const allowed: CharRange[] = []
+
+ for (const range of target) {
+ if (range === "all") {
+ return ALL_RANGES
+ } else if (range === "alphanumeric") {
+ if (target.length === 1) {
+ return ALPHANUMERIC_RANGES
+ }
+ allowed.push(...ALPHANUMERIC_RANGES)
+ } else {
+ // parse
+ const chars = [...range]
+ if (chars.length !== 3 || chars[1] !== "-") {
+ throw new Error(
+ `Invalid format: The range ${JSON.stringify(
+ range,
+ )} is not of the form \`-\`.`,
+ )
+ }
+ const min = chars[0].codePointAt(0)!
+ const max = chars[2].codePointAt(0)!
+ allowed.push({ min, max })
+ }
+ }
+
+ return allowed
+}
+
+/**
+ * Returns the schema of a value accepted by {@link getAllowedCharRanges}.
+ */
+export function getAllowedCharValueSchema(): JSONSchema4 {
+ return {
+ anyOf: [
+ { enum: ["all", "alphanumeric"] },
+ {
+ type: "array",
+ items: [{ enum: ["all", "alphanumeric"] }],
+ minItems: 1,
+ additionalItems: false,
+ },
+ {
+ type: "array",
+ items: {
+ anyOf: [
+ { const: "alphanumeric" },
+ {
+ type: "string",
+ pattern: /^([\ud800-\udbff][\udc00-\udfff]|[^\ud800-\udfff])-([\ud800-\udbff][\udc00-\udfff]|[^\ud800-\udfff])$/
+ .source,
+ },
+ ],
+ },
+ uniqueItems: true,
+ minItems: 1,
+ additionalItems: false,
+ },
+ ],
+ }
+}
+
+/**
+ * Returns whether the given range is in the given list of ranges.
+ */
+export function inRange(
+ ranges: Iterable,
+ min: number,
+ max: number = min,
+): boolean {
+ for (const range of ranges) {
+ if (range.min <= min && max <= range.max) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/lib/utils/index.ts b/lib/utils/index.ts
index 7c0e685a8..be9a4082a 100644
--- a/lib/utils/index.ts
+++ b/lib/utils/index.ts
@@ -668,3 +668,37 @@ export function canUnwrapped(
return true
}
+
+/**
+ * Returns whether the given raw of a character literal is an octal escape
+ * sequence.
+ */
+export function isOctalEscape(raw: string): boolean {
+ return /^\\[0-7]{1,3}$/.test(raw)
+}
+/**
+ * Returns whether the given raw of a character literal is a control escape
+ * sequence.
+ */
+export function isControlEscape(raw: string): boolean {
+ return /^\\c[A-Za-z]$/.test(raw)
+}
+/**
+ * Returns whether the given raw of a character literal is a hexadecimal escape
+ * sequence.
+ */
+export function isHexadecimalEscape(raw: string): boolean {
+ return /^\\(?:x[\dA-Fa-f]{2}|u(?:[\dA-Fa-f]{4}|\{[\dA-Fa-f]{1,8}\}))$/.test(
+ raw,
+ )
+}
+/**
+ * Returns whether the given raw of a character literal is an octal escape
+ * sequence, a control escape sequence, or a hexadecimal escape sequence.
+ */
+export function isEscapeSequence(raw: string): boolean {
+ return (
+ raw.startsWith("\\") &&
+ (isOctalEscape(raw) || isControlEscape(raw) || isHexadecimalEscape(raw))
+ )
+}
diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts
index 720759c79..e503957a7 100644
--- a/lib/utils/rules.ts
+++ b/lib/utils/rules.ts
@@ -13,6 +13,7 @@ import noEscapeBackspace from "../rules/no-escape-backspace"
import noInvisibleCharacter from "../rules/no-invisible-character"
import noLazyEnds from "../rules/no-lazy-ends"
import noLegacyFeatures from "../rules/no-legacy-features"
+import noObscureRange from "../rules/no-obscure-range"
import noOctal from "../rules/no-octal"
import noTriviallyNestedAssertion from "../rules/no-trivially-nested-assertion"
import noUnusedCapturingGroup from "../rules/no-unused-capturing-group"
@@ -56,6 +57,7 @@ export const rules = [
noInvisibleCharacter,
noLazyEnds,
noLegacyFeatures,
+ noObscureRange,
noOctal,
noTriviallyNestedAssertion,
noUnusedCapturingGroup,
diff --git a/tests/lib/rules/no-obscure-range.ts b/tests/lib/rules/no-obscure-range.ts
new file mode 100644
index 000000000..41a52454b
--- /dev/null
+++ b/tests/lib/rules/no-obscure-range.ts
@@ -0,0 +1,129 @@
+import { RuleTester } from "eslint"
+import rule from "../../../lib/rules/no-obscure-range"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run("no-obscure-range", rule as any, {
+ valid: [
+ "/[\\d\\0-\\x1f\\cA-\\cZ\\2-\\5\\012-\\123\\x10-\\uffff a-z a-f]/",
+ {
+ code: "/[а-я А-Я]/",
+ options: [{ allowed: ["alphanumeric", "а-я", "А-Я"] }],
+ },
+ {
+ code: "/[а-я А-Я]/",
+ settings: {
+ regexp: {
+ allowedCharacterRanges: ["alphanumeric", "а-я", "А-Я"],
+ },
+ },
+ },
+ ],
+ invalid: [
+ {
+ // rule options override settings
+ code: "/[а-я А-Я]/",
+ options: [{ allowed: "alphanumeric" }],
+ errors: [
+ "Unexpected obscure character range. The characters of 'а-я' (U+0430 - U+044f) are not obvious.",
+ "Unexpected obscure character range. The characters of 'А-Я' (U+0410 - U+042f) are not obvious.",
+ ],
+ settings: {
+ regexp: {
+ allowedCharacterRanges: ["alphanumeric", "а-я", "А-Я"],
+ },
+ },
+ },
+
+ {
+ code: "/[\\1-\\x13]/",
+ errors: [
+ {
+ message:
+ "Unexpected obscure character range. The characters of '\\1-\\x13' (U+0001 - U+0013) are not obvious.",
+ },
+ ],
+ },
+ {
+ code: "/[\\x20-\\113]/",
+ errors: [
+ {
+ message:
+ "Unexpected obscure character range. The characters of '\\x20-\\113' (U+0020 - U+004b) are not obvious.",
+ },
+ ],
+ },
+
+ {
+ code: "/[\\n-\\r]/",
+ errors: [
+ {
+ message:
+ "Unexpected obscure character range. The characters of '\\n-\\r' (U+000a - U+000d) are not obvious.",
+ },
+ ],
+ },
+
+ {
+ code: "/[\\cA-Z]/",
+ errors: [
+ {
+ message:
+ "Unexpected obscure character range. The characters of '\\cA-Z' (U+0001 - U+005a) are not obvious.",
+ },
+ ],
+ },
+
+ {
+ code: "/[A-z]/",
+ errors: [
+ {
+ message:
+ "Unexpected obscure character range. The characters of 'A-z' (U+0041 - U+007a) are not obvious.",
+ },
+ ],
+ },
+ {
+ code: "/[0-A]/",
+ errors: [
+ {
+ message:
+ "Unexpected obscure character range. The characters of '0-A' (U+0030 - U+0041) are not obvious.",
+ },
+ ],
+ },
+ {
+ code: "/[Z-a]/",
+ errors: [
+ {
+ message:
+ "Unexpected obscure character range. The characters of 'Z-a' (U+005a - U+0061) are not obvious.",
+ },
+ ],
+ },
+ {
+ code: "/[A-\\x43]/",
+ errors: [
+ {
+ message:
+ "Unexpected obscure character range. The characters of 'A-\\x43' (U+0041 - U+0043) are not obvious.",
+ },
+ ],
+ },
+
+ {
+ code: "/[*+-/]/",
+ errors: [
+ {
+ message:
+ "Unexpected obscure character range. The characters of '+-/' (U+002b - U+002f) are not obvious.",
+ },
+ ],
+ },
+ ],
+})
diff --git a/tests/lib/rules/prefer-range.ts b/tests/lib/rules/prefer-range.ts
index 8c2af9045..4b1db8e81 100644
--- a/tests/lib/rules/prefer-range.ts
+++ b/tests/lib/rules/prefer-range.ts
@@ -16,6 +16,7 @@ tester.run("prefer-range", rule as any, {
`/[a-b]/`,
`/[0-9]/`,
`/[A-Z]/`,
+ `/[a-zA-ZZ-a]/`,
`/[ !"#$]/`,
{
code: `/[ !"#$]/`,
@@ -38,6 +39,14 @@ tester.run("prefer-range", rule as any, {
code: `/[ -$]/`,
options: [{ target: ["all"] }],
},
+ {
+ code: `/[ -$]/`,
+ settings: { regexp: { allowedCharacterRanges: "all" } },
+ },
+ {
+ code: `/[ -$]/`,
+ settings: { regexp: { allowedCharacterRanges: ["all"] } },
+ },
{
code: `/[0123456789 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ]/`,
options: [{ target: ["😀-😏"] }],
@@ -233,5 +242,16 @@ tester.run("prefer-range", rule as any, {
"Unexpected multiple adjacent characters. Use '😆-😊' instead.",
],
},
+ {
+ code: `/[😀😁😂😃😄 😆😇😈😉😊]/u`,
+ output: `/[😀-😄 😆-😊]/u`,
+ errors: [
+ "Unexpected multiple adjacent characters. Use '😀-😄' instead.",
+ "Unexpected multiple adjacent characters. Use '😆-😊' instead.",
+ ],
+ settings: {
+ regexp: { allowedCharacterRanges: ["alphanumeric", "😀-😏"] },
+ },
+ },
],
})