Skip to content

Commit d6d8814

Browse files
committed
feat: no-unnecessary-combination
1 parent 75eabae commit d6d8814

File tree

6 files changed

+300
-2
lines changed

6 files changed

+300
-2
lines changed

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import noForward from "./rules/no-forward/no-forward"
1212
import noGetState from "./rules/no-getState/no-getState"
1313
import noGuard from "./rules/no-guard/no-guard"
1414
import noPatronumDebug from "./rules/no-patronum-debug/no-patronum-debug"
15+
import noUnnecessaryCombination from "./rules/no-unnecessary-combination/no-unnecessary-combination"
1516
import noUnnecessaryDuplication from "./rules/no-unnecessary-duplication/no-unnecessary-duplication"
1617
import noUselessMethods from "./rules/no-useless-methods/no-useless-methods"
1718
import noWatch from "./rules/no-watch/no-watch"
@@ -33,6 +34,7 @@ const base = {
3334
"no-getState": noGetState,
3435
"no-guard": noGuard,
3536
"no-patronum-debug": noPatronumDebug,
37+
"no-unnecessary-combination": noUnnecessaryCombination,
3638
"no-unnecessary-duplication": noUnnecessaryDuplication,
3739
"no-useless-methods": noUselessMethods,
3840
"no-watch": noWatch,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
description: Forbid unnecessary combinations in clock and source
3+
---
4+
5+
# effector/no-unnecessary-combination
6+
7+
Call of `combine`/`merge` in `clock`/`source` is unnecessary. It can be omitted from source code.
8+
9+
```ts
10+
// 👎 can be simplified
11+
const badEventOne = guard({
12+
clock: combine($store1, $store2),
13+
filter: $filter,
14+
})
15+
const badEventOne = guard({
16+
clock: combine($store1, $store2, (store1, store2) => ({
17+
x: store1,
18+
y: store2,
19+
})),
20+
filter: $filter,
21+
})
22+
23+
// 👍 better
24+
const goodEventOne = guard({ clock: [$store1, $store2], filter: $filter })
25+
const goodEventTwo = guard({
26+
clock: { x: $store1, y: $store2 },
27+
filter: $filter,
28+
})
29+
```
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { RuleTester } from "@typescript-eslint/rule-tester"
2+
import { parser } from "typescript-eslint"
3+
4+
import { ts } from "@/shared/tag"
5+
6+
import rule from "./no-unnecessary-combination"
7+
8+
const ruleTester = new RuleTester({
9+
languageOptions: {
10+
parser,
11+
parserOptions: {
12+
projectService: { allowDefaultProject: ["*.ts"], defaultProject: "tsconfig.fixture.json" },
13+
},
14+
},
15+
})
16+
17+
ruleTester.run("no-unnecessary-combination", rule, {
18+
valid: [
19+
{
20+
name: "no combination",
21+
code: ts`
22+
import { createEvent, createStore, forward, guard, sample } from "effector"
23+
24+
const a = createEvent()
25+
const b = createEvent()
26+
const $x = createStore(null)
27+
const $y = createStore(null)
28+
29+
sample({ clock: [a, b], source: [$x, $y] })
30+
sample({ clock: a, source: [$x, $y] })
31+
sample({ clock: a, source: { a: $x, b: $y } })
32+
33+
guard({ clock: [a, b], source: $x, filter: Boolean })
34+
guard({ clock: a, source: { a: $x }, filter: Boolean })
35+
`,
36+
},
37+
{
38+
name: "combine in clock",
39+
code: ts`
40+
import { combine, createStore, sample, guard } from "effector"
41+
42+
const $x = createStore(null)
43+
const $y = createStore(null)
44+
45+
guard({ clock: combine({ $x, $y }) })
46+
sample({ clock: combine($x, $y) })
47+
`,
48+
},
49+
{
50+
name: "merge/combine outside",
51+
code: ts`
52+
import { merge, createEvent, createStore, combine } from "effector"
53+
54+
const a = createEvent()
55+
const b = createEvent()
56+
const $x = createStore(null)
57+
const $y = createStore(null)
58+
59+
const merged = merge([a, b])
60+
const combined = combine({ a, b }, (v) => Object.values(v))
61+
`,
62+
},
63+
{
64+
name: "nested source -> clock -> combine",
65+
code: ts`
66+
import { combine, createStore, merge, sample, guard } from "effector"
67+
68+
const a = createEvent()
69+
70+
const $x = createStore(null)
71+
const $y = createStore(null)
72+
73+
sample({ source: sample({ clock: combine({ $x, $y }) }) })
74+
`,
75+
},
76+
],
77+
invalid: [
78+
{
79+
name: "combine in guard.source",
80+
code: ts`
81+
import { combine, createStore, guard } from "effector"
82+
83+
const $x = createStore(null)
84+
const $y = createStore(null)
85+
86+
guard({ source: combine($x, $y), filter: Boolean })
87+
`,
88+
errors: [
89+
{
90+
messageId: "unnecessary",
91+
line: 6,
92+
column: 17,
93+
data: { method: "combine", property: "source", operator: "guard" },
94+
},
95+
],
96+
},
97+
{
98+
name: "combine in sample.source",
99+
code: ts`
100+
import { combine, createStore, sample } from "effector"
101+
102+
const $x = createStore(null)
103+
const $y = createStore(null)
104+
105+
sample({ source: combine({ a: $x, b: $y }), fn: (v) => v })
106+
`,
107+
errors: [
108+
{
109+
messageId: "unnecessary",
110+
line: 6,
111+
column: 18,
112+
data: { method: "combine", property: "source", operator: "sample" },
113+
},
114+
],
115+
},
116+
{
117+
name: "merge in guard.clock",
118+
code: ts`
119+
import { merge, createEvent, guard } from "effector"
120+
121+
const a = createEvent()
122+
const b = createEvent()
123+
124+
guard({ clock: merge([a, b]), filter: Boolean })
125+
`,
126+
errors: [
127+
{
128+
messageId: "unnecessary",
129+
line: 6,
130+
column: 16,
131+
data: { method: "merge", property: "clock", operator: "guard" },
132+
},
133+
],
134+
},
135+
{
136+
name: "merge in sample.clock",
137+
code: ts`
138+
import { merge, createEvent, sample } from "effector"
139+
140+
const a = createEvent()
141+
const b = createEvent()
142+
143+
sample({ clock: merge([a, b]), filter: Boolean })
144+
`,
145+
errors: [
146+
{
147+
messageId: "unnecessary",
148+
line: 6,
149+
column: 17,
150+
data: { method: "merge", property: "clock", operator: "sample" },
151+
},
152+
],
153+
},
154+
155+
{
156+
name: "merge in guard.source",
157+
code: ts`
158+
import { merge, createEvent, guard } from "effector"
159+
160+
const a = createEvent()
161+
const b = createEvent()
162+
163+
guard({ clock: a, source: merge([a, b]), filter: Boolean })
164+
`,
165+
errors: [
166+
{
167+
messageId: "unnecessary",
168+
line: 6,
169+
column: 27,
170+
data: { method: "merge", property: "source", operator: "guard" },
171+
},
172+
],
173+
},
174+
{
175+
name: "merge in sample.source",
176+
code: ts`
177+
import { merge, createEvent, sample } from "effector"
178+
179+
const a = createEvent()
180+
const b = createEvent()
181+
182+
sample({ clock: a, source: merge([a, b]), fn: (v) => v })
183+
`,
184+
errors: [
185+
{
186+
messageId: "unnecessary",
187+
line: 6,
188+
column: 28,
189+
data: { method: "merge", property: "source", operator: "sample" },
190+
},
191+
],
192+
},
193+
],
194+
})
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { type TSESTree as Node, AST_NODE_TYPES as NodeType } from "@typescript-eslint/utils"
2+
3+
import { createRule } from "@/shared/create"
4+
import { locate } from "@/shared/locate"
5+
6+
type CombinatorOperator = "combine" | "merge"
7+
8+
export default createRule({
9+
name: "no-unnecessary-combination",
10+
meta: {
11+
type: "suggestion",
12+
docs: {
13+
description: "Forbid unnecessary combinations in `clock` and `source`.",
14+
},
15+
messages: {
16+
unnecessary: "{{ method }} is used under the hood of {{ property }} in {{ operator }}, you can omit it.",
17+
},
18+
schema: [],
19+
},
20+
defaultOptions: [],
21+
create: (context) => {
22+
const operators = new Set<string>()
23+
const combinators = new Map<string, CombinatorOperator>()
24+
25+
const PACKAGE_NAME = /^effector(?:\u002Fcompat)?$/
26+
27+
const importSelector = `ImportDeclaration[source.value=${PACKAGE_NAME}]`
28+
const operatorSelector = `ImportSpecifier[imported.name=/(sample|guard)/]`
29+
const combinatorSelector = `ImportSpecifier[imported.name=/(combine|merge)/]`
30+
31+
const callSelector = `[callee.type="Identifier"][arguments.length=1]`
32+
const argumentSelector = `ObjectExpression.arguments`
33+
34+
const query = { source: locate.property("source"), clock: locate.property("clock") }
35+
36+
type MethodCall = Node.CallExpression & { callee: Node.Identifier; arguments: [Node.ObjectExpression] }
37+
38+
return {
39+
[`${importSelector} > ${operatorSelector}`]: (node: Node.ImportSpecifier) => operators.add(node.local.name),
40+
41+
[`${importSelector} > ${combinatorSelector}`]: (node: Node.ImportSpecifier & { imported: { name: string } }) =>
42+
combinators.set(node.local.name, node.imported.name as CombinatorOperator),
43+
44+
[`CallExpression${callSelector}:has(${argumentSelector})`]: (node: MethodCall) => {
45+
if (!operators.has(node.callee.name)) return
46+
47+
const [config] = node.arguments
48+
49+
const clock = query.clock(config)?.value
50+
const source = query.source(config)?.value
51+
52+
if (clock?.type === NodeType.CallExpression && clock.callee.type === NodeType.Identifier) {
53+
const method = combinators.get(clock.callee.name)
54+
55+
if (method === "merge") {
56+
const data = { method: clock.callee.name, property: "clock", operator: node.callee.name }
57+
context.report({ node: clock, messageId: "unnecessary", data })
58+
}
59+
}
60+
61+
if (source?.type === NodeType.CallExpression && source.callee.type === NodeType.Identifier) {
62+
const method = combinators.get(source.callee.name)
63+
64+
// both "combine" and "merge" match
65+
if (method) {
66+
const data = { method: source.callee.name, property: "source", operator: node.callee.name }
67+
context.report({ node: source, messageId: "unnecessary", data })
68+
}
69+
}
70+
},
71+
}
72+
},
73+
})

src/rules/no-useless-methods/no-useless-methods.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
description: Forbid useless calls of `sample` and `guard`
2+
description: Forbid useless calls of sample and guard
33
---
44

55
# effector/no-useless-methods

src/ruleset.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const recommended = {
1010
"effector/no-getState": "error",
1111
"effector/no-guard": "error",
1212
"effector/no-patronum-debug": "warn",
13-
// "effector/no-unnecessary-combination": "warn",
13+
"effector/no-unnecessary-combination": "warn",
1414
"effector/no-unnecessary-duplication": "warn",
1515
"effector/no-useless-methods": "error",
1616
"effector/no-watch": "warn",

0 commit comments

Comments
 (0)