diff --git a/lib/rules/id-length.js b/lib/rules/id-length.js index bd269b998f5..87d23c9fb9c 100644 --- a/lib/rules/id-length.js +++ b/lib/rules/id-length.js @@ -9,41 +9,8 @@ //------------------------------------------------------------------------------ // 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); -} +const { getGraphemeCount } = require("../shared/string-utils"); //------------------------------------------------------------------------------ // Rule Definition @@ -169,7 +136,7 @@ module.exports = { const name = node.name; const parent = node.parent; - const nameLength = getStringLength(name); + const nameLength = getGraphemeCount(name); const isShort = nameLength < minLength; const isLong = nameLength > maxLength; diff --git a/lib/rules/key-spacing.js b/lib/rules/key-spacing.js index b0491b95fe3..743752d645f 100644 --- a/lib/rules/key-spacing.js +++ b/lib/rules/key-spacing.js @@ -9,13 +9,7 @@ //------------------------------------------------------------------------------ const astUtils = require("./utils/ast-utils"); -const GraphemeSplitter = require("grapheme-splitter"); - -const splitter = new GraphemeSplitter(); - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ +const { getGraphemeCount } = require("../shared/string-utils"); /** * Checks whether a string contains a line terminator as defined in @@ -523,7 +517,7 @@ module.exports = { const startToken = sourceCode.getFirstToken(property); const endToken = getLastTokenBeforeColon(property.key); - return splitter.countGraphemes(sourceCode.getText().slice(startToken.range[0], endToken.range[1])); + return getGraphemeCount(sourceCode.getText().slice(startToken.range[0], endToken.range[1])); } /** diff --git a/lib/shared/string-utils.js b/lib/shared/string-utils.js index e4a55d79931..84c88a55d43 100644 --- a/lib/shared/string-utils.js +++ b/lib/shared/string-utils.js @@ -5,6 +5,26 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const GraphemeSplitter = require("grapheme-splitter"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +// eslint-disable-next-line no-control-regex -- intentionally including control characters +const ASCII_REGEX = /^[\u0000-\u007f]*$/u; + +/** @type {GraphemeSplitter | undefined} */ +let splitter; + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + /** * Converts the first letter of a string to uppercase. * @param {string} string The string to operate on @@ -17,6 +37,24 @@ function upperCaseFirst(string) { return string[0].toUpperCase() + string.slice(1); } +/** + * Counts graphemes in a given string. + * @param {string} value A string to count graphemes. + * @returns {number} The number of graphemes in `value`. + */ +function getGraphemeCount(value) { + if (ASCII_REGEX.test(value)) { + return value.length; + } + + if (!splitter) { + splitter = new GraphemeSplitter(); + } + + return splitter.countGraphemes(value); +} + module.exports = { - upperCaseFirst + upperCaseFirst, + getGraphemeCount }; diff --git a/tests/lib/shared/string-utils.js b/tests/lib/shared/string-utils.js index bc48afaab4e..deacb54a575 100644 --- a/tests/lib/shared/string-utils.js +++ b/tests/lib/shared/string-utils.js @@ -11,7 +11,23 @@ const assert = require("chai").assert; -const { upperCaseFirst } = require("../../../lib/shared/string-utils"); +const { upperCaseFirst, getGraphemeCount } = require("../../../lib/shared/string-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Replaces raw control characters with the `\xXX` form. + * @param {string} text The text to process. + * @returns {string} `text` with escaped control characters. + */ +function escapeControlCharacters(text) { + return text.replace( + /[\u0000-\u001F\u007F-\u009F]/gu, // eslint-disable-line no-control-regex -- intentionally including control characters + c => `\\x${c.codePointAt(0).toString(16).padStart(2, "0")}` + ); +} //------------------------------------------------------------------------------ // Tests @@ -39,3 +55,34 @@ describe("upperCaseFirst", () => { assert(upperCaseFirst("") === ""); }); }); + +describe("getGraphemeCount", () => { + /* eslint-disable quote-props -- Make consistent here for readability */ + const expectedResults = { + "": 0, + "a": 1, + "ab": 2, + "aa": 2, + "123": 3, + "cccc": 4, + [Array.from({ length: 128 }, (_, i) => String.fromCharCode(i)).join("")]: 128, // all ASCII characters + "πŸ‘": 1, // 1 grapheme, 1 code point, 2 code units + "πŸ‘πŸ‘": 2, + "πŸ‘9πŸ‘": 3, + "aπŸ‘b": 3, + "πŸ‘ΆπŸ½": 1, // 1 grapheme, 2 code points, 4 code units + "πŸ‘¨β€πŸ‘©β€πŸ‘¦": 1, // 1 grapheme, 5 code points, 8 code units + "πŸ‘¨β€πŸ‘©β€πŸ‘¦πŸ‘¨β€πŸ‘©β€πŸ‘¦": 2, + "πŸ‘¨β€πŸ‘©β€πŸ‘¦aπŸ‘¨β€πŸ‘©β€πŸ‘¦": 3, + "aπŸ‘¨β€πŸ‘©β€πŸ‘¦bπŸ‘¨β€πŸ‘©β€πŸ‘¦c": 5, + "πŸ‘¨β€πŸ‘©β€πŸ‘¦πŸ‘": 2, + "πŸ‘ΆπŸ½πŸ‘¨β€πŸ‘©β€πŸ‘¦": 2 + }; + /* eslint-enable quote-props -- Make consistent here for readability */ + + Object.entries(expectedResults).forEach(([key, value]) => { + it(`should return ${value} for ${escapeControlCharacters(key)}`, () => { + assert.strictEqual(getGraphemeCount(key), value); + }); + }); +});