Skip to content

Commit 582ca34

Browse files
authored
Refactor explicit-length-check (#950)
1 parent f3bc798 commit 582ca34

File tree

1 file changed

+88
-118
lines changed

1 file changed

+88
-118
lines changed

rules/explicit-length-check.js

Lines changed: 88 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22
const {isParenthesized} = require('eslint-utils');
33
const getDocumentationUrl = require('./utils/get-documentation-url');
4+
const isLiteralValue = require('./utils/is-literal-value');
45

56
const TYPE_NON_ZERO = 'non-zero';
67
const 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';
1713
const 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;
2420
const 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);
2924
const 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);
3428
const 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

134145
function 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

Comments
 (0)