[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Rule to disallow `\8` and `\9` escape sequences in string literals.
|
---|
| 3 | * @author Milos Djermanovic
|
---|
| 4 | */
|
---|
| 5 |
|
---|
| 6 | "use strict";
|
---|
| 7 |
|
---|
| 8 | //------------------------------------------------------------------------------
|
---|
| 9 | // Helpers
|
---|
| 10 | //------------------------------------------------------------------------------
|
---|
| 11 |
|
---|
| 12 | const QUICK_TEST_REGEX = /\\[89]/u;
|
---|
| 13 |
|
---|
| 14 | /**
|
---|
| 15 | * Returns unicode escape sequence that represents the given character.
|
---|
| 16 | * @param {string} character A single code unit.
|
---|
| 17 | * @returns {string} "\uXXXX" sequence.
|
---|
| 18 | */
|
---|
| 19 | function getUnicodeEscape(character) {
|
---|
| 20 | return `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
---|
| 21 | }
|
---|
| 22 |
|
---|
| 23 | //------------------------------------------------------------------------------
|
---|
| 24 | // Rule Definition
|
---|
| 25 | //------------------------------------------------------------------------------
|
---|
| 26 |
|
---|
| 27 | /** @type {import('../shared/types').Rule} */
|
---|
| 28 | module.exports = {
|
---|
| 29 | meta: {
|
---|
| 30 | type: "suggestion",
|
---|
| 31 |
|
---|
| 32 | docs: {
|
---|
| 33 | description: "Disallow `\\8` and `\\9` escape sequences in string literals",
|
---|
| 34 | recommended: true,
|
---|
| 35 | url: "https://eslint.org/docs/latest/rules/no-nonoctal-decimal-escape"
|
---|
| 36 | },
|
---|
| 37 |
|
---|
| 38 | hasSuggestions: true,
|
---|
| 39 |
|
---|
| 40 | schema: [],
|
---|
| 41 |
|
---|
| 42 | messages: {
|
---|
| 43 | decimalEscape: "Don't use '{{decimalEscape}}' escape sequence.",
|
---|
| 44 |
|
---|
| 45 | // suggestions
|
---|
| 46 | refactor: "Replace '{{original}}' with '{{replacement}}'. This maintains the current functionality.",
|
---|
| 47 | escapeBackslash: "Replace '{{original}}' with '{{replacement}}' to include the actual backslash character."
|
---|
| 48 | }
|
---|
| 49 | },
|
---|
| 50 |
|
---|
| 51 | create(context) {
|
---|
| 52 | const sourceCode = context.sourceCode;
|
---|
| 53 |
|
---|
| 54 | /**
|
---|
| 55 | * Creates a new Suggestion object.
|
---|
| 56 | * @param {string} messageId "refactor" or "escapeBackslash".
|
---|
| 57 | * @param {int[]} range The range to replace.
|
---|
| 58 | * @param {string} replacement New text for the range.
|
---|
| 59 | * @returns {Object} Suggestion
|
---|
| 60 | */
|
---|
| 61 | function createSuggestion(messageId, range, replacement) {
|
---|
| 62 | return {
|
---|
| 63 | messageId,
|
---|
| 64 | data: {
|
---|
| 65 | original: sourceCode.getText().slice(...range),
|
---|
| 66 | replacement
|
---|
| 67 | },
|
---|
| 68 | fix(fixer) {
|
---|
| 69 | return fixer.replaceTextRange(range, replacement);
|
---|
| 70 | }
|
---|
| 71 | };
|
---|
| 72 | }
|
---|
| 73 |
|
---|
| 74 | return {
|
---|
| 75 | Literal(node) {
|
---|
| 76 | if (typeof node.value !== "string") {
|
---|
| 77 | return;
|
---|
| 78 | }
|
---|
| 79 |
|
---|
| 80 | if (!QUICK_TEST_REGEX.test(node.raw)) {
|
---|
| 81 | return;
|
---|
| 82 | }
|
---|
| 83 |
|
---|
| 84 | const regex = /(?:[^\\]|(?<previousEscape>\\.))*?(?<decimalEscape>\\[89])/suy;
|
---|
| 85 | let match;
|
---|
| 86 |
|
---|
| 87 | while ((match = regex.exec(node.raw))) {
|
---|
| 88 | const { previousEscape, decimalEscape } = match.groups;
|
---|
| 89 | const decimalEscapeRangeEnd = node.range[0] + match.index + match[0].length;
|
---|
| 90 | const decimalEscapeRangeStart = decimalEscapeRangeEnd - decimalEscape.length;
|
---|
| 91 | const decimalEscapeRange = [decimalEscapeRangeStart, decimalEscapeRangeEnd];
|
---|
| 92 | const suggest = [];
|
---|
| 93 |
|
---|
| 94 | // When `regex` is matched, `previousEscape` can only capture characters adjacent to `decimalEscape`
|
---|
| 95 | if (previousEscape === "\\0") {
|
---|
| 96 |
|
---|
| 97 | /*
|
---|
| 98 | * Now we have a NULL escape "\0" immediately followed by a decimal escape, e.g.: "\0\8".
|
---|
| 99 | * Fixing this to "\08" would turn "\0" into a legacy octal escape. To avoid producing
|
---|
| 100 | * an octal escape while fixing a decimal escape, we provide different suggestions.
|
---|
| 101 | */
|
---|
| 102 | suggest.push(
|
---|
| 103 | createSuggestion( // "\0\8" -> "\u00008"
|
---|
| 104 | "refactor",
|
---|
| 105 | [decimalEscapeRangeStart - previousEscape.length, decimalEscapeRangeEnd],
|
---|
| 106 | `${getUnicodeEscape("\0")}${decimalEscape[1]}`
|
---|
| 107 | ),
|
---|
| 108 | createSuggestion( // "\8" -> "\u0038"
|
---|
| 109 | "refactor",
|
---|
| 110 | decimalEscapeRange,
|
---|
| 111 | getUnicodeEscape(decimalEscape[1])
|
---|
| 112 | )
|
---|
| 113 | );
|
---|
| 114 | } else {
|
---|
| 115 | suggest.push(
|
---|
| 116 | createSuggestion( // "\8" -> "8"
|
---|
| 117 | "refactor",
|
---|
| 118 | decimalEscapeRange,
|
---|
| 119 | decimalEscape[1]
|
---|
| 120 | )
|
---|
| 121 | );
|
---|
| 122 | }
|
---|
| 123 |
|
---|
| 124 | suggest.push(
|
---|
| 125 | createSuggestion( // "\8" -> "\\8"
|
---|
| 126 | "escapeBackslash",
|
---|
| 127 | decimalEscapeRange,
|
---|
| 128 | `\\${decimalEscape}`
|
---|
| 129 | )
|
---|
| 130 | );
|
---|
| 131 |
|
---|
| 132 | context.report({
|
---|
| 133 | node,
|
---|
| 134 | loc: {
|
---|
| 135 | start: sourceCode.getLocFromIndex(decimalEscapeRangeStart),
|
---|
| 136 | end: sourceCode.getLocFromIndex(decimalEscapeRangeEnd)
|
---|
| 137 | },
|
---|
| 138 | messageId: "decimalEscape",
|
---|
| 139 | data: {
|
---|
| 140 | decimalEscape
|
---|
| 141 | },
|
---|
| 142 | suggest
|
---|
| 143 | });
|
---|
| 144 | }
|
---|
| 145 | }
|
---|
| 146 | };
|
---|
| 147 | }
|
---|
| 148 | };
|
---|