55
66"use strict" ;
77
8+ //------------------------------------------------------------------------------
9+ // Requirements
10+ //------------------------------------------------------------------------------
11+
812const astUtils = require ( "./utils/ast-utils" ) ;
13+ const regexpp = require ( "regexpp" ) ;
14+
15+ //------------------------------------------------------------------------------
16+ // Helpers
17+ //------------------------------------------------------------------------------
18+
19+ const regExpParser = new regexpp . RegExpParser ( ) ;
20+ const DOUBLE_SPACE = / { 2 } / u;
21+
22+ /**
23+ * Check if node is a string
24+ *@param {ASTNode } node node to evaluate
25+ *@returns {boolean } True if its a string
26+ *@private
27+ */
28+ function isString ( node ) {
29+ return node && node . type === "Literal" && typeof node . value === "string" ;
30+ }
931
1032//------------------------------------------------------------------------------
1133// Rule Definition
@@ -27,40 +49,70 @@ module.exports = {
2749} ,
2850
2951create ( context ) {
30- const sourceCode = context . getSourceCode ( ) ;
3152
3253/**
33- * Validate regular expressions
34- *@param {ASTNode } node node to validate
35- *@param {string } value regular expression to validate
36- *@param {number } valueStart The start location of the regex/string literal. It will always be the case that
37- * `sourceCode.getText().slice(valueStart, valueStart + value.length) === value`
54+ * Validate regular expression
55+ *
56+ *@param {ASTNode } nodeToReport Node to report.
57+ *@param {string } pattern Regular expression pattern to validate.
58+ *@param {string } rawPattern Raw representation of the pattern in the source code.
59+ *@param {number } rawPatternStartRange Start range of the pattern in the source code.
60+ *@param {string } flags Regular expression flags.
3861 *@returns {void }
3962 *@private
4063 */
41- function checkRegex ( node , value , valueStart ) {
42- const multipleSpacesRegex = / ( { 2 , } ) ( [ + * { ? ] | [ ^ + * { ? ] | $ ) / u,
43- regexResults = multipleSpacesRegex . exec ( value ) ;
64+ function checkRegex ( nodeToReport , pattern , rawPattern , rawPatternStartRange , flags ) {
4465
45- if ( regexResults !== null ) {
46- const count = regexResults [ 1 ] . length ;
66+ // Skip if there are no consecutive spaces in the source code, to avoid reporting e.g., RegExp(' \ ').
67+ if ( ! DOUBLE_SPACE . test ( rawPattern ) ) {
68+ return ;
69+ }
4770
48- context . report ( {
49- node,
50- message :"Spaces are hard to count. Use {{{count}}}." ,
51- data :{ count} ,
52- fix ( fixer ) {
53- return fixer . replaceTextRange (
54- [ valueStart + regexResults . index , valueStart + regexResults . index + count ] ,
55- ` {${ count } }`
56- ) ;
57- }
58- } ) ;
59-
60- /*
61- * TODO: (platinumazure) Fix message to use rule message
62- * substitution when api.report is fixed in lib/eslint.js.
63- */
71+ const characterClassNodes = [ ] ;
72+ let regExpAST ;
73+
74+ try {
75+ regExpAST = regExpParser . parsePattern ( pattern , 0 , pattern . length , flags . includes ( "u" ) ) ;
76+ } catch ( e ) {
77+
78+ // Ignore regular expressions with syntax errors
79+ return ;
80+ }
81+
82+ regexpp . visitRegExpAST ( regExpAST , {
83+ onCharacterClassEnter ( ccNode ) {
84+ characterClassNodes . push ( ccNode ) ;
85+ }
86+ } ) ;
87+
88+ const spacesPattern = / ( { 2 , } ) (?: [ + * { ? ] | [ ^ + * { ? ] | $ ) / gu;
89+ let match ;
90+
91+ while ( ( match = spacesPattern . exec ( pattern ) ) ) {
92+ const { 1 :{ length} , index} = match ;
93+
94+ // Report only consecutive spaces that are not in character classes.
95+ if (
96+ characterClassNodes . every ( ( { start, end} ) => index < start || end <= index )
97+ ) {
98+ context . report ( {
99+ node :nodeToReport ,
100+ message :"Spaces are hard to count. Use {{{length}}}." ,
101+ data :{ length} ,
102+ fix ( fixer ) {
103+ if ( pattern !== rawPattern ) {
104+ return null ;
105+ }
106+ return fixer . replaceTextRange (
107+ [ rawPatternStartRange + index , rawPatternStartRange + index + length ] ,
108+ ` {${ length } }`
109+ ) ;
110+ }
111+ } ) ;
112+
113+ // Report only the first occurence of consecutive spaces
114+ return ;
115+ }
64116}
65117}
66118
@@ -71,25 +123,22 @@ module.exports = {
71123 *@private
72124 */
73125function checkLiteral ( node ) {
74- const token = sourceCode . getFirstToken ( node ) ,
75- nodeType = token . type ,
76- nodeValue = token . value ;
126+ if ( node . regex ) {
127+ const pattern = node . regex . pattern ;
128+ const rawPattern = node . raw . slice ( 1 , node . raw . lastIndexOf ( "/" ) ) ;
129+ const rawPatternStartRange = node . range [ 0 ] + 1 ;
130+ const flags = node . regex . flags ;
77131
78- if ( nodeType === "RegularExpression" ) {
79- checkRegex ( node , nodeValue , token . range [ 0 ] ) ;
132+ checkRegex (
133+ node ,
134+ pattern ,
135+ rawPattern ,
136+ rawPatternStartRange ,
137+ flags
138+ ) ;
80139}
81140}
82141
83- /**
84- * Check if node is a string
85- *@param {ASTNode } node node to evaluate
86- *@returns {boolean } True if its a string
87- *@private
88- */
89- function isString ( node ) {
90- return node && node . type === "Literal" && typeof node . value === "string" ;
91- }
92-
93142/**
94143 * Validate strings passed to the RegExp constructor
95144 *@param {ASTNode } node node to validate
@@ -100,9 +149,22 @@ module.exports = {
100149const scope = context . getScope ( ) ;
101150const regExpVar = astUtils . getVariableByName ( scope , "RegExp" ) ;
102151const shadowed = regExpVar && regExpVar . defs . length > 0 ;
152+ const patternNode = node . arguments [ 0 ] ;
153+ const flagsNode = node . arguments [ 1 ] ;
103154
104- if ( node . callee . type === "Identifier" && node . callee . name === "RegExp" && isString ( node . arguments [ 0 ] ) && ! shadowed ) {
105- checkRegex ( node , node . arguments [ 0 ] . value , node . arguments [ 0 ] . range [ 0 ] + 1 ) ;
155+ if ( node . callee . type === "Identifier" && node . callee . name === "RegExp" && isString ( patternNode ) && ! shadowed ) {
156+ const pattern = patternNode . value ;
157+ const rawPattern = patternNode . raw . slice ( 1 , - 1 ) ;
158+ const rawPatternStartRange = patternNode . range [ 0 ] + 1 ;
159+ const flags = isString ( flagsNode ) ?flagsNode . value :"" ;
160+
161+ checkRegex (
162+ node ,
163+ pattern ,
164+ rawPattern ,
165+ rawPatternStartRange ,
166+ flags
167+ ) ;
106168}
107169}
108170
@@ -111,6 +173,5 @@ module.exports = {
111173CallExpression :checkFunction ,
112174NewExpression :checkFunction
113175} ;
114-
115176}
116177} ;