Skip to content

Commit 1484f97

Browse files
authored
Merge pull request #32 from buildo/prefer-bimap
Add prefer-bimap
2 parents 337a311 + 9661d2a commit 1484f97

File tree

6 files changed

+366
-4
lines changed

6 files changed

+366
-4
lines changed

README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,16 @@ and enable the rules you want, for example
4040
'fp-ts/lib' (autofixable 🔧)
4141
- [fp-ts/no-pipeable](docs/rules/no-pipeable.md): Disallow imports from the
4242
'pipeable' module (autofixable 🔧)
43-
- [fp-ts/prefer-traverse](docs/rules/prefer-traverse.md): Replace map + sequence
44-
with traverse (autofixable 🔧)
43+
- [fp-ts/no-module-imports](docs/rules/no-module-imports.md): Disallow imports
44+
from fp-ts modules (autofixable 🔧)
4545
- [fp-ts/no-redundant-flow](docs/rules/no-redundant-flow.md): Remove redundant
4646
uses of flow (autofixable 🔧)
47+
- [fp-ts/prefer-traverse](docs/rules/prefer-traverse.md): Replace map + sequence
48+
with traverse (autofixable 🔧)
4749
- [fp-ts/prefer-chain](docs/rules/prefer-chain.md): Replace map + flatten with
4850
chain (autofixable 🔧)
49-
- [fp-ts/no-module-imports](docs/rules/no-module-imports.md): Disallow imports
50-
from fp-ts modules (autofixable 🔧)
51+
- [fp-ts/prefer-bimap](docs/rules/prefer-bimap.md): Replace map + mapLeft with
52+
bimap (autofixable 🔧)
5153

5254
## Configurations
5355

docs/rules/prefer-bimap.md

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Replace map + mapLeft with bimap (fp-ts/prefer-chain)
2+
3+
Suggest replacing the combination of `map` followed by `mapLeft` (or vice-versa)
4+
with `bimap`.
5+
6+
**Fixable**: This rule is automatically fixable using the `--fix` flag on the
7+
command line.
8+
9+
## Rule Details
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```ts
14+
import { pipe } from "fp-ts/function";
15+
import { either } from "fp-ts";
16+
17+
pipe(
18+
getResult(),
19+
either.map((a) => a + 1),
20+
either.mapLeft((e) => e + 1)
21+
);
22+
```
23+
24+
```ts
25+
import { pipe } from "fp-ts/function";
26+
import { either } from "fp-ts";
27+
28+
pipe(
29+
getResult(),
30+
either.mapLeft((e) => e + 1),
31+
either.map((a) => a + 1)
32+
);
33+
```
34+
35+
Example of **correct** code for this rule:
36+
37+
```ts
38+
import { pipe } from "fp-ts/function";
39+
import { either } from "fp-ts";
40+
41+
pipe(
42+
getResult(),
43+
either.bimap(
44+
(e) => e + 1,
45+
(a) => a + 1
46+
)
47+
);
48+
```

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const suggestions = {
1111
"prefer-traverse": require("./rules/prefer-traverse").default,
1212
"no-redundant-flow": require("./rules/no-redundant-flow").default,
1313
"prefer-chain": require("./rules/prefer-chain").default,
14+
"prefer-bimap": require("./rules/prefer-bimap").default,
1415
};
1516

1617
export const rules = {

src/rules/prefer-bimap.ts

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {
2+
AST_NODE_TYPES,
3+
TSESTree,
4+
} from "@typescript-eslint/experimental-utils";
5+
import { boolean, option, apply } from "fp-ts";
6+
import { constVoid, pipe } from "fp-ts/function";
7+
import {
8+
calleeIdentifier,
9+
contextUtils,
10+
createRule,
11+
getAdjacentCombinators,
12+
inferIndent,
13+
prettyPrint,
14+
} from "../utils";
15+
16+
export default createRule({
17+
name: "prefer-bimap",
18+
meta: {
19+
type: "suggestion",
20+
fixable: "code",
21+
schema: [],
22+
docs: {
23+
category: "Best Practices",
24+
description: "Replace map + mapLeft with bimap",
25+
recommended: "warn",
26+
},
27+
messages: {
28+
mapMapLeftIsBimap:
29+
"{{firstNode}} followed by {{secondNode}} can be replaced by bimap",
30+
replaceMapMapLeftBimap:
31+
"replace {{firstNode}} and {{secondNode}} with bimap",
32+
},
33+
},
34+
defaultOptions: [],
35+
create(context) {
36+
const { isPipeOrFlowExpression } = contextUtils(context);
37+
38+
return {
39+
CallExpression(node) {
40+
const mapThenMapLeft = () =>
41+
getAdjacentCombinators<
42+
TSESTree.CallExpression,
43+
TSESTree.CallExpression
44+
>(
45+
node,
46+
{
47+
name: "map",
48+
types: [AST_NODE_TYPES.CallExpression],
49+
},
50+
{
51+
name: "mapLeft",
52+
types: [AST_NODE_TYPES.CallExpression],
53+
},
54+
true
55+
);
56+
57+
const mapLeftThenMap = () =>
58+
getAdjacentCombinators<
59+
TSESTree.CallExpression,
60+
TSESTree.CallExpression
61+
>(
62+
node,
63+
{
64+
name: "mapLeft",
65+
types: [AST_NODE_TYPES.CallExpression],
66+
},
67+
{
68+
name: "map",
69+
types: [AST_NODE_TYPES.CallExpression],
70+
},
71+
true
72+
);
73+
74+
pipe(
75+
node,
76+
isPipeOrFlowExpression,
77+
boolean.fold(constVoid, () =>
78+
pipe(
79+
mapThenMapLeft(),
80+
option.alt(mapLeftThenMap),
81+
option.bindTo("combinators"),
82+
option.bind("calleeIdentifiers", ({ combinators }) =>
83+
apply.sequenceT(option.option)(
84+
calleeIdentifier(combinators[0]),
85+
calleeIdentifier(combinators[1])
86+
)
87+
),
88+
option.map(({ combinators, calleeIdentifiers }) => {
89+
context.report({
90+
loc: {
91+
start: combinators[0].loc.start,
92+
end: combinators[1].loc.end,
93+
},
94+
messageId: "mapMapLeftIsBimap",
95+
data: {
96+
firstNode: calleeIdentifiers[0].name,
97+
secondNode: calleeIdentifiers[1].name,
98+
},
99+
suggest: [
100+
{
101+
messageId: "replaceMapMapLeftBimap",
102+
data: {
103+
firstNode: calleeIdentifiers[0].name,
104+
secondNode: calleeIdentifiers[1].name,
105+
},
106+
fix(fixer) {
107+
const mapFirst = calleeIdentifiers[0].name === "map";
108+
const mapNode = mapFirst
109+
? combinators[0]
110+
: combinators[1];
111+
const mapLeftNode = mapFirst
112+
? combinators[1]
113+
: combinators[0];
114+
return [
115+
fixer.replaceTextRange(
116+
[combinators[0].range[0], combinators[1].range[1]],
117+
`${prettyPrint(mapNode.callee).replace(
118+
/map$/,
119+
"bimap"
120+
)}(\n${inferIndent(mapNode)} ${prettyPrint(
121+
mapLeftNode.arguments[0]!
122+
)},\n${inferIndent(mapNode)} ${prettyPrint(
123+
mapNode.arguments[0]!
124+
)}\n${inferIndent(mapNode)})`
125+
),
126+
];
127+
},
128+
},
129+
],
130+
});
131+
})
132+
)
133+
)
134+
);
135+
},
136+
};
137+
},
138+
});

src/utils.ts

+4
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ export function prettyPrint(node: TSESTree.Node): string {
156156
return generate(node as any);
157157
}
158158

159+
export function inferIndent(node: TSESTree.Node): string {
160+
return new Array(node.loc.start.column + 1).join(" ");
161+
}
162+
159163
export const contextUtils = <
160164
TMessageIds extends string,
161165
TOptions extends readonly unknown[]

tests/rules/prefer-bimap.test.ts

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import rule from "../../src/rules/prefer-bimap";
2+
import { ESLintUtils } from "@typescript-eslint/experimental-utils";
3+
import { stripIndent } from "common-tags";
4+
5+
const ruleTester = new ESLintUtils.RuleTester({
6+
parser: "@typescript-eslint/parser",
7+
});
8+
9+
ruleTester.run("prefer-bimap", rule, {
10+
valid: [
11+
{
12+
code: stripIndent`
13+
import { either } from "fp-ts"
14+
import { pipe } from "fp-ts/function"
15+
16+
pipe(
17+
getResult(),
18+
either.bimap(e => e.toString(), a => a.toString())
19+
)
20+
`,
21+
},
22+
],
23+
invalid: [
24+
{
25+
code: stripIndent`
26+
import { either } from "fp-ts"
27+
import { pipe } from "fp-ts/function"
28+
29+
pipe(
30+
getResult(),
31+
either.map(
32+
a => a.toString()
33+
),
34+
either.mapLeft(
35+
e => e.toString()
36+
)
37+
)
38+
`,
39+
errors: [
40+
{
41+
messageId: "mapMapLeftIsBimap",
42+
suggestions: [
43+
{
44+
messageId: "replaceMapMapLeftBimap",
45+
output: stripIndent`
46+
import { either } from "fp-ts"
47+
import { pipe } from "fp-ts/function"
48+
49+
pipe(
50+
getResult(),
51+
either.bimap(
52+
e => e.toString(),
53+
a => a.toString()
54+
)
55+
)
56+
`,
57+
},
58+
],
59+
},
60+
],
61+
},
62+
{
63+
code: stripIndent`
64+
import { either } from "fp-ts"
65+
import { pipe } from "fp-ts/function"
66+
67+
pipe(
68+
getResult(),
69+
either.mapLeft(
70+
e => e.toString()
71+
),
72+
either.map(
73+
a => a.toString()
74+
)
75+
)
76+
`,
77+
errors: [
78+
{
79+
messageId: "mapMapLeftIsBimap",
80+
suggestions: [
81+
{
82+
messageId: "replaceMapMapLeftBimap",
83+
output: stripIndent`
84+
import { either } from "fp-ts"
85+
import { pipe } from "fp-ts/function"
86+
87+
pipe(
88+
getResult(),
89+
either.bimap(
90+
e => e.toString(),
91+
a => a.toString()
92+
)
93+
)
94+
`,
95+
},
96+
],
97+
},
98+
],
99+
},
100+
{
101+
code: stripIndent`
102+
import { either } from "fp-ts"
103+
import { pipe } from "fp-ts/function"
104+
105+
pipe(
106+
getResult(),
107+
either.mapLeft(e => e.toString()),
108+
either.map(a => a.toString())
109+
)
110+
`,
111+
errors: [
112+
{
113+
messageId: "mapMapLeftIsBimap",
114+
suggestions: [
115+
{
116+
messageId: "replaceMapMapLeftBimap",
117+
output: stripIndent`
118+
import { either } from "fp-ts"
119+
import { pipe } from "fp-ts/function"
120+
121+
pipe(
122+
getResult(),
123+
either.bimap(
124+
e => e.toString(),
125+
a => a.toString()
126+
)
127+
)
128+
`,
129+
},
130+
],
131+
},
132+
],
133+
},
134+
{
135+
code: stripIndent`
136+
import { mapLeft, map } from "fp-ts/Either"
137+
import { pipe } from "fp-ts/function"
138+
139+
pipe(
140+
getResult(),
141+
mapLeft(e => e.toString()),
142+
map(a => a.toString())
143+
)
144+
`,
145+
errors: [
146+
{
147+
messageId: "mapMapLeftIsBimap",
148+
suggestions: [
149+
{
150+
messageId: "replaceMapMapLeftBimap",
151+
output: stripIndent`
152+
import { mapLeft, map } from "fp-ts/Either"
153+
import { pipe } from "fp-ts/function"
154+
155+
pipe(
156+
getResult(),
157+
bimap(
158+
e => e.toString(),
159+
a => a.toString()
160+
)
161+
)
162+
`,
163+
},
164+
],
165+
},
166+
],
167+
},
168+
],
169+
});

0 commit comments

Comments
 (0)