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 });
86 description
: "disallow unnecessary escape characters",
88 url
: "https://eslint.org/docs/rules/no-useless-escape"
94 unnecessaryEscape
: "Unnecessary escape character: \\{{character}}.",
95 removeEscape
: "Remove the `\\`. This maintains the current functionality.",
96 escapeBackslash
: "Replace the `\\` with `\\\\` to include the actual backslash character."
103 const sourceCode
= context
.getSourceCode();
107 * @param {ASTNode} node The node to report
108 * @param {number} startOffset The backslash's offset from the start of the node
109 * @param {string} character The uselessly escaped character (not including the backslash)
112 function report(node
, startOffset
, character
) {
113 const rangeStart
= node
.range
[0] + startOffset
;
114 const range
= [rangeStart
, rangeStart
+ 1];
115 const start
= sourceCode
.getLocFromIndex(rangeStart
);
121 end
: { line
: start
.line
, column
: start
.column
+ 1 }
123 messageId
: "unnecessaryEscape",
127 messageId
: "removeEscape",
129 return fixer
.removeRange(range
);
133 messageId
: "escapeBackslash",
135 return fixer
.insertTextBeforeRange(range
, "\\");
143 * Checks if the escape character in given string slice is unnecessary.
145 * @param {ASTNode} node node to validate.
146 * @param {string} match string slice to validate.
149 function validateString(node
, match
) {
150 const isTemplateElement
= node
.type
=== "TemplateElement";
151 const escapedChar
= match
[0][1];
152 let isUnnecessaryEscape
= !VALID_STRING_ESCAPES
.has(escapedChar
);
155 if (isTemplateElement
) {
156 isQuoteEscape
= escapedChar
=== "`";
158 if (escapedChar
=== "$") {
160 // Warn if `\$` is not followed by `{`
161 isUnnecessaryEscape
= match
.input
[match
.index
+ 2] !== "{";
162 } else if (escapedChar
=== "{") {
165 * Warn if `\{` is not preceded by `$`. If preceded by `$`, escaping
166 * is necessary and the rule should not warn. If preceded by `/$`, the rule
167 * will warn for the `/$` instead, as it is the first unnecessarily escaped character.
169 isUnnecessaryEscape
= match
.input
[match
.index
- 1] !== "$";
172 isQuoteEscape
= escapedChar
=== node
.raw
[0];
175 if (isUnnecessaryEscape
&& !isQuoteEscape
) {
176 report(node
, match
.index
, match
[0].slice(1));
181 * Checks if a node has an escape.
182 * @param {ASTNode} node node to check.
185 function check(node
) {
186 const isTemplateElement
= node
.type
=== "TemplateElement";
191 node
.parent
.parent
&&
192 node
.parent
.parent
.type
=== "TaggedTemplateExpression" &&
193 node
.parent
=== node
.parent
.parent
.quasi
196 // Don't report tagged template literals, because the backslash character is accessible to the tag function.
200 if (typeof node
.value
=== "string" || isTemplateElement
) {
203 * JSXAttribute doesn't have any escape sequence: https://facebook.github.io/jsx/.
204 * In addition, backticks are not supported by JSX yet: https://github.com/facebook/jsx/issues/25.
206 if (node
.parent
.type
=== "JSXAttribute" || node
.parent
.type
=== "JSXElement" || node
.parent
.type
=== "JSXFragment") {
210 const value
= isTemplateElement
? sourceCode
.getText(node
) : node
.raw
;
211 const pattern
= /\\[^\d]/gu;
214 while ((match
= pattern
.exec(value
))) {
215 validateString(node
, match
);
217 } else if (node
.regex
) {
218 parseRegExp(node
.regex
.pattern
)
221 * The '-' character is a special case, because it's only valid to escape it if it's in a character
222 * class, and is not at either edge of the character class. To account for this, don't consider '-'
223 * characters to be valid in general, and filter out '-' characters that appear in the middle of a
226 .filter(charInfo
=> !(charInfo
.text
=== "-" && charInfo
.inCharClass
&& !charInfo
.startsCharClass
&& !charInfo
.endsCharClass
))
229 * The '^' character is also a special case; it must always be escaped outside of character classes, but
230 * it only needs to be escaped in character classes if it's at the beginning of the character class. To
231 * account for this, consider it to be a valid escape character outside of character classes, and filter
232 * out '^' characters that appear at the start of a character class.
234 .filter(charInfo
=> !(charInfo
.text
=== "^" && charInfo
.startsCharClass
))
236 // Filter out characters that aren't escaped.
237 .filter(charInfo
=> charInfo
.escaped
)
239 // Filter out characters that are valid to escape, based on their position in the regular expression.
240 .filter(charInfo
=> !(charInfo
.inCharClass
? REGEX_GENERAL_ESCAPES
: REGEX_NON_CHARCLASS_ESCAPES
).has(charInfo
.text
))
242 // Report all the remaining characters.
243 .forEach(charInfo
=> report(node
, charInfo
.index
, charInfo
.text
));
250 TemplateElement
: check