Skip to content

Commit 31f0523

Browse files
authored
Add regexp/no-useless-string-literal rule (#639)
1 parent bf67254 commit 31f0523

File tree

8 files changed

+261
-0
lines changed

8 files changed

+261
-0
lines changed

.changeset/green-bananas-tie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-regexp": major
3+
---
4+
5+
Add `regexp/no-useless-string-literal` rule

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ The `plugin:regexp/all` config enables all rules. It's meant for testing, not fo
157157
| [no-useless-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-quantifier.html) | disallow quantifiers that can be removed || | 🔧 | 💡 |
158158
| [no-useless-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-range.html) | disallow unnecessary character ranges || | 🔧 | |
159159
| [no-useless-set-operand](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-set-operand.html) | disallow unnecessary elements in expression character classes || | 🔧 | |
160+
| [no-useless-string-literal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-string-literal.html) | disallow string disjunction of single characters in `\q{...}` || | 🔧 | |
160161
| [no-useless-two-nums-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-two-nums-quantifier.html) | disallow unnecessary `{n,m}` quantifier || | 🔧 | |
161162
| [no-zero-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-zero-quantifier.html) | disallow quantifiers with a maximum of zero || | | 💡 |
162163
| [optimal-lookaround-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/optimal-lookaround-quantifier.html) | disallow the alternatives of lookarounds that end with a non-constant quantifier | || | 💡 |

docs/rules/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ sidebarDepth: 0
6464
| [no-useless-quantifier](no-useless-quantifier.md) | disallow quantifiers that can be removed || | 🔧 | 💡 |
6565
| [no-useless-range](no-useless-range.md) | disallow unnecessary character ranges || | 🔧 | |
6666
| [no-useless-set-operand](no-useless-set-operand.md) | disallow unnecessary elements in expression character classes || | 🔧 | |
67+
| [no-useless-string-literal](no-useless-string-literal.md) | disallow string disjunction of single characters in `\q{...}` || | 🔧 | |
6768
| [no-useless-two-nums-quantifier](no-useless-two-nums-quantifier.md) | disallow unnecessary `{n,m}` quantifier || | 🔧 | |
6869
| [no-zero-quantifier](no-zero-quantifier.md) | disallow quantifiers with a maximum of zero || | | 💡 |
6970
| [optimal-lookaround-quantifier](optimal-lookaround-quantifier.md) | disallow the alternatives of lookarounds that end with a non-constant quantifier | || | 💡 |
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/no-useless-string-literal"
5+
description: "disallow string disjunction of single characters in `\\q{...}`"
6+
---
7+
# regexp/no-useless-string-literal
8+
9+
💼 This rule is enabled in the ✅ `plugin:regexp/recommended` config.
10+
11+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
12+
13+
<!-- end auto-generated rule header -->
14+
15+
> disallow string disjunction of single characters in `\q{...}`
16+
17+
## :book: Rule Details
18+
19+
This rule reports the string alternatives of a single character in `\q{...}`.
20+
It can be placed outside `\q{...}`.
21+
22+
<eslint-code-block fix>
23+
24+
```js
25+
/* eslint regexp/no-useless-string-literal: "error" */
26+
27+
/* ✓ GOOD */
28+
var foo = /[\q{abc}]/v
29+
var foo = /[\q{ab|}]/v
30+
31+
/* ✗ BAD */
32+
var foo = /[\q{a}]/v // => /[a]/v
33+
var foo = /[\q{a|bc}]/v // => /[a\q{bc}]/v
34+
```
35+
36+
</eslint-code-block>
37+
38+
## :wrench: Options
39+
40+
Nothing.
41+
42+
## :couple: Related rules
43+
44+
- [regexp/no-empty-alternative]
45+
- [regexp/no-empty-string-literal]
46+
47+
[regexp/no-empty-alternative]: ./no-empty-alternative.md
48+
[regexp/no-empty-string-literal]: ./no-empty-string-literal.md
49+
50+
## :rocket: Version
51+
52+
:exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
53+
54+
## :mag: Implementation
55+
56+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-useless-string-literal.ts)
57+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-useless-string-literal.ts)

lib/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const rules = {
5252
"regexp/no-useless-quantifier": "error",
5353
"regexp/no-useless-range": "error",
5454
"regexp/no-useless-set-operand": "error",
55+
"regexp/no-useless-string-literal": "error",
5556
"regexp/no-useless-two-nums-quantifier": "error",
5657
"regexp/no-zero-quantifier": "error",
5758
"regexp/optimal-lookaround-quantifier": "warn",
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
2+
import type { RegExpContext } from "../utils"
3+
import { createRule, defineRegexpVisitor } from "../utils"
4+
import type {
5+
ClassStringDisjunction,
6+
StringAlternative,
7+
} from "@eslint-community/regexpp/ast"
8+
import { RESERVED_DOUBLE_PUNCTUATOR_CHARS } from "../utils/unicode-set"
9+
10+
export default createRule("no-useless-string-literal", {
11+
meta: {
12+
docs: {
13+
description:
14+
"disallow string disjunction of single characters in `\\q{...}`",
15+
category: "Best Practices",
16+
recommended: true,
17+
},
18+
schema: [],
19+
messages: {
20+
unexpected: "Unexpected string disjunction of single character.",
21+
},
22+
type: "suggestion",
23+
fixable: "code",
24+
},
25+
create(context) {
26+
function createVisitor(
27+
regexpContext: RegExpContext,
28+
): RegExpVisitor.Handlers {
29+
const { node, getRegexpLocation, fixReplaceNode, pattern } =
30+
regexpContext
31+
return {
32+
onStringAlternativeEnter(saNode) {
33+
if (saNode.elements.length === 1) {
34+
const csdNode = saNode.parent
35+
context.report({
36+
node,
37+
loc: getRegexpLocation(saNode),
38+
messageId: "unexpected",
39+
fix: fixReplaceNode(csdNode, () => {
40+
const alternativesText = csdNode.alternatives
41+
.filter((alt) => alt !== saNode)
42+
.map((alt) => alt.raw)
43+
.join("|")
44+
if (!alternativesText.length) {
45+
const escape =
46+
isNeedEscapedInCharacterClass(
47+
csdNode,
48+
saNode,
49+
)
50+
? "\\"
51+
: ""
52+
return `${escape}${saNode.raw}`
53+
}
54+
if (
55+
csdNode.parent.type ===
56+
"ClassIntersection" ||
57+
csdNode.parent.type === "ClassSubtraction"
58+
) {
59+
const escape =
60+
saNode.raw === "^" ? "\\" : ""
61+
return String.raw`[${escape}${saNode.raw}\q{${alternativesText}}]`
62+
}
63+
const escape = isNeedEscapedInCharacterClass(
64+
csdNode,
65+
saNode,
66+
)
67+
? "\\"
68+
: ""
69+
return String.raw`${escape}${saNode.raw}\q{${alternativesText}}`
70+
}),
71+
})
72+
}
73+
},
74+
}
75+
76+
/**
77+
* Checks whether an escape is required if the given character when placed before a \q{...}
78+
*/
79+
function isNeedEscapedInCharacterClass(
80+
disjunction: ClassStringDisjunction,
81+
character: StringAlternative,
82+
) {
83+
const char = character.raw
84+
// Avoid [A&&\q{&}] => [A&&&]
85+
if (
86+
RESERVED_DOUBLE_PUNCTUATOR_CHARS.has(char) &&
87+
// The previous character is the same
88+
pattern[disjunction.start - 1] === char
89+
) {
90+
return true
91+
}
92+
93+
// Avoid [\q{^|ab}] => [^\q{ab}]
94+
return (
95+
char === "^" &&
96+
disjunction.parent.type === "CharacterClass" &&
97+
disjunction.parent.start === disjunction.start - 1
98+
)
99+
}
100+
}
101+
102+
return defineRegexpVisitor(context, {
103+
createVisitor,
104+
})
105+
},
106+
})

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import noUselessNonGreedy from "../rules/no-useless-non-greedy"
4949
import noUselessQuantifier from "../rules/no-useless-quantifier"
5050
import noUselessRange from "../rules/no-useless-range"
5151
import noUselessSetOperand from "../rules/no-useless-set-operand"
52+
import noUselessStringLiteral from "../rules/no-useless-string-literal"
5253
import noUselessTwoNumsQuantifier from "../rules/no-useless-two-nums-quantifier"
5354
import noZeroQuantifier from "../rules/no-zero-quantifier"
5455
import optimalLookaroundQuantifier from "../rules/optimal-lookaround-quantifier"
@@ -135,6 +136,7 @@ export const rules = [
135136
noUselessQuantifier,
136137
noUselessRange,
137138
noUselessSetOperand,
139+
noUselessStringLiteral,
138140
noUselessTwoNumsQuantifier,
139141
noZeroQuantifier,
140142
optimalLookaroundQuantifier,
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../lib/rules/no-useless-string-literal"
3+
4+
const tester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: "latest",
7+
sourceType: "module",
8+
},
9+
})
10+
11+
tester.run("no-useless-string-literal", rule as any, {
12+
valid: [String.raw`/[\q{abc}]/v`, String.raw`/[\q{ab|}]/v`],
13+
invalid: [
14+
{
15+
code: String.raw`/[\q{a}]/v`,
16+
output: String.raw`/[a]/v`,
17+
errors: [
18+
{
19+
message:
20+
"Unexpected string disjunction of single character.",
21+
line: 1,
22+
column: 6,
23+
},
24+
],
25+
},
26+
{
27+
code: String.raw`/[\q{a|bc}]/v`,
28+
output: String.raw`/[a\q{bc}]/v`,
29+
errors: [
30+
{
31+
message:
32+
"Unexpected string disjunction of single character.",
33+
line: 1,
34+
column: 6,
35+
},
36+
],
37+
},
38+
{
39+
code: String.raw`/[\q{ab|c}]/v`,
40+
output: String.raw`/[c\q{ab}]/v`,
41+
errors: [
42+
{
43+
message:
44+
"Unexpected string disjunction of single character.",
45+
line: 1,
46+
column: 9,
47+
},
48+
],
49+
},
50+
{
51+
code: String.raw`/[\q{ab|c|de}]/v`,
52+
output: String.raw`/[c\q{ab|de}]/v`,
53+
errors: [
54+
{
55+
message:
56+
"Unexpected string disjunction of single character.",
57+
line: 1,
58+
column: 9,
59+
},
60+
],
61+
},
62+
{
63+
code: String.raw`/[a\q{ab|\-}]/v`,
64+
output: String.raw`/[a\-\q{ab}]/v`,
65+
errors: ["Unexpected string disjunction of single character."],
66+
},
67+
{
68+
code: String.raw`/[\q{ab|^}]/v`,
69+
output: String.raw`/[\^\q{ab}]/v`,
70+
errors: ["Unexpected string disjunction of single character."],
71+
},
72+
{
73+
code: String.raw`/[\q{ab|c}&&\q{ab}]/v`,
74+
output: String.raw`/[[c\q{ab}]&&\q{ab}]/v`,
75+
errors: ["Unexpected string disjunction of single character."],
76+
},
77+
{
78+
code: String.raw`/[A&&\q{&}]/v`,
79+
output: String.raw`/[A&&\&]/v`,
80+
errors: ["Unexpected string disjunction of single character."],
81+
},
82+
{
83+
code: String.raw`/[A&&\q{^|ab}]/v`,
84+
output: String.raw`/[A&&[\^\q{ab}]]/v`,
85+
errors: ["Unexpected string disjunction of single character."],
86+
},
87+
],
88+
})

0 commit comments

Comments
 (0)