Skip to content

Commit a1b60ad

Browse files
fiskersindresorhus
andauthored
explicit-length-check: Check unsafe LogicalExpressions (#952)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent f4577f7 commit a1b60ad

File tree

6 files changed

+100
-11
lines changed

6 files changed

+100
-11
lines changed

docs/rules/explicit-length-check.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Enforce explicitly checking the length of an object and enforce the comparison style.
44

5-
This rule is fixable.
5+
This rule is fixable, unless it's [unsafe to fix](#unsafe-to-fix-case).
66

77
## Zero comparisons
88

@@ -141,3 +141,25 @@ The `non-zero` option can be configured with one of the following:
141141
- Enforces non-zero to be checked with: `foo.length !== 0`
142142
- `greater-than-or-equal`
143143
- Enforces non-zero to be checked with: `foo.length >= 1`
144+
145+
## Unsafe to fix case
146+
147+
`.length` check inside `LogicalExpression`s are not safe to fix.
148+
149+
Example:
150+
151+
```js
152+
const bothNotEmpty = (a, b) => a.length && b.length;
153+
154+
if (bothNotEmpty(foo, bar)) {}
155+
```
156+
157+
In this case, the `bothNotEmpty` function returns a `number`, but it will most likely be used as a `boolean`. The rule will still report this as an error, but without an auto-fix. You can apply a [suggestion](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions) in your editor, which will fix it to:
158+
159+
```js
160+
const bothNotEmpty = (a, b) => a.length > 0 && b.length > 0;
161+
162+
if (bothNotEmpty(foo, bar)) {}
163+
```
164+
165+
The rule is smart enough to know some `LogicalExpression`s are safe to fix, like when it's inside `if`, `while`, etc.

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ Configure it in `package.json`.
112112
- [error-message](docs/rules/error-message.md) - Enforce passing a `message` value when creating a built-in error.
113113
- [escape-case](docs/rules/escape-case.md) - Require escape sequences to use uppercase values. *(fixable)*
114114
- [expiring-todo-comments](docs/rules/expiring-todo-comments.md) - Add expiration conditions to TODO comments.
115-
- [explicit-length-check](docs/rules/explicit-length-check.md) - Enforce explicitly comparing the `length` property of a value. *(fixable)*
115+
- [explicit-length-check](docs/rules/explicit-length-check.md) - Enforce explicitly comparing the `length` property of a value. *(partly fixable)*
116116
- [filename-case](docs/rules/filename-case.md) - Enforce a case style for filenames.
117117
- [import-index](docs/rules/import-index.md) - Enforce importing index files with `.`. *(fixable)*
118118
- [import-style](docs/rules/import-style.md) - Enforce specific import styles per module.

rules/explicit-length-check.js

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ const isLiteralValue = require('./utils/is-literal-value');
55

66
const TYPE_NON_ZERO = 'non-zero';
77
const TYPE_ZERO = 'zero';
8+
const MESSAGE_ID_SUGGESTION = 'suggestion';
89
const messages = {
910
[TYPE_NON_ZERO]: 'Use `.length {{code}}` when checking length is not zero.',
10-
[TYPE_ZERO]: 'Use `.length {{code}}` when checking length is zero.'
11+
[TYPE_ZERO]: 'Use `.length {{code}}` when checking length is zero.',
12+
[MESSAGE_ID_SUGGESTION]: 'Replace `.length` with `.length {{code}}`.'
1113
};
1214

1315
const isLogicNot = node =>
@@ -172,7 +174,7 @@ function create(context) {
172174
const nonZeroStyle = nonZeroStyles.get(options['non-zero']);
173175
const sourceCode = context.getSourceCode();
174176

175-
function reportProblem({node, isZeroLengthCheck, lengthNode}) {
177+
function reportProblem({node, isZeroLengthCheck, lengthNode, autoFix}) {
176178
const {code, test} = isZeroLengthCheck ? zeroStyle : nonZeroStyle;
177179
if (test(node)) {
178180
return;
@@ -187,17 +189,33 @@ function create(context) {
187189
fixed = `(${fixed})`;
188190
}
189191

190-
context.report({
192+
const fix = fixer => fixer.replaceText(node, fixed);
193+
194+
const problem = {
191195
node,
192196
messageId: isZeroLengthCheck ? TYPE_ZERO : TYPE_NON_ZERO,
193-
data: {code},
194-
fix: fixer => fixer.replaceText(node, fixed)
195-
});
197+
data: {code}
198+
};
199+
200+
if (autoFix) {
201+
problem.fix = fix;
202+
} else {
203+
problem.suggest = [
204+
{
205+
messageId: MESSAGE_ID_SUGGESTION,
206+
data: {code},
207+
fix
208+
}
209+
];
210+
}
211+
212+
context.report(problem);
196213
}
197214

198215
return {
199216
[lengthSelector](lengthNode) {
200217
let node;
218+
let autoFix = true;
201219

202220
let {isZeroLengthCheck, node: lengthCheckNode} = getLengthCheckNode(lengthNode);
203221
if (lengthCheckNode) {
@@ -211,11 +229,15 @@ function create(context) {
211229
if (isBooleanNode(ancestor)) {
212230
isZeroLengthCheck = isNegative;
213231
node = ancestor;
232+
} else if (lengthNode.parent.type === 'LogicalExpression') {
233+
isZeroLengthCheck = isNegative;
234+
node = lengthNode;
235+
autoFix = false;
214236
}
215237
}
216238

217239
if (node) {
218-
reportProblem({node, isZeroLengthCheck, lengthNode});
240+
reportProblem({node, isZeroLengthCheck, lengthNode, autoFix});
219241
}
220242
}
221243
};

test/explicit-length-check.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
import {outdent} from 'outdent';
22
import {test} from './utils/test';
33

4+
const suggestionCase = ({code, output, desc, options = []}) => {
5+
const suggestion = {output};
6+
if (desc) {
7+
suggestion.desc = desc;
8+
}
9+
10+
return {
11+
code,
12+
output: code,
13+
options,
14+
errors: [
15+
{suggestions: [suggestion]}
16+
]
17+
};
18+
};
19+
420
const nonZeroCases = [
521
'foo.length',
622
'!!foo.length',
@@ -82,7 +98,33 @@ test({
8298
'if (foo.length > 1) {}',
8399
'if (foo.length < 2) {}'
84100
],
85-
invalid: []
101+
invalid: [
102+
suggestionCase({
103+
code: 'const x = foo.length || bar()',
104+
output: 'const x = foo.length > 0 || bar()',
105+
desc: 'Replace `.length` with `.length > 0`.'
106+
}),
107+
suggestionCase({
108+
code: 'const x = foo.length || bar()',
109+
output: 'const x = foo.length !== 0 || bar()',
110+
desc: 'Replace `.length` with `.length !== 0`.',
111+
options: [{'non-zero': 'not-equal'}]
112+
}),
113+
suggestionCase({
114+
code: 'const x = foo.length || bar()',
115+
output: 'const x = foo.length >= 1 || bar()',
116+
desc: 'Replace `.length` with `.length >= 1`.',
117+
options: [{'non-zero': 'greater-than-or-equal'}]
118+
}),
119+
suggestionCase({
120+
code: '() => foo.length && bar()',
121+
output: '() => foo.length > 0 && bar()'
122+
}),
123+
suggestionCase({
124+
code: 'alert(foo.length && bar())',
125+
output: 'alert(foo.length > 0 && bar())'
126+
})
127+
]
86128
});
87129

88130
test.visualize([

test/snapshots/explicit-length-check.js.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -871,9 +871,12 @@ Generated by [AVA](https://avajs.dev).
871871
Output:␊
872872
1 | bar(foo.length === 0 || foo.length)␊
873873
874-
Error 1/1:␊
874+
Error 1/2:␊
875875
> 1 | bar(!foo.length || foo.length)␊
876876
| ^^^^^^^^^^^ Use `.length === 0` when checking length is zero.␊
877+
Error 2/2:␊
878+
> 1 | bar(!foo.length || foo.length)␊
879+
| ^^^^^^^^^^ Use `.length > 0` when checking length is not zero.␊
877880
`
878881

879882
## explicit-length-check - #14
12 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)