2 * @fileoverview Look for useless escapes in strings and regexes
3 * @author Onur Temizkan
8 const astUtils
= require("./utils/ast-utils");
10 //------------------------------------------------------------------------------
12 //------------------------------------------------------------------------------
15 * Returns the union of two sets.
16 * @param {Set} setA The first set
17 * @param {Set} setB The second set
18 * @returns {Set} The union of the two sets
20 function union(setA
, setB
) {
21 return new Set(function *() {
27 const VALID_STRING_ESCAPES
= union(new Set("\\nrvtbfux"), astUtils
.LINEBREAKS
);
28 const REGEX_GENERAL_ESCAPES
= new Set("\\bcdDfnpPrsStvwWxu0123456789]");
29 const REGEX_NON_CHARCLASS_ESCAPES
= union(REGEX_GENERAL_ESCAPES
, new Set("^/.$*+?[{}|()Bk"));
32 * Parses a regular expression into a list of characters with character class info.
33 * @param {string} regExpText The raw text used to create the regular expression
34 * @returns {Object[]} A list of characters, each with info on escaping and whether they're in a character class.
37 * parseRegExp("a\\b[cd-]");
41 * { text: "a", index: 0, escaped: false, inCharClass: false, startsCharClass: false, endsCharClass: false },
42 * { text: "b", index: 2, escaped: true, inCharClass: false, startsCharClass: false, endsCharClass: false },
43 * { text: "c", index: 4, escaped: false, inCharClass: true, startsCharClass: true, endsCharClass: false },
44 * { text: "d", index: 5, escaped: false, inCharClass: true, startsCharClass: false, endsCharClass: false },
45 * { text: "-", index: 6, escaped: false, inCharClass: true, startsCharClass: false, endsCharClass: false }
49 function parseRegExp(regExpText
) {
52 regExpText
.split("").reduce((state
, char, index
) => {
53 if (!state
.escapeNextChar
) {
55 return Object
.assign(state
, { escapeNextChar
: true });
57 if (char === "[" && !state
.inCharClass
) {
58 return Object
.assign(state
, { inCharClass
: true, startingCharClass
: true });
60 if (char === "]" && state
.inCharClass
) {
61 if (charList
.length
&& charList
[charList
.length
- 1].inCharClass
) {
62 charList
[charList
.length
- 1].endsCharClass
= true;
64 return Object
.assign(state
, { inCharClass
: false, startingCharClass
: false });
70 escaped
: state
.escapeNextChar
,
71 inCharClass
: state
.inCharClass
,
72 startsCharClass
: state
.startingCharClass
,
75 return Object
.assign(state
, { escapeNextChar
: false, startingCharClass
: false });
76 }, { escapeNextChar
: false, inCharClass
: false, startingCharClass
: false });
81 /** @type {import('../shared/types').Rule} */
87 description
: "disallow unnecessary escape characters",
89 url
: "https://eslint.org/docs/rules/no-useless-escape"
95 unnecessaryEscape
: "Unnecessary escape character: \\{{character}}.",
96 removeEscape
: "Remove the `\\`. This maintains the current functionality.",
97 escapeBackslash
: "Replace the `\\` with `\\\\` to include the actual backslash character."
104 const sourceCode
= context
.getSourceCode();
108 * @param {ASTNode} node The node to report
109 * @param {number} startOffset The backslash's offset from the start of the node
110 * @param {string} character The uselessly escaped character (not including the backslash)
113 function report(node
, startOffset
, character
) {
114 const rangeStart
= node
.range
[0] + startOffset
;
115 const range
= [rangeStart
, rangeStart
+ 1];
116 const start
= sourceCode
.getLocFromIndex(rangeStart
);
122 end
: { line
: start
.line
, column
: start
.column
+ 1 }
124 messageId
: "unnecessaryEscape",
128 messageId
: "removeEscape",
130 return fixer
.removeRange(range
);
134 messageId
: "escapeBackslash",
136 return fixer
.insertTextBeforeRange(range
, "\\");
144 * Checks if the escape character in given string slice is unnecessary.
146 * @param {ASTNode} node node to validate.
147 * @param {string} match string slice to validate.
150 function validateString(node
, match
) {
151 const isTemplateElement
= node
.type
=== "TemplateElement";
152 const escapedChar
= match
[0][1];
153 let isUnnecessaryEscape
= !VALID_STRING_ESCAPES
.has(escapedChar
);
156 if (isTemplateElement
) {
157 isQuoteEscape
= escapedChar
=== "`";
159 if (escapedChar
=== "$") {
161 // Warn if `\$` is not followed by `{`
162 isUnnecessaryEscape
= match
.input
[match
.index
+ 2] !== "{";
163 } else if (escapedChar
=== "{") {
166 * Warn if `\{` is not preceded by `$`. If preceded by `$`, escaping
167 * is necessary and the rule should not warn. If preceded by `/$`, the rule
168 * will warn for the `/$` instead, as it is the first unnecessarily escaped character.
170 isUnnecessaryEscape
= match
.input
[match
.index
- 1] !== "$";
173 isQuoteEscape
= escapedChar
=== node
.raw
[0];
176 if (isUnnecessaryEscape
&& !isQuoteEscape
) {
177 report(node
, match
.index
, match
[0].slice(1));
182 * Checks if a node has an escape.
183 * @param {ASTNode} node node to check.
186 function check(node
) {
187 const isTemplateElement
= node
.type
=== "TemplateElement";
192 node
.parent
.parent
&&
193 node
.parent
.parent
.type
=== "TaggedTemplateExpression" &&
194 node
.parent
=== node
.parent
.parent
.quasi
197 // Don't report tagged template literals, because the backslash character is accessible to the tag function.
201 if (typeof node
.value
=== "string" || isTemplateElement
) {
204 * JSXAttribute doesn't have any escape sequence: https://facebook.github.io/jsx/.
205 * In addition, backticks are not supported by JSX yet: https://github.com/facebook/jsx/issues/25.
207 if (node
.parent
.type
=== "JSXAttribute" || node
.parent
.type
=== "JSXElement" || node
.parent
.type
=== "JSXFragment") {
211 const value
= isTemplateElement
? sourceCode
.getText(node
) : node
.raw
;
212 const pattern
= /\\[^\d]/gu;
215 while ((match
= pattern
.exec(value
))) {
216 validateString(node
, match
);
218 } else if (node
.regex
) {
219 parseRegExp(node
.regex
.pattern
)
222 * The '-' character is a special case, because it's only valid to escape it if it's in a character
223 * class, and is not at either edge of the character class. To account for this, don't consider '-'
224 * characters to be valid in general, and filter out '-' characters that appear in the middle of a
227 .filter(charInfo
=> !(charInfo
.text
=== "-" && charInfo
.inCharClass
&& !charInfo
.startsCharClass
&& !charInfo
.endsCharClass
))
230 * The '^' character is also a special case; it must always be escaped outside of character classes, but
231 * it only needs to be escaped in character classes if it's at the beginning of the character class. To
232 * account for this, consider it to be a valid escape character outside of character classes, and filter
233 * out '^' characters that appear at the start of a character class.
235 .filter(charInfo
=> !(charInfo
.text
=== "^" && charInfo
.startsCharClass
))
237 // Filter out characters that aren't escaped.
238 .filter(charInfo
=> charInfo
.escaped
)
240 // Filter out characters that are valid to escape, based on their position in the regular expression.
241 .filter(charInfo
=> !(charInfo
.inCharClass
? REGEX_GENERAL_ESCAPES
: REGEX_NON_CHARCLASS_ESCAPES
).has(charInfo
.text
))
243 // Report all the remaining characters.
244 .forEach(charInfo
=> report(node
, charInfo
.index
, charInfo
.text
));
251 TemplateElement
: check