--- /dev/null
+/**
+ * @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 = /(?:[^\\]|(?<previousEscape>\\.))*?(?<decimalEscape>\\[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
+ });
+ }
+ }
+ };
+ }
+};