Skip to content

Commit 40704ed

Browse files
authored
Use Intl.Segmenter instead of grapheme-splitter (#642)
1 parent 8382ea2 commit 40704ed

File tree

11 files changed

+105
-27
lines changed

11 files changed

+105
-27
lines changed

.changeset/hungry-coins-help.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-regexp": minor
3+
---
4+
5+
Use Intl.Segmenter instead of grapheme-splitter

docs/.vitepress/theme/index.ts

+28-6
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,42 @@ if (typeof window !== "undefined") {
1111
}
1212
import type { Theme } from "vitepress"
1313
import DefaultTheme from "vitepress/theme"
14-
// @ts-expect-error -- ignore
1514
import Layout from "./Layout.vue"
16-
// @ts-expect-error -- ignore
17-
import ESLintCodeBlock from "./components/eslint-code-block.vue"
18-
// @ts-expect-error -- ignore
19-
import PlaygroundBlock from "./components/playground-block.vue"
2015

2116
const theme: Theme = {
2217
...DefaultTheme,
2318
Layout,
24-
enhanceApp(ctx) {
19+
async enhanceApp(ctx) {
2520
DefaultTheme.enhanceApp(ctx)
21+
22+
if (typeof Intl.Segmenter === "undefined") {
23+
await setupIntlSegmenter()
24+
}
25+
26+
const ESLintCodeBlock = await import(
27+
"./components/eslint-code-block.vue"
28+
).then((m) => m.default ?? m)
29+
const PlaygroundBlock = await import(
30+
"./components/playground-block.vue"
31+
).then((m) => m.default ?? m)
2632
ctx.app.component("eslint-code-block", ESLintCodeBlock)
2733
ctx.app.component("playground-block", PlaygroundBlock)
2834
},
2935
}
3036
export default theme
37+
38+
// We can remove this polyfill once Firefox supports Intl.Segmenter.
39+
async function setupIntlSegmenter() {
40+
// For Firefox
41+
const [{ createIntlSegmenterPolyfill }, breakIteratorUrl] =
42+
await Promise.all([
43+
import("intl-segmenter-polyfill"),
44+
import(
45+
// @ts-expect-error -- polyfill
46+
"intl-segmenter-polyfill/dist/break_iterator.wasm?url"
47+
).then((m) => m.default ?? m),
48+
])
49+
50+
// @ts-expect-error -- polyfill
51+
Intl.Segmenter = await createIntlSegmenterPolyfill(fetch(breakIteratorUrl))
52+
}

lib/rules/no-misleading-unicode-character.ts

+12-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
22
import type { RegExpContext } from "../utils"
33
import { isEscapeSequence, createRule, defineRegexpVisitor } from "../utils"
4-
import GraphemeSplitter from "grapheme-splitter"
54
import type { ReadonlyFlags } from "regexp-ast-analysis"
65
import { mention, mentionChar } from "../utils/mention"
76
import type {
@@ -12,7 +11,7 @@ import type {
1211
import type { PatternRange } from "../utils/ast-utils/pattern-source"
1312
import type { Rule } from "eslint"
1413

15-
const splitter = new GraphemeSplitter()
14+
const segmenter = new Intl.Segmenter()
1615

1716
/** Returns whether the given string starts with a valid surrogate pair. */
1817
function startsWithSurrogate(s: string): boolean {
@@ -66,10 +65,10 @@ function getGraphemeBeforeQuant(quant: Quantifier): string {
6665
quant.element.end - alt.start,
6766
)
6867

69-
const graphemes = splitter.splitGraphemes(before)
70-
const grapheme = graphemes[graphemes.length - 1]
68+
const segments = [...segmenter.segment(before)]
69+
const segment = segments[segments.length - 1]
7170

72-
return grapheme
71+
return segment.segment
7372
}
7473

7574
interface GraphemeProblem {
@@ -86,7 +85,7 @@ function getGraphemeProblems(
8685
cc: CharacterClass,
8786
flags: ReadonlyFlags,
8887
): GraphemeProblem[] {
89-
let offset = cc.negate ? 2 : 1
88+
const offset = cc.negate ? 2 : 1
9089

9190
const ignoreElements = cc.elements.filter(
9291
(element) =>
@@ -95,14 +94,15 @@ function getGraphemeProblems(
9594
element.type === "ClassStringDisjunction",
9695
)
9796

98-
const graphemes = splitter.splitGraphemes(cc.raw.slice(offset, -1))
9997
const problems: GraphemeProblem[] = []
10098

101-
for (const grapheme of graphemes) {
102-
const problem = getProblem(grapheme, flags)
99+
for (const { segment, index } of segmenter.segment(
100+
cc.raw.slice(offset, -1),
101+
)) {
102+
const problem = getProblem(segment, flags)
103103
if (problem !== null) {
104-
const start = offset + cc.start
105-
const end = start + grapheme.length
104+
const start = offset + index + cc.start
105+
const end = start + segment.length
106106

107107
if (
108108
ignoreElements.some(
@@ -113,7 +113,7 @@ function getGraphemeProblems(
113113
}
114114

115115
problems.push({
116-
grapheme,
116+
grapheme: segment,
117117
problem,
118118
start,
119119
end,
@@ -122,7 +122,6 @@ function getGraphemeProblems(
122122
),
123123
})
124124
}
125-
offset += grapheme.length
126125
}
127126

128127
return problems

lib/utils/extract-capturing-group-references.ts

+7
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ const WELL_KNOWN_ARRAY_METHODS: {
184184
flat: {},
185185
// ES2022
186186
at: { result: "element" },
187+
// ES2023
188+
findLast: { elementParameters: [0], result: "element" },
189+
findLastIndex: { elementParameters: [0] },
190+
toReversed: { result: "array" },
191+
toSorted: { elementParameters: [0, 1], result: "array" },
192+
toSpliced: { result: "array" },
193+
with: { result: "array" },
187194
}
188195

189196
/**

lib/utils/type-tracker/type-data/array.ts

+7
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,13 @@ const getPrototypes = cache(() => {
255255
flat: RETURN_UNKNOWN_ARRAY,
256256
// ES2022
257257
at: RETURN_ARRAY_ELEMENT, // element
258+
// ES2023
259+
findLast: RETURN_ARRAY_ELEMENT, // element
260+
findLastIndex: RETURN_NUMBER,
261+
toReversed: RETURN_SELF,
262+
toSorted: RETURN_SELF,
263+
toSpliced: RETURN_SELF,
264+
with: RETURN_SELF,
258265

259266
length: NUMBER,
260267
0: null, // element

lib/utils/type-tracker/type-data/object.ts

+2
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ export function buildObjectConstructor(): TypeGlobalFunction {
196196
getOwnPropertyDescriptors: null,
197197
// ES2019
198198
fromEntries: null,
199+
// ES2022
200+
hasOwn: RETURN_BOOLEAN,
199201

200202
prototype: null,
201203
})

lib/utils/type-tracker/type-data/regexp.ts

+2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ const getPrototypes: () => {
103103
unicode: BOOLEAN, // prop
104104
// ES2018
105105
dotAll: BOOLEAN, // prop
106+
// ES2022
107+
hasIndices: BOOLEAN, // prop
106108

107109
[Symbol.match]: null,
108110
[Symbol.replace]: null,

lib/utils/type-tracker/type-data/string.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ const getPrototypes: () => {
9797
trim: RETURN_STRING,
9898
substr: RETURN_STRING,
9999
valueOf: RETURN_STRING,
100-
// ES2051
100+
// ES2015
101101
codePointAt: RETURN_NUMBER,
102102
includes: RETURN_BOOLEAN,
103103
endsWith: RETURN_BOOLEAN,
@@ -128,6 +128,8 @@ const getPrototypes: () => {
128128
trimEnd: RETURN_STRING,
129129
// ES2020
130130
matchAll: null, // IterableIterator<RegExpMatchArray>
131+
// ES2021
132+
replaceAll: RETURN_STRING,
131133
// ES2022
132134
at: RETURN_STRING,
133135

package-lock.json

+37-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"eslint-plugin-regexp": "~1.15.0",
8888
"eslint-plugin-vue": "^9.0.0",
8989
"eslint-plugin-yml": "^1.0.0",
90+
"intl-segmenter-polyfill": "^0.4.4",
9091
"markdownlint-cli": "^0.37.0",
9192
"mocha": "^10.0.0",
9293
"mocha-chai-jest-snapshot": "^1.1.3",
@@ -105,7 +106,6 @@
105106
"@eslint-community/eslint-utils": "^4.2.0",
106107
"@eslint-community/regexpp": "^4.9.1",
107108
"comment-parser": "^1.4.0",
108-
"grapheme-splitter": "^1.0.4",
109109
"jsdoctypeparser": "^9.0.0",
110110
"refa": "^0.12.1",
111111
"regexp-ast-analysis": "^0.7.1",

tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"target": "es2015",
44
"module": "Node16",
55
"moduleResolution": "Node16",
6-
"lib": ["es2020"],
6+
"lib": ["es2023"],
77
"allowJs": true,
88
"checkJs": true,
99
"outDir": "./dist",

0 commit comments

Comments
 (0)