Skip to content

Add regexp/grapheme-string-literal rule #646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chatty-walls-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-regexp": minor
---

Add `regexp/grapheme-string-literal` rule
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ The `plugin:regexp/all` config enables all rules. It's meant for testing, not fo

| Name                             | Description | 💼 | ⚠️ | 🔧 | 💡 |
| :------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------- | :- | :- | :- | :- |
| [grapheme-string-literal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/grapheme-string-literal.html) | enforce single grapheme in string literal | | | | |
| [hexadecimal-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/hexadecimal-escape.html) | enforce consistent usage of hexadecimal escape | | | 🔧 | |
| [letter-case](https://ota-meshi.github.io/eslint-plugin-regexp/rules/letter-case.html) | enforce into your favorite case | | | 🔧 | |
| [match-any](https://ota-meshi.github.io/eslint-plugin-regexp/rules/match-any.html) | enforce match any character style || | 🔧 | |
Expand Down
55 changes: 55 additions & 0 deletions docs/rules/grapheme-string-literal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
pageClass: "rule-details"
sidebarDepth: 0
title: "regexp/grapheme-string-literal"
description: "enforce single grapheme in string literal"
---
# regexp/grapheme-string-literal

<!-- end auto-generated rule header -->

> enforce single grapheme in string literal
## :book: Rule Details

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.

<eslint-code-block>

```js
/* eslint regexp/grapheme-string-literal: "error" */

/* ✓ GOOD */
var foo = /[\p{RGI_Emoji}--\q{🇦🇨|🇦🇩|🇦🇪|🇦🇫|🇦🇬|🇦🇮|🇦🇱|🇦🇲|🇦🇴|🇦🇶|🇦🇷|🇦🇸|🇦🇹|🇦🇺|🇦🇼|🇦🇽|🇦🇿|🇧🇦|🇧🇧|🇧🇩|🇧🇪|🇧🇫|🇧🇬|🇧🇭|🇧🇮|🇧🇯}]/v
var foo = /[\q{a|b|c}]/v

/* ✗ BAD */
var foo = /[\q{abc|def}]/v
```

</eslint-code-block>

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.

## :wrench: Options

Nothing.

## :couple: Related rules

- [regexp/no-empty-string-literal]
- [regexp/no-empty-alternative]
- [regexp/no-useless-string-literal]

[regexp/no-empty-string-literal]: ./no-empty-string-literal.md
[regexp/no-empty-alternative]: ./no-empty-alternative.md
[regexp/no-useless-string-literal]: ./no-useless-string-literal.md

## :rocket: Version

:exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>

## :mag: Implementation

- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/grapheme-string-literal.ts)
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/grapheme-string-literal.ts)
1 change: 1 addition & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ sidebarDepth: 0

| Name                             | Description | 💼 | ⚠️ | 🔧 | 💡 |
| :---------------------------------------------------------------------- | :--------------------------------------------------------------------- | :- | :- | :- | :- |
| [grapheme-string-literal](grapheme-string-literal.md) | enforce single grapheme in string literal | | | | |
| [hexadecimal-escape](hexadecimal-escape.md) | enforce consistent usage of hexadecimal escape | | | 🔧 | |
| [letter-case](letter-case.md) | enforce into your favorite case | | | 🔧 | |
| [match-any](match-any.md) | enforce match any character style || | 🔧 | |
Expand Down
65 changes: 65 additions & 0 deletions lib/rules/grapheme-string-literal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
import type { RegExpContext } from "../utils"
import { createRule, defineRegexpVisitor } from "../utils"
import type { StringAlternative } from "@eslint-community/regexpp/ast"

const segmenter = new Intl.Segmenter()

export default createRule("grapheme-string-literal", {
meta: {
docs: {
description: "enforce single grapheme in string literal",
category: "Stylistic Issues",
recommended: false,
},
schema: [],
messages: {
onlySingleCharacters:
"Only single characters and graphemes are allowed inside character classes. Use regular alternatives (e.g. `{{alternatives}}`) for strings instead.",
},
type: "suggestion",
},
create(context) {
function createVisitor(
regexpContext: RegExpContext,
): RegExpVisitor.Handlers {
const { node, getRegexpLocation } = regexpContext

function isMultipleGraphemes(saNode: StringAlternative) {
if (saNode.elements.length <= 1) return false
const string = String.fromCodePoint(
...saNode.elements.map((element) => element.value),
)

const segments = [...segmenter.segment(string)]
return segments.length > 1
}

function buildAlternativeExample(saNode: StringAlternative) {
const alternativeRaws = saNode.parent.alternatives
.filter(isMultipleGraphemes)
.map((alt) => alt.raw)
return `(?:${alternativeRaws.join("|")}|[...])`
}

return {
onStringAlternativeEnter(saNode) {
if (!isMultipleGraphemes(saNode)) return

context.report({
node,
loc: getRegexpLocation(saNode),
messageId: "onlySingleCharacters",
data: {
alternatives: buildAlternativeExample(saNode),
},
})
},
}
}

return defineRegexpVisitor(context, {
createVisitor,
})
},
})
2 changes: 2 additions & 0 deletions lib/utils/rules.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { RuleModule } from "../types"
import confusingQuantifier from "../rules/confusing-quantifier"
import controlCharacterEscape from "../rules/control-character-escape"
import graphemeStringLiteral from "../rules/grapheme-string-literal"
import hexadecimalEscape from "../rules/hexadecimal-escape"
import letterCase from "../rules/letter-case"
import matchAny from "../rules/match-any"
Expand Down Expand Up @@ -88,6 +89,7 @@ import useIgnoreCase from "../rules/use-ignore-case"
export const rules = [
confusingQuantifier,
controlCharacterEscape,
graphemeStringLiteral,
hexadecimalEscape,
letterCase,
matchAny,
Expand Down
Loading