diff --git a/config/future.js b/config/future.js index 5752589..07b4ff0 100644 --- a/config/future.js +++ b/config/future.js @@ -3,5 +3,6 @@ module.exports = { "effector/prefer-sample-over-forward-with-mapping": "off", "effector/no-forward": "warn", "effector/no-guard": "warn", + "effector/no-on": "error", }, }; diff --git a/docs/rules/no-on.md b/docs/rules/no-on.md new file mode 100644 index 0000000..45eb93c --- /dev/null +++ b/docs/rules/no-on.md @@ -0,0 +1,20 @@ +# effector/no-on + +This rule forbids `.on` chaining on effector store. + +--- + +```ts +const event = createEvent() +// 👎 could be replaced +const $store = createStore(null).on(event, (_, s) => s) + + +const event = createEvent() +const $store = createStore(null) +// 👍 makes sense +sample({ + clock: event, + target: $store +}) +``` diff --git a/index.js b/index.js index d727851..d330920 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,7 @@ module.exports = { "mandatory-scope-binding": require("./rules/mandatory-scope-binding/mandatory-scope-binding"), "prefer-useUnit": require("./rules/prefer-useUnit/prefer-useUnit"), "no-patronum-debug": require("./rules/no-patronum-debug/no-patronum-debug"), + "no-on": require("./rules/no-on/no-on"), }, configs: { recommended: require("./config/recommended"), diff --git a/rules/no-on/examples/correct.ts b/rules/no-on/examples/correct.ts new file mode 100644 index 0000000..2fe9e81 --- /dev/null +++ b/rules/no-on/examples/correct.ts @@ -0,0 +1,10 @@ +import { createEvent, createStore, sample } from "effector"; + +const event = createEvent(); + +const $store = createStore(null); + +sample({ + clock: event, + target: $store, +}); diff --git a/rules/no-on/examples/incorrect-chaining.ts b/rules/no-on/examples/incorrect-chaining.ts new file mode 100644 index 0000000..a3d090a --- /dev/null +++ b/rules/no-on/examples/incorrect-chaining.ts @@ -0,0 +1,9 @@ +import { createEvent, createStore } from "effector"; + +const event = createEvent(); +const event2 = createEvent(); + +const $store = createStore(null) + .on(event, (_, s) => s) + .on(event2, (_, s) => s) + .on(event, (_, s) => s); diff --git a/rules/no-on/examples/incorrect.ts b/rules/no-on/examples/incorrect.ts new file mode 100644 index 0000000..52c91a4 --- /dev/null +++ b/rules/no-on/examples/incorrect.ts @@ -0,0 +1,5 @@ +import { createEvent, createStore } from "effector"; + +const event = createEvent(); + +const $store = createStore(null).on(event, (_, s) => s); diff --git a/rules/no-on/no-on.js b/rules/no-on/no-on.js new file mode 100644 index 0000000..1458021 --- /dev/null +++ b/rules/no-on/no-on.js @@ -0,0 +1,69 @@ +const { createLinkToRule } = require("../../utils/create-link-to-rule"); +const { + traverseNestedObjectNode, +} = require("../../utils/traverse-nested-object-node"); +const { is } = require("../../utils/is"); +const { ESLintUtils } = require("@typescript-eslint/utils"); + +module.exports = { + meta: { + type: "problem", + docs: { + description: "Forbids `.on` chaining on effector store.", + category: "Quality", + recommended: true, + url: createLinkToRule("no-on"), + }, + messages: { + noOn: "Method `.on` is forbidden on any effector store.", + }, + schema: [], + }, + create(context) { + let parserServices; + try { + parserServices = ESLintUtils.getParserServices(context); + } catch (err) { + // no types information + } + + if (!parserServices?.program) return {}; + + return { + 'CallExpression[callee.property.name="on"]'(node) { + const storeObject = traverseNestedObjectNode( + getNestedCallee(node) ?? getAssignedVariable(node) + ); + + if (!is.store({ context, node: storeObject })) { + return; + } + + context.report({ + node, + messageId: "noOn", + }); + }, + }; + }, +}; + +function getNestedCallee(node) { + const { callee } = node; + + if (callee.object?.type === "CallExpression") { + return getNestedCallee(callee.object); + } + + return callee.object; +} + +function getAssignedVariable(node) { + const { parent } = node; + + if (parent.type === "VariableDeclarator") { + return parent; + } + + return getAssignedVariable(parent); +} diff --git a/rules/no-on/no-on.md b/rules/no-on/no-on.md new file mode 100644 index 0000000..a0b742c --- /dev/null +++ b/rules/no-on/no-on.md @@ -0,0 +1 @@ +https://eslint.effector.dev/rules/no-on.html diff --git a/rules/no-on/no-on.ts.test.js b/rules/no-on/no-on.ts.test.js new file mode 100644 index 0000000..028caf8 --- /dev/null +++ b/rules/no-on/no-on.ts.test.js @@ -0,0 +1,53 @@ +const { RuleTester } = require("@typescript-eslint/rule-tester"); +const { join } = require("path"); + +const { readExample } = require("../../utils/read-example"); +const rule = require("./no-on"); + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + project: "./tsconfig.json", + tsconfigRootDir: join(__dirname, ".."), + }, +}); + +const readExampleForTheRule = (name) => ({ + code: readExample(__dirname, name), + filename: join(__dirname, "examples", name), +}); + +ruleTester.run("effector/no-on.ts.test", rule, { + valid: ["correct.ts"].map(readExampleForTheRule), + + invalid: [ + ...["incorrect.ts"].map(readExampleForTheRule).map((result) => ({ + ...result, + errors: [ + { + messageId: "noOn", + type: "CallExpression", + }, + ], + })), + ...["incorrect-chaining.ts"].map(readExampleForTheRule).map((result) => ({ + ...result, + errors: [ + { + messageId: "noOn", + type: "CallExpression", + }, + { + messageId: "noOn", + type: "CallExpression", + }, + { + messageId: "noOn", + type: "CallExpression", + }, + ], + })), + ], +});