Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rule no watch #51

Merged
merged 12 commits into from
Oct 6, 2021
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ To configure individual rules:
- [effector/no-useless-methods](/rules/no-useless-methods/no-useless-methods.md)
- [effector/no-ambiguity-target](/rules/no-ambiguity-target/no-ambiguity-target.md)
- [effector/prefer-sample-over-forward-with-mapping](/rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.md)
- [effector/no-watch](/rules/no-watch/no-watch.md)
1 change: 1 addition & 0 deletions config/recommended.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ module.exports = {
"effector/no-unnecessary-duplication": "warn",
"effector/prefer-sample-over-forward-with-mapping": "warn",
"effector/no-ambiguity-target": "warn",
"effector/no-watch": "warn",
},
};
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ module.exports = {
"prefer-sample-over-forward-with-mapping": require("./rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping"),
"no-useless-methods": require("./rules/no-useless-methods/no-useless-methods"),
"no-ambiguity-target": require("./rules/no-ambiguity-target/no-ambiguity-target"),
"no-watch": require("./rules/no-watch/no-watch"),
},
configs: {
recommended: require("./config/recommended"),
}
},
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@
"dependencies": {
"prettier": "^2.3.2"
}
}
}
68 changes: 68 additions & 0 deletions rules/no-watch/examples/correct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const event = {
watch() {
return {};
},
};
const effect = {
done: {
watch() {
return {};
},
},
fail: {
watch() {
return {};
},
},
doneData: {
watch() {
return {};
},
},
failData: {
watch() {
return {};
},
},
finally: {
watch() {
return {};
},
},
watch() {
return {};
},
};
const $ = {
updates: {
watch() {
return {};
},
},
watch() {
return {};
},
};
const sample = {
watch() {
return {};
},
};
const guard = {
watch() {
return {};
},
};

event.watch();
effect.done.watch();
effect.fail.watch();
effect.doneData.watch();
effect.failData.watch();
effect.finally.watch();
$.watch();
$.updates.watch();
sample.watch();
guard.watch();

export const T = () => true;
6 changes: 6 additions & 0 deletions rules/no-watch/examples/incorrect/effect/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createEffect } from "effector";

const watcher = <T>(x: T) => x;
const watchableFx = createEffect();

watchableFx.watch(watcher);
7 changes: 7 additions & 0 deletions rules/no-watch/examples/incorrect/effect/done.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createEffect } from "effector";

const watcher = <T>(x: T) => x;
const watchableFx = createEffect();

watchableFx.done.watch(watcher);
watchableFx.doneData.watch(watcher);
7 changes: 7 additions & 0 deletions rules/no-watch/examples/incorrect/effect/fail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createEffect } from "effector";

const watcher = <T>(x: T) => x;
const watchableFx = createEffect();

watchableFx.fail.watch(watcher);
watchableFx.failData.watch(watcher);
6 changes: 6 additions & 0 deletions rules/no-watch/examples/incorrect/effect/finally.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createEffect } from "effector";

const watcher = <T>(x: T) => x;
const watchableFx = createEffect();

watchableFx.finally.watch(watcher);
6 changes: 6 additions & 0 deletions rules/no-watch/examples/incorrect/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createEvent } from "effector";

const watcher = <T>(x: T) => x;
const watchableEvent = createEvent();

watchableEvent.watch(watcher);
9 changes: 9 additions & 0 deletions rules/no-watch/examples/incorrect/guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createEvent, guard } from "effector";

const watcher = <T>(x: T) => x;
const myEvent = createEvent();

guard({
clock: myEvent,
filter: () => Math.random() > 0.5,
}).watch(watcher);
16 changes: 16 additions & 0 deletions rules/no-watch/examples/incorrect/sample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createEvent, sample } from "effector";

const watcher = <T>(x: T) => x;
const myEvent = createEvent();

sample({
clock: myEvent,
fn: () => true,
}).watch(watcher);

const newEvent = sample({
clock: myEvent,
fn: () => true,
});

newEvent.watch(watcher);
7 changes: 7 additions & 0 deletions rules/no-watch/examples/incorrect/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createStore } from "effector";

const watcher = <T>(x: T) => x;
const $watchable = createStore(0);

$watchable.watch(watcher);
$watchable.updates.watch(watcher);
57 changes: 57 additions & 0 deletions rules/no-watch/no-watch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const {
traverseNestedObjectNode,
} = require("../../utils/traverse-nested-object-node");

module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Avoid `.watch` calls on any Effector unit or operator",
category: "Quality",
recommended: true,
},
messages: {
abusiveCall:
"Method `.watch` leads to imperative code. Try to replace it with operators (`sample`, `guard`, etc) or use the `target` parameter of the operators.",
},
schema: [],
},
create(context) {
const { parserServices } = context;
if (!parserServices.hasFullTypeInformation) {
// JavaScript-way https://github.com/effector/eslint-plugin/issues/48#issuecomment-931107829
return {};
}
const checker = parserServices.program.getTypeChecker();

return {
CallExpression(node) {
const methodName = node.callee?.property?.name;
if (methodName !== "watch") {
return;
}

const object = traverseNestedObjectNode(node.callee?.object);
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(object);
const type = checker.getTypeAtLocation(originalNode);

const isEffectorUnit =
["Effect", "Event", "Store"].includes(type?.symbol?.escapedName) &&
type?.symbol?.parent?.escapedName?.includes("effector");

if (!isEffectorUnit) {
return;
}

reportWatchCall({ context, node });
},
};
},
};

function reportWatchCall({ context, node }) {
context.report({
node,
messageId: "abusiveCall",
});
}
42 changes: 42 additions & 0 deletions rules/no-watch/no-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# effector/no-watch

Method `.watch` leads to imperative code. Try replacing it with operators (`forward`, `sample`, etc) or use the `target` parameter of the operators.

> Caution! This rule only works on projects using TypeScript.

```ts
const myFx = createEffect();
const myEvent = createEvent();
const $awesome = createStore();

// 👍 good solutions
forward({
from: myFx.finally,
to: myEvent,
});

guard({
clock: myEvent,
filter: Boolean,
target: myFx,
});

sample({
from: $awesome.updates,
fn: identity,
to: myEvent,
});

// 👎 bad solutions
myFx.finally.watch(myEvent);

myEvent.watch((payload) => {
if (Boolean(payload)) {
myFx(payload);
}
});

$awesome.updates.watch((data) => {
myEvent(identity(data));
});
```
20 changes: 20 additions & 0 deletions rules/no-watch/no-watch.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const { RuleTester } = require("eslint");

const rule = require("./no-watch.js");

const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
},
});

ruleTester.run("effector/no-watch.test", rule, {
valid: [
"myFx.finally.watch(myEvent);",
"myEvent.watch((payload) => {if (Boolean(payload)) {myFx(payload);}});",
"$awesome.updates.watch((data) => {myEvent(identity(data));});",
].map((code) => ({ code })),

invalid: [],
});