Skip to content

Commit 7fffe0b

Browse files
authored
Add regexp/negation rule (#47)
1 parent b63bee2 commit 7fffe0b

File tree

6 files changed

+258
-0
lines changed

6 files changed

+258
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
8181
|:--------|:------------|:---|
8282
| [regexp/letter-case](https://ota-meshi.github.io/eslint-plugin-regexp/rules/letter-case.html) | enforce into your favorite case | :wrench: |
8383
| [regexp/match-any](https://ota-meshi.github.io/eslint-plugin-regexp/rules/match-any.html) | enforce match any character style | :star::wrench: |
84+
| [regexp/negation](https://ota-meshi.github.io/eslint-plugin-regexp/rules/negation.html) | enforce use of escapes on negation | :wrench: |
8485
| [regexp/no-assertion-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-assertion-capturing-group.html) | disallow capturing group that captures assertions. | :star: |
8586
| [regexp/no-dupe-characters-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-dupe-characters-character-class.html) | disallow duplicate characters in the RegExp character class | :star: |
8687
| [regexp/no-dupe-disjunctions](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-dupe-disjunctions.html) | disallow duplicate disjunctions | |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
1313
|:--------|:------------|:---|
1414
| [regexp/letter-case](./letter-case.md) | enforce into your favorite case | :wrench: |
1515
| [regexp/match-any](./match-any.md) | enforce match any character style | :star::wrench: |
16+
| [regexp/negation](./negation.md) | enforce use of escapes on negation | :wrench: |
1617
| [regexp/no-assertion-capturing-group](./no-assertion-capturing-group.md) | disallow capturing group that captures assertions. | :star: |
1718
| [regexp/no-dupe-characters-character-class](./no-dupe-characters-character-class.md) | disallow duplicate characters in the RegExp character class | :star: |
1819
| [regexp/no-dupe-disjunctions](./no-dupe-disjunctions.md) | disallow duplicate disjunctions | |

docs/rules/negation.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/negation"
5+
description: "enforce use of escapes on negation"
6+
---
7+
# regexp/negation
8+
9+
> enforce use of escapes on negation
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 enforces use of `\D`, `\W`, `\S` and `\P` on negation.
17+
18+
<eslint-code-block fix>
19+
20+
```js
21+
/* eslint regexp/negation: "error" */
22+
23+
/* ✓ GOOD */
24+
var foo = /\D/
25+
var foo = /\W/
26+
var foo = /\S/
27+
var foo = /\P{ASCII}/u
28+
29+
var foo = /\d/
30+
var foo = /\w/
31+
var foo = /\s/
32+
var foo = /\p{ASCII}/u
33+
34+
/* ✗ BAD */
35+
var foo = /[^\d]/
36+
var foo = /[^\w]/
37+
var foo = /[^\s]/
38+
var foo = /[^\p{ASCII}]/u
39+
40+
var foo = /[^\D]/
41+
var foo = /[^\W]/
42+
var foo = /[^\S]/
43+
var foo = /[^\P{ASCII}]/u
44+
```
45+
46+
</eslint-code-block>
47+
48+
## :wrench: Options
49+
50+
Nothing.
51+
52+
## :mag: Implementation
53+
54+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/negation.ts)
55+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/negation.ts)

lib/rules/negation.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type { Expression } from "estree"
2+
import type {
3+
EscapeCharacterSet,
4+
UnicodePropertyCharacterSet,
5+
} from "regexpp/ast"
6+
import type { RegExpVisitor } from "regexpp/visitor"
7+
import {
8+
createRule,
9+
defineRegexpVisitor,
10+
fixerApplyEscape,
11+
getRegexpLocation,
12+
getRegexpRange,
13+
} from "../utils"
14+
15+
export default createRule("negation", {
16+
meta: {
17+
docs: {
18+
description: "enforce use of escapes on negation",
19+
// TODO In the major version
20+
// recommended: true,
21+
recommended: false,
22+
},
23+
fixable: "code",
24+
schema: [],
25+
messages: {
26+
unexpected:
27+
"Unexpected negated character class. Use '{{negatedCharSet}}' instead.",
28+
},
29+
type: "suggestion", // "problem",
30+
},
31+
create(context) {
32+
const sourceCode = context.getSourceCode()
33+
34+
/**
35+
* Create visitor
36+
* @param node
37+
*/
38+
function createVisitor(node: Expression): RegExpVisitor.Handlers {
39+
return {
40+
onCharacterClassEnter(ccNode) {
41+
if (!ccNode.negate || ccNode.elements.length !== 1) {
42+
return
43+
}
44+
const element = ccNode.elements[0]
45+
if (element.type === "CharacterSet") {
46+
const negatedCharSet = getNegationText(element)
47+
context.report({
48+
node,
49+
loc: getRegexpLocation(sourceCode, node, ccNode),
50+
messageId: "unexpected",
51+
data: { negatedCharSet },
52+
fix(fixer) {
53+
const range = getRegexpRange(
54+
sourceCode,
55+
node,
56+
ccNode,
57+
)
58+
if (range == null) {
59+
return null
60+
}
61+
return fixer.replaceTextRange(
62+
range,
63+
fixerApplyEscape(negatedCharSet, node),
64+
)
65+
},
66+
})
67+
}
68+
},
69+
}
70+
}
71+
72+
return defineRegexpVisitor(context, {
73+
createVisitor,
74+
})
75+
},
76+
})
77+
78+
/**
79+
* Gets the text that negation the CharacterSet.
80+
*/
81+
function getNegationText(
82+
node: EscapeCharacterSet | UnicodePropertyCharacterSet,
83+
) {
84+
let text: string
85+
if (node.kind === "digit") {
86+
text = "d"
87+
} else if (node.kind === "space") {
88+
text = "s"
89+
} else if (node.kind === "word") {
90+
text = "w"
91+
} else if (node.kind === "property") {
92+
text = "p"
93+
} else {
94+
throw new Error(`unknown kind:${node.kind}`)
95+
}
96+
if (!node.negate) {
97+
text = text.toUpperCase()
98+
}
99+
if (node.kind === "property") {
100+
if (node.value != null) {
101+
text += `{${node.key}=${node.value}}`
102+
} else {
103+
text += `{${node.key}}`
104+
}
105+
}
106+
107+
return `\\${text}`
108+
}

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RuleModule } from "../types"
22
import letterCase from "../rules/letter-case"
33
import matchAny from "../rules/match-any"
4+
import negation from "../rules/negation"
45
import noAssertionCapturingGroup from "../rules/no-assertion-capturing-group"
56
import noDupeCharactersCharacterClass from "../rules/no-dupe-characters-character-class"
67
import noDupeDisjunctions from "../rules/no-dupe-disjunctions"
@@ -33,6 +34,7 @@ import preferW from "../rules/prefer-w"
3334
export const rules = [
3435
letterCase,
3536
matchAny,
37+
negation,
3638
noAssertionCapturingGroup,
3739
noDupeCharactersCharacterClass,
3840
noDupeDisjunctions,

tests/lib/rules/negation.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../lib/rules/negation"
3+
4+
const tester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 2020,
7+
sourceType: "module",
8+
},
9+
})
10+
11+
tester.run("negation", rule as any, {
12+
valid: [String.raw`/[\d]/`, String.raw`/[^\d\s]/`],
13+
invalid: [
14+
{
15+
code: String.raw`/[^\d]/`,
16+
output: String.raw`/\D/`,
17+
errors: [
18+
{
19+
message:
20+
"Unexpected negated character class. Use '\\D' instead.",
21+
line: 1,
22+
column: 2,
23+
endLine: 1,
24+
endColumn: 7,
25+
},
26+
],
27+
},
28+
{
29+
code: String.raw`/[^\D]/`,
30+
output: String.raw`/\d/`,
31+
errors: [
32+
{
33+
message:
34+
"Unexpected negated character class. Use '\\d' instead.",
35+
line: 1,
36+
column: 2,
37+
endLine: 1,
38+
endColumn: 7,
39+
},
40+
],
41+
},
42+
{
43+
code: String.raw`/[^\w]/`,
44+
output: String.raw`/\W/`,
45+
errors: ["Unexpected negated character class. Use '\\W' instead."],
46+
},
47+
{
48+
code: String.raw`/[^\W]/`,
49+
output: String.raw`/\w/`,
50+
errors: ["Unexpected negated character class. Use '\\w' instead."],
51+
},
52+
{
53+
code: String.raw`/[^\s]/`,
54+
output: String.raw`/\S/`,
55+
errors: ["Unexpected negated character class. Use '\\S' instead."],
56+
},
57+
{
58+
code: String.raw`/[^\S]/`,
59+
output: String.raw`/\s/`,
60+
errors: ["Unexpected negated character class. Use '\\s' instead."],
61+
},
62+
{
63+
code: String.raw`/[^\p{ASCII}]/u`,
64+
output: String.raw`/\P{ASCII}/u`,
65+
errors: [
66+
"Unexpected negated character class. Use '\\P{ASCII}' instead.",
67+
],
68+
},
69+
{
70+
code: String.raw`/[^\P{ASCII}]/u`,
71+
output: String.raw`/\p{ASCII}/u`,
72+
errors: [
73+
"Unexpected negated character class. Use '\\p{ASCII}' instead.",
74+
],
75+
},
76+
{
77+
code: String.raw`/[^\p{Script=Hiragana}]/u`,
78+
output: String.raw`/\P{Script=Hiragana}/u`,
79+
errors: [
80+
"Unexpected negated character class. Use '\\P{Script=Hiragana}' instead.",
81+
],
82+
},
83+
{
84+
code: String.raw`/[^\P{Script=Hiragana}]/u`,
85+
output: String.raw`/\p{Script=Hiragana}/u`,
86+
errors: [
87+
"Unexpected negated character class. Use '\\p{Script=Hiragana}' instead.",
88+
],
89+
},
90+
],
91+
})

0 commit comments

Comments
 (0)