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", "😀-😏"] }, + }, + }, ], })