11'use strict' ;
2+ const { isParenthesized} = require ( 'eslint-utils' ) ;
23const getDocumentationUrl = require ( './utils/get-documentation-url' ) ;
34
5+ const TYPE_NON_ZERO = 'non-zero' ;
6+ const TYPE_ZERO = 'zero' ;
47const messages = {
5- 'non-zero' : 'Use `.length {{code}}` when checking length is not zero.' ,
6- zero : 'Use `.length {{code}}` when checking length is zero.'
8+ [ TYPE_NON_ZERO ] : 'Use `.length {{code}}` when checking length is not zero.' ,
9+ [ TYPE_ZERO ] : 'Use `.length {{code}}` when checking length is zero.'
710} ;
811
912const isLengthProperty = node =>
@@ -56,12 +59,17 @@ const zeroStyle = {
5659 test : node => isCompareRight ( node , '===' , 0 )
5760} ;
5861
59- function getNonZeroLengthNode ( node ) {
60- // `foo.length`
61- if ( isLengthProperty ( node ) ) {
62- return node ;
62+ const cache = new WeakMap ( ) ;
63+ function getCheckTypeAndLengthNode ( node ) {
64+ if ( ! cache . has ( node ) ) {
65+ cache . set ( node , getCheckTypeAndLengthNodeWithoutCache ( node ) ) ;
6366 }
6467
68+ return cache . get ( node ) ;
69+ }
70+
71+ function getCheckTypeAndLengthNodeWithoutCache ( node ) {
72+ // Non-Zero length check
6573 if (
6674 // `foo.length !== 0`
6775 isCompareRight ( node , '!==' , 0 ) ||
@@ -72,7 +80,7 @@ function getNonZeroLengthNode(node) {
7280 // `foo.length >= 1`
7381 isCompareRight ( node , '>=' , 1 )
7482 ) {
75- return node . left ;
83+ return { type : TYPE_NON_ZERO , node, lengthNode : node . left } ;
7684 }
7785
7886 if (
@@ -85,11 +93,10 @@ function getNonZeroLengthNode(node) {
8593 // `1 <= foo.length`
8694 isCompareLeft ( node , '<=' , 1 )
8795 ) {
88- return node . right ;
96+ return { type : TYPE_NON_ZERO , node, lengthNode : node . right } ;
8997 }
90- }
9198
92- function getZeroLengthNode ( node ) {
99+ // Zero length check
93100 if (
94101 // `foo.length === 0`
95102 isCompareRight ( node , '===' , 0 ) ||
@@ -98,7 +105,7 @@ function getZeroLengthNode(node) {
98105 // `foo.length < 1`
99106 isCompareRight ( node , '<' , 1 )
100107 ) {
101- return node . left ;
108+ return { type : TYPE_ZERO , node, lengthNode : node . left } ;
102109 }
103110
104111 if (
@@ -109,11 +116,12 @@ function getZeroLengthNode(node) {
109116 // `1 > foo.length`
110117 isCompareLeft ( node , '>' , 1 )
111118 ) {
112- return node . right ;
119+ return { type : TYPE_ZERO , node, lengthNode : node . right } ;
113120 }
114121}
115122
116- const selector = `:matches(${
123+ // TODO: check other `LogicalExpression`s
124+ const booleanNodeSelector = `:matches(${
117125 [
118126 'IfStatement' ,
119127 'ConditionalExpression' ,
@@ -123,65 +131,103 @@ const selector = `:matches(${
123131 ] . join ( ', ' )
124132} ) > *.test`;
125133
126- const create = context => {
134+ function create ( context ) {
127135 const options = {
128136 'non-zero' : 'greater-than' ,
129137 ...context . options [ 0 ]
130138 } ;
131139 const nonZeroStyle = nonZeroStyles . get ( options [ 'non-zero' ] ) ;
132140 const sourceCode = context . getSourceCode ( ) ;
141+ const reportedBinaryExpressions = new Set ( ) ;
133142
134- function checkExpression ( node ) {
135- // Is matched style
136- if ( nonZeroStyle . test ( node ) || zeroStyle . test ( node ) ) {
137- return ;
138- }
139-
140- let isNegative = false ;
141- let expression = node ;
142- while ( isLogicNot ( expression ) ) {
143- isNegative = ! isNegative ;
144- expression = expression . argument ;
145- }
146-
147- if ( expression . type === 'LogicalExpression' ) {
148- checkExpression ( expression . left ) ;
149- checkExpression ( expression . right ) ;
150- return ;
143+ function reportProblem ( { node, type, lengthNode} , isNegative ) {
144+ if ( isNegative ) {
145+ type = type === TYPE_NON_ZERO ? TYPE_ZERO : TYPE_NON_ZERO ;
151146 }
152147
153- let lengthNode ;
154- let isCheckingZero = isNegative ;
155-
156- const zeroLengthNode = getZeroLengthNode ( expression ) ;
157- if ( zeroLengthNode ) {
158- lengthNode = zeroLengthNode ;
159- isCheckingZero = ! isCheckingZero ;
160- } else {
161- const nonZeroLengthNode = getNonZeroLengthNode ( expression ) ;
162- if ( nonZeroLengthNode ) {
163- lengthNode = nonZeroLengthNode ;
164- } else {
165- return ;
166- }
148+ const { code} = type === TYPE_NON_ZERO ? nonZeroStyle : zeroStyle ;
149+ let fixed = `${ sourceCode . getText ( lengthNode ) } ${ code } ` ;
150+ if (
151+ ! isParenthesized ( node , sourceCode ) &&
152+ node . type === 'UnaryExpression' &&
153+ node . parent . type === 'UnaryExpression'
154+ ) {
155+ fixed = `(${ fixed } )` ;
167156 }
168157
169- const { code} = isCheckingZero ? zeroStyle : nonZeroStyle ;
170- const messageId = isCheckingZero ? 'zero' : 'non-zero' ;
171158 context . report ( {
172159 node,
173- messageId,
160+ messageId : type ,
174161 data : { code} ,
175- fix : fixer => fixer . replaceText ( node , ` ${ sourceCode . getText ( lengthNode ) } ${ code } ` )
162+ fix : fixer => fixer . replaceText ( node , fixed )
176163 } ) ;
177164 }
178165
166+ function checkBooleanNode ( node ) {
167+ if ( node . type === 'LogicalExpression' ) {
168+ checkBooleanNode ( node . left ) ;
169+ checkBooleanNode ( node . right ) ;
170+ return ;
171+ }
172+
173+ if ( isLengthProperty ( node ) ) {
174+ reportProblem ( { node, type : TYPE_NON_ZERO , lengthNode : node } ) ;
175+ }
176+ }
177+
178+ const binaryExpressions = [ ] ;
179179 return {
180- [ selector ] ( node ) {
181- checkExpression ( node ) ;
180+ // The outer `!` expression
181+ 'UnaryExpression[operator="!"]:not(UnaryExpression[operator="!"] > .argument)' ( node ) {
182+ let isNegative = false ;
183+ let expression = node ;
184+ while ( isLogicNot ( expression ) ) {
185+ isNegative = ! isNegative ;
186+ expression = expression . argument ;
187+ }
188+
189+ if ( expression . type === 'LogicalExpression' ) {
190+ checkBooleanNode ( expression ) ;
191+ return ;
192+ }
193+
194+ if ( isLengthProperty ( expression ) ) {
195+ reportProblem ( { type : TYPE_NON_ZERO , node, lengthNode : expression } , isNegative ) ;
196+ return ;
197+ }
198+
199+ const result = getCheckTypeAndLengthNode ( expression ) ;
200+ if ( result ) {
201+ reportProblem ( { ...result , node} , isNegative ) ;
202+ reportedBinaryExpressions . add ( result . lengthNode ) ;
203+ }
204+ } ,
205+ [ booleanNodeSelector ] ( node ) {
206+ checkBooleanNode ( node ) ;
207+ } ,
208+ BinaryExpression ( node ) {
209+ // Delay check on this, so we don't need take two steps for this case
210+ // `const isEmpty = !(foo.length >= 1);`
211+ binaryExpressions . push ( node ) ;
212+ } ,
213+ 'Program:exit' ( ) {
214+ for ( const node of binaryExpressions ) {
215+ if (
216+ reportedBinaryExpressions . has ( node ) ||
217+ zeroStyle . test ( node ) ||
218+ nonZeroStyle . test ( node )
219+ ) {
220+ continue ;
221+ }
222+
223+ const result = getCheckTypeAndLengthNode ( node ) ;
224+ if ( result ) {
225+ reportProblem ( result ) ;
226+ }
227+ }
182228 }
183229 } ;
184- } ;
230+ }
185231
186232const schema = [
187233 {
0 commit comments