/** * @fileoverview Rule to disallow `\8` and `\9` escape sequences in string literals. * @author Milos Djermanovic */ "use strict"; //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ const QUICK_TEST_REGEX = /\\[89]/u; /** * Returns unicode escape sequence that represents the given character. * @param {string} character A single code unit. * @returns {string} "\uXXXX" sequence. */ function getUnicodeEscape(character) { return `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`; } //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { type: "suggestion", docs: { description: "disallow `\\8` and `\\9` escape sequences in string literals", category: "Best Practices", recommended: false, url: "https://eslint.org/docs/rules/no-nonoctal-decimal-escape", suggestion: true }, schema: [], messages: { decimalEscape: "Don't use '{{decimalEscape}}' escape sequence.", // suggestions refactor: "Replace '{{original}}' with '{{replacement}}'. This maintains the current functionality.", escapeBackslash: "Replace '{{original}}' with '{{replacement}}' to include the actual backslash character." } }, create(context) { const sourceCode = context.getSourceCode(); /** * Creates a new Suggestion object. * @param {string} messageId "refactor" or "escapeBackslash". * @param {int[]} range The range to replace. * @param {string} replacement New text for the range. * @returns {Object} Suggestion */ function createSuggestion(messageId, range, replacement) { return { messageId, data: { original: sourceCode.getText().slice(...range), replacement }, fix(fixer) { return fixer.replaceTextRange(range, replacement); } }; } return { Literal(node) { if (typeof node.value !== "string") { return; } if (!QUICK_TEST_REGEX.test(node.raw)) { return; } const regex = /(?:[^\\]|(?\\.))*?(?\\[89])/suy; let match; while ((match = regex.exec(node.raw))) { const { previousEscape, decimalEscape } = match.groups; const decimalEscapeRangeEnd = node.range[0] + match.index + match[0].length; const decimalEscapeRangeStart = decimalEscapeRangeEnd - decimalEscape.length; const decimalEscapeRange = [decimalEscapeRangeStart, decimalEscapeRangeEnd]; const suggest = []; // When `regex` is matched, `previousEscape` can only capture characters adjacent to `decimalEscape` if (previousEscape === "\\0") { /* * Now we have a NULL escape "\0" immediately followed by a decimal escape, e.g.: "\0\8". * Fixing this to "\08" would turn "\0" into a legacy octal escape. To avoid producing * an octal escape while fixing a decimal escape, we provide different suggestions. */ suggest.push( createSuggestion( // "\0\8" -> "\u00008" "refactor", [decimalEscapeRangeStart - previousEscape.length, decimalEscapeRangeEnd], `${getUnicodeEscape("\0")}${decimalEscape[1]}` ), createSuggestion( // "\8" -> "\u0038" "refactor", decimalEscapeRange, getUnicodeEscape(decimalEscape[1]) ) ); } else { suggest.push( createSuggestion( // "\8" -> "8" "refactor", decimalEscapeRange, decimalEscape[1] ) ); } suggest.push( createSuggestion( // "\8" -> "\\8" "escapeBackslash", decimalEscapeRange, `\\${decimalEscape}` ) ); context.report({ node, loc: { start: sourceCode.getLocFromIndex(decimalEscapeRangeStart), end: sourceCode.getLocFromIndex(decimalEscapeRangeEnd) }, messageId: "decimalEscape", data: { decimalEscape }, suggest }); } } }; } };