Skip to content

Commit f3bc798

Browse files
authored
explicit-length-check: Make rule work in many more cases (#943)
1 parent 5c7ea92 commit f3bc798

File tree

5 files changed

+212
-80
lines changed

5 files changed

+212
-80
lines changed

docs/rules/explicit-length-check.md

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,42 +11,38 @@ Enforce comparison with `=== 0` when checking for zero length.
1111
### Fail
1212

1313
```js
14-
if (!foo.length) {}
14+
const isEmpty = !foo.length;
1515
```
1616

1717
```js
18-
while (foo.length == 0) {}
18+
const isEmpty = foo.length == 0;
1919
```
2020

2121
```js
22-
do {} while (foo.length < 1);
22+
const isEmpty = foo.length < 1;
2323
```
2424

2525
```js
26-
if (; 0 === foo.length;) {}
26+
const isEmpty = 0 === foo.length;
2727
```
2828

2929
```js
30-
const unicorn = 0 == foo.length ? 1 : 2;
30+
const isEmpty = 0 == foo.length;
3131
```
3232

3333
```js
34-
if (1 > foo.length) {}
34+
const isEmpty = 1 > foo.length;
3535
```
3636

3737
```js
38-
// Negative style is forbid too
39-
if (!(foo.length > 0)) {}
38+
// Negative style is forbidden too
39+
const isEmpty = !(foo.length > 0);
4040
```
4141

4242
### Pass
4343

4444
```js
45-
if (foo.length === 0) {}
46-
```
47-
48-
```js
49-
const unicorn = foo.length === 0 ? 1 : 2;
45+
const isEmpty = foo.length === 0;
5046
```
5147

5248
## Non-zero comparisons
@@ -56,46 +52,66 @@ Enforce comparison with `> 0` when checking for non-zero length.
5652
### Fail
5753

5854
```js
59-
if (foo.length !== 0) {}
55+
const isNotEmpty = foo.length !== 0;
56+
```
57+
58+
```js
59+
const isNotEmpty = foo.length != 0;
60+
```
61+
62+
```js
63+
const isNotEmpty = foo.length >= 1;
64+
```
65+
66+
```js
67+
const isNotEmpty = 0 !== foo.length;
68+
```
69+
70+
```js
71+
const isNotEmpty = 0 != foo.length;
72+
```
73+
74+
```js
75+
const isNotEmpty = 0 < foo.length;
6076
```
6177

6278
```js
63-
while (foo.length != 0) {}
79+
const isNotEmpty = 1 <= foo.length;
6480
```
6581

6682
```js
67-
do {} while (foo.length >= 1);
83+
// Negative style is forbidden too
84+
const isNotEmpty = !(foo.length === 0);
6885
```
6986

7087
```js
71-
for (; 0 !== foo.length; ) {}
88+
if (foo.length || bar.length) {}
7289
```
7390

7491
```js
75-
const unicorn = 0 != foo.length ? 1 : 2;
92+
const unicorn = foo.length ? 1 : 2;
7693
```
7794

7895
```js
79-
if (0 < foo.length) {}
96+
while (foo.length) {}
8097
```
8198

8299
```js
83-
if (1 <= foo.length) {}
100+
do {} while (foo.length);
84101
```
85102

86103
```js
87-
// Negative style is forbid too
88-
if (!(foo.length === 0)) {}
104+
for (; foo.length; ) {};
89105
```
90106

91107
### Pass
92108

93109
```js
94-
if (foo.length > 0) {}
110+
const isNotEmpty = foo.length > 0;
95111
```
96112

97113
```js
98-
const unicorn = foo.length > 0 ? 1 : 2;
114+
if (foo.length > 0 || bar.length > 0) {}
99115
```
100116

101117
### Options

rules/explicit-length-check.js

Lines changed: 98 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
'use strict';
2+
const {isParenthesized} = require('eslint-utils');
23
const getDocumentationUrl = require('./utils/get-documentation-url');
34

5+
const TYPE_NON_ZERO = 'non-zero';
6+
const TYPE_ZERO = 'zero';
47
const 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

912
const 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

186232
const schema = [
187233
{

test/explicit-length-check.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ test({
3232
'if (foo[length]) {}',
3333
'if (foo["length"]) {}',
3434

35-
// Not in `IfStatement` or `ConditionalExpression`
36-
'foo.length',
35+
// Already in wanted style
3736
'foo.length === 0',
38-
'foo.length !== 0',
3937
'foo.length > 0',
4038

39+
// Not boolean
40+
'const bar = foo.length',
41+
'const bar = +foo.length',
42+
4143
// Checking 'non-zero'
4244
'if (foo.length > 0) {}',
4345
{
@@ -113,5 +115,9 @@ test.visualize([
113115
'if (!(foo.length === 0)) {}',
114116
'while (foo.length >= 1) {}',
115117
'do {} while (foo.length);',
116-
'for (let i = 0; (bar && !foo.length); i ++) {}'
118+
'for (let i = 0; (bar && !foo.length); i ++) {}',
119+
'const isEmpty = foo.length < 1;',
120+
'bar(foo.length >= 1)',
121+
'bar(!foo.length || foo.length)',
122+
'const bar = void !foo.length;'
117123
]);

0 commit comments

Comments
 (0)