diff --git a/docs/src/rules/id-length.md b/docs/src/rules/id-length.md index fb69db1850f..f0a5640ecb4 100644 --- a/docs/src/rules/id-length.md +++ b/docs/src/rules/id-length.md @@ -20,6 +20,8 @@ var x = 5; // too short; difficult to understand its purpose without context This rule enforces a minimum and/or maximum identifier length convention. +This rule counts [graphemes](https://unicode.org/reports/tr29/#Default_Grapheme_Cluster_Table) instead of using [`String length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length). + ## Options Examples of **incorrect** code for this rule with the default options: diff --git a/lib/rules/id-length.js b/lib/rules/id-length.js index 99f833fc73b..bd269b998f5 100644 --- a/lib/rules/id-length.js +++ b/lib/rules/id-length.js @@ -6,6 +6,45 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ +const GraphemeSplitter = require("grapheme-splitter"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks if the string given as argument is ASCII or not. + * @param {string} value A string that you want to know if it is ASCII or not. + * @returns {boolean} `true` if `value` is ASCII string. + */ +function isASCII(value) { + if (typeof value !== "string") { + return false; + } + return /^[\u0020-\u007f]*$/u.test(value); +} + +/** @type {GraphemeSplitter | undefined} */ +let splitter; + +/** + * Gets the length of the string. If the string is not in ASCII, counts graphemes. + * @param {string} value A string that you want to get the length. + * @returns {number} The length of `value`. + */ +function getStringLength(value) { + if (isASCII(value)) { + return value.length; + } + if (!splitter) { + splitter = new GraphemeSplitter(); + } + return splitter.countGraphemes(value); +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -130,8 +169,10 @@ module.exports = { const name = node.name; const parent = node.parent; - const isShort = name.length < minLength; - const isLong = name.length > maxLength; + const nameLength = getStringLength(name); + + const isShort = nameLength < minLength; + const isLong = nameLength > maxLength; if (!(isShort || isLong) || exceptions.has(name) || matchesExceptionPattern(name)) { return; // Nothing to report diff --git a/tests/lib/rules/id-length.js b/tests/lib/rules/id-length.js index e9a023bcfd1..871db908f92 100644 --- a/tests/lib/rules/id-length.js +++ b/tests/lib/rules/id-length.js @@ -113,6 +113,123 @@ ruleTester.run("id-length", rule, { code: "class Foo { #abc = 1 }", options: [{ max: 3 }], parserOptions: { ecmaVersion: 2022 } + }, + + // Identifier consisting of two code units + { + code: "var 𠮟 = 2", + options: [{ min: 1, max: 1 }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "var 葛󠄀 = 2", // 2 code points but only 1 grapheme + options: [{ min: 1, max: 1 }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "var a = { 𐌘: 1 };", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 6 + } + }, + { + code: "(𐌘) => { 𐌘 * 𐌘 };", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 6 + } + }, + { + code: "class 𠮟 { }", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 6 + } + }, + { + code: "class F { 𐌘() {} }", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 6 + } + }, + { + code: "class F { #𐌘() {} }", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 2022 + } + }, + { + code: "class F { 𐌘 = 1 }", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 2022 + } + }, + { + code: "class F { #𐌘 = 1 }", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 2022 + } + }, + { + code: "function f(...𐌘) { }", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 6 + } + }, + { + code: "function f([𐌘]) { }", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 6 + } + }, + { + code: "var [ 𐌘 ] = a;", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 6 + } + }, + { + code: "var { p: [𐌘]} = {};", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 6 + } + }, + { + code: "function f({𐌘}) { }", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 6 + } + }, + { + code: "var { 𐌘 } = {};", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 6 + } + }, + { + code: "var { p: 𐌘} = {};", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 6 + } + }, + { + code: "({ prop: o.𐌘 } = {});", + options: [{ min: 1, max: 1 }], + parserOptions: { + ecmaVersion: 6 + } } ], invalid: [ @@ -564,6 +681,157 @@ ruleTester.run("id-length", rule, { errors: [ tooLongErrorPrivate ] + }, + + // Identifier consisting of two code units + { + code: "var 𠮟 = 2", + parserOptions: { ecmaVersion: 6 }, + errors: [ + tooShortError + ] + }, + { + code: "var 葛󠄀 = 2", // 2 code points but only 1 grapheme + parserOptions: { ecmaVersion: 6 }, + errors: [ + tooShortError + ] + }, + { + code: "var myObj = { 𐌘: 1 };", + parserOptions: { + ecmaVersion: 6 + }, + errors: [ + tooShortError + ] + }, + { + code: "(𐌘) => { 𐌘 * 𐌘 };", + parserOptions: { + ecmaVersion: 6 + }, + errors: [ + tooShortError + ] + }, + { + code: "class 𠮟 { }", + parserOptions: { + ecmaVersion: 6 + }, + errors: [ + tooShortError + ] + }, + { + code: "class Foo { 𐌘() {} }", + parserOptions: { + ecmaVersion: 6 + }, + errors: [ + tooShortError + ] + }, + { + code: "class Foo1 { #𐌘() {} }", + parserOptions: { + ecmaVersion: 2022 + }, + errors: [ + tooShortErrorPrivate + ] + }, + { + code: "class Foo2 { 𐌘 = 1 }", + parserOptions: { + ecmaVersion: 2022 + }, + errors: [ + tooShortError + ] + }, + { + code: "class Foo3 { #𐌘 = 1 }", + parserOptions: { + ecmaVersion: 2022 + }, + errors: [ + tooShortErrorPrivate + ] + }, + { + code: "function foo1(...𐌘) { }", + parserOptions: { + ecmaVersion: 6 + }, + errors: [ + tooShortError + ] + }, + { + code: "function foo([𐌘]) { }", + parserOptions: { + ecmaVersion: 6 + }, + errors: [ + tooShortError + ] + }, + { + code: "var [ 𐌘 ] = arr;", + parserOptions: { + ecmaVersion: 6 + }, + errors: [ + tooShortError + ] + }, + { + code: "var { prop: [𐌘]} = {};", + parserOptions: { + ecmaVersion: 6 + }, + errors: [ + tooShortError + ] + }, + { + code: "function foo({𐌘}) { }", + parserOptions: { + ecmaVersion: 6 + }, + errors: [ + tooShortError + ] + }, + { + code: "var { 𐌘 } = {};", + parserOptions: { + ecmaVersion: 6 + }, + errors: [ + tooShortError + ] + }, + { + code: "var { prop: 𐌘} = {};", + parserOptions: { + ecmaVersion: 6 + }, + errors: [ + tooShortError + ] + }, + { + code: "({ prop: obj.𐌘 } = {});", + parserOptions: { + ecmaVersion: 6 + }, + errors: [ + tooShortError + ] } ] });