Repository: eslint-community/eslint-plugin-es-x
Pull request #65 (2023-03-15 05:56:41)

Add `no-regexp-unicode-property-escapes-2023` rule
This PR adds `es-x/no-regexp-unicode-property-escapes-2023` rule that disallow the new values of RegExp Unicode property escape sequences in ES2023.


Patch:
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 57adf55..d4065c9 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -10,6 +10,9 @@ ignorePatterns:
   - "!.vuepress"
   - /docs/.vuepress/dist
 
+globals:
+  fetch: readonly
+
 overrides:
   - files: lib/rules/**/*.js
     rules:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index e1dc66e..d64d720 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -16,6 +16,7 @@ There is a config that enables the rules in this category: `plugin:es-x/no-new-i
 | [es-x/no-array-prototype-tospliced](./no-array-prototype-tospliced.md) | disallow the `Array.prototype.toSpliced` method. |  |
 | [es-x/no-array-prototype-with](./no-array-prototype-with.md) | disallow the `Array.prototype.with` method. |  |
 | [es-x/no-hashbang](./no-hashbang.md) | disallow Hashbang comments. |  |
+| [es-x/no-regexp-unicode-property-escapes-2023](./no-regexp-unicode-property-escapes-2023.md) | disallow the new values of RegExp Unicode property escape sequences in ES2023. |  |
 
 ## ES2023 Intl API
 
diff --git a/docs/rules/no-regexp-unicode-property-escapes-2022.md b/docs/rules/no-regexp-unicode-property-escapes-2022.md
index b4d752e..c1bf10f 100644
--- a/docs/rules/no-regexp-unicode-property-escapes-2022.md
+++ b/docs/rules/no-regexp-unicode-property-escapes-2022.md
@@ -11,7 +11,7 @@ since: "v6.0.0"
 
 This rule reports the new values of ES2018 [RegExp Unicode property escape sequences](https://github.com/tc39/proposal-regexp-unicode-property-escapes#readme) which were added in ES2022.
 
-For example, the following patterns are valid in ES2022, but syntax error in ES2020 environments:
+For example, the following patterns are valid in ES2022, but syntax error in ES2021 environments:
 
 - `\p{Script=Cpmn}`
 - `\p{Script=Cypro_Minoan}`
diff --git a/docs/rules/no-regexp-unicode-property-escapes-2023.md b/docs/rules/no-regexp-unicode-property-escapes-2023.md
new file mode 100644
index 0000000..cb3e198
--- /dev/null
+++ b/docs/rules/no-regexp-unicode-property-escapes-2023.md
@@ -0,0 +1,38 @@
+---
+title: "es-x/no-regexp-unicode-property-escapes-2023"
+description: "disallow the new values of RegExp Unicode property escape sequences in ES2023"
+---
+
+# es-x/no-regexp-unicode-property-escapes-2023
+> disallow the new values of RegExp Unicode property escape sequences in ES2023
+
+- ❗ <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- ✅ The following configurations enable this rule: `plugin:es-x/no-new-in-esnext`
+
+This rule reports the new values of ES2018 [RegExp Unicode property escape sequences](https://github.com/tc39/proposal-regexp-unicode-property-escapes#readme) which were added in ES2023.
+
+For example, the following patterns are valid in ES2023, but syntax error in ES2022 environments:
+
+- `\p{Script=Kawi}`
+- `\p{Script=Nag_Mundari}`
+- `\p{Script=Nagm}`
+
+## 💡 Examples
+
+⛔ Examples of **incorrect** code for this rule:
+
+<eslint-playground type="bad">
+
+```js
+/*eslint es-x/no-regexp-unicode-property-escapes-2023: error */
+const r1 = /\p{Script=Kawi}/u
+const r2 = /\p{Script=Nag_Mundari}/u
+const r2 = /\p{Script=Nagm}/u
+```
+
+</eslint-playground>
+
+## 📚 References
+
+- [Rule source](https://github.com/eslint-community/eslint-plugin-es-x/blob/master/lib/rules/no-regexp-unicode-property-escapes-2023.js)
+- [Test source](https://github.com/eslint-community/eslint-plugin-es-x/blob/master/tests/lib/rules/no-regexp-unicode-property-escapes-2023.js)
diff --git a/lib/configs/no-new-in-esnext.js b/lib/configs/no-new-in-esnext.js
index 840c2f4..5095b7d 100644
--- a/lib/configs/no-new-in-esnext.js
+++ b/lib/configs/no-new-in-esnext.js
@@ -13,5 +13,6 @@ module.exports = {
         "es-x/no-array-prototype-tospliced": "error",
         "es-x/no-array-prototype-with": "error",
         "es-x/no-hashbang": "error",
+        "es-x/no-regexp-unicode-property-escapes-2023": "error",
     },
 }
diff --git a/lib/index.js b/lib/index.js
index 272af65..3cfb6d6 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -227,6 +227,7 @@ module.exports = {
         "no-regexp-unicode-property-escapes-2020": require("./rules/no-regexp-unicode-property-escapes-2020"),
         "no-regexp-unicode-property-escapes-2021": require("./rules/no-regexp-unicode-property-escapes-2021"),
         "no-regexp-unicode-property-escapes-2022": require("./rules/no-regexp-unicode-property-escapes-2022"),
+        "no-regexp-unicode-property-escapes-2023": require("./rules/no-regexp-unicode-property-escapes-2023"),
         "no-regexp-y-flag": require("./rules/no-regexp-y-flag"),
         "no-rest-parameters": require("./rules/no-rest-parameters"),
         "no-rest-spread-properties": require("./rules/no-rest-spread-properties"),
diff --git a/lib/rules/no-regexp-unicode-property-escapes-2023.js b/lib/rules/no-regexp-unicode-property-escapes-2023.js
new file mode 100644
index 0000000..c1084c6
--- /dev/null
+++ b/lib/rules/no-regexp-unicode-property-escapes-2023.js
@@ -0,0 +1,66 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+"use strict"
+
+const { defineRegExpHandler } = require("../util/define-regexp-handler")
+const {
+    scNameSet,
+    scValueSets,
+    binPropertySets,
+} = require("../util/unicode-properties")
+
+function isNewUnicodePropertyKeyValuePair(key, value) {
+    return scNameSet.has(key) && scValueSets.es2023.has(value)
+}
+
+function isNewBinaryUnicodeProperty(key) {
+    return binPropertySets.es2023.has(key)
+}
+
+module.exports = {
+    meta: {
+        docs: {
+            description:
+                "disallow the new values of RegExp Unicode property escape sequences in ES2023",
+            category: "ES2023",
+            recommended: false,
+            url: "http://eslint-community.github.io/eslint-plugin-es-x/rules/no-regexp-unicode-property-escapes-2023.html",
+        },
+        fixable: null,
+        messages: {
+            forbidden: "ES2023 '{{value}}' is forbidden.",
+        },
+        schema: [],
+        type: "problem",
+    },
+    create(context) {
+        return defineRegExpHandler(context, (node, { pattern }) => {
+            let foundValue = ""
+            return {
+                onUnicodePropertyCharacterSet(start, end, _kind, key, value) {
+                    if (foundValue) {
+                        return
+                    }
+                    if (
+                        value
+                            ? isNewUnicodePropertyKeyValuePair(key, value)
+                            : isNewBinaryUnicodeProperty(key)
+                    ) {
+                        foundValue = pattern.slice(start, end)
+                    }
+                },
+                onExit() {
+                    if (foundValue) {
+                        context.report({
+                            node,
+                            messageId: "forbidden",
+                            data: { value: foundValue },
+                        })
+                    }
+                },
+            }
+        })
+    },
+}
diff --git a/lib/util/unicode-properties.js b/lib/util/unicode-properties.js
index 4bfd838..a062aae 100644
--- a/lib/util/unicode-properties.js
+++ b/lib/util/unicode-properties.js
@@ -2,13 +2,14 @@
 "use strict"
 
 class DataSet {
-    constructor(raw2018, raw2019, raw2020, raw2021, raw2022, raw2023) {
+    constructor(raw2018, raw2019, raw2020, raw2021, raw2022, raw2023, raw2024) {
         this._raw2018 = raw2018
         this._raw2019 = raw2019
         this._raw2020 = raw2020
         this._raw2021 = raw2021
         this._raw2022 = raw2022
         this._raw2023 = raw2023
+        this._raw2024 = raw2024
     }
 
     get es2018() {
@@ -46,6 +47,12 @@ class DataSet {
             this._set2023 || (this._set2023 = new Set(this._raw2023.split(" ")))
         )
     }
+
+    get es2024() {
+        return (
+            this._set2024 || (this._set2024 = new Set(this._raw2024.split(" ")))
+        )
+    }
 }
 
 const gcNameSet = new Set(["General_Category", "gc"])
@@ -57,6 +64,7 @@ const gcValueSets = new DataSet(
     "",
     "",
     "",
+    "",
 )
 const scValueSets = new DataSet(
     "Adlam Adlm Aghb Ahom Anatolian_Hieroglyphs Arab Arabic Armenian Armi Armn Avestan Avst Bali Balinese Bamu Bamum Bass Bassa_Vah Batak Batk Beng Bengali Bhaiksuki Bhks Bopo Bopomofo Brah Brahmi Brai Braille Bugi Buginese Buhd Buhid Cakm Canadian_Aboriginal Cans Cari Carian Caucasian_Albanian Chakma Cham Cher Cherokee Common Copt Coptic Cprt Cuneiform Cypriot Cyrillic Cyrl Deseret Deva Devanagari Dsrt Dupl Duployan Egyp Egyptian_Hieroglyphs Elba Elbasan Ethi Ethiopic Geor Georgian Glag Glagolitic Gonm Goth Gothic Gran Grantha Greek Grek Gujarati Gujr Gurmukhi Guru Han Hang Hangul Hani Hano Hanunoo Hatr Hatran Hebr Hebrew Hira Hiragana Hluw Hmng Hung Imperial_Aramaic Inherited Inscriptional_Pahlavi Inscriptional_Parthian Ital Java Javanese Kaithi Kali Kana Kannada Katakana Kayah_Li Khar Kharoshthi Khmer Khmr Khoj Khojki Khudawadi Knda Kthi Lana Lao Laoo Latin Latn Lepc Lepcha Limb Limbu Lina Linb Linear_A Linear_B Lisu Lyci Lycian Lydi Lydian Mahajani Mahj Malayalam Mand Mandaic Mani Manichaean Marc Marchen Masaram_Gondi Meetei_Mayek Mend Mende_Kikakui Merc Mero Meroitic_Cursive Meroitic_Hieroglyphs Miao Mlym Modi Mong Mongolian Mro Mroo Mtei Mult Multani Myanmar Mymr Nabataean Narb Nbat New_Tai_Lue Newa Nko Nkoo Nshu Nushu Ogam Ogham Ol_Chiki Olck Old_Hungarian Old_Italic Old_North_Arabian Old_Permic Old_Persian Old_South_Arabian Old_Turkic Oriya Orkh Orya Osage Osge Osma Osmanya Pahawh_Hmong Palm Palmyrene Pau_Cin_Hau Pauc Perm Phag Phags_Pa Phli Phlp Phnx Phoenician Plrd Prti Psalter_Pahlavi Qaac Qaai Rejang Rjng Runic Runr Samaritan Samr Sarb Saur Saurashtra Sgnw Sharada Shavian Shaw Shrd Sidd Siddham SignWriting Sind Sinh Sinhala Sora Sora_Sompeng Soyo Soyombo Sund Sundanese Sylo Syloti_Nagri Syrc Syriac Tagalog Tagb Tagbanwa Tai_Le Tai_Tham Tai_Viet Takr Takri Tale Talu Tamil Taml Tang Tangut Tavt Telu Telugu Tfng Tglg Thaa Thaana Thai Tibetan Tibt Tifinagh Tirh Tirhuta Ugar Ugaritic Vai Vaii Wara Warang_Citi Xpeo Xsux Yi Yiii Zanabazar_Square Zanb Zinh Zyyy",
@@ -64,6 +72,7 @@ const scValueSets = new DataSet(
     "Elym Elymaic Hmnp Nand Nandinagari Nyiakeng_Puachue_Hmong Wancho Wcho",
     "Chorasmian Chrs Diak Dives_Akuru Khitan_Small_Script Kits Yezi Yezidi",
     "Cpmn Cypro_Minoan Old_Uyghur Ougr Tangsa Tnsa Toto Vith Vithkuqi",
+    "Hrkt Katakana_Or_Hiragana Kawi Nag_Mundari Nagm Unknown Zzzz",
     "",
 )
 const binPropertySets = new DataSet(
@@ -73,6 +82,7 @@ const binPropertySets = new DataSet(
     "EBase EComp EMod EPres ExtPict",
     "",
     "",
+    "",
 )
 module.exports = {
     gcNameSet,
diff --git a/package.json b/package.json
index 0138f7e..7d8f935 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,7 @@
   },
   "dependencies": {
     "@eslint-community/eslint-utils": "^4.1.2",
-    "@eslint-community/regexpp": "^4.2.0"
+    "@eslint-community/regexpp": "^4.5.0"
   },
   "devDependencies": {
     "@typescript-eslint/parser": "^5.14.0",
diff --git a/scripts/fetch-lines.js b/scripts/fetch-lines.js
new file mode 100644
index 0000000..7fd07a6
--- /dev/null
+++ b/scripts/fetch-lines.js
@@ -0,0 +1,8 @@
+"use strict"
+
+module.exports = { fetchLines }
+
+async function* fetchLines(url) {
+    const response = await fetch(url)
+    yield* (await response.text()).split("\n")
+}
diff --git a/scripts/update-unicode-properties.js b/scripts/update-unicode-properties.js
index 0de64ee..9594e97 100644
--- a/scripts/update-unicode-properties.js
+++ b/scripts/update-unicode-properties.js
@@ -4,6 +4,12 @@ const fs = require("fs")
 const path = require("path")
 const { JSDOM } = require("jsdom")
 const { ESLint } = require("eslint")
+const {
+    getLatestUnicodeGeneralCategoryValues,
+} = require("./get-latest-unicode-general-category-values")
+const {
+    getLatestUnicodeScriptValues,
+} = require("./get-latest-unicode-script-values")
 
 const DATA_SOURCES = [
     {
@@ -42,11 +48,18 @@ const DATA_SOURCES = [
         scValues: "#table-unicode-script-values",
     },
     {
-        url: "https://tc39.es/ecma262/multipage/text-processing.html",
+        url: "https://tc39.es/ecma262/2023/multipage/text-processing.html",
         version: 2023,
         binProperties: "#table-binary-unicode-properties",
-        gcValues: "#table-unicode-general-category-values",
-        scValues: "#table-unicode-script-values",
+        gcValues: getLatestUnicodeGeneralCategoryValues,
+        scValues: getLatestUnicodeScriptValues,
+    },
+    {
+        url: "https://tc39.es/ecma262/multipage/text-processing.html",
+        version: 2024,
+        binProperties: "#table-binary-unicode-properties",
+        gcValues: getLatestUnicodeGeneralCategoryValues,
+        scValues: getLatestUnicodeScriptValues,
     },
 ]
 const FILE_PATH = path.resolve(__dirname, "../lib/util/unicode-properties.js")
@@ -91,13 +104,21 @@ const logger = console
         } while (window == null)
 
         logger.log("Parsing tables")
-        datum.binProperties = collectValues(
+        datum.binProperties = await collectValues(
             window,
             binProperties,
             existing.binProperties,
         )
-        datum.gcValues = collectValues(window, gcValues, existing.gcValues)
-        datum.scValues = collectValues(window, scValues, existing.scValues)
+        datum.gcValues = await collectValues(
+            window,
+            gcValues,
+            existing.gcValues,
+        )
+        datum.scValues = await collectValues(
+            window,
+            scValues,
+            existing.scValues,
+        )
 
         logger.log("Done")
     }
@@ -136,28 +157,51 @@ module.exports = {gcNameSet, scNameSet, gcValueSets, scValueSets, binPropertySet
     process.exitCode = 1
 })
 
-function collectValues(window, id, existingSet) {
-    const selector = `${id} td:nth-child(1) code`
-    const nodes = window.document.querySelectorAll(selector)
-    const values = Array.from(nodes, (node) => node.textContent || "")
-        .filter((value) => {
-            if (existingSet.has(value)) {
-                return false
-            }
-            existingSet.add(value)
-            return true
-        })
-        .sort(undefined)
+async function collectValues(window, idSelectorOrProvider, existingSet) {
+    const getValues =
+        typeof idSelectorOrProvider === "function"
+            ? idSelectorOrProvider
+            : function* () {
+                  const selector = `${idSelectorOrProvider} td:nth-child(1) code`
+                  const nodes = window.document.querySelectorAll(selector)
+                  if (nodes.length === 0) {
+                      throw new Error(`No nodes found for selector ${selector}`)
+                  }
+                  logger.log(
+                      "%o nodes of %o were found.",
+                      nodes.length,
+                      selector,
+                  )
+                  for (const node of Array.from(nodes)) {
+                      yield node.textContent ?? ""
+                  }
+              }
+
+    const missing = new Set(existingSet)
+    const values = new Set()
+    let allCount = 0
+
+    for await (const value of getValues()) {
+        allCount++
+        missing.delete(value)
+        if (existingSet.has(value)) {
+            continue
+        }
+        existingSet.add(value)
+        values.add(value)
+    }
+
+    if (missing.size > 0) {
+        throw new Error(`Missing values: ${Array.from(missing).join(", ")}`)
+    }
 
     logger.log(
-        "%o nodes of %o were found, then %o adopted and %o ignored as duplication.",
-        nodes.length,
-        selector,
-        values.length,
-        nodes.length - values.length,
+        "%o adopted and %o ignored as duplication.",
+        values.size,
+        allCount - values.size,
     )
 
-    return values
+    return [...values].sort(undefined)
 }
 
 function makeClassDeclarationCode(versions) {


Test patch:
diff --git a/scripts/get-latest-unicode-general-category-values.js b/scripts/get-latest-unicode-general-category-values.js
new file mode 100644
index 0000000..87f70a0
--- /dev/null
+++ b/scripts/get-latest-unicode-general-category-values.js
@@ -0,0 +1,17 @@
+"use strict"
+
+const {
+    getLatestUnicodePropertyValues,
+} = require("./get-latest-unicode-property-values")
+
+module.exports = { getLatestUnicodeGeneralCategoryValues }
+
+async function* getLatestUnicodeGeneralCategoryValues() {
+    for await (const value of getLatestUnicodePropertyValues()) {
+        if (value.propertyAlias !== "gc") {
+            continue
+        }
+
+        yield* value.aliases
+    }
+}
diff --git a/scripts/get-latest-unicode-property-values.js b/scripts/get-latest-unicode-property-values.js
new file mode 100644
index 0000000..ae14ff9
--- /dev/null
+++ b/scripts/get-latest-unicode-property-values.js
@@ -0,0 +1,39 @@
+"use strict"
+
+const { fetchLines } = require("./fetch-lines")
+const DB_URL =
+    "https://unicode.org/Public/UCD/latest/ucd/PropertyValueAliases.txt"
+const logger = console
+
+let cache = undefined
+module.exports = { getLatestUnicodePropertyValues }
+
+async function* getLatestUnicodePropertyValues() {
+    logger.log("Fetching data... (%s)", DB_URL)
+    const iterable = cache
+        ? cache
+        : (async function* () {
+              const newCache = []
+              for await (const line of fetchLines(DB_URL)) {
+                  if (!line || line.startsWith("#")) {
+                      continue
+                  }
+                  const [propertyAlias, alias, canonical, ...remaining] = line
+                      .split("#")[0] // strip comments
+                      .split(";") // split by semicolon
+                      .map((x) => x.trim()) // trim
+
+                  const value = {
+                      propertyAlias,
+                      aliases: [canonical, alias, ...remaining],
+                      canonical,
+                  }
+                  newCache.push(value)
+                  yield value
+              }
+              cache = newCache
+          })()
+    for await (const value of iterable) {
+        yield value
+    }
+}
diff --git a/scripts/get-latest-unicode-script-values.js b/scripts/get-latest-unicode-script-values.js
new file mode 100644
index 0000000..2b87c0c
--- /dev/null
+++ b/scripts/get-latest-unicode-script-values.js
@@ -0,0 +1,17 @@
+"use strict"
+
+const {
+    getLatestUnicodePropertyValues,
+} = require("./get-latest-unicode-property-values")
+
+module.exports = { getLatestUnicodeScriptValues }
+
+async function* getLatestUnicodeScriptValues() {
+    for await (const value of getLatestUnicodePropertyValues()) {
+        if (value.propertyAlias !== "sc") {
+            continue
+        }
+
+        yield* value.aliases
+    }
+}
diff --git a/tests/lib/rules/no-regexp-unicode-property-escapes-2023.js b/tests/lib/rules/no-regexp-unicode-property-escapes-2023.js
new file mode 100644
index 0000000..b3e0b40
--- /dev/null
+++ b/tests/lib/rules/no-regexp-unicode-property-escapes-2023.js
@@ -0,0 +1,49 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+"use strict"
+
+const RuleTester = require("../../tester")
+const rule = require("../../../lib/rules/no-regexp-unicode-property-escapes-2023.js")
+
+if (!RuleTester.isSupported(2023)) {
+    //eslint-disable-next-line no-console
+    console.log("Skip the tests of no-regexp-unicode-property-escapes-2023.")
+    return
+}
+
+new RuleTester().run("no-regexp-unicode-property-escapes-2023", rule, {
+    valid: [
+        String.raw`/\p{Letter}/u`,
+        String.raw`/\P{Letter}/u`,
+        String.raw`/\p{Script=Hiragana}/u`,
+        String.raw`/\P{Script=Hiragana}/u`,
+        String.raw`/\p{Extended_Pictographic}/`,
+        String.raw`/\P{Extended_Pictographic}/`,
+        String.raw`/\p{Script=Dogr}/`,
+        String.raw`/\P{Script=Dogr}/`,
+        String.raw`new RegExp('\\p{Extended_Pictographic}')`,
+        String.raw`new RegExp('\\\\p{Extended_Pictographic}', 'u')`,
+        String.raw`/\p{Extended_Pictographic}/u`,
+        String.raw`/\p{Script=Dogr}/u`,
+        String.raw`/\p{Script=Elym}/u`,
+        String.raw`/\p{EBase}/u`,
+        String.raw`/\p{Script=Chorasmian}/u`,
+        String.raw`/\p{Script=Cpmn}/u`,
+    ],
+    invalid: [
+        {
+            code: String.raw`/\p{Script=Kawi}/u`,
+            errors: ["ES2023 '\\p{Script=Kawi}' is forbidden."],
+        },
+        {
+            code: String.raw`/\p{Script=Nag_Mundari}/u`,
+            errors: ["ES2023 '\\p{Script=Nag_Mundari}' is forbidden."],
+        },
+        {
+            code: String.raw`/\p{Script=Nagm}/u`,
+            errors: ["ES2023 '\\p{Script=Nagm}' is forbidden."],
+        },
+    ],
+})