diff --git a/lib/rules/prefer-template.js b/lib/rules/prefer-template.js index e8f980ebd38..cb967660a62 100644 --- a/lib/rules/prefer-template.js +++ b/lib/rules/prefer-template.js @@ -39,33 +39,25 @@ function getTopConcatBinaryExpression(node) { } /** - * Determines whether a given node is a octal escape sequence + * Checks whether or not a node contains a string literal with an octal or non-octal decimal escape sequence * @param {ASTNode} node A node to check - * @returns {boolean} `true` if the node is an octal escape sequence + * @returns {boolean} `true` if at least one string literal within the node contains + * an octal or non-octal decimal escape sequence */ -function isOctalEscapeSequence(node) { - - // No need to check TemplateLiterals – would throw error with octal escape - const isStringLiteral = node.type === "Literal" && typeof node.value === "string"; - - if (!isStringLiteral) { - return false; +function hasOctalOrNonOctalDecimalEscapeSequence(node) { + if (isConcatenation(node)) { + return ( + hasOctalOrNonOctalDecimalEscapeSequence(node.left) || + hasOctalOrNonOctalDecimalEscapeSequence(node.right) + ); } - return astUtils.hasOctalEscapeSequence(node.raw); -} - -/** - * Checks whether or not a node contains a octal escape sequence - * @param {ASTNode} node A node to check - * @returns {boolean} `true` if the node contains an octal escape sequence - */ -function hasOctalEscapeSequence(node) { - if (isConcatenation(node)) { - return hasOctalEscapeSequence(node.left) || hasOctalEscapeSequence(node.right); + // No need to check TemplateLiterals – would throw parsing error + if (node.type === "Literal" && typeof node.value === "string") { + return astUtils.hasOctalOrNonOctalDecimalEscapeSequence(node.raw); } - return isOctalEscapeSequence(node); + return false; } /** @@ -237,7 +229,7 @@ module.exports = { function fixNonStringBinaryExpression(fixer, node) { const topBinaryExpr = getTopConcatBinaryExpression(node.parent); - if (hasOctalEscapeSequence(topBinaryExpr)) { + if (hasOctalOrNonOctalDecimalEscapeSequence(topBinaryExpr)) { return null; } diff --git a/lib/rules/quotes.js b/lib/rules/quotes.js index d1f4443b903..da7e127493e 100644 --- a/lib/rules/quotes.js +++ b/lib/rules/quotes.js @@ -282,9 +282,12 @@ module.exports = { description: settings.description }, fix(fixer) { - if (quoteOption === "backtick" && astUtils.hasOctalEscapeSequence(rawVal)) { + if (quoteOption === "backtick" && astUtils.hasOctalOrNonOctalDecimalEscapeSequence(rawVal)) { - // An octal escape sequence in a template literal would produce syntax error, even in non-strict mode. + /* + * An octal or non-octal decimal escape sequence in a template literal would + * produce syntax error, even in non-strict mode. + */ return null; } diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index cff5a1ec693..1fd6340df7c 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -38,7 +38,9 @@ const LINEBREAKS = new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"]); const STATEMENT_LIST_PARENTS = new Set(["Program", "BlockStatement", "SwitchCase"]); const DECIMAL_INTEGER_PATTERN = /^(?:0|0[0-7]*[89]\d*|[1-9](?:_?\d)*)$/u; -const OCTAL_ESCAPE_PATTERN = /^(?:[^\\]|\\[^0-7]|\\0(?![0-9]))*\\(?:[1-7]|0[0-9])/u; + +// Tests the presence of at least one LegacyOctalEscapeSequence or NonOctalDecimalEscapeSequence in a raw string +const OCTAL_OR_NON_OCTAL_DECIMAL_ESCAPE_PATTERN = /^(?:[^\\]|\\.)*\\(?:[1-9]|0[0-9])/su; const LOGICAL_ASSIGNMENT_OPERATORS = new Set(["&&=", "||=", "??="]); @@ -1777,17 +1779,19 @@ module.exports = { }, /** - * Determines whether the given raw string contains an octal escape sequence. + * Determines whether the given raw string contains an octal escape sequence + * or a non-octal decimal escape sequence ("\8", "\9"). * - * "\1", "\2" ... "\7" - * "\00", "\01" ... "\09" + * "\1", "\2" ... "\7", "\8", "\9" + * "\00", "\01" ... "\07", "\08", "\09" * * "\0", when not followed by a digit, is not an octal escape sequence. * @param {string} rawString A string in its raw representation. - * @returns {boolean} `true` if the string contains at least one octal escape sequence. + * @returns {boolean} `true` if the string contains at least one octal escape sequence + * or at least one non-octal decimal escape sequence. */ - hasOctalEscapeSequence(rawString) { - return OCTAL_ESCAPE_PATTERN.test(rawString); + hasOctalOrNonOctalDecimalEscapeSequence(rawString) { + return OCTAL_OR_NON_OCTAL_DECIMAL_ESCAPE_PATTERN.test(rawString); }, isLogicalExpression, diff --git a/tests/lib/rules/prefer-template.js b/tests/lib/rules/prefer-template.js index 3ac3879d636..eea0255f1ac 100644 --- a/tests/lib/rules/prefer-template.js +++ b/tests/lib/rules/prefer-template.js @@ -194,6 +194,11 @@ ruleTester.run("prefer-template", rule, { output: null, errors }, + { + code: "foo + 'does not autofix non-octal decimal escape sequence' + '\\8'", + output: null, + errors + }, { code: "foo + '\\n other text \\033'", output: null, diff --git a/tests/lib/rules/quotes.js b/tests/lib/rules/quotes.js index 86dc711f794..a7e223aab9b 100644 --- a/tests/lib/rules/quotes.js +++ b/tests/lib/rules/quotes.js @@ -614,6 +614,19 @@ ruleTester.run("quotes", rule, { type: "Literal" } ] + }, + { + code: "var nonOctalDecimalEscape = '\\8'", + output: null, + options: ["backtick"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "wrongQuotes", + data: { description: "backtick" }, + type: "Literal" + } + ] } ] }); diff --git a/tests/lib/rules/utils/ast-utils.js b/tests/lib/rules/utils/ast-utils.js index fb38dc9ccfb..d0ad2748431 100644 --- a/tests/lib/rules/utils/ast-utils.js +++ b/tests/lib/rules/utils/ast-utils.js @@ -1648,7 +1648,7 @@ describe("ast-utils", () => { }); }); - describe("hasOctalEscapeSequence", () => { + describe("hasOctalOrNonOctalDecimalEscapeSequence", () => { /* eslint-disable quote-props */ const expectedResults = { @@ -1683,19 +1683,24 @@ describe("ast-utils", () => { "\\\\\\1": true, "\\\\\\01": true, "\\\\\\08": true, + "\\8": true, + "\\9": true, + "a\\8a": true, + "\\0\\8": true, + "\\8\\0": true, + "\\80": true, + "\\81": true, + "\\\\\\8": true, + "\\\n\\1": true, + "foo\\\nbar\\2baz": true, + "\\\n\\8": true, + "foo\\\nbar\\9baz": true, "\\0": false, - "\\8": false, - "\\9": false, " \\0": false, "\\0 ": false, "a\\0": false, "\\0a": false, - "a\\8a": false, - "\\0\\8": false, - "\\8\\0": false, - "\\80": false, - "\\81": false, "\\\\": false, "\\\\0": false, "\\\\01": false, @@ -1703,7 +1708,6 @@ describe("ast-utils", () => { "\\\\1": false, "\\\\12": false, "\\\\\\0": false, - "\\\\\\8": false, "\\0\\\\": false, "0": false, "1": false, @@ -1713,7 +1717,10 @@ describe("ast-utils", () => { "80": false, "12": false, "\\a": false, - "\\n": false + "\\n": false, + "\\\n": false, + "foo\\\nbar": false, + "128\\\n349": false }; /* eslint-enable quote-props */ @@ -1721,7 +1728,7 @@ describe("ast-utils", () => { it(`should return ${expectedResults[key]} for ${key}`, () => { const ast = espree.parse(`"${key}"`); - assert.strictEqual(astUtils.hasOctalEscapeSequence(ast.body[0].expression.raw), expectedResults[key]); + assert.strictEqual(astUtils.hasOctalOrNonOctalDecimalEscapeSequence(ast.body[0].expression.raw), expectedResults[key]); }); }); });