Skip to content

Commit

Permalink
tools: Add no useless regex char class rule
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
princejwesley authored and addaleax committed Dec 5, 2016
1 parent a673d44 commit 53d1752
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 0 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ rules:
align-multiline-assignment: 2
assert-fail-single-argument: 2
new-with-error: [2, Error, RangeError, TypeError, SyntaxError, ReferenceError]
no-useless-regex-char-class-escape: [2, { override: ['[', ']'] }]

# Global scoped method and vars
globals:
Expand Down
190 changes: 190 additions & 0 deletions tools/eslint-rules/no-useless-regex-char-class-escape.js
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
};
}
};

0 comments on commit 53d1752

Please sign in to comment.