From b4b8585b1906eed44af48aff0e7f5e1d01fa0754 Mon Sep 17 00:00:00 2001 From: Rantetsu Inori Date: Tue, 18 Feb 2025 01:15:59 +0800 Subject: [PATCH 1/8] `escape-case`: Support lowercase --- docs/rules/escape-case.md | 28 ++- docs/rules/number-literal-case.md | 95 ++++++- rules/escape-case.js | 33 ++- test/escape-case.js | 395 ++++++++++++++++++++++++++---- 4 files changed, 492 insertions(+), 59 deletions(-) diff --git a/docs/rules/escape-case.md b/docs/rules/escape-case.md index 18b9782ebe..195a11d4a3 100644 --- a/docs/rules/escape-case.md +++ b/docs/rules/escape-case.md @@ -7,7 +7,7 @@ -Enforces defining escape sequence values with uppercase characters rather than lowercase ones. This promotes readability by making the escaped value more distinguishable from the identifier. +Enforces a consistent escaped value style by defining escape sequence values with uppercase or lowercase characters. The default style is uppercase, which promotes readability by making the escaped value more distinguishable from the identifier. ## Fail @@ -26,3 +26,29 @@ const foo = '\uD834'; const foo = '\u{1D306}'; const foo = '\cA'; ``` + +## Options + +Type: `string`\ +Default: `'uppercase'` + +- `'uppercase'` (default) + - Always use escape sequence values with uppercase characters. +- `'lowercase'` + - Always use escape sequence values with lowercase characters. + +```js +// eslint unicorn/escape-case: ["error", "lowercase"] + +// Fail +const foo = '\xA9'; +const foo = '\uD834'; +const foo = '\u{1D306}'; +const foo = '\cA'; + +// Pass +const foo = '\xa9'; +const foo = '\ud834'; +const foo = '\u{1d306}'; +const foo = '\ca'; +``` diff --git a/docs/rules/number-literal-case.md b/docs/rules/number-literal-case.md index dbb092416e..b64c7b9fb1 100644 --- a/docs/rules/number-literal-case.md +++ b/docs/rules/number-literal-case.md @@ -7,7 +7,7 @@ -Differentiating the casing of the identifier and value clearly separates them and makes your code more readable. +Differentiating the casing of the identifier and value clearly separates them and makes your code more readable. The default style is: - Lowercase identifier and uppercase value for [`Number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) and [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#BigInt_type). - Lowercase `e` for exponential notation. @@ -64,3 +64,96 @@ const foo = 0xFFn; ```js const foo = 2e+5; ``` + +## Options + +Type: `object` + +Default options: + +```js +{ + 'unicorn/number-literal-case': [ + 'error', + { + hexadecimalValue: true, + radixIdentifier: false, + exponentialNotation: false + } + ] +} +``` + +### hexadecimalValue + +Type: `'uppercase' | 'lowercase' | 'ignore'`\ +Default: `'uppercase'` + +Specify whether the hexadecimal number value (ABCDEF) should be in `uppercase`, `lowercase`, or `ignore` the check. Defaults to `'uppercase'`. + +Example: +```js +// eslint unicorn/number-literal-case: ["error", {"hexadecimalValue": "lowercase"}] + +// Fail +const foo = 0XFF; +const foo = 0xFF; +const foo = 0XFFn; +const foo = 0xFFn; + +// Pass +const foo = 0Xff; +const foo = 0xff; +const foo = 0Xffn; +const foo = 0xffn; +``` + +### radixIdentifier + +Type: `'uppercase' | 'lowercase' | 'ignore'`\ +Default: `'lowercase'` + +Specify whether the radix indentifer (`0x`, `0o`, `0b`) should be in `uppercase`, `lowercase`, or `ignore` the check. Defaults to `'lowercase'`. + +Example: +```js +// eslint unicorn/number-literal-case: ["error", {"radixIdentifier": "uppercase"}] + +// Fail +const foo = 0xFF; +const foo = 0o76; +const foo = 0b10; +const foo = 0xFFn; +const foo = 0o76n; +const foo = 0b10n; + +// Pass +const foo = 0XFF; +const foo = 0O76; +const foo = 0B10; +const foo = 0XFFn; +const foo = 0O76n; +const foo = 0B10n; +``` + +### exponentialNotation + +Type: `'uppercase' | 'lowercase' | 'ignore'`\ +Default: `'lowercase'` + +Specify whether the exponential notation (`e`) should be in `uppercase`, `lowercase`, or `ignore` the check. Defaults to `'lowercase'`. + +Example: +```js +// eslint unicorn/number-literal-case: ["error", {"exponentialNotation": "uppercase"}] + +// Fail +const foo = 2e-5; +const foo = 2e+5; +const foo = 2e99; + +// Pass +const foo = 2E-5; +const foo = 2E+5; +const foo = 2E99; +``` diff --git a/rules/escape-case.js b/rules/escape-case.js index 72b7c4d8f9..90c1a5e120 100644 --- a/rules/escape-case.js +++ b/rules/escape-case.js @@ -1,20 +1,22 @@ import {replaceTemplateElement} from './fix/index.js'; import {isRegexLiteral, isStringLiteral, isTaggedTemplateLiteral} from './ast/index.js'; -const MESSAGE_ID = 'escape-case'; +const MESSAGE_ID_UPPERCASE = 'escape-uppercase'; +const MESSAGE_ID_LOWERCASE = 'escape-lowercase'; const messages = { - [MESSAGE_ID]: 'Use uppercase characters for the value of the escape sequence.', + [MESSAGE_ID_UPPERCASE]: 'Use uppercase characters for the value of the escape sequence.', + [MESSAGE_ID_LOWERCASE]: 'Use lowercase characters for the value of the escape sequence.', }; -const escapeWithLowercase = /(?<=(?:^|[^\\])(?:\\\\)*\\)(?x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|u{[\dA-Fa-f]+})/g; -const escapePatternWithLowercase = /(?<=(?:^|[^\\])(?:\\\\)*\\)(?x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|u{[\dA-Fa-f]+}|c[a-z])/g; -const getProblem = ({node, original, regex = escapeWithLowercase, fix}) => { - const fixed = original.replace(regex, data => data.slice(0, 1) + data.slice(1).toUpperCase()); +const escapeCase = /(?<=(?:^|[^\\])(?:\\\\)*\\)(?x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|u{[\dA-Fa-f]+})/g; +const escapePatternCase = /(?<=(?:^|[^\\])(?:\\\\)*\\)(?x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|u{[\dA-Fa-f]+}|c[A-Za-z])/g; +const getProblem = ({node, original, regex = escapeCase, lowercase, fix}) => { + const fixed = original.replace(regex, data => data[0] + data.slice(1)[lowercase ? 'toLowerCase' : 'toUpperCase']()); if (fixed !== original) { return { node, - messageId: MESSAGE_ID, + messageId: lowercase ? MESSAGE_ID_LOWERCASE : MESSAGE_ID_UPPERCASE, fix: fixer => fix ? fix(fixer, fixed) : fixer.replaceText(node, fixed), }; } @@ -22,11 +24,14 @@ const getProblem = ({node, original, regex = escapeWithLowercase, fix}) => { /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { + const lowercase = context.options[0] === 'lowercase'; + context.on('Literal', node => { if (isStringLiteral(node)) { return getProblem({ node, original: node.raw, + lowercase, }); } }); @@ -36,7 +41,8 @@ const create = context => { return getProblem({ node, original: node.raw, - regex: escapePatternWithLowercase, + regex: escapePatternCase, + lowercase, }); } }); @@ -49,21 +55,30 @@ const create = context => { return getProblem({ node, original: node.value.raw, + lowercase, fix: (fixer, fixed) => replaceTemplateElement(fixer, node, fixed), }); }); }; +const schema = [ + { + enum: ['uppercase', 'lowercase'], + }, +]; + /** @type {import('eslint').Rule.RuleModule} */ const config = { create, meta: { type: 'suggestion', docs: { - description: 'Require escape sequences to use uppercase values.', + description: 'Require escape sequences to use uppercase or lowercase values.', recommended: true, }, fixable: 'code', + schema, + defaultOptions: ['uppercase'], messages, }, }; diff --git a/test/escape-case.js b/test/escape-case.js index 22e8f4ef96..2d205c018f 100644 --- a/test/escape-case.js +++ b/test/escape-case.js @@ -3,12 +3,16 @@ import {getTester} from './utils/test.js'; const {test} = getTester(import.meta); -const errors = [ - { - messageId: 'escape-case', - }, -]; +const uppercaseTest = { + errors: [{messageId: 'escape-uppercase'}], +}; +const lowercaseTest = { + options: ['lowercase'], + errors: [{messageId: 'escape-lowercase'}], +}; + +// 'uppercase' test({ valid: [ // Literal string @@ -66,239 +70,534 @@ test({ // Literal string { code: String.raw`const foo = "\xa9";`, - errors, output: String.raw`const foo = "\xA9";`, + ...uppercaseTest, }, // Mixed cases { code: String.raw`const foo = "\xAa";`, - errors, output: String.raw`const foo = "\xAA";`, + ...uppercaseTest, }, { code: String.raw`const foo = "\uAaAa";`, - errors, output: String.raw`const foo = "\uAAAA";`, + ...uppercaseTest, }, { code: String.raw`const foo = "\u{AaAa}";`, - errors, output: String.raw`const foo = "\u{AAAA}";`, + ...uppercaseTest, }, // Many { code: String.raw`const foo = "\xAab\xaab\xAAb\uAaAab\uaaaab\uAAAAb\u{AaAa}b\u{aaaa}b\u{AAAA}";`, - errors, output: String.raw`const foo = "\xAAb\xAAb\xAAb\uAAAAb\uAAAAb\uAAAAb\u{AAAA}b\u{AAAA}b\u{AAAA}";`, + ...uppercaseTest, }, { code: String.raw`const foo = "\ud834";`, - errors, output: String.raw`const foo = "\uD834";`, + ...uppercaseTest, }, { code: String.raw`const foo = "\u{1d306}";`, - errors, output: String.raw`const foo = "\u{1D306}";`, + ...uppercaseTest, }, { code: String.raw`const foo = "\ud834foo";`, - errors, output: String.raw`const foo = "\uD834foo";`, + ...uppercaseTest, }, { code: String.raw`const foo = "foo\ud834";`, - errors, output: String.raw`const foo = "foo\uD834";`, + ...uppercaseTest, }, { code: String.raw`const foo = "foo \ud834";`, - errors, output: String.raw`const foo = "foo \uD834";`, + ...uppercaseTest, }, { code: String.raw`const foo = "\\\ud834foo";`, - errors, output: String.raw`const foo = "\\\uD834foo";`, + ...uppercaseTest, }, { code: String.raw`const foo = "foo\\\ud834";`, - errors, output: String.raw`const foo = "foo\\\uD834";`, + ...uppercaseTest, }, { code: String.raw`const foo = "foo \\\ud834";`, - errors, output: String.raw`const foo = "foo \\\uD834";`, + ...uppercaseTest, }, // TemplateLiteral { code: 'const foo = `\\xa9`;', - errors, output: 'const foo = `\\xA9`;', + ...uppercaseTest, }, { code: 'const foo = `\\ud834`;', - errors, output: 'const foo = `\\uD834`;', + ...uppercaseTest, }, { code: 'const foo = `\\u{1d306}`;', - errors, output: 'const foo = `\\u{1D306}`;', + ...uppercaseTest, }, { code: 'const foo = `\\ud834foo`;', - errors, output: 'const foo = `\\uD834foo`;', + ...uppercaseTest, }, { code: 'const foo = `foo\\ud834`;', - errors, output: 'const foo = `foo\\uD834`;', + ...uppercaseTest, }, { code: 'const foo = `foo \\ud834`;', - errors, output: 'const foo = `foo \\uD834`;', + ...uppercaseTest, }, { code: 'const foo = `${"\ud834 foo"} \\ud834`;', - errors, output: 'const foo = `${"\uD834 foo"} \\uD834`;', + ...uppercaseTest, }, { code: 'const foo = `\\ud834${foo}\\ud834${foo}\\ud834`;', - errors: Array.from({length: 3}, () => errors[0]), output: 'const foo = `\\uD834${foo}\\uD834${foo}\\uD834`;', + ...uppercaseTest, + errors: Array.from({length: 3}, () => uppercaseTest.errors[0]), }, { code: 'const foo = `\\\\\\ud834foo`;', - errors, output: 'const foo = `\\\\\\uD834foo`;', + ...uppercaseTest, }, { code: 'const foo = `foo\\\\\\ud834`;', - errors, output: 'const foo = `foo\\\\\\uD834`;', + ...uppercaseTest, }, { code: 'const foo = `foo \\\\\\ud834`;', - errors, output: 'const foo = `foo \\\\\\uD834`;', + ...uppercaseTest, }, // TODO: This is not safe, it will be broken if `tagged` uses `arguments[0].raw` // #2341 { code: 'const foo = tagged`\\uAaAa`;', - errors, output: 'const foo = tagged`\\uAAAA`;', + ...uppercaseTest, }, { code: 'const foo = `\\uAaAa```;', - errors, output: 'const foo = `\\uAAAA```;', + ...uppercaseTest, }, // Mixed cases { code: 'const foo = `\\xAa`;', - errors, output: 'const foo = `\\xAA`;', + ...uppercaseTest, }, { code: 'const foo = `\\uAaAa`;', - errors, output: 'const foo = `\\uAAAA`;', + ...uppercaseTest, }, { code: 'const foo = `\\u{AaAa}`;', - errors, output: 'const foo = `\\u{AAAA}`;', + ...uppercaseTest, }, // Many { code: 'const foo = `\\xAab\\xaab\\xAA${foo}\\uAaAab\\uaaaab\\uAAAAb\\u{AaAa}${foo}\\u{aaaa}b\\u{AAAA}`;', - errors: Array.from({length: 3}, () => errors[0]), output: 'const foo = `\\xAAb\\xAAb\\xAA${foo}\\uAAAAb\\uAAAAb\\uAAAAb\\u{AAAA}${foo}\\u{AAAA}b\\u{AAAA}`;', + ...uppercaseTest, + errors: Array.from({length: 3}, () => uppercaseTest.errors[0]), }, // Literal regex { code: String.raw`const foo = /\xa9/;`, - errors, output: String.raw`const foo = /\xA9/;`, + ...uppercaseTest, }, { code: String.raw`const foo = /\ud834/`, - errors, output: String.raw`const foo = /\uD834/`, + ...uppercaseTest, }, { code: String.raw`const foo = /\u{1d306}/u`, - errors, output: String.raw`const foo = /\u{1D306}/u`, + ...uppercaseTest, }, { code: String.raw`const foo = /\ca/`, - errors, output: String.raw`const foo = /\cA/`, + ...uppercaseTest, }, { code: String.raw`const foo = /foo\\\xa9/;`, - errors, output: String.raw`const foo = /foo\\\xA9/;`, + ...uppercaseTest, }, { code: String.raw`const foo = /foo\\\\\xa9/;`, - errors, output: String.raw`const foo = /foo\\\\\xA9/;`, + ...uppercaseTest, }, // Mixed cases { code: String.raw`const foo = /\xAa/;`, - errors, output: String.raw`const foo = /\xAA/;`, + ...uppercaseTest, }, { code: String.raw`const foo = /\uAaAa/;`, - errors, output: String.raw`const foo = /\uAAAA/;`, + ...uppercaseTest, }, { code: String.raw`const foo = /\u{AaAa}/;`, - errors, output: String.raw`const foo = /\u{AAAA}/;`, + ...uppercaseTest, }, // Many { code: String.raw`const foo = /\xAab\xaab\xAAb\uAaAab\uaaaab\uAAAAb\u{AaAa}b\u{aaaa}b\u{AAAA}b\ca/;`, - errors, output: String.raw`const foo = /\xAAb\xAAb\xAAb\uAAAAb\uAAAAb\uAAAAb\u{AAAA}b\u{AAAA}b\u{AAAA}b\cA/;`, + ...uppercaseTest, }, // RegExp { code: String.raw`const foo = new RegExp("/\xa9")`, - errors, output: String.raw`const foo = new RegExp("/\xA9")`, + ...uppercaseTest, }, { code: String.raw`const foo = new RegExp("/\ud834/")`, - errors, output: String.raw`const foo = new RegExp("/\uD834/")`, + ...uppercaseTest, }, { code: String.raw`const foo = new RegExp("/\u{1d306}/", "u")`, - errors, output: String.raw`const foo = new RegExp("/\u{1D306}/", "u")`, + ...uppercaseTest, + }, + ], +}); + +// 'lowercase' +test({ + valid: [ + // Literal string + String.raw`const foo = "\xa9";`, + String.raw`const foo = "\ud834";`, + String.raw`const foo = "\u{1d306}";`, + String.raw`const foo = "\ud834foo";`, + String.raw`const foo = "foo\ud834";`, + String.raw`const foo = "foo \ud834";`, + String.raw`const foo = "foo\\xBAR";`, + String.raw`const foo = "foo\\uBARBAZ";`, + String.raw`const foo = "foo\\\\xBAR";`, + String.raw`const foo = "foo\\\\uBARBAZ";`, + String.raw`const foo = "\cA";`, + + // TemplateLiteral + 'const foo = `\\xa9`;', + 'const foo = `\\ud834`;', + 'const foo = `\\u{1d306}`;', + 'const foo = `\\ud834FOO`;', + 'const foo = `foo\\ud834`;', + 'const foo = `foo \\ud834`;', + 'const foo = `${"\ud834 foo"} \\ud834`;', + 'const foo = `foo\\\\xBAR`;', + 'const foo = `foo\\\\uBARBAZ`;', + 'const foo = `foo\\\\\\\\xBAR`;', + 'const foo = `foo\\\\\\\\uBARBAZ`;', + 'const foo = `\\ca`;', + 'const foo = String.raw`\\uAaAa`;', + + // Literal regex + String.raw`const foo = /foo\xa9/`, + String.raw`const foo = /foo\ud834/`, + String.raw`const foo = /foo\u{1d306}/u`, + String.raw`const foo = /foo\ca/`, + // Escape + String.raw`const foo = /foo\\xA9/;`, + String.raw`const foo = /foo\\\\xA9/;`, + String.raw`const foo = /foo\\ud834/`, + String.raw`const foo = /foo\\u{1}/u`, + String.raw`const foo = /foo\\ca/`, + + // RegExp + String.raw`const foo = new RegExp("/\xa9")`, + String.raw`const foo = new RegExp("/\ud834/")`, + String.raw`const foo = new RegExp("/\u{1d306}/", "u")`, + String.raw`const foo = new RegExp("/\ca/")`, + String.raw`const foo = new RegExp("/\cA/")`, + ].map(code => ({code, options: lowercaseTest.options})), + invalid: [ + // Literal string + { + code: String.raw`const foo = "\xA9";`, + output: String.raw`const foo = "\xa9";`, + ...lowercaseTest, + }, + + // Mixed cases + { + code: String.raw`const foo = "\xaA";`, + output: String.raw`const foo = "\xaa";`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = "\uaAaA";`, + output: String.raw`const foo = "\uaaaa";`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = "\u{aAaA}";`, + output: String.raw`const foo = "\u{aaaa}";`, + ...lowercaseTest, + }, + + // Many + { + code: String.raw`const foo = "\xAab\xaAb\xaab\xAAb\uAaAab\uaaaab\uAAAAb\u{AaAa}b\u{aaaa}b\u{AAAA}";`, + output: String.raw`const foo = "\xaab\xaab\xaab\xaab\uaaaab\uaaaab\uaaaab\u{aaaa}b\u{aaaa}b\u{aaaa}";`, + ...lowercaseTest, + }, + + { + code: String.raw`const foo = "\uD834";`, + output: String.raw`const foo = "\ud834";`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = "\u{1D306}";`, + output: String.raw`const foo = "\u{1d306}";`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = "\uD834FOO";`, + output: String.raw`const foo = "\ud834FOO";`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = "FOO\uD834";`, + output: String.raw`const foo = "FOO\ud834";`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = "FOO \uD834";`, + output: String.raw`const foo = "FOO \ud834";`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = "\\\uD834FOO";`, + output: String.raw`const foo = "\\\ud834FOO";`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = "FOO\\\uD834";`, + output: String.raw`const foo = "FOO\\\ud834";`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = "FOO \\\uD834";`, + output: String.raw`const foo = "FOO \\\ud834";`, + ...lowercaseTest, + }, + + // TemplateLiteral + { + code: 'const foo = `\\xA9`;', + output: 'const foo = `\\xa9`;', + ...lowercaseTest, + }, + { + code: 'const foo = `\\uD834`;', + output: 'const foo = `\\ud834`;', + ...lowercaseTest, + }, + { + code: 'const foo = `\\u{1D306}`;', + output: 'const foo = `\\u{1d306}`;', + ...lowercaseTest, + }, + { + code: 'const foo = `\\uD834FOO`;', + output: 'const foo = `\\ud834FOO`;', + ...lowercaseTest, + }, + { + code: 'const foo = `FOO\\uD834`;', + output: 'const foo = `FOO\\ud834`;', + ...lowercaseTest, + }, + { + code: 'const foo = `FOO \\uD834`;', + output: 'const foo = `FOO \\ud834`;', + ...lowercaseTest, + }, + { + code: 'const foo = `${"\uD834 FOO"} \\uD834`;', + output: 'const foo = `${"\ud834 FOO"} \\ud834`;', + ...lowercaseTest, + }, + { + code: 'const foo = `\\uD834${FOO}\\uD834${FOO}\\uD834`;', + output: 'const foo = `\\ud834${FOO}\\ud834${FOO}\\ud834`;', + ...lowercaseTest, + errors: Array.from({length: 3}, () => lowercaseTest.errors[0]), + }, + { + code: 'const foo = `\\\\\\uD834FOO`;', + output: 'const foo = `\\\\\\ud834FOO`;', + ...lowercaseTest, + }, + { + code: 'const foo = `FOO\\\\\\uD834`;', + output: 'const foo = `FOO\\\\\\ud834`;', + ...lowercaseTest, + }, + { + code: 'const foo = `FOO \\\\\\uD834`;', + output: 'const foo = `FOO \\\\\\ud834`;', + ...lowercaseTest, + }, + // TODO: This is not safe, it will be broken if `tagged` uses `arguments[0].raw` + // #2341 + { + code: 'const foo = tagged`\\uaAaA`;', + output: 'const foo = tagged`\\uaaaa`;', + ...lowercaseTest, + }, + { + code: 'const foo = `\\uaAaA```;', + output: 'const foo = `\\uaaaa```;', + ...lowercaseTest, + }, + + // Mixed cases + { + code: 'const foo = `\\xaA`;', + output: 'const foo = `\\xaa`;', + ...lowercaseTest, + }, + { + code: 'const foo = `\\uaAaA`;', + output: 'const foo = `\\uaaaa`;', + ...lowercaseTest, + }, + { + code: 'const foo = `\\u{aAaA}`;', + output: 'const foo = `\\u{aaaa}`;', + ...lowercaseTest, + }, + + // Many + { + code: 'const foo = `\\xAab\\xaab\\xaAb\\xAA${foo}\\uAaAab\\uaaaab\\uAAAAb\\u{AaAa}${foo}\\u{aaaa}b\\u{AAAA}`;', + output: 'const foo = `\\xaab\\xaab\\xaab\\xaa${foo}\\uaaaab\\uaaaab\\uaaaab\\u{aaaa}${foo}\\u{aaaa}b\\u{aaaa}`;', + ...lowercaseTest, + errors: Array.from({length: 3}, () => lowercaseTest.errors[0]), + }, + + // Literal regex + { + code: String.raw`const foo = /\xA9/;`, + output: String.raw`const foo = /\xa9/;`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = /\uD834/`, + output: String.raw`const foo = /\ud834/`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = /\u{1D306}/u`, + output: String.raw`const foo = /\u{1d306}/u`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = /\cA/`, + output: String.raw`const foo = /\ca/`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = /FOO\\\xA9/;`, + output: String.raw`const foo = /FOO\\\xa9/;`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = /FOO\\\\\xA9/;`, + output: String.raw`const foo = /FOO\\\\\xa9/;`, + ...lowercaseTest, + }, + + // Mixed cases + { + code: String.raw`const foo = /\xaA/;`, + output: String.raw`const foo = /\xaa/;`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = /\uaAaA/;`, + output: String.raw`const foo = /\uaaaa/;`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = /\u{aAaA}/;`, + output: String.raw`const foo = /\u{aaaa}/;`, + ...lowercaseTest, + }, + + // Many + { + code: String.raw`const foo = /\xAab\xaAb\xaab\xAAb\uAaAab\uaaaab\uAAAAb\u{AaAa}b\u{aaaa}b\u{AAAA}b\cA/;`, + output: String.raw`const foo = /\xaab\xaab\xaab\xaab\uaaaab\uaaaab\uaaaab\u{aaaa}b\u{aaaa}b\u{aaaa}b\ca/;`, + ...lowercaseTest, + }, + + // RegExp + { + code: String.raw`const foo = new RegExp("/\xA9")`, + output: String.raw`const foo = new RegExp("/\xa9")`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = new RegExp("/\uD834/")`, + output: String.raw`const foo = new RegExp("/\ud834/")`, + ...lowercaseTest, + }, + { + code: String.raw`const foo = new RegExp("/\u{1D306}/", "u")`, + output: String.raw`const foo = new RegExp("/\u{1d306}/", "u")`, + ...lowercaseTest, }, ], }); From 9a432b6f66d5aa6c123c35152946f9d6cdb7df7f Mon Sep 17 00:00:00 2001 From: Rantetsu Inori Date: Tue, 18 Feb 2025 02:41:30 +0800 Subject: [PATCH 2/8] `number-literal-case`: Support different cases --- docs/rules/number-literal-case.md | 6 +- rules/number-literal-case.js | 79 ++++++++++++++++++++-- test/number-literal-case.js | 107 +++++++++++++++++++++++++----- 3 files changed, 167 insertions(+), 25 deletions(-) diff --git a/docs/rules/number-literal-case.md b/docs/rules/number-literal-case.md index b64c7b9fb1..d3284df31e 100644 --- a/docs/rules/number-literal-case.md +++ b/docs/rules/number-literal-case.md @@ -76,9 +76,9 @@ Default options: 'unicorn/number-literal-case': [ 'error', { - hexadecimalValue: true, - radixIdentifier: false, - exponentialNotation: false + hexadecimalValue: 'uppercase', + radixIdentifier: 'lowercase', + exponentialNotation: 'lowercase' } ] } diff --git a/rules/number-literal-case.js b/rules/number-literal-case.js index da71d35759..be85c4ad87 100644 --- a/rules/number-literal-case.js +++ b/rules/number-literal-case.js @@ -6,25 +6,62 @@ const messages = { [MESSAGE_ID]: 'Invalid number literal casing.', }; -const fix = raw => { - let fixed = raw.toLowerCase(); - if (fixed.startsWith('0x')) { - fixed = '0x' + fixed.slice(2).toUpperCase(); +/** + @param {string} raw + @param {Options[keyof Options]} option + */ +const convertCase = (raw, option) => { + if (option === 'uppercase') { + return raw.toUpperCase(); + } + + if (option === 'lowercase') { + return raw.toLowerCase(); + } + + return raw; +}; + +/** + @param {string} raw + @param {Options} options + */ +const fix = (raw, options) => { + let fixed = raw; + let isSpecialBase = false; // Indicates that the number is hexadecimal, octal, or binary. + fixed = fixed.replace(/^(0[xob])(.*)/i, (_, radix, value) => { + isSpecialBase = true; + radix = convertCase(radix, options.radixIdentifier); + if (radix.toLowerCase() === '0x') { + value = convertCase(value, options.hexadecimalValue); + } + + return radix + value; + }); + + if (!isSpecialBase) { + fixed = fixed.replaceAll(/e/ig, expo => convertCase(expo, options.exponentialNotation)); } return fixed; }; /** @param {import('eslint').Rule.RuleContext} context */ -const create = () => ({ +const create = context => ({ Literal(node) { const {raw} = node; + /** @type {Options} */ + const options = context.options[0] ?? {}; + options.hexadecimalValue ??= 'uppercase'; + options.radixIdentifier ??= 'lowercase'; + options.exponentialNotation ??= 'lowercase'; + let fixed = raw; if (isNumberLiteral(node)) { - fixed = fix(raw); + fixed = fix(raw, options); } else if (isBigIntLiteral(node)) { - fixed = fix(raw.slice(0, -1)) + 'n'; + fixed = fix(raw.slice(0, -1), options) + 'n'; } if (raw !== fixed) { @@ -37,6 +74,28 @@ const create = () => ({ }, }); +/** @typedef {Record} Options */ + +const caseEnum = /** @type {const} */ ({ + enum: [ + 'uppercase', + 'lowercase', + 'ignore', + ], +}); + +const schema = [ + { + type: 'object', + additionalProperties: false, + properties: { + hexadecimalValue: caseEnum, + radixIdentifier: caseEnum, + exponentialNotation: caseEnum, + }, + }, +]; + /** @type {import('eslint').Rule.RuleModule} */ const config = { create: checkVueTemplate(create), @@ -47,6 +106,12 @@ const config = { recommended: true, }, fixable: 'code', + schema, + defaultOptions: [{ + hexadecimalValue: 'uppercase', + radixIdentifier: 'lowercase', + exponentialNotation: 'lowercase', + }], messages, }, }; diff --git a/test/number-literal-case.js b/test/number-literal-case.js index e43a8a4d0b..9008251e21 100644 --- a/test/number-literal-case.js +++ b/test/number-literal-case.js @@ -63,72 +63,66 @@ const tests = { 'const foo = 0b10_10n', 'const foo = 0o1_234_567n', 'const foo = 0xDEED_BEEFn', + + // Negative number + 'const foo = -1234', + 'const foo = -0b10', + 'const foo = -0o1234567', + 'const foo = -0xABCDEF', ], invalid: [ // Number { code: 'const foo = 0B10', - errors: [error], output: 'const foo = 0b10', }, { code: 'const foo = 0O1234567', - errors: [error], output: 'const foo = 0o1234567', }, { code: 'const foo = 0XaBcDeF', - errors: [error], output: 'const foo = 0xABCDEF', }, // BigInt { code: 'const foo = 0B10n', - errors: [error], output: 'const foo = 0b10n', }, { code: 'const foo = 0O1234567n', - errors: [error], output: 'const foo = 0o1234567n', }, { code: 'const foo = 0XaBcDeFn', - errors: [error], output: 'const foo = 0xABCDEFn', }, // `0n` { code: 'const foo = 0B0n', - errors: [error], output: 'const foo = 0b0n', }, { code: 'const foo = 0O0n', - errors: [error], output: 'const foo = 0o0n', }, { code: 'const foo = 0X0n', - errors: [error], output: 'const foo = 0x0n', }, // Exponential notation { code: 'const foo = 1.2E3', - errors: [error], output: 'const foo = 1.2e3', }, { code: 'const foo = 1.2E-3', - errors: [error], output: 'const foo = 1.2e-3', }, { code: 'const foo = 1.2E+3', - errors: [error], output: 'const foo = 1.2e+3', }, { @@ -139,7 +133,6 @@ const tests = { console.log('invalid'); } `, - errors: [error], output: outdent` const foo = 255; @@ -152,10 +145,94 @@ const tests = { // Numeric separator { code: 'const foo = 0XdeEd_Beefn', - errors: [error], output: 'const foo = 0xDEED_BEEFn', }, - ], + + // Negative number + { + code: 'const foo = -0B10', + output: 'const foo = -0b10', + }, + { + code: 'const foo = -0O1234567', + output: 'const foo = -0o1234567', + }, + { + code: 'const foo = -0XaBcDeF', + output: 'const foo = -0xABCDEF', + }, + + // Lowercase hexadecimal number value + ...[ + { + code: 'const foo = 0XaBcDeF', + output: 'const foo = 0xabcdef', + }, + { + code: 'const foo = 0XaBcDeFn', + output: 'const foo = 0xabcdefn', + }, + ].map(item => ({...item, options: [{hexadecimalValue: 'lowercase'}]})), + + // Uppercase radix indentifer + ...[ + { + code: 'const foo = 0b10', + output: 'const foo = 0B10', + }, + { + code: 'const foo = 0o1234567', + output: 'const foo = 0O1234567', + }, + { + code: 'const foo = 0xaBcDeF', + output: 'const foo = 0XABCDEF', + }, + ].map(item => ({...item, options: [{radixIdentifier: 'uppercase'}]})), + + // Uppercase exponential notation + ...[ + { + code: 'const foo = 1.2e3', + output: 'const foo = 1.2E3', + }, + { + code: 'const foo = 1.2e-3', + output: 'const foo = 1.2E-3', + }, + { + code: 'const foo = 1.2e+3', + output: 'const foo = 1.2E+3', + }, + ].map(item => ({...item, options: [{exponentialNotation: 'uppercase'}]})), + + // Mixed options + { + code: 'const foo = 0xaBcDeF', + output: 'const foo = 0Xabcdef', + options: [{hexadecimalValue: 'lowercase', radixIdentifier: 'uppercase'}], + }, + { + code: 'const foo = 0XaBcDeF', + output: 'const foo = 0xaBcDeF', + options: [{hexadecimalValue: 'ignore', radixIdentifier: 'lowercase'}], + }, + { + code: 'const foo = 0xaBcDeF', + output: 'const foo = 0XaBcDeF', + options: [{hexadecimalValue: 'ignore', radixIdentifier: 'uppercase'}], + }, + { + code: 'const foo = 0XaBcDeF', + output: 'const foo = 0XABCDEF', + options: [{hexadecimalValue: 'uppercase', radixIdentifier: 'ignore'}], + }, + { + code: 'const foo = 1.2E+3', + output: 'const foo = 1.2e+3', + options: [{hexadecimalValue: 'ignore', radixIdentifier: 'ignore'}], + }, + ].map(item => ({...item, errors: [error]})), }; test(tests); From 406bd83d98184b71768cd1890ea03e39512b4e30 Mon Sep 17 00:00:00 2001 From: Rantetsu Inori Date: Tue, 18 Feb 2025 02:52:06 +0800 Subject: [PATCH 3/8] Run lint --- rules/number-literal-case.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rules/number-literal-case.js b/rules/number-literal-case.js index be85c4ad87..2d41ef01a1 100644 --- a/rules/number-literal-case.js +++ b/rules/number-literal-case.js @@ -29,7 +29,7 @@ const convertCase = (raw, option) => { const fix = (raw, options) => { let fixed = raw; let isSpecialBase = false; // Indicates that the number is hexadecimal, octal, or binary. - fixed = fixed.replace(/^(0[xob])(.*)/i, (_, radix, value) => { + fixed = fixed.replace(/^(0[box])(.*)/i, (_, radix, value) => { isSpecialBase = true; radix = convertCase(radix, options.radixIdentifier); if (radix.toLowerCase() === '0x') { @@ -40,7 +40,7 @@ const fix = (raw, options) => { }); if (!isSpecialBase) { - fixed = fixed.replaceAll(/e/ig, expo => convertCase(expo, options.exponentialNotation)); + fixed = fixed.replaceAll(/e/gi, expo => convertCase(expo, options.exponentialNotation)); } return fixed; From 7f7a67022e6c07f82a88f9d08660dea9367943e7 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Tue, 18 Feb 2025 01:59:11 +0700 Subject: [PATCH 4/8] Update escape-case.md --- docs/rules/escape-case.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rules/escape-case.md b/docs/rules/escape-case.md index 195a11d4a3..86d93e8b19 100644 --- a/docs/rules/escape-case.md +++ b/docs/rules/escape-case.md @@ -33,9 +33,9 @@ Type: `string`\ Default: `'uppercase'` - `'uppercase'` (default) - - Always use escape sequence values with uppercase characters. + - Always use escape sequence values with uppercase characters. - `'lowercase'` - - Always use escape sequence values with lowercase characters. + - Always use escape sequence values with lowercase characters. ```js // eslint unicorn/escape-case: ["error", "lowercase"] From 49853bff2f539ed5b4df9bc5106279994a2fe12d Mon Sep 17 00:00:00 2001 From: Rantetsu Inori Date: Tue, 18 Feb 2025 03:02:51 +0800 Subject: [PATCH 5/8] Update escape-case.md --- docs/rules/escape-case.md | 66 +++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/docs/rules/escape-case.md b/docs/rules/escape-case.md index 86d93e8b19..cd115c3825 100644 --- a/docs/rules/escape-case.md +++ b/docs/rules/escape-case.md @@ -9,21 +9,37 @@ Enforces a consistent escaped value style by defining escape sequence values with uppercase or lowercase characters. The default style is uppercase, which promotes readability by making the escaped value more distinguishable from the identifier. -## Fail +## Examples ```js +// ❌ const foo = '\xa9'; -const foo = '\ud834'; -const foo = '\u{1d306}'; -const foo = '\ca'; -``` -## Pass +// ✅ +const foo = '\xA9'; +``` ```js -const foo = '\xA9'; +// ❌ +const foo = '\ud834'; + +// ✅ const foo = '\uD834'; +``` + +```js +// ❌ +const foo = '\u{1d306}'; + +// ✅ const foo = '\u{1D306}'; +``` + +```js +// ❌ +const foo = '\ca'; + +// ✅ const foo = '\cA'; ``` @@ -37,18 +53,42 @@ Default: `'uppercase'` - `'lowercase'` - Always use escape sequence values with lowercase characters. +Example: + ```js -// eslint unicorn/escape-case: ["error", "lowercase"] +{ + 'unicorn/escape-case': ['error', 'lowercase'] +} +``` -// Fail +```js +// ❌ const foo = '\xA9'; -const foo = '\uD834'; -const foo = '\u{1D306}'; -const foo = '\cA'; -// Pass +// ✅ const foo = '\xa9'; +``` + +```js +// ❌ +const foo = '\uD834'; + +// ✅ const foo = '\ud834'; +``` + +```js +// ❌ +const foo = '\u{1D306}'; + +// ✅ const foo = '\u{1d306}'; +``` + +```js +// ❌ +const foo = '\cA'; + +// ✅ const foo = '\ca'; ``` From 24daea89ee11f3a06fcf355593bcb14e8d29a109 Mon Sep 17 00:00:00 2001 From: Rantetsu Inori Date: Tue, 18 Feb 2025 03:11:41 +0800 Subject: [PATCH 6/8] Update number-literal-case.md --- docs/rules/number-literal-case.md | 55 ++++++++++++++++--------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/docs/rules/number-literal-case.md b/docs/rules/number-literal-case.md index d3284df31e..c72ea78916 100644 --- a/docs/rules/number-literal-case.md +++ b/docs/rules/number-literal-case.md @@ -17,52 +17,53 @@ Differentiating the casing of the identifier and value clearly separates them an [Hexadecimal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Hexadecimal) ```js +// ❌ const foo = 0XFF; const foo = 0xff; const foo = 0Xff; const foo = 0Xffn; + +// ✅ +const foo = 0xFF; +const foo = 0xFFn; ``` [Binary](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Binary) ```js +// ❌ const foo = 0B10; const foo = 0B10n; + +// ✅ +const foo = 0b10; +const foo = 0b10n; ``` [Octal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Octal) ```js +// ❌ const foo = 0O76; const foo = 0O76n; -``` - -Exponential notation - -```js -const foo = 2E-5; -``` - -## Pass - -```js -const foo = 0xFF; -``` - -```js -const foo = 0b10; -``` -```js +// ✅ const foo = 0o76; +const foo = 0o76n; ``` -```js -const foo = 0xFFn; -``` +[Exponential notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Exponential) ```js +// ❌ +const foo = 2E-5; +const foo = 2E+5; +const foo = 2E5; + +// ✅ +const foo = 2e-5; const foo = 2e+5; +const foo = 2e5; ``` ## Options @@ -95,13 +96,13 @@ Example: ```js // eslint unicorn/number-literal-case: ["error", {"hexadecimalValue": "lowercase"}] -// Fail +// ❌ const foo = 0XFF; const foo = 0xFF; const foo = 0XFFn; const foo = 0xFFn; -// Pass +// ✅ const foo = 0Xff; const foo = 0xff; const foo = 0Xffn; @@ -119,7 +120,7 @@ Example: ```js // eslint unicorn/number-literal-case: ["error", {"radixIdentifier": "uppercase"}] -// Fail +// ❌ const foo = 0xFF; const foo = 0o76; const foo = 0b10; @@ -127,7 +128,7 @@ const foo = 0xFFn; const foo = 0o76n; const foo = 0b10n; -// Pass +// ✅ const foo = 0XFF; const foo = 0O76; const foo = 0B10; @@ -147,12 +148,12 @@ Example: ```js // eslint unicorn/number-literal-case: ["error", {"exponentialNotation": "uppercase"}] -// Fail +// ❌ const foo = 2e-5; const foo = 2e+5; const foo = 2e99; -// Pass +// ✅ const foo = 2E-5; const foo = 2E+5; const foo = 2E99; From 9de2fd5e4da118b61a0c7972b2749dc83c726477 Mon Sep 17 00:00:00 2001 From: Rantetsu Inori Date: Tue, 18 Feb 2025 03:15:17 +0800 Subject: [PATCH 7/8] Update number-literal-case.md Fix wrong option examples. --- docs/rules/number-literal-case.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rules/number-literal-case.md b/docs/rules/number-literal-case.md index c72ea78916..73d1a9e9d4 100644 --- a/docs/rules/number-literal-case.md +++ b/docs/rules/number-literal-case.md @@ -94,7 +94,7 @@ Specify whether the hexadecimal number value (ABCDEF) should be in `uppercase`, Example: ```js -// eslint unicorn/number-literal-case: ["error", {"hexadecimalValue": "lowercase"}] +// eslint unicorn/number-literal-case: ["error", {"hexadecimalValue": "lowercase", "radixIdentifier": "ignore"}] // ❌ const foo = 0XFF; @@ -118,7 +118,7 @@ Specify whether the radix indentifer (`0x`, `0o`, `0b`) should be in `uppercase` Example: ```js -// eslint unicorn/number-literal-case: ["error", {"radixIdentifier": "uppercase"}] +// eslint unicorn/number-literal-case: ["error", {"radixIdentifier": "uppercase", "hexadecimalValue": "ignore"}] // ❌ const foo = 0xFF; From f0cf28d06a39491ba60328c5994700595606c0b1 Mon Sep 17 00:00:00 2001 From: Rantetsu Inori Date: Wed, 19 Feb 2025 16:57:27 +0800 Subject: [PATCH 8/8] Add disclaimer --- docs/rules/number-literal-case.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/rules/number-literal-case.md b/docs/rules/number-literal-case.md index 73d1a9e9d4..cf5c8aabe9 100644 --- a/docs/rules/number-literal-case.md +++ b/docs/rules/number-literal-case.md @@ -116,6 +116,8 @@ Default: `'lowercase'` Specify whether the radix indentifer (`0x`, `0o`, `0b`) should be in `uppercase`, `lowercase`, or `ignore` the check. Defaults to `'lowercase'`. +**Note: Adjusting this option to values other than `'lowercase'` may make your code unreadable, please use caution.** + Example: ```js // eslint unicorn/number-literal-case: ["error", {"radixIdentifier": "uppercase", "hexadecimalValue": "ignore"}] @@ -144,6 +146,8 @@ Default: `'lowercase'` Specify whether the exponential notation (`e`) should be in `uppercase`, `lowercase`, or `ignore` the check. Defaults to `'lowercase'`. +**Note: Adjusting this option to values other than `'lowercase'` may make your code unreadable, please use caution.** + Example: ```js // eslint unicorn/number-literal-case: ["error", {"exponentialNotation": "uppercase"}]