Repository: effector/eslint-plugin
Pull request #155 (2024-02-12 16:09:04)

rule `no-duplicate-clock-or-source-array-values`
closes https://github.com/effector/eslint-plugin/issues/143

- [x]  docs
- [x]  test-cases

igorkamyshev: Thanks for the contribution 😇 I'll take a look this week.
igorkamyshev: Great job, thanks 🙏 

I'll release it with other changes soon.

Patch:
diff --git a/docs/rules/no-duplicate-clock-or-source-array-values.md b/docs/rules/no-duplicate-clock-or-source-array-values.md
new file mode 100644
index 0000000..b6a258e
--- /dev/null
+++ b/docs/rules/no-duplicate-clock-or-source-array-values.md
@@ -0,0 +1,46 @@
+# effector/no-duplicate-clock-or-source-array-values
+
+This rule forbids unit duplicates on `source` and `clock` with sample and guard methods.
+
+```js
+// from
+import { createEvent, createStore, sample } from "effector";
+import { createEffect } from "effector";
+
+const currentOrderUpdated = createEvent();
+const setUnloadDeliveryDateFx = createEffect();
+
+const $store = createStore(null);
+
+sample({
+  clock: [
+    setUnloadDeliveryDateFx.doneData,
+    setUnloadDeliveryDateFx.doneData,
+    $store,
+  ],
+  filter: Boolean,
+  target: currentOrderUpdated,
+});
+```
+
+---
+
+```js
+// to
+import { createEvent, createStore, sample } from "effector";
+import { createEffect } from "effector";
+
+const currentOrderUpdated = createEvent();
+const setUnloadDeliveryDateFx = createEffect();
+
+const $store = createStore(null);
+
+sample({
+  clock: [
+    setUnloadDeliveryDateFx.doneData, // dublicate removed
+    $store,
+  ],
+  filter: Boolean,
+  target: currentOrderUpdated,
+});
+```
diff --git a/index.js b/index.js
index d727851..ea36aa9 100644
--- a/index.js
+++ b/index.js
@@ -5,6 +5,7 @@ module.exports = {
     "no-getState": require("./rules/no-getState/no-getState"),
     "no-unnecessary-duplication": require("./rules/no-unnecessary-duplication/no-unnecessary-duplication"),
     "prefer-sample-over-forward-with-mapping": require("./rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping"),
+    "no-duplicate-clock-or-source-array-values": require("./rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values"),
     "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"),
diff --git a/rules/no-duplicate-clock-or-source-array-values/examples/correct-sample.ts b/rules/no-duplicate-clock-or-source-array-values/examples/correct-sample.ts
new file mode 100644
index 0000000..e27a50b
--- /dev/null
+++ b/rules/no-duplicate-clock-or-source-array-values/examples/correct-sample.ts
@@ -0,0 +1,21 @@
+import { createEvent, createStore, sample } from "effector";
+import { createEffect } from "effector";
+
+const currentOrderUpdated = createEvent();
+
+const setUnloadDeliveryDateFx = createEffect();
+const setLoadDeliveryDateFx = createEffect();
+
+const $store = createStore(null);
+const clickOnBtn = createEvent();
+
+sample({
+  clock: [
+    setUnloadDeliveryDateFx.doneData,
+    setLoadDeliveryDateFx,
+    $store,
+    clickOnBtn,
+  ],
+  filter: Boolean,
+  target: currentOrderUpdated,
+});
diff --git a/rules/no-duplicate-clock-or-source-array-values/examples/incorrect-guard.ts b/rules/no-duplicate-clock-or-source-array-values/examples/incorrect-guard.ts
new file mode 100644
index 0000000..171470f
--- /dev/null
+++ b/rules/no-duplicate-clock-or-source-array-values/examples/incorrect-guard.ts
@@ -0,0 +1,26 @@
+import { createEvent, createStore, guard } from "effector";
+import { createEffect } from "effector";
+
+const currentOrderUpdated = createEvent();
+
+const setUnloadDeliveryDateFx = createEffect();
+
+const $$order = {
+  setUnloadDeliveryDateFx,
+};
+
+const $store = createStore(null);
+const clickOnBtn = createEvent();
+
+guard({
+  source: [$store],
+  clock: [
+    setUnloadDeliveryDateFx.doneData,
+    $$order.setUnloadDeliveryDateFx.doneData,
+    clickOnBtn,
+    setUnloadDeliveryDateFx.doneData,
+    clickOnBtn,
+  ],
+  filter: Boolean,
+  target: currentOrderUpdated,
+});
diff --git a/rules/no-duplicate-clock-or-source-array-values/examples/incorrect-sample.ts b/rules/no-duplicate-clock-or-source-array-values/examples/incorrect-sample.ts
new file mode 100644
index 0000000..bca2f9e
--- /dev/null
+++ b/rules/no-duplicate-clock-or-source-array-values/examples/incorrect-sample.ts
@@ -0,0 +1,24 @@
+import { createEvent, createStore, sample } from "effector";
+import { createEffect } from "effector";
+
+const currentOrderUpdated = createEvent();
+
+const setUnloadDeliveryDateFx = createEffect();
+
+const $$order = {
+  setUnloadDeliveryDateFx,
+};
+
+const $store = createStore(null);
+const clickOnBtn = createEvent();
+
+sample({
+  source: [$store, $store],
+  clock: [
+    setUnloadDeliveryDateFx.doneData,
+    $$order.setUnloadDeliveryDateFx.doneData,
+    setUnloadDeliveryDateFx.doneData,
+  ],
+  filter: Boolean,
+  target: currentOrderUpdated,
+});
diff --git a/rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.js b/rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.js
new file mode 100644
index 0000000..0dfef27
--- /dev/null
+++ b/rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.js
@@ -0,0 +1,120 @@
+const { extractImportedFrom } = require("../../utils/extract-imported-from");
+const { createLinkToRule } = require("../../utils/create-link-to-rule");
+const { method } = require("../../utils/method");
+const {} = require("../../utils/traverse-nested-object-node");
+
+module.exports = {
+  meta: {
+    type: "problem",
+    hasSuggestions: true,
+    docs: {
+      description: "Forbids unit duplicates on `source` and `clock``",
+      category: "Quality",
+      recommended: true,
+      url: createLinkToRule("no-duplicate-clock-or-source-array-values"),
+    },
+    messages: {
+      duplicatesInClock: "Clock contains duplicate units - {{ memberPath }}.",
+      duplicatesInSource: "Source contains duplicate units - {{ memberPath }}.",
+      removeDuplicate: "Remove duplicate {{ memberPath }}.",
+    },
+    schema: [],
+  },
+  create(context) {
+    const importedFromEffector = new Map();
+
+    return {
+      ImportDeclaration(node) {
+        extractImportedFrom({
+          importMap: importedFromEffector,
+          node,
+          packageName: "effector",
+        });
+      },
+      CallExpression(node) {
+        if (
+          method.isNot(["sample", "guard"], {
+            node,
+            importMap: importedFromEffector,
+          })
+        ) {
+          return;
+        }
+
+        const properties = getSourceOrClockProperties(node.arguments[0]);
+
+        properties.forEach(({ key, value }) => {
+          const propType = key.name;
+          const elements = value.elements;
+
+          const usedUnits = new Set();
+
+          for (const node of elements) {
+            const memberPath = createMemberExpressionPath(node);
+
+            if (usedUnits.has(memberPath)) {
+              const messageId = getMessageIdByPropType(propType);
+
+              context.report({
+                node,
+                messageId,
+                data: {
+                  memberPath,
+                },
+                suggest: [
+                  {
+                    messageId: "removeDuplicate",
+                    data: { memberPath },
+                    fix(fixer) {
+                      return fixer.remove(node);
+                    },
+                  },
+                ],
+              });
+
+              return;
+            }
+
+            usedUnits.add(memberPath);
+          }
+        });
+      },
+    };
+  },
+};
+
+function createMemberExpressionPath(node, chain = "") {
+  const compactStrings = (...args) => args.filter(Boolean).join(".");
+
+  if (node.type === "MemberExpression") {
+    const propertyName = node.property.name;
+
+    const updatedChain = compactStrings(propertyName, chain);
+
+    return createMemberExpressionPath(node.object, updatedChain);
+  }
+
+  chain = compactStrings(node.name, chain);
+
+  // remove last dot
+  return chain.slice(0, -1);
+}
+
+function getSourceOrClockProperties(node) {
+  if (node.type !== "ObjectExpression") return [];
+
+  const allowedProps = ["clock", "source"];
+
+  const isClockOrSourceArray = (prop) => {
+    return (
+      allowedProps.includes(prop.key.name) &&
+      prop.value.type === "ArrayExpression"
+    );
+  };
+
+  return node.properties.filter(isClockOrSourceArray);
+}
+
+function getMessageIdByPropType(propType) {
+  return propType === "clock" ? "duplicatesInClock" : "duplicatesInSource";
+}
diff --git a/rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.md b/rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.md
new file mode 100644
index 0000000..9ac460c
--- /dev/null
+++ b/rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.md
@@ -0,0 +1,1 @@
+https://eslint.effector.dev/rules/no-duplicate-clock-or-source-array-values.html


Test patch:
diff --git a/rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.ts.test.js b/rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.ts.test.js
new file mode 100644
index 0000000..a0c745c
--- /dev/null
+++ b/rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.ts.test.js
@@ -0,0 +1,68 @@
+const { RuleTester } = require("@typescript-eslint/rule-tester");
+const { join } = require("path");
+
+const { readExample } = require("../../utils/read-example");
+const rule = require("./no-duplicate-clock-or-source-array-values");
+
+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-duplicate-clock-or-source-array-values.ts.test",
+  rule,
+  {
+    valid: ["correct-sample.ts"].map(readExampleForTheRule),
+
+    invalid: [
+      ...["incorrect-sample.ts"].map(readExampleForTheRule).map((result) => ({
+        ...result,
+        errors: [
+          {
+            messageId: "duplicatesInSource",
+            type: "Identifier",
+            suggestions: [
+              {
+                messageId: "removeDuplicate",
+              },
+            ],
+          },
+          {
+            messageId: "duplicatesInClock",
+            type: "MemberExpression",
+            suggestions: [
+              {
+                messageId: "removeDuplicate",
+              },
+            ],
+          },
+        ],
+      })),
+      ...["incorrect-guard.ts"].map(readExampleForTheRule).map((result) => ({
+        ...result,
+        errors: [
+          {
+            messageId: "duplicatesInClock",
+            type: "MemberExpression",
+            suggestions: [
+              {
+                messageId: "removeDuplicate",
+              },
+            ],
+          },
+        ],
+      })),
+    ],
+  }
+);