Skip to content

Commit

Permalink
Implement require-pickup-in-persist rule (#163)
Browse files Browse the repository at this point in the history
  • Loading branch information
kireevmp authored Nov 4, 2024
1 parent 5399f72 commit 4105e57
Show file tree
Hide file tree
Showing 16 changed files with 206 additions and 0 deletions.
1 change: 1 addition & 0 deletions config/scope.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
rules: {
"effector/strict-effect-handlers": "error",
"effector/require-pickup-in-persist": "error",
},
};
19 changes: 19 additions & 0 deletions docs/rules/require-pickup-in-persist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# effector/require-pickup-in-persist

Requires every `persist` call of the [`effector-storage`](https://github.com/yumauri/effector-storage) library to include a `pickup` event when using [`Scope`s](https://effector.dev/api/effector/scope/). This ensures the correct initial value is loaded into the store for each `Scope`.

```ts
import { persist } from "effector-storage/query";

const $store = createStore("example");

// 👎 no pickup, does not work with Scope
persist({ store: $store });
```

```ts
const pickup = createEvent();

// 👍 pickup is specified
persist({ store: $store, pickup });
```
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = {
"no-guard": require("./rules/no-guard/no-guard"),
"mandatory-scope-binding": require("./rules/mandatory-scope-binding/mandatory-scope-binding"),
"prefer-useUnit": require("./rules/prefer-useUnit/prefer-useUnit"),
"require-pickup-in-persist": require("./rules/require-pickup-in-persist/require-pickup-in-persist"),
"no-patronum-debug": require("./rules/no-patronum-debug/no-patronum-debug"),
},
configs: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createStore, createEvent } from "effector";

import { persist } from "effector-storage/local";

const $store = createStore("example");
const updated = createEvent();

const appStarted = createEvent();

persist({
source: $store.updates,
target: updated,

pickup: appStarted,

key: "store",
keyPrefix: "local",
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createStore, createEvent } from "effector";

import { persist } from "effector-storage";
import { local as localAdapter } from "effector-storage/local";

const $store = createStore("example");
const pickup = createEvent();

persist({ store: $store, pickup, adapter: localAdapter });
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createStore } from "effector";

import { persist } from "other-persist";
import { persist as persistNested } from "other-persist/nested";

const $store = createStore("example");

persist({ store: $store });
persistNested({ store: $store });
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createStore, createEvent } from "effector";

import { persist as persistQuery } from "effector-storage/query";

const $store = createStore("example");
const pickup = createEvent();

persistQuery({ store: $store, pickup });
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createStore, createEvent } from "effector";

import { persist as persistAsync } from "@effector-storage/react-native-async-storage";

const $store = createStore("example");
const pickup = createEvent();

persistAsync({ store: $store, pickup });
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createStore } from "effector";

import { persist } from "effector-storage";

const randomCall = () => ({ store: createStore() });

persist();
persist("invalid");
persist(randomCall());
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createStore, createEvent } from "effector";

import { persist } from "effector-storage/local";

const $store = createStore("example");
const updated = createEvent();

persist({
source: $store,
target: updated,

key: "store",
keyPrefix: "local",
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createStore } from "effector";

import { persist } from "effector-storage";

const $store = createStore("example");

persist({ store: $store, adapter: localAdapter });
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createStore, createEvent } from "effector";

import { persist as persistAsync } from "@effector-storage/react-native-async-storage";

const $store = createStore("example");

persistAsync({ store: $store });
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { combine } from "effector";

import { persist } from "effector-storage/local";

persist({
store: combine({ pickup: true }),
param: { pickup: "yes" },
});
47 changes: 47 additions & 0 deletions rules/require-pickup-in-persist/require-pickup-in-persist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const { createLinkToRule } = require("../../utils/create-link-to-rule");

module.exports = {
meta: {
type: "problem",
docs: {
category: "Quality",
url: createLinkToRule("require-pickup-in-persist"),
},
messages: {
pickupMissing:
"This `persist` call does not specify a `pickup` event that is required for scoped usage of `effector-storage`.",
},
schema: [],
},
create(context) {
const pickupImports = new Set();

/**
* Finds `effector-storage` packages, scoped and unscoped, including
* contents of these packages. See examples for a full list.
*/
const PACKAGE_NAME = /^@?effector-storage(\u002F[\w-]+)*$/;

const declarationSelector = `ImportDeclaration[source.value=${PACKAGE_NAME}]`;
const persistImportSelector = `ImportSpecifier[imported.name="persist"]`;

const configSelector = `[arguments.length=1][arguments.0.type="ObjectExpression"]`;
const callSelector = `[callee.type="Identifier"]`;

return {
[`${declarationSelector} > ${persistImportSelector}`](node) {
pickupImports.add(node.local.name);
},
[`CallExpression${configSelector}${callSelector}`](node) {
if (!pickupImports.has(node.callee.name)) return;

const config = node.arguments[0];

if (config.properties.some((prop) => prop.key?.name === "pickup"))
return;

context.report({ node, messageId: "pickupMissing" });
},
};
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://eslint.effector.dev/rules/require-pickup-in-persist.html
40 changes: 40 additions & 0 deletions rules/require-pickup-in-persist/require-pickup-in-persist.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const { RuleTester } = require("eslint");
const { join } = require("path");

const {
readExample,
getCorrectExamples,
getIncorrectExamples,
} = require("../../utils/read-example");

const rule = require("./require-pickup-in-persist");

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

const readExampleForTheRule = (name) => ({
code: readExample(__dirname, name),
filename: join(__dirname, "examples", name),
});

ruleTester.run("effector/require-pickup-in-persist.js.test", rule, {
valid: getCorrectExamples(__dirname).map(readExampleForTheRule),

invalid:
// Errors
getIncorrectExamples(__dirname)
.map(readExampleForTheRule)
.map((result) => ({
...result,
errors: [
{
messageId: "pickupMissing",
type: "CallExpression",
},
],
})),
});

0 comments on commit 4105e57

Please sign in to comment.