Skip to content

Commit 8d7954c

Browse files
fiskersindresorhus
andauthored
Add consistent-empty-array-spread rule (#2349)
Co-authored-by: Sindre Sorhus <[email protected]>
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+
});

0 commit comments

Comments
 (0)