Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit d63befa

Browse filesBrowse 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
Copy full SHA for d63befa

File tree

Expand file treeCollapse file tree

2 files changed

+191
-0
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

2 files changed

+191
-0
lines changed
Open diff view settings
Collapse file

‎.eslintrc.yaml‎

Copy file name to clipboardExpand all lines: .eslintrc.yaml
+1Lines 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:
Collapse file
+190Lines 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+
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.