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}
48 function parseRegExp(regExpText
) {
51 regExpText
.split("").reduce((state
, char, index
) => {
52 if (!state
.escapeNextChar
) {
54 return Object
.assign(state
, { escapeNextChar
: true });
56 if (char === "[" && !state
.inCharClass
) {
57 return Object
.assign(state
, { inCharClass
: true, startingCharClass
: true });
59 if (char === "]" && state
.inCharClass
) {
60 if (charList
.length
&& charList
[charList
.length
- 1].inCharClass
) {
61 charList
[charList
.length
- 1].endsCharClass
= true;
63 return Object
.assign(state
, { inCharClass
: false, startingCharClass
: false });
69 escaped
: state
.escapeNextChar
,
70 inCharClass
: state
.inCharClass
,
71 startsCharClass
: state
.startingCharClass
,
74 return Object
.assign(state
, { escapeNextChar
: false, startingCharClass
: false });
75 }, { escapeNextChar
: false, inCharClass
: false, startingCharClass
: false });
85 description
: "disallow unnecessary escape characters",
86 category
: "Best Practices",
88 url
: "https://eslint.org/docs/rules/no-useless-escape",
93 unnecessaryEscape
: "Unnecessary escape character: \\{{character}}.",
94 removeEscape
: "Remove the `\\`. This maintains the current functionality.",
95 escapeBackslash
: "Replace the `\\` with `\\\\` to include the actual backslash character."
102 const sourceCode
= context
.getSourceCode();
106 * @param {ASTNode} node The node to report
107 * @param {number} startOffset The backslash's offset from the start of the node
108 * @param {string} character The uselessly escaped character (not including the backslash)
111 function report(node
, startOffset
, character
) {
112 const rangeStart
= node
.range
[0] + startOffset
;
113 const range
= [rangeStart
, rangeStart
+ 1];
114 const start
= sourceCode
.getLocFromIndex(rangeStart
);
120 end
: { line
: start
.line
, column
: start
.column
+ 1 }
122 messageId
: "unnecessaryEscape",
126 messageId
: "removeEscape",
128 return fixer
.removeRange(range
);
132 messageId
: "escapeBackslash",
134 return fixer
.insertTextBeforeRange(range
, "\\");
142 * Checks if the escape character in given string slice is unnecessary.
144 * @param {ASTNode} node node to validate.
145 * @param {string} match string slice to validate.
148 function validateString(node
, match
) {
149 const isTemplateElement
= node
.type
=== "TemplateElement";
150 const escapedChar
= match
[0][1];
151 let isUnnecessaryEscape
= !VALID_STRING_ESCAPES
.has(escapedChar
);
154 if (isTemplateElement
) {
155 isQuoteEscape
= escapedChar
=== "`";
157 if (escapedChar
=== "$") {
159 // Warn if `\$` is not followed by `{`
160 isUnnecessaryEscape
= match
.input
[match
.index
+ 2] !== "{";
161 } else if (escapedChar
=== "{") {
164 * Warn if `\{` is not preceded by `$`. If preceded by `$`, escaping
165 * is necessary and the rule should not warn. If preceded by `/$`, the rule
166 * will warn for the `/$` instead, as it is the first unnecessarily escaped character.
168 isUnnecessaryEscape
= match
.input
[match
.index
- 1] !== "$";
171 isQuoteEscape
= escapedChar
=== node
.raw
[0];
174 if (isUnnecessaryEscape
&& !isQuoteEscape
) {
175 report(node
, match
.index
, match
[0].slice(1));
180 * Checks if a node has an escape.
181 * @param {ASTNode} node node to check.
184 function check(node
) {
185 const isTemplateElement
= node
.type
=== "TemplateElement";
190 node
.parent
.parent
&&
191 node
.parent
.parent
.type
=== "TaggedTemplateExpression" &&
192 node
.parent
=== node
.parent
.parent
.quasi
195 // Don't report tagged template literals, because the backslash character is accessible to the tag function.
199 if (typeof node
.value
=== "string" || isTemplateElement
) {
202 * JSXAttribute doesn't have any escape sequence: https://facebook.github.io/jsx/.
203 * In addition, backticks are not supported by JSX yet: https://github.com/facebook/jsx/issues/25.
205 if (node
.parent
.type
=== "JSXAttribute" || node
.parent
.type
=== "JSXElement" || node
.parent
.type
=== "JSXFragment") {
209 const value
= isTemplateElement
? sourceCode
.getText(node
) : node
.raw
;
210 const pattern
= /\\[^\d]/gu;
213 while ((match
= pattern
.exec(value
))) {
214 validateString(node
, match
);
216 } else if (node
.regex
) {
217 parseRegExp(node
.regex
.pattern
)
220 * The '-' character is a special case, because it's only valid to escape it if it's in a character
221 * class, and is not at either edge of the character class. To account for this, don't consider '-'
222 * characters to be valid in general, and filter out '-' characters that appear in the middle of a
225 .filter(charInfo
=> !(charInfo
.text
=== "-" && charInfo
.inCharClass
&& !charInfo
.startsCharClass
&& !charInfo
.endsCharClass
))
228 * The '^' character is also a special case; it must always be escaped outside of character classes, but
229 * it only needs to be escaped in character classes if it's at the beginning of the character class. To
230 * account for this, consider it to be a valid escape character outside of character classes, and filter
231 * out '^' characters that appear at the start of a character class.
233 .filter(charInfo
=> !(charInfo
.text
=== "^" && charInfo
.startsCharClass
))
235 // Filter out characters that aren't escaped.
236 .filter(charInfo
=> charInfo
.escaped
)
238 // Filter out characters that are valid to escape, based on their position in the regular expression.
239 .filter(charInfo
=> !(charInfo
.inCharClass
? REGEX_GENERAL_ESCAPES
: REGEX_NON_CHARCLASS_ESCAPES
).has(charInfo
.text
))
241 // Report all the remaining characters.
242 .forEach(charInfo
=> report(node
, charInfo
.index
, charInfo
.text
));
249 TemplateElement
: check