Skip to content

Commit c01e6a6

Browse files
authored
Add regexp/grapheme-string-literal rule (#646)
1 parent 110de11 commit c01e6a6

File tree

7 files changed

+350
-0
lines changed

7 files changed

+350
-0
lines changed

.changeset/chatty-walls-juggle.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-regexp": minor
3+
---
4+
5+
Add `regexp/grapheme-string-literal` rule

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ The `plugin:regexp/all` config enables all rules. It's meant for testing, not fo
180180

181181
| Name                             | Description | 💼 | ⚠️ | 🔧 | 💡 |
182182
| :------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------- | :- | :- | :- | :- |
183+
| [grapheme-string-literal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/grapheme-string-literal.html) | enforce single grapheme in string literal | | | | |
183184
| [hexadecimal-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/hexadecimal-escape.html) | enforce consistent usage of hexadecimal escape | | | 🔧 | |
184185
| [letter-case](https://ota-meshi.github.io/eslint-plugin-regexp/rules/letter-case.html) | enforce into your favorite case | | | 🔧 | |
185186
| [match-any](https://ota-meshi.github.io/eslint-plugin-regexp/rules/match-any.html) | enforce match any character style || | 🔧 | |

docs/rules/grapheme-string-literal.md

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/grapheme-string-literal"
5+
description: "enforce single grapheme in string literal"
6+
---
7+
# regexp/grapheme-string-literal
8+
9+
<!-- end auto-generated rule header -->
10+
11+
> enforce single grapheme in string literal
12+
13+
## :book: Rule Details
14+
15+
This rule is aimed to clarify the difference between using a string literal and normal disjunctions by not using string literals for purposes other than expressing a single grapheme.
16+
17+
<eslint-code-block>
18+
19+
```js
20+
/* eslint regexp/grapheme-string-literal: "error" */
21+
22+
/* ✓ GOOD */
23+
var foo = /[\p{RGI_Emoji}--\q{🇦🇨|🇦🇩|🇦🇪|🇦🇫|🇦🇬|🇦🇮|🇦🇱|🇦🇲|🇦🇴|🇦🇶|🇦🇷|🇦🇸|🇦🇹|🇦🇺|🇦🇼|🇦🇽|🇦🇿|🇧🇦|🇧🇧|🇧🇩|🇧🇪|🇧🇫|🇧🇬|🇧🇭|🇧🇮|🇧🇯}]/v
24+
var foo = /[\q{a|b|c}]/v
25+
26+
/* ✗ BAD */
27+
var foo = /[\q{abc|def}]/v
28+
```
29+
30+
</eslint-code-block>
31+
32+
This rule does not report empty string literals. Use [regexp/no-empty-string-literal] and [regexp/no-empty-alternative] if you want to check them.
33+
34+
## :wrench: Options
35+
36+
Nothing.
37+
38+
## :couple: Related rules
39+
40+
- [regexp/no-empty-string-literal]
41+
- [regexp/no-empty-alternative]
42+
- [regexp/no-useless-string-literal]
43+
44+
[regexp/no-empty-string-literal]: ./no-empty-string-literal.md
45+
[regexp/no-empty-alternative]: ./no-empty-alternative.md
46+
[regexp/no-useless-string-literal]: ./no-useless-string-literal.md
47+
48+
## :rocket: Version
49+
50+
:exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
51+
52+
## :mag: Implementation
53+
54+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/grapheme-string-literal.ts)
55+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/grapheme-string-literal.ts)

docs/rules/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ sidebarDepth: 0
8686

8787
| Name                             | Description | 💼 | ⚠️ | 🔧 | 💡 |
8888
| :---------------------------------------------------------------------- | :--------------------------------------------------------------------- | :- | :- | :- | :- |
89+
| [grapheme-string-literal](grapheme-string-literal.md) | enforce single grapheme in string literal | | | | |
8990
| [hexadecimal-escape](hexadecimal-escape.md) | enforce consistent usage of hexadecimal escape | | | 🔧 | |
9091
| [letter-case](letter-case.md) | enforce into your favorite case | | | 🔧 | |
9192
| [match-any](match-any.md) | enforce match any character style || | 🔧 | |

lib/rules/grapheme-string-literal.ts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
2+
import type { RegExpContext } from "../utils"
3+
import { createRule, defineRegexpVisitor } from "../utils"
4+
import type { StringAlternative } from "@eslint-community/regexpp/ast"
5+
6+
const segmenter = new Intl.Segmenter()
7+
8+
export default createRule("grapheme-string-literal", {
9+
meta: {
10+
docs: {
11+
description: "enforce single grapheme in string literal",
12+
category: "Stylistic Issues",
13+
recommended: false,
14+
},
15+
schema: [],
16+
messages: {
17+
onlySingleCharacters:
18+
"Only single characters and graphemes are allowed inside character classes. Use regular alternatives (e.g. `{{alternatives}}`) for strings instead.",
19+
},
20+
type: "suggestion",
21+
},
22+
create(context) {
23+
function createVisitor(
24+
regexpContext: RegExpContext,
25+
): RegExpVisitor.Handlers {
26+
const { node, getRegexpLocation } = regexpContext
27+
28+
function isMultipleGraphemes(saNode: StringAlternative) {
29+
if (saNode.elements.length <= 1) return false
30+
const string = String.fromCodePoint(
31+
...saNode.elements.map((element) => element.value),
32+
)
33+
34+
const segments = [...segmenter.segment(string)]
35+
return segments.length > 1
36+
}
37+
38+
function buildAlternativeExample(saNode: StringAlternative) {
39+
const alternativeRaws = saNode.parent.alternatives
40+
.filter(isMultipleGraphemes)
41+
.map((alt) => alt.raw)
42+
return `(?:${alternativeRaws.join("|")}|[...])`
43+
}
44+
45+
return {
46+
onStringAlternativeEnter(saNode) {
47+
if (!isMultipleGraphemes(saNode)) return
48+
49+
context.report({
50+
node,
51+
loc: getRegexpLocation(saNode),
52+
messageId: "onlySingleCharacters",
53+
data: {
54+
alternatives: buildAlternativeExample(saNode),
55+
},
56+
})
57+
},
58+
}
59+
}
60+
61+
return defineRegexpVisitor(context, {
62+
createVisitor,
63+
})
64+
},
65+
})

lib/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RuleModule } from "../types"
22
import confusingQuantifier from "../rules/confusing-quantifier"
33
import controlCharacterEscape from "../rules/control-character-escape"
4+
import graphemeStringLiteral from "../rules/grapheme-string-literal"
45
import hexadecimalEscape from "../rules/hexadecimal-escape"
56
import letterCase from "../rules/letter-case"
67
import matchAny from "../rules/match-any"
@@ -88,6 +89,7 @@ import useIgnoreCase from "../rules/use-ignore-case"
8889
export const rules = [
8990
confusingQuantifier,
9091
controlCharacterEscape,
92+
graphemeStringLiteral,
9193
hexadecimalEscape,
9294
letterCase,
9395
matchAny,

0 commit comments

Comments
 (0)