Skip to content

Commit 258c975

Browse files
Add control-character-escape rule (#168)
* Add `control-character-escape` rule * Added description
1 parent 30c3a1b commit 258c975

File tree

6 files changed

+255
-0
lines changed

6 files changed

+255
-0
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/control-character-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/control-character-escape.html) | enforce consistent escaping of control characters | :wrench: |
8788
| [regexp/hexadecimal-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/hexadecimal-escape.html) | enforce consistent usage of hexadecimal escape | :wrench: |
8889
| [regexp/letter-case](https://ota-meshi.github.io/eslint-plugin-regexp/rules/letter-case.html) | enforce into your favorite case | :wrench: |
8990
| [regexp/match-any](https://ota-meshi.github.io/eslint-plugin-regexp/rules/match-any.html) | enforce match any character style | :star::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/control-character-escape](./control-character-escape.md) | enforce consistent escaping of control characters | :wrench: |
1516
| [regexp/hexadecimal-escape](./hexadecimal-escape.md) | enforce consistent usage of hexadecimal escape | :wrench: |
1617
| [regexp/letter-case](./letter-case.md) | enforce into your favorite case | :wrench: |
1718
| [regexp/match-any](./match-any.md) | enforce match any character style | :star::wrench: |
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/control-character-escape"
5+
description: "enforce consistent escaping of control characters"
6+
---
7+
# regexp/control-character-escape
8+
9+
> enforce consistent escaping of control characters
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+
This rule reports control characters that were not escaped using a control escape (`\0`, `t`, `\n`, `\v`, `f`, `\r`).
17+
18+
<eslint-code-block fix>
19+
20+
```js
21+
/* eslint regexp/control-character-escape: "error" */
22+
23+
/* ✓ GOOD */
24+
var foo = /[\n\r]/;
25+
var foo = /\t/;
26+
var foo = RegExp("\t+\n");
27+
28+
/* ✗ BAD */
29+
var foo = / /;
30+
var foo = /\u0009/;
31+
var foo = /\u{a}/u;
32+
var foo = RegExp("\\u000a");
33+
```
34+
35+
</eslint-code-block>
36+
37+
## :wrench: Options
38+
39+
Nothing.
40+
41+
## :mag: Implementation
42+
43+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/control-character-escape.ts)
44+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/control-character-escape.ts)

lib/rules/control-character-escape.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type { RegExpVisitor } from "regexpp/visitor"
2+
import type { RegExpContext } from "../utils"
3+
import {
4+
CP_VT,
5+
CP_CR,
6+
CP_FF,
7+
CP_LF,
8+
CP_TAB,
9+
createRule,
10+
defineRegexpVisitor,
11+
isRegexpLiteral,
12+
} from "../utils"
13+
14+
const CONTROL_CHARS = new Map<number, string>([
15+
[0, "\\0"],
16+
[CP_TAB, "\\t"],
17+
[CP_LF, "\\n"],
18+
[CP_VT, "\\v"],
19+
[CP_FF, "\\f"],
20+
[CP_CR, "\\r"],
21+
])
22+
23+
export default createRule("control-character-escape", {
24+
meta: {
25+
docs: {
26+
description: "enforce consistent escaping of control characters",
27+
// TODO Switch to recommended in the major version.
28+
// recommended: true,
29+
recommended: false,
30+
},
31+
fixable: "code",
32+
schema: [],
33+
messages: {
34+
unexpected:
35+
"Unexpected control character escape '{{actual}}' ({{cp}}). Use '{{expected}}' instead.",
36+
},
37+
type: "suggestion", // "problem",
38+
},
39+
create(context) {
40+
/**
41+
* Create visitor
42+
*/
43+
function createVisitor({
44+
node,
45+
getRegexpLocation,
46+
fixReplaceNode,
47+
}: RegExpContext): RegExpVisitor.Handlers {
48+
return {
49+
onCharacterEnter(cNode) {
50+
if (cNode.parent.type === "CharacterClassRange") {
51+
// ignore ranges
52+
return
53+
}
54+
55+
const expectedRaw = CONTROL_CHARS.get(cNode.value)
56+
if (expectedRaw === undefined) {
57+
// not a known control character
58+
return
59+
}
60+
if (cNode.raw === expectedRaw) {
61+
// all good
62+
return
63+
}
64+
if (
65+
!isRegexpLiteral(node) &&
66+
cNode.raw === String.fromCodePoint(cNode.value)
67+
) {
68+
// we allow the direct usage of control characters in
69+
// string if it's not in regexp literal
70+
// e.g. `RegExp("[\t\n]")` is ok
71+
return
72+
}
73+
74+
context.report({
75+
node,
76+
loc: getRegexpLocation(cNode),
77+
messageId: "unexpected",
78+
data: {
79+
actual: cNode.raw,
80+
cp: `U+${cNode.value
81+
.toString(16)
82+
.padStart(4, "0")}`,
83+
expected: expectedRaw,
84+
},
85+
fix: fixReplaceNode(cNode, expectedRaw),
86+
})
87+
},
88+
}
89+
}
90+
91+
return defineRegexpVisitor(context, {
92+
createVisitor,
93+
})
94+
},
95+
})

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 controlCharacterEscape from "../rules/control-character-escape"
34
import hexadecimalEscape from "../rules/hexadecimal-escape"
45
import letterCase from "../rules/letter-case"
56
import matchAny from "../rules/match-any"
@@ -54,6 +55,7 @@ import unicodeEscape from "../rules/unicode-escape"
5455

5556
export const rules = [
5657
confusingQuantifier,
58+
controlCharacterEscape,
5759
hexadecimalEscape,
5860
letterCase,
5961
matchAny,
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../lib/rules/control-character-escape"
3+
4+
const tester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 2020,
7+
sourceType: "module",
8+
},
9+
})
10+
11+
tester.run("control-character-escape", rule as any, {
12+
valid: [
13+
String.raw`/\0\t\n\v\f\r/`,
14+
String.raw`RegExp(/\0\t\n\v\f\r/, "i")`,
15+
String.raw`RegExp("\0\t\n\v\f\r", "i")`,
16+
String.raw`RegExp("\\0\\t\\n\\v\\f\\r", "i")`,
17+
"/\\t/",
18+
"new RegExp('\t')",
19+
],
20+
invalid: [
21+
{
22+
code: String.raw`/\x00/`,
23+
output: String.raw`/\0/`,
24+
errors: [
25+
"Unexpected control character escape '\\x00' (U+0000). Use '\\0' instead.",
26+
],
27+
},
28+
{
29+
code: String.raw`/\x0a/`,
30+
output: String.raw`/\n/`,
31+
errors: [
32+
"Unexpected control character escape '\\x0a' (U+000a). Use '\\n' instead.",
33+
],
34+
},
35+
{
36+
code: String.raw`/\cJ/`,
37+
output: String.raw`/\n/`,
38+
errors: [
39+
"Unexpected control character escape '\\cJ' (U+000a). Use '\\n' instead.",
40+
],
41+
},
42+
{
43+
code: String.raw`/\u{a}/u`,
44+
output: String.raw`/\n/u`,
45+
errors: [
46+
"Unexpected control character escape '\\u{a}' (U+000a). Use '\\n' instead.",
47+
],
48+
},
49+
{
50+
code: String.raw`RegExp("\\cJ")`,
51+
output: String.raw`RegExp("\\n")`,
52+
errors: [
53+
"Unexpected control character escape '\\cJ' (U+000a). Use '\\n' instead.",
54+
],
55+
},
56+
{
57+
code: String.raw`RegExp("\\u{a}", "u")`,
58+
output: String.raw`RegExp("\\n", "u")`,
59+
errors: [
60+
"Unexpected control character escape '\\u{a}' (U+000a). Use '\\n' instead.",
61+
],
62+
},
63+
64+
{
65+
code: "/\\u0009/",
66+
output: "/\\t/",
67+
errors: [
68+
{
69+
message:
70+
"Unexpected control character escape '\\u0009' (U+0009). Use '\\t' instead.",
71+
column: 2,
72+
endColumn: 8,
73+
},
74+
],
75+
},
76+
{
77+
code: "/\t/",
78+
output: "/\\t/",
79+
errors: [
80+
{
81+
message:
82+
"Unexpected control character escape '\t' (U+0009). Use '\\t' instead.",
83+
column: 2,
84+
endColumn: 3,
85+
},
86+
],
87+
},
88+
{
89+
code: String.raw`
90+
const s = "\\u0009"
91+
new RegExp(s)
92+
`,
93+
output: String.raw`
94+
const s = "\\t"
95+
new RegExp(s)
96+
`,
97+
errors: [
98+
"Unexpected control character escape '\\u0009' (U+0009). Use '\\t' instead.",
99+
],
100+
},
101+
{
102+
code: String.raw`
103+
const s = "\\u"+"0009"
104+
new RegExp(s)
105+
`,
106+
output: null,
107+
errors: [
108+
"Unexpected control character escape '\\u0009' (U+0009). Use '\\t' instead.",
109+
],
110+
},
111+
],
112+
})

0 commit comments

Comments
 (0)