Skip to content

Commit 0fbd30e

Browse files
committed
feat: Add prefer-array-from rule. Fixes #3
1 parent 9e93e54 commit 0fbd30e

File tree

8 files changed

+270
-9
lines changed

8 files changed

+270
-9
lines changed

README.md

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@
44

55
Rules for Array functions and methods.
66

7+
## Contents
8+
- [Installation](#Installation)
9+
- [Rules](#Rules)
10+
- [`from-map`](#from-map)
11+
- [Examples](#Examples)
12+
- [`no-unnecessary-this-arg`](#no-unnecessary-this-arg)
13+
- [Checked Functions](#checked-functions)
14+
- [Checked Methods](#checked-methods)
15+
- [Examples](#Examples2)
16+
- [`prefer-array-from`](#prefer-array-from)
17+
- [Examples](#Examples3)
18+
- [`array-func/recommended` Configuration](#array-func-recommended-configuration)
19+
- [Using the Configuration](#using-the-configuration)
20+
- [License](#License)
21+
722
## Installation
823

924
Install [ESLint](https://www.github.com/eslint/eslint) either locally or globally.
@@ -52,10 +67,10 @@ The `this` parameter is useless when providing arrow functions, since the `this`
5267

5368
The fix is usually to omit the parameter. The Array methods can't be auto-fixed, since the detection of array methods is not confident enough to know that the method is being called on an array.
5469

55-
#### Checked functions
70+
#### Checked Functions
5671
- `from` (fixable)
5772

58-
#### Checked methods
73+
#### Checked Methods
5974
- `every`
6075
- `filter`
6176
- `find`
@@ -112,15 +127,40 @@ array.forEach(function(char) {
112127
array.filter(this.isGood, this);
113128
```
114129

130+
### `prefer-array-from`
131+
Use `Array.from` instead of `[...iterable]` for performance benefits.
132+
133+
This rule is auto fixable.
134+
135+
#### Examples
136+
Code that triggers this rule:
137+
```js
138+
const iterable = [..."string"];
139+
140+
const arrayCopy = [...iterable];
141+
```
142+
143+
Code that doesn't trigger this rule:
144+
```js
145+
const array = [1, 2, 3];
146+
147+
const extendedArray = [0, ...array];
148+
149+
const arrayCopy = Array.from(array);
150+
151+
const characterArray = Array.from("string");
152+
```
153+
115154
## `array-func/recommended` Configuration
116155
The recommended configuration will set your parser ECMA Version to 2015, since that's when the Array functions and methods were added.
117156

118-
Rule | Error level
119-
---- | -----------
120-
`from-map` | Error
121-
`no-unnecessary-this-arg` | Error
157+
Rule | Error level | Fixable
158+
---- | ----------- | -------
159+
`from-map` | Error | Yes
160+
`no-unnecessary-this-arg` | Error | Sometimes
161+
`prefer-array-from` | Error | Yes
122162

123-
### Using the configuration
163+
### Using the Configuration
124164
To enable this configuration use the `extends` property in your `.eslintrc.json` config file (may look different for other config file styles):
125165
```json
126166
{

index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
module.exports = {
88
rules: {
99
"from-map": require("./rules/from-map"),
10-
"no-unnecessary-this-arg": require("./rules/no-unnecessary-this-arg")
10+
"no-unnecessary-this-arg": require("./rules/no-unnecessary-this-arg"),
11+
"prefer-array-from": require("./rules/prefer-array-from")
1112
},
1213
configs: {
1314
recommended: {
@@ -17,7 +18,8 @@ module.exports = {
1718
plugins: [ 'array-func' ],
1819
rules: {
1920
"array-func/from-map": "error",
20-
"array-func/no-unnecessary-this-arg": "error"
21+
"array-func/no-unnecessary-this-arg": "error",
22+
"array-func/prefer-array-from": "error"
2123
}
2224
}
2325
}

lib/helpers/call-expression.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @author Martin Giger
3+
* @license MIT
4+
*/
5+
"use strict";
6+
7+
const {
8+
MEMBER_EXPRESSION,
9+
IDENTIFIER
10+
} = require("../type");
11+
12+
// Helper functions for call expression nodes.
13+
14+
exports.isMethod = (node, name) => "callee" in node && node.callee.type === MEMBER_EXPRESSION && node.callee.property.name === name;
15+
16+
exports.getParent = (node) => node.callee.object;
17+
18+
exports.isOnObject = (node, name) => "object" in node.callee && node.callee.object.type === IDENTIFIER && node.callee.object.name === name;

lib/type.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* @author Martin Giger
3+
* @license MIT
4+
*/
5+
"use strict";
6+
7+
exports.MEMBER_EXPRESSION = "MemberExpression";
8+
exports.ARROW_FUNCTION_EXPRESSION = "ArrowFunctionExpression";
9+
exports.IDENTIFIER = "Identifier";
10+
exports.SPREAD_ELEMENT = "SpreadElement";

rules/prefer-array-from.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* @author Martin Giger
3+
* @license MIT
4+
*/
5+
"use strict";
6+
7+
const { SPREAD_ELEMENT } = require("../lib/type");
8+
9+
const SINGLE_ELEMENT = 1,
10+
firstElement = (arr) => {
11+
const [ el ] = arr;
12+
return el;
13+
};
14+
15+
module.exports = {
16+
meta: {
17+
docs: {
18+
description: "Prefer using Array.from over spreading an interable in an array literal.",
19+
recommended: true
20+
},
21+
schema: [],
22+
fixable: "code"
23+
},
24+
create(context) {
25+
return {
26+
"ArrayExpression:exit"(node) {
27+
if(node.elements.length !== SINGLE_ELEMENT || firstElement(node.elements).type !== SPREAD_ELEMENT) {
28+
return;
29+
}
30+
context.report({
31+
node,
32+
loc: node.loc,
33+
message: "Use Array.from to convert from iterable to array",
34+
fix(fixer) {
35+
const sourceCode = context.getSourceCode();
36+
return fixer.replaceText(node, `Array.from(${sourceCode.getText(firstElement(node.elements).argument)})`);
37+
}
38+
});
39+
}
40+
};
41+
}
42+
};

test/helpers-call-expression.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import test from 'ava';
2+
import { isMethod, getParent, isOnObject } from '../lib/helpers/call-expression';
3+
import { MEMBER_EXPRESSION, IDENTIFIER } from '../lib/type';
4+
5+
test('is method', (t) => {
6+
const name = "test";
7+
t.true(isMethod({
8+
callee: {
9+
type: MEMBER_EXPRESSION,
10+
property: {
11+
name
12+
}
13+
}
14+
}, name));
15+
});
16+
17+
test('not is method', (t) => {
18+
const name = 'test';
19+
t.false(isMethod({}, name));
20+
t.false(isMethod({
21+
callee: {
22+
type: IDENTIFIER,
23+
property: {
24+
name
25+
}
26+
}
27+
}, name));
28+
t.false(isMethod({
29+
callee: {
30+
type: MEMBER_EXPRESSION,
31+
property: {
32+
name: 'foo'
33+
}
34+
}
35+
}));
36+
});
37+
38+
test('get parent', (t) => {
39+
const parent = 'foo';
40+
t.is(getParent({
41+
callee: {
42+
object: parent
43+
}
44+
}), parent);
45+
});
46+
47+
test('is on object', (t) => {
48+
const name = 'test';
49+
t.true(isOnObject({
50+
callee: {
51+
object: {
52+
type: IDENTIFIER,
53+
name
54+
}
55+
}
56+
}, name));
57+
});
58+
59+
test('is not on object', (t) => {
60+
const name = 'test';
61+
t.false(isOnObject({
62+
callee: {}
63+
}, name));
64+
t.false(isOnObject({
65+
callee: {
66+
type: MEMBER_EXPRESSION,
67+
name
68+
}
69+
}, name));
70+
t.false(isOnObject({
71+
callee: {
72+
type: IDENTIFIER,
73+
name: 'foo'
74+
}
75+
}, name));
76+
});

test/rules/prefer-array-from.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import test from 'ava';
2+
import AvaRuleTester from 'eslint-ava-rule-tester';
3+
import rule from '../../rules/prefer-array-from';
4+
5+
const ruleTester = new AvaRuleTester(test, {
6+
parserOptions: {
7+
ecmaVersion: 2015
8+
}
9+
});
10+
11+
const message = "Use Array.from to convert from iterable to array";
12+
13+
ruleTester.run('from-map', rule, {
14+
valid: [
15+
'Array.from(new Set())',
16+
'Array.from(iterable)',
17+
'[1, ...iterable]',
18+
'[1, 2, 3]',
19+
'[iterable]'
20+
],
21+
invalid: [
22+
{
23+
code: '[...iterable]',
24+
errors: [ {
25+
message,
26+
column: 1,
27+
line: 1
28+
} ],
29+
output: 'Array.from(iterable)'
30+
},
31+
{
32+
code: '[...[1, 2]]',
33+
errors: [ {
34+
message,
35+
column: 1,
36+
line: 1
37+
} ],
38+
output: 'Array.from([1, 2])'
39+
},
40+
{
41+
code: '[..."test"]',
42+
errors: [ {
43+
message,
44+
column: 1,
45+
line: 1
46+
} ],
47+
output: 'Array.from("test")'
48+
}
49+
]
50+
});

test/type.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import test from 'ava';
2+
import {
3+
SPREAD_ELEMENT,
4+
ARROW_FUNCTION_EXPRESSION,
5+
MEMBER_EXPRESSION,
6+
IDENTIFIER
7+
} from '../lib/type';
8+
9+
test('Spread element', (t) => {
10+
t.is(SPREAD_ELEMENT, "SpreadElement");
11+
});
12+
13+
test('Arrow function expression', (t) => {
14+
t.is(ARROW_FUNCTION_EXPRESSION, "ArrowFunctionExpression");
15+
});
16+
17+
test('Member expression', (t) => {
18+
t.is(MEMBER_EXPRESSION, "MemberExpression");
19+
});
20+
21+
test('Identifier', (t) => {
22+
t.is(IDENTIFIER, "Identifier");
23+
});

0 commit comments

Comments
 (0)