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 <[email protected]>
1 parent 87534d6 commit d63befa

File tree

2 files changed

+191
-0
lines changed

2 files changed

+191
-0
lines changed

‎.eslintrc.yaml‎

Lines changed: 1 addition & 0 deletions
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:
Lines changed: 190 additions & 0 deletions
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+
constREGEX_CHARCLASS_ESCAPES=newSet('\\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+
functionparseRegExpCharClass(regExpText){
38+
constcharList=[];
39+
letcharListIdx=-1;
40+
constinitState={
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+
returnObject.assign(state,{escapeNextChar: true});
50+
}
51+
if(char==='['&&!state.inCharClass){
52+
charListIdx+=1;
53+
charList.push({start: index+1,chars: [],end: -1});
54+
returnObject.assign(state,{
55+
inCharClass: true,
56+
startingCharClass: true
57+
});
58+
}
59+
if(char===']'&&state.inCharClass){
60+
constcharClass=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+
returnObject.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+
returnObject.assign(state,{
81+
escapeNextChar: false,
82+
startingCharClass: false
83+
});
84+
},initState);
85+
86+
returncharList;
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+
constoverrideSet=newSet(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+
functionreport(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+
conststart=node.range[0]+startOffset;
136+
returnfixer.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+
functioncheck(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)