-
Notifications
You must be signed in to change notification settings - Fork 30.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
1 parent
a673d44
commit 53d1752
Showing
2 changed files
with
191 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
190 changes: 190 additions & 0 deletions
190
tools/eslint-rules/no-useless-regex-char-class-escape.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
/** | ||
* @fileoverview Disallow useless escape in regex character class | ||
* Based on 'no-useless-escape' rule | ||
*/ | ||
'use strict'; | ||
|
||
//------------------------------------------------------------------------------ | ||
// Rule Definition | ||
//------------------------------------------------------------------------------ | ||
|
||
const REGEX_CHARCLASS_ESCAPES = new Set('\\bcdDfnrsStvwWxu0123456789]'); | ||
|
||
/** | ||
* Parses a regular expression into a list of regex character class list. | ||
* @param {string} regExpText raw text used to create the regular expression | ||
* @returns {Object[]} A list of character classes tokens with index and | ||
* escape info | ||
* @example | ||
* | ||
* parseRegExpCharClass('a\\b[cd-]') | ||
* | ||
* returns: | ||
* [ | ||
* { | ||
* empty: false, | ||
* start: 4, | ||
* end: 6, | ||
* chars: [ | ||
* {text: 'c', index: 4, escaped: false}, | ||
* {text: 'd', index: 5, escaped: false}, | ||
* {text: '-', index: 6, escaped: false} | ||
* ] | ||
* } | ||
* ] | ||
*/ | ||
|
||
function parseRegExpCharClass(regExpText) { | ||
const charList = []; | ||
let charListIdx = -1; | ||
const initState = { | ||
escapeNextChar: false, | ||
inCharClass: false, | ||
startingCharClass: false | ||
}; | ||
|
||
regExpText.split('').reduce((state, char, index) => { | ||
if (!state.escapeNextChar) { | ||
if (char === '\\') { | ||
return Object.assign(state, { escapeNextChar: true }); | ||
} | ||
if (char === '[' && !state.inCharClass) { | ||
charListIdx += 1; | ||
charList.push({ start: index + 1, chars: [], end: -1 }); | ||
return Object.assign(state, { | ||
inCharClass: true, | ||
startingCharClass: true | ||
}); | ||
} | ||
if (char === ']' && state.inCharClass) { | ||
const charClass = charList[charListIdx]; | ||
charClass.empty = charClass.chars.length === 0; | ||
if (charClass.empty) { | ||
charClass.start = charClass.end = -1; | ||
} else { | ||
charList[charListIdx].end = index - 1; | ||
} | ||
return Object.assign(state, { | ||
inCharClass: false, | ||
startingCharClass: false | ||
}); | ||
} | ||
} | ||
if (state.inCharClass) { | ||
charList[charListIdx].chars.push({ | ||
text: char, | ||
index, escaped: | ||
state.escapeNextChar | ||
}); | ||
} | ||
return Object.assign(state, { | ||
escapeNextChar: false, | ||
startingCharClass: false | ||
}); | ||
}, initState); | ||
|
||
return charList; | ||
} | ||
|
||
module.exports = { | ||
meta: { | ||
docs: { | ||
description: 'disallow unnecessary regex characer class escape sequences', | ||
category: 'Best Practices', | ||
recommended: false | ||
}, | ||
fixable: 'code', | ||
schema: [{ | ||
'type': 'object', | ||
'properties': { | ||
'override': { | ||
'type': 'array', | ||
'items': { 'type': 'string' }, | ||
'uniqueItems': true | ||
} | ||
}, | ||
'additionalProperties': false | ||
}] | ||
}, | ||
|
||
create(context) { | ||
const overrideSet = new Set(context.options.length | ||
? context.options[0].override || [] | ||
: []); | ||
|
||
/** | ||
* Reports a node | ||
* @param {ASTNode} node The node to report | ||
* @param {number} startOffset The backslash's offset | ||
* from the start of the node | ||
* @param {string} character The uselessly escaped character | ||
* (not including the backslash) | ||
* @returns {void} | ||
*/ | ||
function report(node, startOffset, character) { | ||
context.report({ | ||
node, | ||
loc: { | ||
line: node.loc.start.line, | ||
column: node.loc.start.column + startOffset | ||
}, | ||
message: 'Unnecessary regex escape in character' + | ||
' class: \\{{character}}', | ||
data: { character }, | ||
fix: (fixer) => { | ||
const start = node.range[0] + startOffset; | ||
return fixer.replaceTextRange([start, start + 1], ''); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Checks if a node has superflous escape character | ||
* in regex character class. | ||
* | ||
* @param {ASTNode} node - node to check. | ||
* @returns {void} | ||
*/ | ||
function check(node) { | ||
if (node.regex) { | ||
parseRegExpCharClass(node.regex.pattern) | ||
.forEach((charClass) => { | ||
charClass | ||
.chars | ||
// The '-' character is a special case if is not at | ||
// either edge of the character class. To account for this, | ||
// filter out '-' characters that appear in the middle of a | ||
// character class. | ||
.filter((charInfo) => !(charInfo.text === '-' && | ||
(charInfo.index !== charClass.start && | ||
charInfo.index !== charClass.end))) | ||
|
||
// The '^' character is a special case if it's at the beginning | ||
// of the character class. To account for this, filter out '^' | ||
// characters that appear at the start of a character class. | ||
// | ||
.filter((charInfo) => !(charInfo.text === '^' && | ||
charInfo.index === charClass.start)) | ||
|
||
// Filter out characters that aren't escaped. | ||
.filter((charInfo) => charInfo.escaped) | ||
|
||
// Filter out characters that are valid to escape, based on | ||
// their position in the regular expression. | ||
.filter((charInfo) => !REGEX_CHARCLASS_ESCAPES.has(charInfo.text)) | ||
|
||
// Filter out overridden character list. | ||
.filter((charInfo) => !overrideSet.has(charInfo.text)) | ||
|
||
// Report all the remaining characters. | ||
.forEach((charInfo) => | ||
report(node, charInfo.index, charInfo.text)); | ||
}); | ||
} | ||
} | ||
|
||
return { | ||
Literal: check | ||
}; | ||
} | ||
}; |