Skip to content

Commit 8d7954c

Browse files
fiskersindresorhus
andauthoredMay 10, 2024··
Add consistent-empty-array-spread rule (#2349)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 6fde3fe commit 8d7954c

7 files changed

+437
-6
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Prefer consistent types when spreading a ternary in an array literal
2+
3+
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs-eslintconfigjs).
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
9+
10+
When spreading a ternary in an array, we can use both `[]` and `''` as fallbacks, but it's better to have consistent types in both branches.
11+
12+
## Fail
13+
14+
```js
15+
const array = [
16+
a,
17+
...(foo ? [b, c] : ''),
18+
];
19+
```
20+
21+
```js
22+
const array = [
23+
a,
24+
...(foo ? 'bc' : []),
25+
];
26+
```
27+
28+
## Pass
29+
30+
```js
31+
const array = [
32+
a,
33+
...(foo ? [b, c] : []),
34+
];
35+
```
36+
37+
```js
38+
const array = [
39+
a,
40+
...(foo ? 'bc' : ''),
41+
];
42+
```

‎readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
113113
| [better-regex](docs/rules/better-regex.md) | Improve regexes by making them shorter, consistent, and safer. || 🔧 | |
114114
| [catch-error-name](docs/rules/catch-error-name.md) | Enforce a specific parameter name in catch clauses. || 🔧 | |
115115
| [consistent-destructuring](docs/rules/consistent-destructuring.md) | Use destructured variables over properties. | | 🔧 | 💡 |
116+
| [consistent-empty-array-spread](docs/rules/consistent-empty-array-spread.md) | Prefer consistent types when spreading a ternary in an array literal. || 🔧 | |
116117
| [consistent-function-scoping](docs/rules/consistent-function-scoping.md) | Move function definitions to the highest possible scope. || | |
117118
| [custom-error-definition](docs/rules/custom-error-definition.md) | Enforce correct `Error` subclassing. | | 🔧 | |
118119
| [empty-brace-spaces](docs/rules/empty-brace-spaces.md) | Enforce no spaces between braces. || 🔧 | |
+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
'use strict';
2+
const {getStaticValue} = require('@eslint-community/eslint-utils');
3+
4+
const MESSAGE_ID = 'consistent-empty-array-spread';
5+
const messages = {
6+
[MESSAGE_ID]: 'Prefer using empty {{replacementDescription}} since the {{anotherNodePosition}} is {{anotherNodeDescription}}.',
7+
};
8+
9+
const isEmptyArrayExpression = node =>
10+
node.type === 'ArrayExpression'
11+
&& node.elements.length === 0;
12+
13+
const isEmptyStringLiteral = node =>
14+
node.type === 'Literal'
15+
&& node.value === '';
16+
17+
const isString = (node, context) => {
18+
const staticValueResult = getStaticValue(node, context.sourceCode.getScope(node));
19+
return typeof staticValueResult?.value === 'string';
20+
};
21+
22+
const isArray = (node, context) => {
23+
if (node.type === 'ArrayExpression') {
24+
return true;
25+
}
26+
27+
const staticValueResult = getStaticValue(node, context.sourceCode.getScope(node));
28+
return Array.isArray(staticValueResult?.value);
29+
};
30+
31+
const cases = [
32+
{
33+
oneSidePredicate: isEmptyStringLiteral,
34+
anotherSidePredicate: isArray,
35+
anotherNodeDescription: 'an array',
36+
replacementDescription: 'array',
37+
replacementCode: '[]',
38+
},
39+
{
40+
oneSidePredicate: isEmptyArrayExpression,
41+
anotherSidePredicate: isString,
42+
anotherNodeDescription: 'a string',
43+
replacementDescription: 'string',
44+
replacementCode: '\'\'',
45+
},
46+
];
47+
48+
function createProblem({
49+
problemNode,
50+
anotherNodePosition,
51+
anotherNodeDescription,
52+
replacementDescription,
53+
replacementCode,
54+
}) {
55+
return {
56+
node: problemNode,
57+
messageId: MESSAGE_ID,
58+
data: {
59+
replacementDescription,
60+
anotherNodePosition,
61+
anotherNodeDescription,
62+
},
63+
fix: fixer => fixer.replaceText(problemNode, replacementCode),
64+
};
65+
}
66+
67+
function getProblem(conditionalExpression, context) {
68+
const {
69+
consequent,
70+
alternate,
71+
} = conditionalExpression;
72+
73+
for (const problemCase of cases) {
74+
const {
75+
oneSidePredicate,
76+
anotherSidePredicate,
77+
} = problemCase;
78+
79+
if (oneSidePredicate(consequent, context) && anotherSidePredicate(alternate, context)) {
80+
return createProblem({
81+
...problemCase,
82+
problemNode: consequent,
83+
anotherNodePosition: 'alternate',
84+
});
85+
}
86+
87+
if (oneSidePredicate(alternate, context) && anotherSidePredicate(consequent, context)) {
88+
return createProblem({
89+
...problemCase,
90+
problemNode: alternate,
91+
anotherNodePosition: 'consequent',
92+
});
93+
}
94+
}
95+
}
96+
97+
/** @param {import('eslint').Rule.RuleContext} context */
98+
const create = context => ({
99+
* ArrayExpression(arrayExpression) {
100+
for (const element of arrayExpression.elements) {
101+
if (
102+
element?.type !== 'SpreadElement'
103+
|| element.argument.type !== 'ConditionalExpression'
104+
) {
105+
continue;
106+
}
107+
108+
yield getProblem(element.argument, context);
109+
}
110+
},
111+
});
112+
113+
/** @type {import('eslint').Rule.RuleModule} */
114+
module.exports = {
115+
create,
116+
meta: {
117+
type: 'suggestion',
118+
docs: {
119+
description: 'Prefer consistent types when spreading a ternary in an array literal.',
120+
recommended: true,
121+
},
122+
fixable: 'code',
123+
124+
messages,
125+
},
126+
};

‎scripts/template/rule.js.jst

+5-6
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@ const messages = {
1717
};
1818
<% } %>
1919

20-
const selector = [
21-
'Literal',
22-
'[value="unicorn"]',
23-
].join('');
24-
2520
/** @param {import('eslint').Rule.RuleContext} context */
2621
const create = context => {
2722
return {
28-
[selector](node) {
23+
Literal(node) {
24+
if (node.value !== 'unicorn') {
25+
return;
26+
}
27+
2928
return {
3029
node,
3130
messageId: <% if (hasSuggestions) { %>MESSAGE_ID_ERROR<% } else { %>MESSAGE_ID<% } %>,
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import outdent from 'outdent';
2+
import {getTester} from './utils/test.mjs';
3+
4+
const {test} = getTester(import.meta);
5+
6+
test.snapshot({
7+
valid: [
8+
'[,,,]',
9+
'[...(test ? [] : [a, b])]',
10+
'[...(test ? [a, b] : [])]',
11+
'[...(test ? "" : "ab")]',
12+
'[...(test ? "ab" : "")]',
13+
'[...(test ? "" : unknown)]',
14+
'[...(test ? unknown : "")]',
15+
'[...(test ? [] : unknown)]',
16+
'[...(test ? unknown : [])]',
17+
'_ = {...(test ? "" : [a, b])}',
18+
'_ = {...(test ? [] : "ab")}',
19+
'call(...(test ? "" : [a, b]))',
20+
'call(...(test ? [] : "ab"))',
21+
'[...(test ? "ab" : [a, b])]',
22+
// Not checking
23+
'const EMPTY_STRING = ""; [...(test ? EMPTY_STRING : [a, b])]',
24+
],
25+
invalid: [
26+
outdent`
27+
[
28+
...(test ? [] : "ab"),
29+
...(test ? "ab" : []),
30+
];
31+
`,
32+
outdent`
33+
const STRING = "ab";
34+
[
35+
...(test ? [] : STRING),
36+
...(test ? STRING : []),
37+
];
38+
`,
39+
outdent`
40+
[
41+
...(test ? "" : [a, b]),
42+
...(test ? [a, b] : ""),
43+
];
44+
`,
45+
outdent`
46+
const ARRAY = ["a", "b"];
47+
[
48+
/* hole */,
49+
...(test ? "" : ARRAY),
50+
/* hole */,
51+
...(test ? ARRAY : ""),
52+
/* hole */,
53+
];
54+
`,
55+
'[...(foo ? "" : [])]',
56+
],
57+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# Snapshot report for `test/consistent-empty-array-spread.mjs`
2+
3+
The actual snapshot is saved in `consistent-empty-array-spread.mjs.snap`.
4+
5+
Generated by [AVA](https://avajs.dev).
6+
7+
## invalid(1): [ ...(test ? [] : "ab"), ...(test ? "ab" : []), ];
8+
9+
> Input
10+
11+
`␊
12+
1 | [␊
13+
2 | ...(test ? [] : "ab"),␊
14+
3 | ...(test ? "ab" : []),␊
15+
4 | ];␊
16+
`
17+
18+
> Output
19+
20+
`␊
21+
1 | [␊
22+
2 | ...(test ? '' : "ab"),␊
23+
3 | ...(test ? "ab" : ''),␊
24+
4 | ];␊
25+
`
26+
27+
> Error 1/2
28+
29+
`␊
30+
1 | [␊
31+
> 2 | ...(test ? [] : "ab"),␊
32+
| ^^ Prefer using empty string since the alternate is a string.␊
33+
3 | ...(test ? "ab" : []),␊
34+
4 | ];␊
35+
`
36+
37+
> Error 2/2
38+
39+
`␊
40+
1 | [␊
41+
2 | ...(test ? [] : "ab"),␊
42+
> 3 | ...(test ? "ab" : []),␊
43+
| ^^ Prefer using empty string since the consequent is a string.␊
44+
4 | ];␊
45+
`
46+
47+
## invalid(2): const STRING = "ab"; [ ...(test ? [] : STRING), ...(test ? STRING : []), ];
48+
49+
> Input
50+
51+
`␊
52+
1 | const STRING = "ab";␊
53+
2 | [␊
54+
3 | ...(test ? [] : STRING),␊
55+
4 | ...(test ? STRING : []),␊
56+
5 | ];␊
57+
`
58+
59+
> Output
60+
61+
`␊
62+
1 | const STRING = "ab";␊
63+
2 | [␊
64+
3 | ...(test ? '' : STRING),␊
65+
4 | ...(test ? STRING : ''),␊
66+
5 | ];␊
67+
`
68+
69+
> Error 1/2
70+
71+
`␊
72+
1 | const STRING = "ab";␊
73+
2 | [␊
74+
> 3 | ...(test ? [] : STRING),␊
75+
| ^^ Prefer using empty string since the alternate is a string.␊
76+
4 | ...(test ? STRING : []),␊
77+
5 | ];␊
78+
`
79+
80+
> Error 2/2
81+
82+
`␊
83+
1 | const STRING = "ab";␊
84+
2 | [␊
85+
3 | ...(test ? [] : STRING),␊
86+
> 4 | ...(test ? STRING : []),␊
87+
| ^^ Prefer using empty string since the consequent is a string.␊
88+
5 | ];␊
89+
`
90+
91+
## invalid(3): [ ...(test ? "" : [a, b]), ...(test ? [a, b] : ""), ];
92+
93+
> Input
94+
95+
`␊
96+
1 | [␊
97+
2 | ...(test ? "" : [a, b]),␊
98+
3 | ...(test ? [a, b] : ""),␊
99+
4 | ];␊
100+
`
101+
102+
> Output
103+
104+
`␊
105+
1 | [␊
106+
2 | ...(test ? [] : [a, b]),␊
107+
3 | ...(test ? [a, b] : []),␊
108+
4 | ];␊
109+
`
110+
111+
> Error 1/2
112+
113+
`␊
114+
1 | [␊
115+
> 2 | ...(test ? "" : [a, b]),␊
116+
| ^^ Prefer using empty array since the alternate is an array.␊
117+
3 | ...(test ? [a, b] : ""),␊
118+
4 | ];␊
119+
`
120+
121+
> Error 2/2
122+
123+
`␊
124+
1 | [␊
125+
2 | ...(test ? "" : [a, b]),␊
126+
> 3 | ...(test ? [a, b] : ""),␊
127+
| ^^ Prefer using empty array since the consequent is an array.␊
128+
4 | ];␊
129+
`
130+
131+
## invalid(4): const ARRAY = ["a", "b"]; [ /* hole */, ...(test ? "" : ARRAY), /* hole */, ...(test ? ARRAY : ""), /* hole */, ];
132+
133+
> Input
134+
135+
`␊
136+
1 | const ARRAY = ["a", "b"];␊
137+
2 | [␊
138+
3 | /* hole */,␊
139+
4 | ...(test ? "" : ARRAY),␊
140+
5 | /* hole */,␊
141+
6 | ...(test ? ARRAY : ""),␊
142+
7 | /* hole */,␊
143+
8 | ];␊
144+
`
145+
146+
> Output
147+
148+
`␊
149+
1 | const ARRAY = ["a", "b"];␊
150+
2 | [␊
151+
3 | /* hole */,␊
152+
4 | ...(test ? [] : ARRAY),␊
153+
5 | /* hole */,␊
154+
6 | ...(test ? ARRAY : []),␊
155+
7 | /* hole */,␊
156+
8 | ];␊
157+
`
158+
159+
> Error 1/2
160+
161+
`␊
162+
1 | const ARRAY = ["a", "b"];␊
163+
2 | [␊
164+
3 | /* hole */,␊
165+
> 4 | ...(test ? "" : ARRAY),␊
166+
| ^^ Prefer using empty array since the alternate is an array.␊
167+
5 | /* hole */,␊
168+
6 | ...(test ? ARRAY : ""),␊
169+
7 | /* hole */,␊
170+
8 | ];␊
171+
`
172+
173+
> Error 2/2
174+
175+
`␊
176+
1 | const ARRAY = ["a", "b"];␊
177+
2 | [␊
178+
3 | /* hole */,␊
179+
4 | ...(test ? "" : ARRAY),␊
180+
5 | /* hole */,␊
181+
> 6 | ...(test ? ARRAY : ""),␊
182+
| ^^ Prefer using empty array since the consequent is an array.␊
183+
7 | /* hole */,␊
184+
8 | ];␊
185+
`
186+
187+
## invalid(5): [...(foo ? "" : [])]
188+
189+
> Input
190+
191+
`␊
192+
1 | [...(foo ? "" : [])]␊
193+
`
194+
195+
> Output
196+
197+
`␊
198+
1 | [...(foo ? [] : [])]␊
199+
`
200+
201+
> Error 1/1
202+
203+
`␊
204+
> 1 | [...(foo ? "" : [])]␊
205+
| ^^ Prefer using empty array since the alternate is an array.␊
206+
`
Binary file not shown.

0 commit comments

Comments
 (0)
Please sign in to comment.