11'use strict' ;
22const { isParenthesized} = require ( 'eslint-utils' ) ;
33const getDocumentationUrl = require ( './utils/get-documentation-url' ) ;
4+ const isLiteralValue = require ( './utils/is-literal-value' ) ;
45
56const TYPE_NON_ZERO = 'non-zero' ;
67const TYPE_ZERO = 'zero' ;
@@ -9,28 +10,21 @@ const messages = {
910 [ TYPE_ZERO ] : 'Use `.length {{code}}` when checking length is zero.'
1011} ;
1112
12- const isLengthProperty = node =>
13- node . type === 'MemberExpression' &&
14- node . computed === false &&
15- node . property . type === 'Identifier' &&
16- node . property . name === 'length' ;
1713const isLogicNot = node =>
1814 node . type === 'UnaryExpression' &&
1915 node . operator === '!' ;
20- const isLiteralNumber = ( node , value ) =>
21- node . type === 'Literal' &&
22- typeof node . value === 'number' &&
23- node . value === value ;
16+ const isLogicNotArgument = node =>
17+ node . parent &&
18+ isLogicNot ( node . parent ) &&
19+ node . parent . argument === node ;
2420const isCompareRight = ( node , operator , value ) =>
2521 node . type === 'BinaryExpression' &&
2622 node . operator === operator &&
27- isLengthProperty ( node . left ) &&
28- isLiteralNumber ( node . right , value ) ;
23+ isLiteralValue ( node . right , value ) ;
2924const isCompareLeft = ( node , operator , value ) =>
3025 node . type === 'BinaryExpression' &&
3126 node . operator === operator &&
32- isLengthProperty ( node . right ) &&
33- isLiteralNumber ( node . left , value ) ;
27+ isLiteralValue ( node . left , value ) ;
3428const nonZeroStyles = new Map ( [
3529 [
3630 'greater-than' ,
@@ -59,16 +53,44 @@ const zeroStyle = {
5953 test : node => isCompareRight ( node , '===' , 0 )
6054} ;
6155
62- const cache = new WeakMap ( ) ;
63- function getCheckTypeAndLengthNode ( node ) {
64- if ( ! cache . has ( node ) ) {
65- cache . set ( node , getCheckTypeAndLengthNodeWithoutCache ( node ) ) ;
56+ const lengthSelector = [
57+ 'MemberExpression' ,
58+ '[computed=false]' ,
59+ '[property.type="Identifier"]' ,
60+ '[property.name="length"]'
61+ ] . join ( '' ) ;
62+
63+ function getBooleanAncestor ( node ) {
64+ let isNegative = false ;
65+ while ( isLogicNotArgument ( node ) ) {
66+ isNegative = ! isNegative ;
67+ node = node . parent ;
6668 }
6769
68- return cache . get ( node ) ;
70+ return { node, isNegative } ;
6971}
7072
71- function getCheckTypeAndLengthNodeWithoutCache ( node ) {
73+ function getLengthCheckNode ( node ) {
74+ node = node . parent ;
75+
76+ // Zero length check
77+ if (
78+ // `foo.length === 0`
79+ isCompareRight ( node , '===' , 0 ) ||
80+ // `foo.length == 0`
81+ isCompareRight ( node , '==' , 0 ) ||
82+ // `foo.length < 1`
83+ isCompareRight ( node , '<' , 1 ) ||
84+ // `0 === foo.length`
85+ isCompareLeft ( node , '===' , 0 ) ||
86+ // `0 == foo.length`
87+ isCompareLeft ( node , '==' , 0 ) ||
88+ // `1 > foo.length`
89+ isCompareLeft ( node , '>' , 1 )
90+ ) {
91+ return { isZeroLengthCheck : true , node} ;
92+ }
93+
7294 // Non-Zero length check
7395 if (
7496 // `foo.length !== 0`
@@ -78,12 +100,7 @@ function getCheckTypeAndLengthNodeWithoutCache(node) {
78100 // `foo.length > 0`
79101 isCompareRight ( node , '>' , 0 ) ||
80102 // `foo.length >= 1`
81- isCompareRight ( node , '>=' , 1 )
82- ) {
83- return { type : TYPE_NON_ZERO , node, lengthNode : node . left } ;
84- }
85-
86- if (
103+ isCompareRight ( node , '>=' , 1 ) ||
87104 // `0 !== foo.length`
88105 isCompareLeft ( node , '!==' , 0 ) ||
89106 // `0 !== foo.length`
@@ -93,43 +110,37 @@ function getCheckTypeAndLengthNodeWithoutCache(node) {
93110 // `1 <= foo.length`
94111 isCompareLeft ( node , '<=' , 1 )
95112 ) {
96- return { type : TYPE_NON_ZERO , node, lengthNode : node . right } ;
113+ return { isZeroLengthCheck : false , node} ;
97114 }
98115
99- // Zero length check
100- if (
101- // `foo.length === 0`
102- isCompareRight ( node , '===' , 0 ) ||
103- // `foo.length == 0`
104- isCompareRight ( node , '==' , 0 ) ||
105- // `foo.length < 1`
106- isCompareRight ( node , '<' , 1 )
107- ) {
108- return { type : TYPE_ZERO , node, lengthNode : node . left } ;
116+ return { } ;
117+ }
118+
119+ function isBooleanNode ( node ) {
120+ if ( isLogicNot ( node ) || isLogicNotArgument ( node ) ) {
121+ return true ;
109122 }
110123
124+ const { parent} = node ;
111125 if (
112- // `0 === foo.length`
113- isCompareLeft ( node , '===' , 0 ) ||
114- // `0 == foo.length`
115- isCompareLeft ( node , '==' , 0 ) ||
116- // `1 > foo.length`
117- isCompareLeft ( node , '>' , 1 )
126+ (
127+ parent . type === 'IfStatement' ||
128+ parent . type === 'ConditionalExpression' ||
129+ parent . type === 'WhileStatement' ||
130+ parent . type === 'DoWhileStatement' ||
131+ parent . type === 'ForStatement'
132+ ) &&
133+ parent . test === node
118134 ) {
119- return { type : TYPE_ZERO , node , lengthNode : node . right } ;
135+ return true ;
120136 }
121- }
122137
123- // TODO: check other `LogicalExpression`s
124- const booleanNodeSelector = `:matches(${
125- [
126- 'IfStatement' ,
127- 'ConditionalExpression' ,
128- 'WhileStatement' ,
129- 'DoWhileStatement' ,
130- 'ForStatement'
131- ] . join ( ', ' )
132- } ) > *.test`;
138+ if ( parent . type === 'LogicalExpression' ) {
139+ return isBooleanNode ( parent ) ;
140+ }
141+
142+ return false ;
143+ }
133144
134145function create ( context ) {
135146 const options = {
@@ -138,14 +149,13 @@ function create(context) {
138149 } ;
139150 const nonZeroStyle = nonZeroStyles . get ( options [ 'non-zero' ] ) ;
140151 const sourceCode = context . getSourceCode ( ) ;
141- const reportedBinaryExpressions = new Set ( ) ;
142152
143- function reportProblem ( { node, type, lengthNode} , isNegative ) {
144- if ( isNegative ) {
145- type = type === TYPE_NON_ZERO ? TYPE_ZERO : TYPE_NON_ZERO ;
153+ function reportProblem ( { node, isZeroLengthCheck, lengthNode} ) {
154+ const { code, test} = isZeroLengthCheck ? zeroStyle : nonZeroStyle ;
155+ if ( test ( node ) ) {
156+ return ;
146157 }
147158
148- const { code} = type === TYPE_NON_ZERO ? nonZeroStyle : zeroStyle ;
149159 let fixed = `${ sourceCode . getText ( lengthNode ) } ${ code } ` ;
150160 if (
151161 ! isParenthesized ( node , sourceCode ) &&
@@ -157,74 +167,34 @@ function create(context) {
157167
158168 context . report ( {
159169 node,
160- messageId : type ,
170+ messageId : isZeroLengthCheck ? TYPE_ZERO : TYPE_NON_ZERO ,
161171 data : { code} ,
162172 fix : fixer => fixer . replaceText ( node , fixed )
163173 } ) ;
164174 }
165175
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 = [ ] ;
179176 return {
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 ;
177+ [ lengthSelector ] ( lengthNode ) {
178+ let node ;
179+
180+ let { isZeroLengthCheck, node : lengthCheckNode } = getLengthCheckNode ( lengthNode ) ;
181+ if ( lengthCheckNode ) {
182+ const { isNegative, node : ancestor } = getBooleanAncestor ( lengthCheckNode ) ;
183+ node = ancestor ;
184+ if ( isNegative ) {
185+ isZeroLengthCheck = ! isZeroLengthCheck ;
221186 }
222-
223- const result = getCheckTypeAndLengthNode ( node ) ;
224- if ( result ) {
225- reportProblem ( result ) ;
187+ } else {
188+ const { isNegative, node : ancestor } = getBooleanAncestor ( lengthNode ) ;
189+ if ( isBooleanNode ( ancestor ) ) {
190+ isZeroLengthCheck = isNegative ;
191+ node = ancestor ;
226192 }
227193 }
194+
195+ if ( node ) {
196+ reportProblem ( { node, isZeroLengthCheck, lengthNode} ) ;
197+ }
228198 }
229199 } ;
230200}
0 commit comments