Skip to content

Commit 48390c1

Browse files
fiskersindresorhus
andauthored
Add no-new-array rule (#992)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent dc7f79b commit 48390c1

File tree

7 files changed

+455
-0
lines changed

7 files changed

+455
-0
lines changed

docs/rules/no-new-array.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Disallow `new Array()`
2+
3+
The ESLint built-in rule [`no-array-constructor`](https://eslint.org/docs/rules/no-array-constructor) enforces using an array literal instead of the `Array` constructor, but it still allows using the `Array` constructor with **one** argument. This rule fills that gap.
4+
5+
When using the `Array` constructor with one argument, it's not clear whether the argument is meant to be the length of the array or the only element.
6+
7+
This rule is fixable if the value type of the argument is known.
8+
9+
## Fail
10+
11+
```js
12+
const array = new Array(length);
13+
```
14+
15+
```js
16+
const array = new Array(onlyElement);
17+
```
18+
19+
```js
20+
const array = new Array(...unknownArgumentsList);
21+
```
22+
23+
## Pass
24+
25+
```js
26+
const array = Array.from({length});
27+
```
28+
29+
```js
30+
const array = [onlyElement];
31+
```
32+
33+
```js
34+
const array = [...items];
35+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ module.exports = {
6464
'unicorn/no-lonely-if': 'error',
6565
'no-nested-ternary': 'off',
6666
'unicorn/no-nested-ternary': 'error',
67+
'unicorn/no-new-array': 'error',
6768
'unicorn/no-new-buffer': 'error',
6869
'unicorn/no-null': 'error',
6970
'unicorn/no-object-as-default-parameter': 'error',

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Configure it in `package.json`.
5858
"unicorn/no-lonely-if": "error",
5959
"no-nested-ternary": "off",
6060
"unicorn/no-nested-ternary": "error",
61+
"unicorn/no-new-array": "error",
6162
"unicorn/no-new-buffer": "error",
6263
"unicorn/no-null": "error",
6364
"unicorn/no-object-as-default-parameter": "error",
@@ -130,6 +131,7 @@ Configure it in `package.json`.
130131
- [no-keyword-prefix](docs/rules/no-keyword-prefix.md) - Disallow identifiers starting with `new` or `class`.
131132
- [no-lonely-if](docs/rules/no-lonely-if.md) - Disallow `if` statements as the only statement in `if` blocks without `else`. *(fixable)*
132133
- [no-nested-ternary](docs/rules/no-nested-ternary.md) - Disallow nested ternary expressions. *(partly fixable)*
134+
- [no-new-array](docs/rules/no-new-array.md) - Disallow `new Array()`. *(partly fixable)*
133135
- [no-new-buffer](docs/rules/no-new-buffer.md) - Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`. *(fixable)*
134136
- [no-null](docs/rules/no-null.md) - Disallow the use of the `null` literal.
135137
- [no-object-as-default-parameter](docs/rules/no-object-as-default-parameter.md) - Disallow the use of objects as default parameters.

rules/no-new-array.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
'use strict';
2+
const {isParenthesized, getStaticValue} = require('eslint-utils');
3+
const getDocumentationUrl = require('./utils/get-documentation-url');
4+
const needsSemicolon = require('./utils/needs-semicolon');
5+
6+
const MESSAGE_ID_ERROR = 'error';
7+
const MESSAGE_ID_LENGTH = 'array-length';
8+
const MESSAGE_ID_ONLY_ELEMENT = 'only-element';
9+
const MESSAGE_ID_SPREAD = 'spread';
10+
const messages = {
11+
[MESSAGE_ID_ERROR]: 'Do not use `new Array()`.',
12+
[MESSAGE_ID_LENGTH]: 'The argument is the length of array.',
13+
[MESSAGE_ID_ONLY_ELEMENT]: 'The argument is the only element of array.',
14+
[MESSAGE_ID_SPREAD]: 'Spread the argument.'
15+
};
16+
const newArraySelector = [
17+
'NewExpression',
18+
'[callee.type="Identifier"]',
19+
'[callee.name="Array"]',
20+
'[arguments.length=1]'
21+
].join('');
22+
23+
function getProblem(context, node) {
24+
const problem = {
25+
node,
26+
messageId: MESSAGE_ID_ERROR
27+
};
28+
29+
const [argumentNode] = node.arguments;
30+
31+
const sourceCode = context.getSourceCode();
32+
let text = sourceCode.getText(argumentNode);
33+
if (isParenthesized(argumentNode, sourceCode)) {
34+
text = `(${text})`;
35+
}
36+
37+
const maybeSemiColon = needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, '[') ?
38+
';' :
39+
'';
40+
41+
// We are not sure how many `arguments` passed
42+
if (argumentNode.type === 'SpreadElement') {
43+
problem.suggest = [
44+
{
45+
messageId: MESSAGE_ID_SPREAD,
46+
fix: fixer => fixer.replaceText(node, `${maybeSemiColon}[${text}]`)
47+
}
48+
];
49+
return problem;
50+
}
51+
52+
const result = getStaticValue(argumentNode, context.getScope());
53+
const fromLengthText = `Array.from(${text === 'length' ? '{length}' : `{length: ${text}}`})`;
54+
const onlyElementText = `${maybeSemiColon}[${text}]`;
55+
56+
// We don't know the argument is number or not
57+
if (result === null) {
58+
problem.suggest = [
59+
{
60+
messageId: MESSAGE_ID_LENGTH,
61+
fix: fixer => fixer.replaceText(node, fromLengthText)
62+
},
63+
{
64+
messageId: MESSAGE_ID_ONLY_ELEMENT,
65+
fix: fixer => fixer.replaceText(node, onlyElementText)
66+
}
67+
];
68+
return problem;
69+
}
70+
71+
problem.fix = fixer => fixer.replaceText(
72+
node,
73+
typeof result.value === 'number' ? fromLengthText : onlyElementText
74+
);
75+
76+
return problem;
77+
}
78+
79+
const create = context => ({
80+
[newArraySelector](node) {
81+
context.report(getProblem(context, node));
82+
}
83+
});
84+
85+
module.exports = {
86+
create,
87+
meta: {
88+
type: 'suggestion',
89+
docs: {
90+
url: getDocumentationUrl(__filename)
91+
},
92+
fixable: 'code',
93+
messages
94+
}
95+
};

test/no-new-array.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import {outdent} from 'outdent';
2+
import {test} from './utils/test';
3+
4+
const MESSAGE_ID_ERROR = 'error';
5+
const MESSAGE_ID_LENGTH = 'array-length';
6+
const MESSAGE_ID_ONLY_ELEMENT = 'only-element';
7+
const MESSAGE_ID_SPREAD = 'spread';
8+
9+
const suggestionCase = ({code, suggestions}) => ({
10+
code,
11+
output: code,
12+
errors: [
13+
{
14+
messageId: MESSAGE_ID_ERROR,
15+
suggestions
16+
}
17+
]
18+
});
19+
20+
test({
21+
valid: [
22+
'const array = Array.from({length: 1})',
23+
24+
// ESLint builtin rule `no-array-constructor` cases
25+
'const array = new Array()',
26+
'const array = new Array',
27+
'const array = new Array(1, 2)',
28+
'const array = Array(1, 2)',
29+
30+
// `unicorn/new-for-builtins` cases
31+
'const array = Array(1)'
32+
],
33+
invalid: [
34+
suggestionCase({
35+
code: 'const array = new Array(foo)',
36+
suggestions: [
37+
{
38+
messageId: MESSAGE_ID_LENGTH,
39+
output: 'const array = Array.from({length: foo})'
40+
},
41+
{
42+
messageId: MESSAGE_ID_ONLY_ELEMENT,
43+
output: 'const array = [foo]'
44+
}
45+
]
46+
}),
47+
suggestionCase({
48+
code: 'const array = new Array(length)',
49+
suggestions: [
50+
{
51+
messageId: MESSAGE_ID_LENGTH,
52+
output: 'const array = Array.from({length})'
53+
},
54+
{
55+
messageId: MESSAGE_ID_ONLY_ELEMENT,
56+
output: 'const array = [length]'
57+
}
58+
]
59+
}),
60+
suggestionCase({
61+
code: outdent`
62+
const foo = []
63+
new Array(bar).forEach(baz)
64+
`,
65+
suggestions: [
66+
{
67+
messageId: MESSAGE_ID_LENGTH,
68+
output: outdent`
69+
const foo = []
70+
Array.from({length: bar}).forEach(baz)
71+
`
72+
},
73+
{
74+
messageId: MESSAGE_ID_ONLY_ELEMENT,
75+
output: outdent`
76+
const foo = []
77+
;[bar].forEach(baz)
78+
`
79+
}
80+
]
81+
}),
82+
...[
83+
'...[foo]',
84+
'...foo',
85+
'...[...foo]',
86+
// The following cases we can know the result, but we are not auto-fixing them
87+
'...[1]',
88+
'...["1"]',
89+
'...[1, "1"]'
90+
].map(argumentText => {
91+
const code = `const array = new Array(${argumentText})`;
92+
return {
93+
code,
94+
output: code,
95+
errors: [
96+
{
97+
messageId: MESSAGE_ID_ERROR,
98+
suggestions: [
99+
{
100+
messageId: MESSAGE_ID_SPREAD,
101+
output: `const array = [${argumentText}]`
102+
}
103+
]
104+
}
105+
]
106+
};
107+
}),
108+
suggestionCase({
109+
code: outdent`
110+
const foo = []
111+
new Array(...bar).forEach(baz)
112+
`,
113+
suggestions: [
114+
{
115+
messageId: MESSAGE_ID_SPREAD,
116+
output: outdent`
117+
const foo = []
118+
;[...bar].forEach(baz)
119+
`
120+
}
121+
]
122+
})
123+
]
124+
});
125+
126+
test.visualize([
127+
'const array = new Array(1)',
128+
// This is actually `[]`, but we fix to `Array.from({length: zero})`
129+
outdent`
130+
const zero = 0;
131+
const array = new Array(zero);
132+
`,
133+
// Use shorthand
134+
outdent`
135+
const length = 1;
136+
const array = new Array(length);
137+
`,
138+
'const array = new Array(1.5)',
139+
'const array = new Array(Number("1"))',
140+
'const array = new Array("1")',
141+
'const array = new Array(null)',
142+
'const array = new Array(("1"))',
143+
'const array = new Array((0, 1))',
144+
outdent`
145+
const foo = []
146+
new Array("bar").forEach(baz)
147+
`
148+
]);

0 commit comments

Comments
 (0)