Skip to content

Commit d440daa

Browse files
MrHensindresorhus
authored andcommitted
Add prefer-flat-map rule (#284)
1 parent e36d25c commit d440daa

File tree

7 files changed

+277
-10
lines changed

7 files changed

+277
-10
lines changed

docs/rules/prefer-flat-map.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Prefer `.flatMap(…)` over `.map(…).flat()`
2+
3+
[`Array#flatMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap) performs [`Array#map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) and [`Array#flat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat) in one step.
4+
5+
This rule is fixable.
6+
7+
8+
## Fail
9+
10+
```js
11+
[1, 2, 3].map(i => [i]).flat();
12+
```
13+
14+
## Pass
15+
16+
```js
17+
[1, 2, 3].flatMap(i => [i]);
18+
[1, 2, 3].map(i => [i]).foo().flat();
19+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ module.exports = {
4040
'unicorn/number-literal-case': 'error',
4141
'unicorn/prefer-add-event-listener': 'error',
4242
'unicorn/prefer-exponentiation-operator': 'error',
43+
'unicorn/prefer-flat-map': 'error',
4344
'unicorn/prefer-includes': 'error',
4445
'unicorn/prefer-node-append': 'error',
4546
'unicorn/prefer-node-remove': '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/number-literal-case": "error",
5959
"unicorn/prefer-add-event-listener": "error",
6060
"unicorn/prefer-exponentiation-operator": "error",
61+
"unicorn/prefer-flat-map": "error",
6162
"unicorn/prefer-includes": "error",
6263
"unicorn/prefer-node-append": "error",
6364
"unicorn/prefer-node-remove": "error",
@@ -100,6 +101,7 @@ Configure it in `package.json`.
100101
- [number-literal-case](docs/rules/number-literal-case.md) - Enforce lowercase identifier and uppercase value for number literals. *(fixable)*
101102
- [prefer-add-event-listener](docs/rules/prefer-add-event-listener.md) - Prefer `addEventListener` over `on`-functions. *(fixable)*
102103
- [prefer-exponentiation-operator](docs/rules/prefer-exponentiation-operator.md) - Prefer the exponentiation operator over `Math.pow()` *(fixable)*
104+
- [prefer-flat-map](docs/rules/prefer-flat-map.md) - Prefer `.flatMap(…)` over `.map(…).flat()`. *(fixable)*
103105
- [prefer-includes](docs/rules/prefer-includes.md) - Prefer `.includes()` over `.indexOf()` when checking for existence or non-existence. *(fixable)*
104106
- [prefer-node-append](docs/rules/prefer-node-append.md) - Prefer `append` over `appendChild`. *(fixable)*
105107
- [prefer-node-remove](docs/rules/prefer-node-remove.md) - Prefer `remove` over `parentNode.removeChild` and `parentElement.removeChild`. *(fixable)*

rules/prefer-flat-map.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
'use strict';
2+
const getDocsUrl = require('./utils/get-docs-url');
3+
const isMethodNamed = require('./utils/is-method-named');
4+
5+
const MESSAGE_ID = 'preferFlatMap';
6+
7+
const report = (context, nodeFlat, nodeMap) => {
8+
const source = context.getSourceCode();
9+
10+
// Node covers:
11+
// map(…).flat();
12+
// ^^^^
13+
// (map(…)).flat();
14+
// ^^^^
15+
const flatIdentifer = nodeFlat.callee.property;
16+
17+
// Location will be:
18+
// map(…).flat();
19+
// ^
20+
// (map(…)).flat();
21+
// ^
22+
const dot = source.getTokenBefore(flatIdentifer);
23+
24+
// Location will be:
25+
// map(…).flat();
26+
// ^
27+
// (map(…)).flat();
28+
// ^
29+
const maybeSemicolon = source.getTokenAfter(nodeFlat);
30+
const hasSemicolon = Boolean(maybeSemicolon) && maybeSemicolon.value === ';';
31+
32+
// Location will be:
33+
// (map(…)).flat();
34+
// ^
35+
const tokenBetween = source.getLastTokenBetween(nodeMap, dot);
36+
37+
// Location will be:
38+
// map(…).flat();
39+
// ^
40+
// (map(…)).flat();
41+
// ^
42+
const beforeSemicolon = tokenBetween || nodeMap;
43+
44+
// Location will be:
45+
// map(…).flat();
46+
// ^
47+
// (map(…)).flat();
48+
// ^
49+
const fixEnd = nodeFlat.end;
50+
51+
// Location will be:
52+
// map(…).flat();
53+
// ^
54+
// (map(…)).flat();
55+
// ^
56+
const fixStart = dot.start;
57+
58+
const mapProperty = nodeMap.callee.property;
59+
60+
context.report({
61+
node: nodeFlat,
62+
messageId: MESSAGE_ID,
63+
fix: fixer => {
64+
const fixings = [
65+
// Removes:
66+
// map(…).flat();
67+
// ^^^^^^^
68+
// (map(…)).flat();
69+
// ^^^^^^^
70+
fixer.removeRange([fixStart, fixEnd]),
71+
72+
// Renames:
73+
// map(…).flat();
74+
// ^^^
75+
// (map(…)).flat();
76+
// ^^^
77+
fixer.replaceText(mapProperty, 'flatMap')
78+
];
79+
80+
if (hasSemicolon) {
81+
// Moves semicolon to:
82+
// map(…).flat();
83+
// ^
84+
// (map(…)).flat();
85+
// ^
86+
fixings.push(fixer.insertTextAfter(beforeSemicolon, ';'));
87+
fixings.push(fixer.remove(maybeSemicolon));
88+
}
89+
90+
return fixings;
91+
}
92+
});
93+
};
94+
95+
const create = context => ({
96+
CallExpression: node => {
97+
if (!isMethodNamed(node, 'flat')) {
98+
return;
99+
}
100+
101+
const parent = node.callee.object;
102+
103+
if (!isMethodNamed(parent, 'map')) {
104+
return;
105+
}
106+
107+
report(context, node, parent);
108+
}
109+
});
110+
111+
module.exports = {
112+
create,
113+
meta: {
114+
type: 'suggestion',
115+
docs: {
116+
url: getDocsUrl(__filename)
117+
},
118+
fixable: 'code',
119+
messages: {
120+
[MESSAGE_ID]: 'Prefer `.flatMap(…)` over `.map(…).flat()`.'
121+
}
122+
}
123+
};

rules/prefer-includes.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
'use strict';
22
const getDocsUrl = require('./utils/get-docs-url');
3-
4-
const isIndexOf = node => {
5-
return (
6-
node.type === 'CallExpression' &&
7-
node.callee.type === 'MemberExpression' &&
8-
node.callee.property.type === 'Identifier' &&
9-
node.callee.property.name === 'indexOf'
10-
);
11-
};
3+
const isMethodNamed = require('./utils/is-method-named');
124

135
const isNegativeOne = (operator, value) => operator === '-' && value === 1;
146

@@ -33,7 +25,7 @@ const create = context => ({
3325
BinaryExpression: node => {
3426
const {left, right} = node;
3527

36-
if (isIndexOf(left)) {
28+
if (isMethodNamed(left, 'indexOf')) {
3729
const target = left.callee.object;
3830
const pattern = left.arguments[0];
3931

rules/utils/is-method-named.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const isMethodNamed = (node, name) => {
2+
return (
3+
node.type === 'CallExpression' &&
4+
node.callee.type === 'MemberExpression' &&
5+
node.callee.property.type === 'Identifier' &&
6+
node.callee.property.name === name
7+
);
8+
};
9+
10+
module.exports = isMethodNamed;

test/prefer-flat-map.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import test from 'ava';
2+
import avaRuleTester from 'eslint-ava-rule-tester';
3+
import rule from '../rules/prefer-flat-map';
4+
5+
const ruleTester = avaRuleTester(test, {
6+
env: {
7+
es6: true
8+
}
9+
});
10+
11+
const error = {
12+
ruleId: 'prefer-flat-map',
13+
messageId: 'preferFlatMap'
14+
};
15+
16+
ruleTester.run('prefer-flat-map', rule, {
17+
valid: [
18+
'const bar = [1,2,3].map()',
19+
'const bar = [1,2,3].map(i => i)',
20+
'const bar = [1,2,3].map((i) => i)',
21+
'const bar = [1,2,3].map((i) => { return i; })',
22+
'const bar = foo.map(i => i)',
23+
'const bar = [[1],[2],[3]].flat()',
24+
'const bar = [1,2,3].map(i => [i]).sort().flat()',
25+
`
26+
let bar = [1,2,3].map(i => [i]);
27+
bar = bar.flat();
28+
`
29+
],
30+
invalid: [
31+
{
32+
code: 'const bar = [1,2,3].map(i => [i]).flat()',
33+
output: 'const bar = [1,2,3].flatMap(i => [i])',
34+
errors: [error]
35+
},
36+
{
37+
code: 'const bar = [1,2,3].map((i) => [i]).flat()',
38+
output: 'const bar = [1,2,3].flatMap((i) => [i])',
39+
errors: [error]
40+
},
41+
{
42+
code: 'const bar = [1,2,3].map((i) => { return [i]; }).flat()',
43+
output: 'const bar = [1,2,3].flatMap((i) => { return [i]; })',
44+
errors: [error]
45+
},
46+
{
47+
code: 'const bar = [1,2,3].map(foo).flat()',
48+
output: 'const bar = [1,2,3].flatMap(foo)',
49+
errors: [error]
50+
},
51+
{
52+
code: 'const bar = foo.map(i => [i]).flat()',
53+
output: 'const bar = foo.flatMap(i => [i])',
54+
errors: [error]
55+
},
56+
{
57+
code: 'const bar = { map: () => {} }.map(i => [i]).flat()',
58+
output: 'const bar = { map: () => {} }.flatMap(i => [i])',
59+
errors: [error]
60+
},
61+
{
62+
code: 'const bar = [1,2,3].map(i => i).map(i => [i]).flat()',
63+
output: 'const bar = [1,2,3].map(i => i).flatMap(i => [i])',
64+
errors: [error]
65+
},
66+
{
67+
code: 'const bar = [1,2,3].sort().map(i => [i]).flat()',
68+
output: 'const bar = [1,2,3].sort().flatMap(i => [i])',
69+
errors: [error]
70+
},
71+
{
72+
code: 'const bar = (([1,2,3].map(i => [i]))).flat()',
73+
output: 'const bar = (([1,2,3].flatMap(i => [i])))',
74+
errors: [error]
75+
},
76+
{
77+
code: `
78+
let bar = [1,2,3].map(i => {
79+
return [i];
80+
}).flat();
81+
`,
82+
output: `
83+
let bar = [1,2,3].flatMap(i => {
84+
return [i];
85+
});
86+
`,
87+
errors: [error]
88+
},
89+
{
90+
code: 'let bar = [1,2,3].map(i => {\nreturn [i];\n})\n.flat();',
91+
output: 'let bar = [1,2,3].flatMap(i => {\nreturn [i];\n});\n',
92+
errors: [error]
93+
},
94+
{
95+
code: 'let bar = [1,2,3].map(i => {\nreturn [i];\n}) // comment\n.flat();',
96+
output: 'let bar = [1,2,3].flatMap(i => {\nreturn [i];\n}); // comment\n',
97+
errors: [error]
98+
},
99+
{
100+
code: 'let bar = [1,2,3].map(i => {\nreturn [i];\n}) // comment\n.flat(); // other',
101+
output: 'let bar = [1,2,3].flatMap(i => {\nreturn [i];\n}); // comment\n // other',
102+
errors: [error]
103+
},
104+
{
105+
code: 'let bar = [1,2,3]\n .map(i => { return [i]; })\n .flat();',
106+
output: 'let bar = [1,2,3]\n .flatMap(i => { return [i]; });\n ',
107+
errors: [error]
108+
},
109+
{
110+
code: 'let bar = [1,2,3].map(i => { return [i]; })\n .flat();',
111+
output: 'let bar = [1,2,3].flatMap(i => { return [i]; });\n ',
112+
errors: [error]
113+
},
114+
{
115+
code: 'let bar = [1,2,3] . map( x => y ) . flat () // 🤪',
116+
output: 'let bar = [1,2,3] . flatMap( x => y ) // 🤪',
117+
errors: [error]
118+
}
119+
]
120+
});

0 commit comments

Comments
 (0)