From 7c0a9ab38832ff9b3c99c341156590c19909b162 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Wed, 14 Apr 2021 23:30:35 +0200 Subject: [PATCH 01/11] Added `regexp/no-obscure-range` rule --- README.md | 1 + docs/rules/README.md | 1 + docs/rules/no-obscure-range.md | 48 ++++++++++++ lib/configs/recommended.ts | 1 + lib/rules/no-obscure-range.ts | 112 ++++++++++++++++++++++++++++ lib/rules/no-octal.ts | 9 ++- lib/utils/index.ts | 24 ++++++ lib/utils/rules.ts | 2 + tests/lib/rules/no-obscure-range.ts | 93 +++++++++++++++++++++++ 9 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 docs/rules/no-obscure-range.md create mode 100644 lib/rules/no-obscure-range.ts create mode 100644 tests/lib/rules/no-obscure-range.ts diff --git a/README.md b/README.md index dec121c5d..8dd501dd8 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 | :star: | | [regexp/no-octal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-octal.html) | disallow octal escape sequence | :star: | | [regexp/no-unused-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-unused-capturing-group.html) | disallow unused capturing group | | | [regexp/no-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-backreference.html) | disallow useless backreferences in regular expressions | | diff --git a/docs/rules/README.md b/docs/rules/README.md index a8037c69e..952e424d4 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 | :star: | | [regexp/no-octal](./no-octal.md) | disallow octal escape sequence | :star: | | [regexp/no-unused-capturing-group](./no-unused-capturing-group.md) | disallow unused capturing group | | | [regexp/no-useless-backreference](./no-useless-backreference.md) | disallow useless backreferences in regular expressions | | diff --git a/docs/rules/no-obscure-range.md b/docs/rules/no-obscure-range.md new file mode 100644 index 000000000..06ca364e2 --- /dev/null +++ b/docs/rules/no-obscure-range.md @@ -0,0 +1,48 @@ +--- +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.*** +- :gear: This rule is included in `"plugin:regexp/recommended"`. + +## :book: Rule Details + +This rule reports ???. + + + +```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 = /[*/+-^&|]/; +``` + + + +## :wrench: Options + +Nothing. + +## :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/lib/configs/recommended.ts b/lib/configs/recommended.ts index d0dd64442..f9fdfcca8 100644 --- a/lib/configs/recommended.ts +++ b/lib/configs/recommended.ts @@ -22,6 +22,7 @@ export = { "regexp/no-empty-lookarounds-assertion": "error", "regexp/no-escape-backspace": "error", "regexp/no-invisible-character": "error", + "regexp/no-obscure-range": "error", "regexp/no-octal": "error", "regexp/no-useless-exactly-quantifier": "error", "regexp/no-useless-two-nums-quantifier": "error", diff --git a/lib/rules/no-obscure-range.ts b/lib/rules/no-obscure-range.ts new file mode 100644 index 000000000..ba83c856c --- /dev/null +++ b/lib/rules/no-obscure-range.ts @@ -0,0 +1,112 @@ +import type { Expression } from "estree" +import type { RegExpVisitor } from "regexpp/visitor" +import { + CP_CAPITAL_A, + CP_CAPITAL_Z, + CP_DIGIT_NINE, + CP_DIGIT_ZERO, + CP_SMALL_A, + CP_SMALL_Z, + createRule, + defineRegexpVisitor, + getRegexpLocation, + isControlEscape, + isHexadecimalEscape, + isOctalEscape, +} from "../utils" + +const allowedRanges: readonly { min: number; max: number }[] = [ + // 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 }, + // Cyrillic + { min: "А".charCodeAt(0), max: "Я".charCodeAt(0) }, + { min: "а".charCodeAt(0), max: "я".charCodeAt(0) }, +] + +/** + * Returns whether the given range is an allowed one. + */ +function isAllowedRange(min: number, max: number): boolean { + for (const range of allowedRanges) { + if (range.min <= min && max <= range.max) { + return true + } + } + return false +} + +export default createRule("no-obscure-range", { + meta: { + docs: { + description: "disallow obscure character ranges", + recommended: true, + }, + schema: [], + messages: { + unexpected: + "Unexpected obscure character range. The characters of '{{range}}' ({{unicode}}) are not obvious.", + }, + type: "suggestion", // "problem", + }, + create(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 (isAllowedRange(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 baf27a289..3f695e2da 100644 --- a/lib/rules/no-octal.ts +++ b/lib/rules/no-octal.ts @@ -1,6 +1,11 @@ import type { Expression } from "estree" import type { RegExpVisitor } from "regexpp/visitor" -import { createRule, defineRegexpVisitor, getRegexpLocation } from "../utils" +import { + createRule, + defineRegexpVisitor, + getRegexpLocation, + isOctalEscape, +} from "../utils" export default createRule("no-octal", { meta: { @@ -28,7 +33,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/utils/index.ts b/lib/utils/index.ts index 7c0e685a8..af4ea41e9 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -668,3 +668,27 @@ 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, + ) +} diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts index a0dd6937d..297b9c449 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 noUnusedCapturingGroup from "../rules/no-unused-capturing-group" import noUselessBackreference from "../rules/no-useless-backreference" @@ -55,6 +56,7 @@ export const rules = [ noInvisibleCharacter, noLazyEnds, noLegacyFeatures, + noObscureRange, noOctal, noUnusedCapturingGroup, noUselessBackreference, diff --git a/tests/lib/rules/no-obscure-range.ts b/tests/lib/rules/no-obscure-range.ts new file mode 100644 index 000000000..8c9c78eed --- /dev/null +++ b/tests/lib/rules/no-obscure-range.ts @@ -0,0 +1,93 @@ +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 а-я А-Я]/", + ], + invalid: [ + { + 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: "/[*+-/]/", + errors: [ + { + message: + "Unexpected obscure character range. The characters of '+-/' (U+002b - U+002f) are not obvious.", + }, + ], + }, + ], +}) From 391ebfcab01c9152b6602e24c9a766ff9cc793b7 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Wed, 14 Apr 2021 23:32:59 +0200 Subject: [PATCH 02/11] Added docs --- docs/rules/no-obscure-range.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-obscure-range.md b/docs/rules/no-obscure-range.md index 06ca364e2..76aeacd88 100644 --- a/docs/rules/no-obscure-range.md +++ b/docs/rules/no-obscure-range.md @@ -13,7 +13,7 @@ description: "disallow obscure character ranges" ## :book: Rule Details -This rule reports ???. +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. From bdb541a4a425d772e8c7666bc1e1f36068c8d9fa Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Wed, 14 Apr 2021 23:30:35 +0200 Subject: [PATCH 03/11] Added `regexp/no-obscure-range` rule --- README.md | 1 + docs/rules/README.md | 1 + docs/rules/no-obscure-range.md | 48 ++++++++++++ lib/configs/recommended.ts | 1 + lib/rules/no-obscure-range.ts | 112 ++++++++++++++++++++++++++++ lib/rules/no-octal.ts | 9 ++- lib/utils/index.ts | 24 ++++++ lib/utils/rules.ts | 2 + tests/lib/rules/no-obscure-range.ts | 93 +++++++++++++++++++++++ 9 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 docs/rules/no-obscure-range.md create mode 100644 lib/rules/no-obscure-range.ts create mode 100644 tests/lib/rules/no-obscure-range.ts diff --git a/README.md b/README.md index dec121c5d..8dd501dd8 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 | :star: | | [regexp/no-octal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-octal.html) | disallow octal escape sequence | :star: | | [regexp/no-unused-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-unused-capturing-group.html) | disallow unused capturing group | | | [regexp/no-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-backreference.html) | disallow useless backreferences in regular expressions | | diff --git a/docs/rules/README.md b/docs/rules/README.md index a8037c69e..952e424d4 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 | :star: | | [regexp/no-octal](./no-octal.md) | disallow octal escape sequence | :star: | | [regexp/no-unused-capturing-group](./no-unused-capturing-group.md) | disallow unused capturing group | | | [regexp/no-useless-backreference](./no-useless-backreference.md) | disallow useless backreferences in regular expressions | | diff --git a/docs/rules/no-obscure-range.md b/docs/rules/no-obscure-range.md new file mode 100644 index 000000000..06ca364e2 --- /dev/null +++ b/docs/rules/no-obscure-range.md @@ -0,0 +1,48 @@ +--- +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.*** +- :gear: This rule is included in `"plugin:regexp/recommended"`. + +## :book: Rule Details + +This rule reports ???. + + + +```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 = /[*/+-^&|]/; +``` + + + +## :wrench: Options + +Nothing. + +## :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/lib/configs/recommended.ts b/lib/configs/recommended.ts index d0dd64442..f9fdfcca8 100644 --- a/lib/configs/recommended.ts +++ b/lib/configs/recommended.ts @@ -22,6 +22,7 @@ export = { "regexp/no-empty-lookarounds-assertion": "error", "regexp/no-escape-backspace": "error", "regexp/no-invisible-character": "error", + "regexp/no-obscure-range": "error", "regexp/no-octal": "error", "regexp/no-useless-exactly-quantifier": "error", "regexp/no-useless-two-nums-quantifier": "error", diff --git a/lib/rules/no-obscure-range.ts b/lib/rules/no-obscure-range.ts new file mode 100644 index 000000000..ba83c856c --- /dev/null +++ b/lib/rules/no-obscure-range.ts @@ -0,0 +1,112 @@ +import type { Expression } from "estree" +import type { RegExpVisitor } from "regexpp/visitor" +import { + CP_CAPITAL_A, + CP_CAPITAL_Z, + CP_DIGIT_NINE, + CP_DIGIT_ZERO, + CP_SMALL_A, + CP_SMALL_Z, + createRule, + defineRegexpVisitor, + getRegexpLocation, + isControlEscape, + isHexadecimalEscape, + isOctalEscape, +} from "../utils" + +const allowedRanges: readonly { min: number; max: number }[] = [ + // 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 }, + // Cyrillic + { min: "А".charCodeAt(0), max: "Я".charCodeAt(0) }, + { min: "а".charCodeAt(0), max: "я".charCodeAt(0) }, +] + +/** + * Returns whether the given range is an allowed one. + */ +function isAllowedRange(min: number, max: number): boolean { + for (const range of allowedRanges) { + if (range.min <= min && max <= range.max) { + return true + } + } + return false +} + +export default createRule("no-obscure-range", { + meta: { + docs: { + description: "disallow obscure character ranges", + recommended: true, + }, + schema: [], + messages: { + unexpected: + "Unexpected obscure character range. The characters of '{{range}}' ({{unicode}}) are not obvious.", + }, + type: "suggestion", // "problem", + }, + create(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 (isAllowedRange(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 baf27a289..3f695e2da 100644 --- a/lib/rules/no-octal.ts +++ b/lib/rules/no-octal.ts @@ -1,6 +1,11 @@ import type { Expression } from "estree" import type { RegExpVisitor } from "regexpp/visitor" -import { createRule, defineRegexpVisitor, getRegexpLocation } from "../utils" +import { + createRule, + defineRegexpVisitor, + getRegexpLocation, + isOctalEscape, +} from "../utils" export default createRule("no-octal", { meta: { @@ -28,7 +33,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/utils/index.ts b/lib/utils/index.ts index 7c0e685a8..af4ea41e9 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -668,3 +668,27 @@ 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, + ) +} diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts index a0dd6937d..297b9c449 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 noUnusedCapturingGroup from "../rules/no-unused-capturing-group" import noUselessBackreference from "../rules/no-useless-backreference" @@ -55,6 +56,7 @@ export const rules = [ noInvisibleCharacter, noLazyEnds, noLegacyFeatures, + noObscureRange, noOctal, noUnusedCapturingGroup, noUselessBackreference, diff --git a/tests/lib/rules/no-obscure-range.ts b/tests/lib/rules/no-obscure-range.ts new file mode 100644 index 000000000..8c9c78eed --- /dev/null +++ b/tests/lib/rules/no-obscure-range.ts @@ -0,0 +1,93 @@ +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 а-я А-Я]/", + ], + invalid: [ + { + 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: "/[*+-/]/", + errors: [ + { + message: + "Unexpected obscure character range. The characters of '+-/' (U+002b - U+002f) are not obvious.", + }, + ], + }, + ], +}) From 3f8c29bcc422a9752da4811287fa6eaaa393c937 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Wed, 14 Apr 2021 23:32:59 +0200 Subject: [PATCH 04/11] Added docs --- docs/rules/no-obscure-range.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-obscure-range.md b/docs/rules/no-obscure-range.md index 06ca364e2..76aeacd88 100644 --- a/docs/rules/no-obscure-range.md +++ b/docs/rules/no-obscure-range.md @@ -13,7 +13,7 @@ description: "disallow obscure character ranges" ## :book: Rule Details -This rule reports ???. +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. From 3cf064d2e0d5e54bcee828ea47cec751eb97eb58 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Thu, 15 Apr 2021 11:57:42 +0200 Subject: [PATCH 05/11] Updated recommended --- lib/rules/no-obscure-range.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/rules/no-obscure-range.ts b/lib/rules/no-obscure-range.ts index ba83c856c..f36a4e0c4 100644 --- a/lib/rules/no-obscure-range.ts +++ b/lib/rules/no-obscure-range.ts @@ -43,7 +43,9 @@ export default createRule("no-obscure-range", { meta: { docs: { description: "disallow obscure character ranges", - recommended: true, + // TODO Switch to recommended in the major version. + // recommended: true, + recommended: false, }, schema: [], messages: { From 85df91f1de25b1f1c4bc367eaed1e18c7b5b1475 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Thu, 15 Apr 2021 11:59:23 +0200 Subject: [PATCH 06/11] Updated docs --- README.md | 2 +- docs/rules/README.md | 2 +- docs/rules/no-obscure-range.md | 1 - lib/configs/recommended.ts | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8dd501dd8..c5d341430 100644 --- a/README.md +++ b/README.md @@ -97,7 +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 | :star: | +| [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-unused-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-unused-capturing-group.html) | disallow unused capturing group | | | [regexp/no-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-backreference.html) | disallow useless backreferences in regular expressions | | diff --git a/docs/rules/README.md b/docs/rules/README.md index 952e424d4..83966f736 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -25,7 +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 | :star: | +| [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-unused-capturing-group](./no-unused-capturing-group.md) | disallow unused capturing group | | | [regexp/no-useless-backreference](./no-useless-backreference.md) | disallow useless backreferences in regular expressions | | diff --git a/docs/rules/no-obscure-range.md b/docs/rules/no-obscure-range.md index 76aeacd88..d6f6dba0a 100644 --- a/docs/rules/no-obscure-range.md +++ b/docs/rules/no-obscure-range.md @@ -9,7 +9,6 @@ description: "disallow obscure character ranges" > disallow obscure character ranges - :exclamation: ***This rule has not been released yet.*** -- :gear: This rule is included in `"plugin:regexp/recommended"`. ## :book: Rule Details diff --git a/lib/configs/recommended.ts b/lib/configs/recommended.ts index f9fdfcca8..d0dd64442 100644 --- a/lib/configs/recommended.ts +++ b/lib/configs/recommended.ts @@ -22,7 +22,6 @@ export = { "regexp/no-empty-lookarounds-assertion": "error", "regexp/no-escape-backspace": "error", "regexp/no-invisible-character": "error", - "regexp/no-obscure-range": "error", "regexp/no-octal": "error", "regexp/no-useless-exactly-quantifier": "error", "regexp/no-useless-two-nums-quantifier": "error", From e825579e06ae731b637bf31c1383d92f5c179921 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Thu, 15 Apr 2021 22:37:38 +0200 Subject: [PATCH 07/11] Support setting --- lib/rules/no-obscure-range.ts | 51 +++++------- lib/rules/prefer-range.ts | 96 +++++----------------- lib/utils/char-ranges.ts | 123 ++++++++++++++++++++++++++++ tests/lib/rules/no-obscure-range.ts | 33 +++++++- tests/lib/rules/prefer-range.ts | 19 +++++ 5 files changed, 213 insertions(+), 109 deletions(-) create mode 100644 lib/utils/char-ranges.ts diff --git a/lib/rules/no-obscure-range.ts b/lib/rules/no-obscure-range.ts index f36a4e0c4..ea5469a3a 100644 --- a/lib/rules/no-obscure-range.ts +++ b/lib/rules/no-obscure-range.ts @@ -1,12 +1,11 @@ import type { Expression } from "estree" +import { + getAllowedCharRanges, + inRange, + getAllowedCharValueSchema, +} from "../utils/char-ranges" import type { RegExpVisitor } from "regexpp/visitor" import { - CP_CAPITAL_A, - CP_CAPITAL_Z, - CP_DIGIT_NINE, - CP_DIGIT_ZERO, - CP_SMALL_A, - CP_SMALL_Z, createRule, defineRegexpVisitor, getRegexpLocation, @@ -15,30 +14,6 @@ import { isOctalEscape, } from "../utils" -const allowedRanges: readonly { min: number; max: number }[] = [ - // 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 }, - // Cyrillic - { min: "А".charCodeAt(0), max: "Я".charCodeAt(0) }, - { min: "а".charCodeAt(0), max: "я".charCodeAt(0) }, -] - -/** - * Returns whether the given range is an allowed one. - */ -function isAllowedRange(min: number, max: number): boolean { - for (const range of allowedRanges) { - if (range.min <= min && max <= range.max) { - return true - } - } - return false -} - export default createRule("no-obscure-range", { meta: { docs: { @@ -47,7 +22,15 @@ export default createRule("no-obscure-range", { // recommended: true, recommended: false, }, - schema: [], + schema: [ + { + type: "object", + properties: { + allowed: getAllowedCharValueSchema(), + }, + additionalProperties: false, + }, + ], messages: { unexpected: "Unexpected obscure character range. The characters of '{{range}}' ({{unicode}}) are not obvious.", @@ -55,6 +38,10 @@ export default createRule("no-obscure-range", { type: "suggestion", // "problem", }, create(context) { + const allowedRanges = getAllowedCharRanges( + context.options[0]?.allowed, + context, + ) const sourceCode = context.getSourceCode() /** @@ -87,7 +74,7 @@ export default createRule("no-obscure-range", { return } - if (isAllowedRange(min.value, max.value)) { + if (inRange(allowedRanges, min.value, max.value)) { 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..40c83b3c8 --- /dev/null +++ b/lib/utils/char-ranges.ts @@ -0,0 +1,123 @@ +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/allowed-character-ranges"] + + 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/tests/lib/rules/no-obscure-range.ts b/tests/lib/rules/no-obscure-range.ts index 8c9c78eed..1da8adb18 100644 --- a/tests/lib/rules/no-obscure-range.ts +++ b/tests/lib/rules/no-obscure-range.ts @@ -10,9 +10,40 @@ const tester = new RuleTester({ tester.run("no-obscure-range", rule as any, { valid: [ - "/[\\d\\0-\\x1f\\cA-\\cZ\\2-\\5\\012-\\123\\x10-\\uffff a-z а-я А-Я]/", + "/[\\d\\0-\\x1f\\cA-\\cZ\\2-\\5\\012-\\123\\x10-\\uffff a-z a-f]/", + { + code: "/[а-я А-Я]/", + options: [{ allowed: ["alphanumeric", "а-я", "А-Я"] }], + }, + { + code: "/[а-я А-Я]/", + settings: { + "regexp/allowed-character-ranges": [ + "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/allowed-character-ranges": [ + "alphanumeric", + "а-я", + "А-Я", + ], + }, + }, + { code: "/[\\1-\\x13]/", errors: [ diff --git a/tests/lib/rules/prefer-range.ts b/tests/lib/rules/prefer-range.ts index 8c2af9045..b64124aec 100644 --- a/tests/lib/rules/prefer-range.ts +++ b/tests/lib/rules/prefer-range.ts @@ -38,6 +38,14 @@ tester.run("prefer-range", rule as any, { code: `/[ -$]/`, options: [{ target: ["all"] }], }, + { + code: `/[ -$]/`, + settings: { "regexp/allowed-character-ranges": "all" }, + }, + { + code: `/[ -$]/`, + settings: { "regexp/allowed-character-ranges": ["all"] }, + }, { code: `/[0123456789 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ]/`, options: [{ target: ["😀-😏"] }], @@ -233,5 +241,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/allowed-character-ranges": ["alphanumeric", "😀-😏"], + }, + }, ], }) From 451141d696e9b440d1aee246f39e7086185ed790 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Thu, 15 Apr 2021 22:53:20 +0200 Subject: [PATCH 08/11] Added test --- tests/lib/rules/prefer-range.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/lib/rules/prefer-range.ts b/tests/lib/rules/prefer-range.ts index b64124aec..79995fe53 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: `/[ !"#$]/`, From 3daa87a4c344561c7f08791606746b525748cd4f Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Fri, 16 Apr 2021 12:33:29 +0200 Subject: [PATCH 09/11] Renamed setting --- lib/utils/char-ranges.ts | 3 +-- tests/lib/rules/no-obscure-range.ts | 16 ++++++---------- tests/lib/rules/prefer-range.ts | 6 +++--- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/utils/char-ranges.ts b/lib/utils/char-ranges.ts index 40c83b3c8..4b5df8fde 100644 --- a/lib/utils/char-ranges.ts +++ b/lib/utils/char-ranges.ts @@ -32,8 +32,7 @@ export function getAllowedCharRanges( context: Rule.RuleContext, ): readonly CharRange[] { let target: string | readonly string[] | undefined = - allowedByRuleOption || - context.settings["regexp/allowed-character-ranges"] + allowedByRuleOption || context.settings.regexp?.allowedCharacterRanges if (!target) { // defaults to "alphanumeric" diff --git a/tests/lib/rules/no-obscure-range.ts b/tests/lib/rules/no-obscure-range.ts index 1da8adb18..55749d9bc 100644 --- a/tests/lib/rules/no-obscure-range.ts +++ b/tests/lib/rules/no-obscure-range.ts @@ -18,11 +18,9 @@ tester.run("no-obscure-range", rule as any, { { code: "/[а-я А-Я]/", settings: { - "regexp/allowed-character-ranges": [ - "alphanumeric", - "а-я", - "А-Я", - ], + regexp: { + allowedCharacterRanges: ["alphanumeric", "а-я", "А-Я"], + }, }, }, ], @@ -36,11 +34,9 @@ tester.run("no-obscure-range", rule as any, { "Unexpected obscure character range. The characters of 'А-Я' (U+0410 - U+042f) are not obvious.", ], settings: { - "regexp/allowed-character-ranges": [ - "alphanumeric", - "а-я", - "А-Я", - ], + regexp: { + allowedCharacterRanges: ["alphanumeric", "а-я", "А-Я"], + }, }, }, diff --git a/tests/lib/rules/prefer-range.ts b/tests/lib/rules/prefer-range.ts index 79995fe53..4b1db8e81 100644 --- a/tests/lib/rules/prefer-range.ts +++ b/tests/lib/rules/prefer-range.ts @@ -41,11 +41,11 @@ tester.run("prefer-range", rule as any, { }, { code: `/[ -$]/`, - settings: { "regexp/allowed-character-ranges": "all" }, + settings: { regexp: { allowedCharacterRanges: "all" } }, }, { code: `/[ -$]/`, - settings: { "regexp/allowed-character-ranges": ["all"] }, + settings: { regexp: { allowedCharacterRanges: ["all"] } }, }, { code: `/[0123456789 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ]/`, @@ -250,7 +250,7 @@ tester.run("prefer-range", rule as any, { "Unexpected multiple adjacent characters. Use '😆-😊' instead.", ], settings: { - "regexp/allowed-character-ranges": ["alphanumeric", "😀-😏"], + regexp: { allowedCharacterRanges: ["alphanumeric", "😀-😏"] }, }, }, ], From 7b9ba663c5464b8f15b066f2652d68c5b0eff53f Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Fri, 16 Apr 2021 15:12:24 +0200 Subject: [PATCH 10/11] Fixed false negative --- lib/rules/no-obscure-range.ts | 7 ++++++- lib/utils/index.ts | 10 ++++++++++ tests/lib/rules/no-obscure-range.ts | 9 +++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/rules/no-obscure-range.ts b/lib/rules/no-obscure-range.ts index ea5469a3a..9905c25d6 100644 --- a/lib/rules/no-obscure-range.ts +++ b/lib/rules/no-obscure-range.ts @@ -10,6 +10,7 @@ import { defineRegexpVisitor, getRegexpLocation, isControlEscape, + isEscapeSequence, isHexadecimalEscape, isOctalEscape, } from "../utils" @@ -74,7 +75,11 @@ export default createRule("no-obscure-range", { return } - if (inRange(allowedRanges, min.value, max.value)) { + if ( + !isEscapeSequence(min.raw) && + !isEscapeSequence(max.raw) && + inRange(allowedRanges, min.value, max.value) + ) { return } diff --git a/lib/utils/index.ts b/lib/utils/index.ts index af4ea41e9..be9a4082a 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -692,3 +692,13 @@ export function isHexadecimalEscape(raw: string): boolean { 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/tests/lib/rules/no-obscure-range.ts b/tests/lib/rules/no-obscure-range.ts index 55749d9bc..41a52454b 100644 --- a/tests/lib/rules/no-obscure-range.ts +++ b/tests/lib/rules/no-obscure-range.ts @@ -106,6 +106,15 @@ tester.run("no-obscure-range", rule as any, { }, ], }, + { + code: "/[A-\\x43]/", + errors: [ + { + message: + "Unexpected obscure character range. The characters of 'A-\\x43' (U+0041 - U+0043) are not obvious.", + }, + ], + }, { code: "/[*+-/]/", From 68027778f13260bab7b4f5c9808e418fe1293cd1 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Fri, 16 Apr 2021 15:14:58 +0200 Subject: [PATCH 11/11] Updated docs --- docs/.vuepress/config.js | 3 +- docs/README.md | 4 ++ docs/rules/no-obscure-range.md | 83 +++++++++++++++++++++++++++++++++- docs/rules/prefer-range.md | 15 +++--- docs/settings/README.md | 75 ++++++++++++++++++++++++++++++ docs/user-guide/README.md | 4 +- 6 files changed, 172 insertions(+), 12 deletions(-) create mode 100644 docs/settings/README.md 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/no-obscure-range.md b/docs/rules/no-obscure-range.md index d6f6dba0a..3c0a3647c 100644 --- a/docs/rules/no-obscure-range.md +++ b/docs/rules/no-obscure-range.md @@ -32,14 +32,93 @@ var foo = /[\cA-\cZ]/; /* ✗ BAD */ var foo = /[A-\x43]/; var foo = /[\41-\x45]/; -var foo = /[*/+-^&|]/; +var foo = /[!-$]/; +var foo = /[😀-😄]/u; ``` ## :wrench: Options -Nothing. + +```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 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).