Skip to content

Commit d63befa

Browse files
princejwesleyMylesBorins
authored andcommitted
tools: Add no useless regex char class rule
Eslint Rule: Disallow useless escape in regex character class with optional override characters option and auto fixable with eslint --fix option. Usage: no-useless-regex-char-class-escape: [2, { override: ['[', ']'] }] PR-URL: #9591 Reviewed-By: Teddy Katz <teddy.katz@gmail.com>
1 parent 87534d6 commit d63befa

File tree

2 files changed

+191
-0
lines changed

2 files changed

+191
-0
lines changed

.eslintrc.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ rules:
139139
assert-fail-single-argument: 2
140140
assert-throws-arguments: [2, { requireTwo: false }]
141141
new-with-error: [2, Error, RangeError, TypeError, SyntaxError, ReferenceError]
142+
no-useless-regex-char-class-escape: [2, { override: ['[', ']'] }]
142143

143144
# Global scoped method and vars
144145
globals:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* @fileoverview Disallow useless escape in regex character class
3+
* Based on 'no-useless-escape' rule
4+
*/
5+
'use strict';
6+
7+
//------------------------------------------------------------------------------
8+
// Rule Definition
9+
//------------------------------------------------------------------------------
10+
11+
const REGEX_CHARCLASS_ESCAPES = new Set('\\bcdDfnrsStvwWxu0123456789]');
12+
13+
/**
14+
* Parses a regular expression into a list of regex character class list.
15+
* @param {string} regExpText raw text used to create the regular expression
16+
* @returns {Object[]} A list of character classes tokens with index and
17+
* escape info
18+
* @example
19+
*
20+
* parseRegExpCharClass('a\\b[cd-]')
21+
*
22+
* returns:
23+
* [
24+
* {
25+
* empty: false,
26+
* start: 4,
27+
* end: 6,
28+
* chars: [
29+
* {text: 'c', index: 4, escaped: false},
30+
* {text: 'd', index: 5, escaped: false},
31+
* {text: '-', index: 6, escaped: false}
32+
* ]
33+
* }
34+
* ]
35+
*/
36+
37+
function parseRegExpCharClass(regExpText) {
38+
const charList = [];
39+
let charListIdx = -1;
40+
const initState = {
41+
escapeNextChar: false,
42+
inCharClass: false,
43+
startingCharClass: false
44+
};
45+
46+
regExpText.split('').reduce((state, char, index) => {
47+
if (!state.escapeNextChar) {
48+
if (char === '\\') {
49+
return Object.assign(state, { escapeNextChar: true });
50+
}
51+
if (char === '[' && !state.inCharClass) {
52+
charListIdx += 1;
53+
charList.push({ start: index + 1, chars: [], end: -1 });
54+
return Object.assign(state, {
55+
inCharClass: true,
56+
startingCharClass: true
57+
});
58+
}
59+
if (char === ']' && state.inCharClass) {
60+
const charClass = charList[charListIdx];
61+
charClass.empty = charClass.chars.length === 0;
62+
if (charClass.empty) {
63+
charClass.start = charClass.end = -1;
64+
} else {
65+
charList[charListIdx].end = index - 1;
66+
}
67+
return Object.assign(state, {
68+
inCharClass: false,
69+
startingCharClass: false
70+
});
71+
}
72+
}
73+
if (state.inCharClass) {
74+
charList[charListIdx].chars.push({
75+
text: char,
76+
index, escaped:
77+
state.escapeNextChar
78+
});
79+
}
80+
return Object.assign(state, {
81+
escapeNextChar: false,
82+
startingCharClass: false
83+
});
84+
}, initState);
85+
86+
return charList;
87+
}
88+
89+
module.exports = {
90+
meta: {
91+
docs: {
92+
description: 'disallow unnecessary regex characer class escape sequences',
93+
category: 'Best Practices',
94+
recommended: false
95+
},
96+
fixable: 'code',
97+
schema: [{
98+
'type': 'object',
99+
'properties': {
100+
'override': {
101+
'type': 'array',
102+
'items': { 'type': 'string' },
103+
'uniqueItems': true
104+
}
105+
},
106+
'additionalProperties': false
107+
}]
108+
},
109+
110+
create(context) {
111+
const overrideSet = new Set(context.options.length
112+
? context.options[0].override || []
113+
: []);
114+
115+
/**
116+
* Reports a node
117+
* @param {ASTNode} node The node to report
118+
* @param {number} startOffset The backslash's offset
119+
* from the start of the node
120+
* @param {string} character The uselessly escaped character
121+
* (not including the backslash)
122+
* @returns {void}
123+
*/
124+
function report(node, startOffset, character) {
125+
context.report({
126+
node,
127+
loc: {
128+
line: node.loc.start.line,
129+
column: node.loc.start.column + startOffset
130+
},
131+
message: 'Unnecessary regex escape in character' +
132+
' class: \\{{character}}',
133+
data: { character },
134+
fix: (fixer) => {
135+
const start = node.range[0] + startOffset;
136+
return fixer.replaceTextRange([start, start + 1], '');
137+
}
138+
});
139+
}
140+
141+
/**
142+
* Checks if a node has superflous escape character
143+
* in regex character class.
144+
*
145+
* @param {ASTNode} node - node to check.
146+
* @returns {void}
147+
*/
148+
function check(node) {
149+
if (node.regex) {
150+
parseRegExpCharClass(node.regex.pattern)
151+
.forEach((charClass) => {
152+
charClass
153+
.chars
154+
// The '-' character is a special case if is not at
155+
// either edge of the character class. To account for this,
156+
// filter out '-' characters that appear in the middle of a
157+
// character class.
158+
.filter((charInfo) => !(charInfo.text === '-' &&
159+
(charInfo.index !== charClass.start &&
160+
charInfo.index !== charClass.end)))
161+
162+
// The '^' character is a special case if it's at the beginning
163+
// of the character class. To account for this, filter out '^'
164+
// characters that appear at the start of a character class.
165+
//
166+
.filter((charInfo) => !(charInfo.text === '^' &&
167+
charInfo.index === charClass.start))
168+
169+
// Filter out characters that aren't escaped.
170+
.filter((charInfo) => charInfo.escaped)
171+
172+
// Filter out characters that are valid to escape, based on
173+
// their position in the regular expression.
174+
.filter((charInfo) => !REGEX_CHARCLASS_ESCAPES.has(charInfo.text))
175+
176+
// Filter out overridden character list.
177+
.filter((charInfo) => !overrideSet.has(charInfo.text))
178+
179+
// Report all the remaining characters.
180+
.forEach((charInfo) =>
181+
report(node, charInfo.index, charInfo.text));
182+
});
183+
}
184+
}
185+
186+
return {
187+
Literal: check
188+
};
189+
}
190+
};

0 commit comments

Comments
 (0)