diff --git a/lib/rules/prefer-numeric-literals.js b/lib/rules/prefer-numeric-literals.js index 662136c4aad..cc82e6653c0 100644 --- a/lib/rules/prefer-numeric-literals.js +++ b/lib/rules/prefer-numeric-literals.js @@ -103,6 +103,16 @@ module.exports = { /* * If the newly-produced literal would be invalid, (e.g. 0b1234), * or it would yield an incorrect parseInt result for some other reason, don't make a fix. + * + * If `str` had numeric separators, `+replacement` will evaluate to `NaN` because unary `+` + * per the specification doesn't support numeric separators. Thus, the above condition will be `true` + * (`NaN !== anything` is always `true`) regardless of the `parseInt(str, radix)` value. + * Consequently, no autofixes will be made. This is correct behavior because `parseInt` also + * doesn't support numeric separators, but it does parse part of the string before the first `_`, + * so the autofix would be invalid: + * + * parseInt("1_1", 2) // === 1 + * 0b1_1 // === 3 */ return null; } diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index d0dd770d199..8f4d863e999 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -37,7 +37,7 @@ const LINEBREAKS = new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"]); // A set of node types that can contain a list of statements const STATEMENT_LIST_PARENTS = new Set(["Program", "BlockStatement", "SwitchCase"]); -const DECIMAL_INTEGER_PATTERN = /^(0|[1-9]\d*)$/u; +const DECIMAL_INTEGER_PATTERN = /^(0|[1-9](?:_?\d)*)$/u; const OCTAL_ESCAPE_PATTERN = /^(?:[^\\]|\\[^0-7]|\\0(?![0-9]))*\\(?:[1-7]|0[0-9])/u; /** @@ -1228,16 +1228,25 @@ module.exports = { * @returns {boolean} `true` if this node is a decimal integer. * @example * - * 5 // true - * 5. // false - * 5.0 // false - * 05 // false - * 0x5 // false - * 0b101 // false - * 0o5 // false - * 5e0 // false - * '5' // false - * 5n // false + * 0 // true + * 5 // true + * 50 // true + * 5_000 // true + * 1_234_56 // true + * 5. // false + * .5 // false + * 5.0 // false + * 5.00_00 // false + * 05 // false + * 0x5 // false + * 0b101 // false + * 0b11_01 // false + * 0o5 // false + * 5e0 // false + * 5e1_000 // false + * 5n // false + * 1_000n // false + * '5' // false */ isDecimalInteger(node) { return node.type === "Literal" && typeof node.value === "number" && diff --git a/tests/lib/rules/dot-location.js b/tests/lib/rules/dot-location.js index e102bd6927d..82e23acab9b 100644 --- a/tests/lib/rules/dot-location.js +++ b/tests/lib/rules/dot-location.js @@ -231,6 +231,34 @@ ruleTester.run("dot-location", rule, { options: ["object"], errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 2, column: 1 }] }, + { + code: "5_000\n.toExponential()", + output: "5_000 .\ntoExponential()", + options: ["object"], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 2, column: 1 }] + }, + { + code: "5_000_00\n.toExponential()", + output: "5_000_00 .\ntoExponential()", + options: ["object"], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 2, column: 1 }] + }, + { + code: "5.000_000\n.toExponential()", + output: "5.000_000.\ntoExponential()", + options: ["object"], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 2, column: 1 }] + }, + { + code: "0b1010_1010\n.toExponential()", + output: "0b1010_1010.\ntoExponential()", + options: ["object"], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 2, column: 1 }] + }, { code: "foo /* a */ . /* b */ \n /* c */ bar", output: "foo /* a */ /* b */ \n /* c */ .bar", diff --git a/tests/lib/rules/dot-notation.js b/tests/lib/rules/dot-notation.js index 341ef25ec67..56ea1321d97 100644 --- a/tests/lib/rules/dot-notation.js +++ b/tests/lib/rules/dot-notation.js @@ -219,6 +219,40 @@ ruleTester.run("dot-notation", rule, { options: [{ allowKeywords: false }], errors: [{ messageId: "useBrackets", data: { key: "if" } }] }, + { + code: "5['prop']", + output: "5 .prop", + errors: [{ messageId: "useDot", data: { key: q("prop") } }] + }, + { + code: "-5['prop']", + output: "-5 .prop", + errors: [{ messageId: "useDot", data: { key: q("prop") } }] + }, + { + code: "5_000['prop']", + output: "5_000 .prop", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ messageId: "useDot", data: { key: q("prop") } }] + }, + { + code: "5_000_00['prop']", + output: "5_000_00 .prop", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ messageId: "useDot", data: { key: q("prop") } }] + }, + { + code: "5.000_000['prop']", + output: "5.000_000.prop", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ messageId: "useDot", data: { key: q("prop") } }] + }, + { + code: "0b1010_1010['prop']", + output: "0b1010_1010.prop", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ messageId: "useDot", data: { key: q("prop") } }] + }, // Optional chaining { diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js index 6d7f8ef7da0..6a4ba87a9d9 100644 --- a/tests/lib/rules/no-extra-parens.js +++ b/tests/lib/rules/no-extra-parens.js @@ -44,7 +44,7 @@ function invalid(code, output, type, line, config) { const ruleTester = new RuleTester({ parserOptions: { - ecmaVersion: 2020, + ecmaVersion: 2021, ecmaFeatures: { jsx: true } @@ -190,6 +190,8 @@ ruleTester.run("no-extra-parens", rule, { // special cases "(0).a", + "(5_000).a", + "(5_000_00).a", "(function(){ }())", "({a: function(){}}.a());", "({a:0}.a ? b : c)", @@ -775,7 +777,9 @@ ruleTester.run("no-extra-parens", rule, { invalid("(a).b", "a.b", "Identifier"), invalid("(0)[a]", "0[a]", "Literal"), invalid("(0.0).a", "0.0.a", "Literal"), + invalid("(0.0_0).a", "0.0_0.a", "Literal"), invalid("(0xBEEF).a", "0xBEEF.a", "Literal"), + invalid("(0xBE_EF).a", "0xBE_EF.a", "Literal"), invalid("(1e6).a", "1e6.a", "Literal"), invalid("(0123).a", "0123.a", "Literal"), invalid("a[(function() {})]", "a[function() {}]", "FunctionExpression"), diff --git a/tests/lib/rules/no-whitespace-before-property.js b/tests/lib/rules/no-whitespace-before-property.js index 31d50449583..5dceeeadf9e 100644 --- a/tests/lib/rules/no-whitespace-before-property.js +++ b/tests/lib/rules/no-whitespace-before-property.js @@ -842,6 +842,24 @@ ruleTester.run("no-whitespace-before-property", rule, { data: { propName: "toExponential" } }] }, + { + code: "5_000 .toExponential()", + output: null, // Not fixed, + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "unexpectedWhitespace", + data: { propName: "toExponential" } + }] + }, + { + code: "5_000_00 .toExponential()", + output: null, // Not fixed, + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "unexpectedWhitespace", + data: { propName: "toExponential" } + }] + }, { code: "5. .toExponential()", output: "5..toExponential()", @@ -858,6 +876,15 @@ ruleTester.run("no-whitespace-before-property", rule, { data: { propName: "toExponential" } }] }, + { + code: "5.0_0 .toExponential()", + output: "5.0_0.toExponential()", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "unexpectedWhitespace", + data: { propName: "toExponential" } + }] + }, { code: "0x5 .toExponential()", output: "0x5.toExponential()", @@ -866,6 +893,15 @@ ruleTester.run("no-whitespace-before-property", rule, { data: { propName: "toExponential" } }] }, + { + code: "0x56_78 .toExponential()", + output: "0x56_78.toExponential()", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "unexpectedWhitespace", + data: { propName: "toExponential" } + }] + }, { code: "5e0 .toExponential()", output: "5e0.toExponential()", diff --git a/tests/lib/rules/prefer-numeric-literals.js b/tests/lib/rules/prefer-numeric-literals.js index d5d5751a6e3..39cc11a2425 100644 --- a/tests/lib/rules/prefer-numeric-literals.js +++ b/tests/lib/rules/prefer-numeric-literals.js @@ -16,7 +16,7 @@ const rule = require("../../../lib/rules/prefer-numeric-literals"), // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2021 } }); ruleTester.run("prefer-numeric-literals", rule, { valid: [ @@ -345,6 +345,30 @@ ruleTester.run("prefer-numeric-literals", rule, { code: "(Number?.parseInt)?.(\"1F7\", 16) === 255;", output: "0x1F7 === 255;", errors: [{ message: "Use hexadecimal literals instead of Number?.parseInt()." }] + }, + + // `parseInt` doesn't support numeric separators. The rule shouldn't autofix in those cases. + { + code: "parseInt('1_0', 2);", + output: null, + errors: [{ message: "Use binary literals instead of parseInt()." }] + }, + { + code: "Number.parseInt('5_000', 8);", + output: null, + errors: [{ message: "Use octal literals instead of Number.parseInt()." }] + }, + { + code: "parseInt('0_1', 16);", + output: null, + errors: [{ message: "Use hexadecimal literals instead of parseInt()." }] + }, + { + + // this would be indeed the same as `0x0_0`, but there's no need to autofix this edge case that looks more like a mistake. + code: "Number.parseInt('0_0', 16);", + output: null, + errors: [{ message: "Use hexadecimal literals instead of Number.parseInt()." }] } ] }); diff --git a/tests/lib/rules/quote-props.js b/tests/lib/rules/quote-props.js index 136e12fea9e..5186f87d140 100644 --- a/tests/lib/rules/quote-props.js +++ b/tests/lib/rules/quote-props.js @@ -79,7 +79,13 @@ ruleTester.run("quote-props", rule, { { code: "({ 1n: 1 })", options: ["consistent"], parserOptions: { ecmaVersion: 2020 } }, { code: "({ 1n: 1 })", options: ["consistent-as-needed"], parserOptions: { ecmaVersion: 2020 } }, { code: "({ '99999999999999999': 1 })", options: ["as-needed"], parserOptions: { ecmaVersion: 2020 } }, - { code: "({ '1n': 1 })", options: ["as-needed"], parserOptions: { ecmaVersion: 2020 } } + { code: "({ '1n': 1 })", options: ["as-needed"], parserOptions: { ecmaVersion: 2020 } }, + { code: "({ 1_0: 1 })", options: ["as-needed"], parserOptions: { ecmaVersion: 2021 } }, + { code: "({ 1_0: 1 })", options: ["as-needed", { numbers: false }], parserOptions: { ecmaVersion: 2021 } }, + { code: "({ '1_0': 1 })", options: ["as-needed"], parserOptions: { ecmaVersion: 2021 } }, + { code: "({ '1_0': 1 })", options: ["as-needed", { numbers: false }], parserOptions: { ecmaVersion: 2021 } }, + { code: "({ '1_0': 1 })", options: ["as-needed", { numbers: true }], parserOptions: { ecmaVersion: 2021 } }, + { code: "({ 1_0: 1, 1: 1 })", options: ["consistent-as-needed"], parserOptions: { ecmaVersion: 2021 } } ], invalid: [{ code: "({ a: 0 })", @@ -380,5 +386,41 @@ ruleTester.run("quote-props", rule, { messageId: "unquotedNumericProperty", data: { property: "1" } }] + }, { + code: "({ 1_0: 1 })", + output: "({ \"10\": 1 })", + options: ["as-needed", { numbers: true }], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "unquotedNumericProperty", + data: { property: "10" } + }] + }, { + code: "({ 1_2.3_4e0_2: 1 })", + output: "({ \"1234\": 1 })", + options: ["always"], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "unquotedPropertyFound", + data: { property: "1234" } + }] + }, { + code: "({ 0b1_000: 1 })", + output: "({ \"8\": 1 })", + options: ["always"], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "unquotedPropertyFound", + data: { property: "8" } + }] + }, { + code: "({ 1_000: a, '1_000': b })", + output: "({ \"1000\": a, '1_000': b })", + options: ["consistent-as-needed"], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "inconsistentlyQuotedProperty", + data: { key: "1000" } + }] }] }); diff --git a/tests/lib/rules/utils/ast-utils.js b/tests/lib/rules/utils/ast-utils.js index e3790f50f3f..4c4bcc9c8fc 100644 --- a/tests/lib/rules/utils/ast-utils.js +++ b/tests/lib/rules/utils/ast-utils.js @@ -737,21 +737,65 @@ describe("ast-utils", () => { { const expectedResults = { - 5: true, 0: true, + 5: true, + 50: true, + 123: true, + "1_0": true, + "1_0_1": true, + "12_3": true, + "5_000": true, + "500_0": true, + "500_00": true, + "5_000_00": true, + "1_234_56": true, + "1_2_3_4": true, + "11_22_33_44": true, + "1_23_4_56_7_89": true, + "0.": false, "5.": false, + ".0": false, + ".5": false, "5.0": false, + "5.00_00": false, + "5.0_1": false, + "0.1_0": false, + "5.1_2": false, + "1.23_45": false, + ".0_1": false, + ".12_34": false, "05": false, "0x5": false, + "0b11_01": false, + "0o0_1": false, + "0x56_78": false, "5e0": false, + "0.e1": false, + ".0e1": false, + "5e0_1": false, + "5e1_000": false, + "5e12_34": false, "5e-0": false, + "5e-0_1": false, + "5e-1_2": false, + "1_2.3_4e5_6": false, + "1n": false, + "1_2n": false, + "1_000n": false, "'5'": false }; + const ecmaVersion = espree.latestEcmaVersion; + describe("isDecimalInteger", () => { Object.keys(expectedResults).forEach(key => { it(`should return ${expectedResults[key]} for ${key}`, () => { - assert.strictEqual(astUtils.isDecimalInteger(espree.parse(key).body[0].expression), expectedResults[key]); + assert.strictEqual( + astUtils.isDecimalInteger( + espree.parse(key, { ecmaVersion }).body[0].expression + ), + expectedResults[key] + ); }); }); }); @@ -759,7 +803,12 @@ describe("ast-utils", () => { describe("isDecimalIntegerNumericToken", () => { Object.keys(expectedResults).forEach(key => { it(`should return ${expectedResults[key]} for ${key}`, () => { - assert.strictEqual(astUtils.isDecimalIntegerNumericToken(espree.tokenize(key)[0]), expectedResults[key]); + assert.strictEqual( + astUtils.isDecimalIntegerNumericToken( + espree.tokenize(key, { ecmaVersion })[0] + ), + expectedResults[key] + ); }); }); });