diff --git a/.changeset/hungry-coins-help.md b/.changeset/hungry-coins-help.md new file mode 100644 index 000000000..2f901f00c --- /dev/null +++ b/.changeset/hungry-coins-help.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-regexp": minor +--- + +Use Intl.Segmenter instead of grapheme-splitter diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index d93be2029..11e317918 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -11,20 +11,42 @@ if (typeof window !== "undefined") { } import type { Theme } from "vitepress" import DefaultTheme from "vitepress/theme" -// @ts-expect-error -- ignore import Layout from "./Layout.vue" -// @ts-expect-error -- ignore -import ESLintCodeBlock from "./components/eslint-code-block.vue" -// @ts-expect-error -- ignore -import PlaygroundBlock from "./components/playground-block.vue" const theme: Theme = { ...DefaultTheme, Layout, - enhanceApp(ctx) { + async enhanceApp(ctx) { DefaultTheme.enhanceApp(ctx) + + if (typeof Intl.Segmenter === "undefined") { + await setupIntlSegmenter() + } + + const ESLintCodeBlock = await import( + "./components/eslint-code-block.vue" + ).then((m) => m.default ?? m) + const PlaygroundBlock = await import( + "./components/playground-block.vue" + ).then((m) => m.default ?? m) ctx.app.component("eslint-code-block", ESLintCodeBlock) ctx.app.component("playground-block", PlaygroundBlock) }, } export default theme + +// We can remove this polyfill once Firefox supports Intl.Segmenter. +async function setupIntlSegmenter() { + // For Firefox + const [{ createIntlSegmenterPolyfill }, breakIteratorUrl] = + await Promise.all([ + import("intl-segmenter-polyfill"), + import( + // @ts-expect-error -- polyfill + "intl-segmenter-polyfill/dist/break_iterator.wasm?url" + ).then((m) => m.default ?? m), + ]) + + // @ts-expect-error -- polyfill + Intl.Segmenter = await createIntlSegmenterPolyfill(fetch(breakIteratorUrl)) +} diff --git a/lib/rules/no-misleading-unicode-character.ts b/lib/rules/no-misleading-unicode-character.ts index 80c5f5752..8ecf87f7a 100644 --- a/lib/rules/no-misleading-unicode-character.ts +++ b/lib/rules/no-misleading-unicode-character.ts @@ -1,7 +1,6 @@ import type { RegExpVisitor } from "@eslint-community/regexpp/visitor" import type { RegExpContext } from "../utils" import { isEscapeSequence, createRule, defineRegexpVisitor } from "../utils" -import GraphemeSplitter from "grapheme-splitter" import type { ReadonlyFlags } from "regexp-ast-analysis" import { mention, mentionChar } from "../utils/mention" import type { @@ -12,7 +11,7 @@ import type { import type { PatternRange } from "../utils/ast-utils/pattern-source" import type { Rule } from "eslint" -const splitter = new GraphemeSplitter() +const segmenter = new Intl.Segmenter() /** Returns whether the given string starts with a valid surrogate pair. */ function startsWithSurrogate(s: string): boolean { @@ -66,10 +65,10 @@ function getGraphemeBeforeQuant(quant: Quantifier): string { quant.element.end - alt.start, ) - const graphemes = splitter.splitGraphemes(before) - const grapheme = graphemes[graphemes.length - 1] + const segments = [...segmenter.segment(before)] + const segment = segments[segments.length - 1] - return grapheme + return segment.segment } interface GraphemeProblem { @@ -86,7 +85,7 @@ function getGraphemeProblems( cc: CharacterClass, flags: ReadonlyFlags, ): GraphemeProblem[] { - let offset = cc.negate ? 2 : 1 + const offset = cc.negate ? 2 : 1 const ignoreElements = cc.elements.filter( (element) => @@ -95,14 +94,15 @@ function getGraphemeProblems( element.type === "ClassStringDisjunction", ) - const graphemes = splitter.splitGraphemes(cc.raw.slice(offset, -1)) const problems: GraphemeProblem[] = [] - for (const grapheme of graphemes) { - const problem = getProblem(grapheme, flags) + for (const { segment, index } of segmenter.segment( + cc.raw.slice(offset, -1), + )) { + const problem = getProblem(segment, flags) if (problem !== null) { - const start = offset + cc.start - const end = start + grapheme.length + const start = offset + index + cc.start + const end = start + segment.length if ( ignoreElements.some( @@ -113,7 +113,7 @@ function getGraphemeProblems( } problems.push({ - grapheme, + grapheme: segment, problem, start, end, @@ -122,7 +122,6 @@ function getGraphemeProblems( ), }) } - offset += grapheme.length } return problems diff --git a/lib/utils/extract-capturing-group-references.ts b/lib/utils/extract-capturing-group-references.ts index 1605639ab..b43965974 100644 --- a/lib/utils/extract-capturing-group-references.ts +++ b/lib/utils/extract-capturing-group-references.ts @@ -184,6 +184,13 @@ const WELL_KNOWN_ARRAY_METHODS: { flat: {}, // ES2022 at: { result: "element" }, + // ES2023 + findLast: { elementParameters: [0], result: "element" }, + findLastIndex: { elementParameters: [0] }, + toReversed: { result: "array" }, + toSorted: { elementParameters: [0, 1], result: "array" }, + toSpliced: { result: "array" }, + with: { result: "array" }, } /** diff --git a/lib/utils/type-tracker/type-data/array.ts b/lib/utils/type-tracker/type-data/array.ts index f06acf96e..123bb5a18 100644 --- a/lib/utils/type-tracker/type-data/array.ts +++ b/lib/utils/type-tracker/type-data/array.ts @@ -255,6 +255,13 @@ const getPrototypes = cache(() => { flat: RETURN_UNKNOWN_ARRAY, // ES2022 at: RETURN_ARRAY_ELEMENT, // element + // ES2023 + findLast: RETURN_ARRAY_ELEMENT, // element + findLastIndex: RETURN_NUMBER, + toReversed: RETURN_SELF, + toSorted: RETURN_SELF, + toSpliced: RETURN_SELF, + with: RETURN_SELF, length: NUMBER, 0: null, // element diff --git a/lib/utils/type-tracker/type-data/object.ts b/lib/utils/type-tracker/type-data/object.ts index f7fbb0868..43a41ac06 100644 --- a/lib/utils/type-tracker/type-data/object.ts +++ b/lib/utils/type-tracker/type-data/object.ts @@ -196,6 +196,8 @@ export function buildObjectConstructor(): TypeGlobalFunction { getOwnPropertyDescriptors: null, // ES2019 fromEntries: null, + // ES2022 + hasOwn: RETURN_BOOLEAN, prototype: null, }) diff --git a/lib/utils/type-tracker/type-data/regexp.ts b/lib/utils/type-tracker/type-data/regexp.ts index b837627d7..b292f7a9a 100644 --- a/lib/utils/type-tracker/type-data/regexp.ts +++ b/lib/utils/type-tracker/type-data/regexp.ts @@ -103,6 +103,8 @@ const getPrototypes: () => { unicode: BOOLEAN, // prop // ES2018 dotAll: BOOLEAN, // prop + // ES2022 + hasIndices: BOOLEAN, // prop [Symbol.match]: null, [Symbol.replace]: null, diff --git a/lib/utils/type-tracker/type-data/string.ts b/lib/utils/type-tracker/type-data/string.ts index e5ed41fc4..bef4b0d72 100644 --- a/lib/utils/type-tracker/type-data/string.ts +++ b/lib/utils/type-tracker/type-data/string.ts @@ -97,7 +97,7 @@ const getPrototypes: () => { trim: RETURN_STRING, substr: RETURN_STRING, valueOf: RETURN_STRING, - // ES2051 + // ES2015 codePointAt: RETURN_NUMBER, includes: RETURN_BOOLEAN, endsWith: RETURN_BOOLEAN, @@ -128,6 +128,8 @@ const getPrototypes: () => { trimEnd: RETURN_STRING, // ES2020 matchAll: null, // IterableIterator + // ES2021 + replaceAll: RETURN_STRING, // ES2022 at: RETURN_STRING, diff --git a/package-lock.json b/package-lock.json index ee54e7139..82a5c3f4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,17 @@ { "name": "eslint-plugin-regexp", - "version": "2.0.0-next.10", + "version": "2.0.0-next.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "eslint-plugin-regexp", - "version": "2.0.0-next.10", + "version": "2.0.0-next.12", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.9.1", "comment-parser": "^1.4.0", - "grapheme-splitter": "^1.0.4", "jsdoctypeparser": "^9.0.0", "refa": "^0.12.1", "regexp-ast-analysis": "^0.7.1", @@ -46,6 +45,7 @@ "eslint-plugin-regexp": "~1.15.0", "eslint-plugin-vue": "^9.0.0", "eslint-plugin-yml": "^1.0.0", + "intl-segmenter-polyfill": "^0.4.4", "markdownlint-cli": "^0.37.0", "mocha": "^10.0.0", "mocha-chai-jest-snapshot": "^1.1.3", @@ -5738,6 +5738,12 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==", + "dev": true + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -6197,7 +6203,8 @@ "node_modules/grapheme-splitter": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true }, "node_modules/graphemer": { "version": "1.4.0", @@ -6466,6 +6473,15 @@ "node": ">= 0.4" } }, + "node_modules/intl-segmenter-polyfill": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/intl-segmenter-polyfill/-/intl-segmenter-polyfill-0.4.4.tgz", + "integrity": "sha512-dIOcmvH+Q1WYGkjMqxPfaCgHEwOegH5UPcd/LLeaeY8aguHadC46MzGb40q8C1LrsuyJxJGKeKqoVtIh9ADRXQ==", + "dev": true, + "dependencies": { + "fast-text-encoding": "^1.0.2" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -15544,6 +15560,12 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==", + "dev": true + }, "fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -15882,7 +15904,8 @@ "grapheme-splitter": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true }, "graphemer": { "version": "1.4.0", @@ -16074,6 +16097,15 @@ "side-channel": "^1.0.4" } }, + "intl-segmenter-polyfill": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/intl-segmenter-polyfill/-/intl-segmenter-polyfill-0.4.4.tgz", + "integrity": "sha512-dIOcmvH+Q1WYGkjMqxPfaCgHEwOegH5UPcd/LLeaeY8aguHadC46MzGb40q8C1LrsuyJxJGKeKqoVtIh9ADRXQ==", + "dev": true, + "requires": { + "fast-text-encoding": "^1.0.2" + } + }, "is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", diff --git a/package.json b/package.json index b61e170f8..597ea2a79 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "eslint-plugin-regexp": "~1.15.0", "eslint-plugin-vue": "^9.0.0", "eslint-plugin-yml": "^1.0.0", + "intl-segmenter-polyfill": "^0.4.4", "markdownlint-cli": "^0.37.0", "mocha": "^10.0.0", "mocha-chai-jest-snapshot": "^1.1.3", @@ -105,7 +106,6 @@ "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.9.1", "comment-parser": "^1.4.0", - "grapheme-splitter": "^1.0.4", "jsdoctypeparser": "^9.0.0", "refa": "^0.12.1", "regexp-ast-analysis": "^0.7.1", diff --git a/tsconfig.json b/tsconfig.json index 934164df2..cb9003859 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "target": "es2015", "module": "Node16", "moduleResolution": "Node16", - "lib": ["es2020"], + "lib": ["es2023"], "allowJs": true, "checkJs": true, "outDir": "./dist",