]>
Commit | Line | Data |
---|---|---|
456be15e TL |
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 | ||
34eeec05 | 27 | /** @type {import('../shared/types').Rule} */ |
456be15e TL |
28 | module.exports = { |
29 | meta: { | |
30 | type: "suggestion", | |
31 | ||
32 | docs: { | |
8f9d1d4d | 33 | description: "Disallow `\\8` and `\\9` escape sequences in string literals", |
609c276f | 34 | recommended: true, |
f2a92ac6 | 35 | url: "https://eslint.org/docs/latest/rules/no-nonoctal-decimal-escape" |
456be15e TL |
36 | }, |
37 | ||
609c276f TL |
38 | hasSuggestions: true, |
39 | ||
456be15e TL |
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) { | |
f2a92ac6 | 52 | const sourceCode = context.sourceCode; |
456be15e TL |
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 | }; |