mirror of https://github.com/lukechilds/node.git
Browse Source
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: https://github.com/nodejs/node/pull/9591 Reviewed-By: Teddy Katz <teddy.katz@gmail.com>v6
Prince J Wesley
8 years ago
committed by
Rich Trott
2 changed files with 191 additions and 0 deletions
@ -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 |
|||
}; |
|||
} |
|||
}; |
Loading…
Reference in new issue