Skip to content

Commit d841adc

Browse files
authored
Add regexp/hexadecimal-escape rule (#163)
1 parent 4db794e commit d841adc

File tree

9 files changed

+362
-13
lines changed

9 files changed

+362
-13
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
8484
| Rule ID | Description | |
8585
|:--------|:------------|:---|
8686
| [regexp/confusing-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/confusing-quantifier.html) | disallow confusing quantifiers | |
87+
| [regexp/hexadecimal-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/hexadecimal-escape.html) | enforce consistent usage of hexadecimal escape | :wrench: |
8788
| [regexp/letter-case](https://ota-meshi.github.io/eslint-plugin-regexp/rules/letter-case.html) | enforce into your favorite case | :wrench: |
8889
| [regexp/match-any](https://ota-meshi.github.io/eslint-plugin-regexp/rules/match-any.html) | enforce match any character style | :star::wrench: |
8990
| [regexp/negation](https://ota-meshi.github.io/eslint-plugin-regexp/rules/negation.html) | enforce use of escapes on negation | :wrench: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
1212
| Rule ID | Description | |
1313
|:--------|:------------|:---|
1414
| [regexp/confusing-quantifier](./confusing-quantifier.md) | disallow confusing quantifiers | |
15+
| [regexp/hexadecimal-escape](./hexadecimal-escape.md) | enforce consistent usage of hexadecimal escape | :wrench: |
1516
| [regexp/letter-case](./letter-case.md) | enforce into your favorite case | :wrench: |
1617
| [regexp/match-any](./match-any.md) | enforce match any character style | :star::wrench: |
1718
| [regexp/negation](./negation.md) | enforce use of escapes on negation | :wrench: |

docs/rules/hexadecimal-escape.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/hexadecimal-escape"
5+
description: "enforce consistent usage of hexadecimal escape"
6+
---
7+
# regexp/hexadecimal-escape
8+
9+
> enforce consistent usage of hexadecimal escape
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
13+
14+
## :book: Rule Details
15+
16+
Characters that can use hexadecimal escape can use both hexadecimal escape and unicode escape.
17+
18+
This rule aims is enforces the consistent use of hexadecimal escapes.
19+
20+
<eslint-code-block fix>
21+
22+
```js
23+
/* eslint regexp/hexadecimal-escape: "error" */
24+
25+
/* ✓ GOOD */
26+
var foo = /\x0a/;
27+
28+
/* ✗ BAD */
29+
var foo = /\u000a/;
30+
var foo = /\u{a}/u;
31+
```
32+
33+
</eslint-code-block>
34+
35+
## :wrench: Options
36+
37+
```json5
38+
{
39+
"regexp/hexadecimal-escape": [
40+
"error",
41+
"always", // or "never"
42+
]
43+
}
44+
```
45+
46+
- `"always"` ... Unicode escape characters that can use hexadecimal escape must always use hexadecimal escape. This is default.
47+
- `"never"` ... Disallows the use of hexadecimal escapes on all characters.
48+
49+
### `"never"`
50+
51+
<eslint-code-block fix>
52+
53+
```js
54+
/* eslint regexp/hexadecimal-escape: ["error", "never"] */
55+
56+
/* ✓ GOOD */
57+
var foo = /\u000a/;
58+
var foo = /\u{a}/u;
59+
60+
/* ✗ BAD */
61+
var foo = /\x0a/;
62+
```
63+
64+
</eslint-code-block>
65+
66+
## :mag: Implementation
67+
68+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/hexadecimal-escape.ts)
69+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/hexadecimal-escape.ts)

lib/rules/hexadecimal-escape.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import type { RegExpVisitor } from "regexpp/visitor"
2+
import type { Character } from "regexpp/ast"
3+
import type { RegExpContext } from "../utils"
4+
import {
5+
defineRegexpVisitor,
6+
createRule,
7+
getEscapeSequenceKind,
8+
EscapeSequenceKind,
9+
} from "../utils"
10+
11+
export default createRule("hexadecimal-escape", {
12+
meta: {
13+
docs: {
14+
description: "enforce consistent usage of hexadecimal escape",
15+
recommended: false,
16+
},
17+
fixable: "code",
18+
schema: [
19+
{
20+
enum: ["always", "never"], // default always
21+
},
22+
],
23+
messages: {
24+
expectedHexEscape:
25+
"Expected hexadecimal escape ('{{hexEscape}}'), but {{unexpectedKind}} escape ('{{rejectEscape}}') is used.",
26+
unexpectedHexEscape:
27+
"Unexpected hexadecimal escape ('{{hexEscape}}').",
28+
},
29+
type: "suggestion", // "problem",
30+
},
31+
create(context) {
32+
const always = context.options[0] !== "never"
33+
34+
/**
35+
* Verify for always
36+
*/
37+
function verifyForAlways(
38+
{ node, getRegexpLocation, fixReplaceNode }: RegExpContext,
39+
kind: EscapeSequenceKind,
40+
cNode: Character,
41+
) {
42+
if (
43+
kind !== EscapeSequenceKind.unicode &&
44+
kind !== EscapeSequenceKind.unicodeCodePoint
45+
) {
46+
return
47+
}
48+
49+
const hexEscape = `\\x${cNode.value.toString(16).padStart(2, "0")}`
50+
51+
context.report({
52+
node,
53+
loc: getRegexpLocation(cNode),
54+
messageId: "expectedHexEscape",
55+
data: {
56+
hexEscape,
57+
unexpectedKind: kind,
58+
rejectEscape: cNode.raw,
59+
},
60+
fix: fixReplaceNode(cNode, hexEscape),
61+
})
62+
}
63+
64+
/**
65+
* Verify for never
66+
*/
67+
function verifyForNever(
68+
{ node, getRegexpLocation, fixReplaceNode }: RegExpContext,
69+
kind: EscapeSequenceKind,
70+
cNode: Character,
71+
) {
72+
if (kind !== EscapeSequenceKind.hexadecimal) {
73+
return
74+
}
75+
context.report({
76+
node,
77+
loc: getRegexpLocation(cNode),
78+
messageId: "unexpectedHexEscape",
79+
data: {
80+
hexEscape: cNode.raw,
81+
},
82+
fix: fixReplaceNode(cNode, () => `\\u00${cNode.raw.slice(2)}`),
83+
})
84+
}
85+
86+
const verify = always ? verifyForAlways : verifyForNever
87+
88+
/**
89+
* Create visitor
90+
*/
91+
function createVisitor(
92+
regexpContext: RegExpContext,
93+
): RegExpVisitor.Handlers {
94+
return {
95+
onCharacterEnter(cNode) {
96+
if (cNode.value > 0xff) {
97+
return
98+
}
99+
const kind = getEscapeSequenceKind(cNode.raw)
100+
if (!kind) {
101+
return
102+
}
103+
104+
verify(regexpContext, kind, cNode)
105+
},
106+
}
107+
}
108+
109+
return defineRegexpVisitor(context, {
110+
createVisitor,
111+
})
112+
},
113+
})

lib/rules/letter-case.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
isLetter,
88
isLowercaseLetter,
99
isUppercaseLetter,
10+
EscapeSequenceKind,
11+
getEscapeSequenceKind,
1012
} from "../utils"
1113

1214
const CASE_SCHEMA = ["lowercase", "uppercase", "ignore"] as const
@@ -235,13 +237,17 @@ export default createRule("letter-case", {
235237
if (flags.ignoreCase) {
236238
verifyCharacterInCaseInsensitive(regexpContext, cNode)
237239
}
238-
if (cNode.raw.startsWith("\\u")) {
240+
const escapeKind = getEscapeSequenceKind(cNode.raw)
241+
if (
242+
escapeKind === EscapeSequenceKind.unicode ||
243+
escapeKind === EscapeSequenceKind.unicodeCodePoint
244+
) {
239245
verifyCharacterInUnicodeEscape(regexpContext, cNode)
240246
}
241-
if (/^\\x.+$/u.test(cNode.raw)) {
247+
if (escapeKind === EscapeSequenceKind.hexadecimal) {
242248
verifyCharacterInHexadecimalEscape(regexpContext, cNode)
243249
}
244-
if (/^\\c[A-Za-z]$/u.test(cNode.raw)) {
250+
if (escapeKind === EscapeSequenceKind.control) {
245251
verifyCharacterInControl(regexpContext, cNode)
246252
}
247253
},

lib/rules/no-obscure-range.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
defineRegexpVisitor,
1111
isControlEscape,
1212
isEscapeSequence,
13-
isHexadecimalEscape,
13+
isUseHexEscape,
1414
isOctalEscape,
1515
} from "../utils"
1616

@@ -68,8 +68,8 @@ export default createRule("no-obscure-range", {
6868
return
6969
}
7070
if (
71-
(isHexadecimalEscape(min.raw) || min.value === 0) &&
72-
isHexadecimalEscape(max.raw)
71+
(isUseHexEscape(min.raw) || min.value === 0) &&
72+
isUseHexEscape(max.raw)
7373
) {
7474
// both min and max are hexadecimal (with a small exception for \0)
7575
return

lib/utils/index.ts

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -901,17 +901,72 @@ export function isControlEscape(raw: string): boolean {
901901
* sequence.
902902
*/
903903
export function isHexadecimalEscape(raw: string): boolean {
904-
return /^\\(?:x[\dA-Fa-f]{2}|u(?:[\dA-Fa-f]{4}|\{[\dA-Fa-f]{1,8}\}))$/.test(
905-
raw,
904+
return /^\\x[\dA-Fa-f]{2}$/.test(raw)
905+
}
906+
/**
907+
* Returns whether the given raw of a character literal is a unicode escape
908+
* sequence.
909+
*/
910+
export function isUnicodeEscape(raw: string): boolean {
911+
return /^\\u[\dA-Fa-f]{4}$/.test(raw)
912+
}
913+
/**
914+
* Returns whether the given raw of a character literal is a unicode code point
915+
* escape sequence.
916+
*/
917+
export function isUnicodeCodePointEscape(raw: string): boolean {
918+
return /^\\u\{[\dA-Fa-f]{1,8}\}$/.test(raw)
919+
}
920+
921+
export enum EscapeSequenceKind {
922+
octal = "octal",
923+
control = "control",
924+
hexadecimal = "hexadecimal",
925+
unicode = "unicode",
926+
unicodeCodePoint = "unicode code point",
927+
}
928+
/**
929+
* Returns which escape sequence kind was used for the given raw of a character literal.
930+
*/
931+
export function getEscapeSequenceKind(raw: string): EscapeSequenceKind | null {
932+
if (!raw.startsWith("\\")) {
933+
return null
934+
}
935+
if (isOctalEscape(raw)) {
936+
return EscapeSequenceKind.octal
937+
}
938+
if (isControlEscape(raw)) {
939+
return EscapeSequenceKind.control
940+
}
941+
if (isHexadecimalEscape(raw)) {
942+
return EscapeSequenceKind.hexadecimal
943+
}
944+
if (isUnicodeEscape(raw)) {
945+
return EscapeSequenceKind.unicode
946+
}
947+
if (isUnicodeCodePointEscape(raw)) {
948+
return EscapeSequenceKind.unicodeCodePoint
949+
}
950+
return null
951+
}
952+
/**
953+
* Returns whether the given raw of a character literal is a hexadecimal escape
954+
* sequence, a unicode escape sequence, or a unicode code point escape sequence.
955+
*/
956+
export function isUseHexEscape(raw: string): boolean {
957+
const kind = getEscapeSequenceKind(raw)
958+
return (
959+
kind === EscapeSequenceKind.hexadecimal ||
960+
kind === EscapeSequenceKind.unicode ||
961+
kind === EscapeSequenceKind.unicodeCodePoint
906962
)
907963
}
964+
908965
/**
909966
* Returns whether the given raw of a character literal is an octal escape
910-
* sequence, a control escape sequence, or a hexadecimal escape sequence.
967+
* sequence, a control escape sequence, a hexadecimal escape sequence, a unicode
968+
* escape sequence, or a unicode code point escape sequence.
911969
*/
912970
export function isEscapeSequence(raw: string): boolean {
913-
return (
914-
raw.startsWith("\\") &&
915-
(isOctalEscape(raw) || isControlEscape(raw) || isHexadecimalEscape(raw))
916-
)
971+
return Boolean(getEscapeSequenceKind(raw))
917972
}

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { RuleModule } from "../types"
22
import confusingQuantifier from "../rules/confusing-quantifier"
3+
import hexadecimalEscape from "../rules/hexadecimal-escape"
34
import letterCase from "../rules/letter-case"
45
import matchAny from "../rules/match-any"
56
import negation from "../rules/negation"
@@ -51,6 +52,7 @@ import sortFlags from "../rules/sort-flags"
5152

5253
export const rules = [
5354
confusingQuantifier,
55+
hexadecimalEscape,
5456
letterCase,
5557
matchAny,
5658
negation,

0 commit comments

Comments
 (0)