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 | };
|
---|