├── .nvmrc ├── docs ├── scripts │ ├── package.json │ ├── prepare-changelog.js │ ├── prepare-presets.js │ └── prepare-toc.js ├── rules │ ├── index.md │ ├── no-ambiguity-target.md │ ├── no-useless-methods.md │ ├── no-duplicate-on.md │ ├── enforce-gate-naming-convention.md │ ├── enforce-effect-naming-convention.md │ ├── mandatory-scope-binding.md │ ├── require-pickup-in-persist.md │ ├── no-unnecessary-duplication.md │ ├── prefer-sample-over-forward-with-mapping.md │ ├── no-unnecessary-combination.md │ ├── no-getState.md │ ├── keep-options-order.md │ ├── no-forward.md │ ├── no-patronum-debug.md │ ├── no-guard.md │ ├── no-watch.md │ ├── no-duplicate-clock-or-source-array-values.md │ ├── strict-effect-handlers.md │ ├── enforce-store-naming-convention.md │ └── prefer-useUnit.md ├── presets │ ├── index.md │ ├── recommended.md │ ├── future.md │ ├── react.md │ ├── patronum.md │ └── scope.md ├── public │ ├── comet-192.png │ ├── comet-512.png │ ├── favicon.ico │ ├── apple-touch-icon.png │ └── manifest.webmanifest ├── .vitepress │ ├── theme │ │ ├── index.js │ │ └── custom.css │ └── config.js ├── shared │ └── install.md └── index.md ├── rules ├── no-guard │ ├── no-guard.md │ └── no-guard.js ├── no-watch │ ├── no-watch.md │ ├── examples │ │ ├── incorrect │ │ │ ├── effect │ │ │ │ ├── base.ts │ │ │ │ ├── finally.ts │ │ │ │ ├── done.ts │ │ │ │ └── fail.ts │ │ │ ├── event.ts │ │ │ ├── store.ts │ │ │ ├── guard.ts │ │ │ └── sample.ts │ │ └── correct.ts │ ├── no-watch.test.js │ └── no-watch.js ├── no-forward │ ├── no-forward.md │ └── no-forward.js ├── no-getState │ ├── no-getState.md │ ├── examples │ │ ├── correct.ts │ │ ├── incorrect-with-convential-name.ts │ │ ├── incorrect-with-random-name.ts │ │ ├── file-for-cross-import.ts │ │ ├── incorrect-with-nested-object.ts │ │ └── incorrect-with-deep-nested-object.ts │ ├── no-getState.test.js │ ├── no-getState.ts.test.js │ └── no-getState.js ├── no-patronum-debug │ ├── examples │ │ ├── correct-issue-127.ts │ │ ├── incorrect-with-debug.ts │ │ ├── incorrect-with-import-alias.ts │ │ ├── correct.ts │ │ └── incorrect-with-debug-fork.ts │ ├── no-patronum-debug.md │ └── no-patronum-debug.ts.test.js ├── prefer-useUnit │ ├── prefer-useUnit.md │ ├── examples │ │ ├── incorrect-useStore.jsx │ │ ├── incorrect-useStore.tsx │ │ ├── incorrect-useEvent-with-event.jsx │ │ ├── incorrect-useEvent-with-event.tsx │ │ ├── incorrect-useEvent-with-effect.jsx │ │ ├── incorrect-useEvent-with-effect.tsx │ │ ├── correct-single-useUnit.jsx │ │ ├── correct-single-useUnit.tsx │ │ ├── correct-multiple-useUnit.jsx │ │ └── correct-multiple-useUnit.tsx │ ├── prefer-useUnit.ts.test.js │ ├── prefer-useUnit.js.test.js │ └── prefer-useUnit.js ├── no-duplicate-on │ ├── no-duplicate-on.md │ ├── examples │ │ ├── correct-with-empty-on.ts │ │ ├── incorrect-with-invalid-naming.ts │ │ ├── correct.ts │ │ ├── correct-with-scopes.ts │ │ └── correct-with-nesting.ts │ ├── no-duplicate-on.ts.test.js │ └── no-duplicate-on.test.js ├── keep-options-order │ ├── keep-options-order.md │ ├── config.js │ ├── examples │ │ ├── incorrect-guard.js │ │ ├── incorrect-sample.js │ │ ├── correct-guard.js │ │ └── correct-sample.js │ └── keep-options-order.test.js ├── no-useless-methods │ ├── no-useless-methods.md │ ├── examples │ │ ├── incorrect-guard-clock.ts │ │ ├── incorrect-sample-clock.ts │ │ ├── incorrect-sample-source.ts │ │ ├── incorrect-guard-source.ts │ │ ├── correct-nested.ts │ │ ├── correct-examples-issue-74.js │ │ └── correct.ts │ ├── no-useless-methods.test.js │ ├── no-useless-methods.ts.test.js │ └── no-useless-methods.js ├── no-ambiguity-target │ ├── no-ambiguity-target.md │ ├── examples │ │ ├── incorrect-sample.ts │ │ ├── incorrect-guard.ts │ │ ├── incorrect-guard-nested.ts │ │ ├── correct-example-issue-133.js │ │ ├── correct.ts │ │ └── correct-examples-issue-49.ts │ ├── no-ambiguity-target.test.js │ ├── no-ambiguity-target.ts.test.js │ └── no-ambiguity-target.js ├── mandatory-scope-binding │ ├── mandatory-scope-binding.md │ ├── examples │ │ ├── incorrect-event-as-prop.tsx │ │ ├── incorrect-direct-event-call-in-callback.tsx │ │ ├── incorrect-event-as-hook-callback.tsx │ │ ├── incorrect-direct-effect-call-in-hook.tsx │ │ ├── incorrect-direct-event-call-in-hook.tsx │ │ ├── incorrect-useEvent-from-react.tsx │ │ ├── correct-generic.tsx │ │ ├── incorrect-direct-event-call-in-hook-cleanup-callback.tsx │ │ ├── correct-event-via-useEvent.tsx │ │ ├── correct-effect-via-useEvent.tsx │ │ ├── incorrect-class-extends.ts │ │ ├── correct-useEvent-aliased.tsx │ │ ├── correct-useEvent-as-property.tsx │ │ ├── model.ts │ │ ├── correct-effect-store-via-useStore.tsx │ │ ├── correct-event-as-object-prop.tsx │ │ ├── correct-class-no-extends.ts │ │ ├── correct-scope-import.tsx │ │ ├── correct-object-shape-via-useEvent.tsx │ │ └── correct-array-shape-via-useEvent.tsx │ ├── mandatory-scope-binding.ts.test.js │ └── mandatory-scope-binding.js ├── strict-effect-handlers │ ├── strict-effect-handlers.md │ ├── examples │ │ ├── correct-empty-function.js │ │ ├── correct-only-async.js │ │ ├── incorrect-mix-in-simple-function.js │ │ ├── incorrect-mix-in-simple-function.ts │ │ ├── correct-only-fx.js │ │ ├── incorrect-mix-async-fx.js │ │ ├── incorrect-mix-async-fx.ts │ │ ├── incorrect-mix-async-fx-in-func.js │ │ ├── incorrect-mix-async-fx-in-func.ts │ │ ├── incorrect-mix-async-fx-in-named-func.js │ │ ├── incorrect-mix-async-fx-in-named-func.ts │ │ └── correct.ts │ ├── strict-effect-handlers.test.js │ ├── strict-effect-handlers.ts.test.js │ └── strict-effect-handlers.js ├── no-unnecessary-combination │ ├── no-unnecessary-combination.md │ ├── examples │ │ ├── unnecessary-merge-in-clock-sample.ts │ │ ├── unnecessary-merge-in-source-sample.ts │ │ ├── correct-combine-in-clock-sample.ts │ │ ├── unnecessary-merge-in-clock-guard.ts │ │ ├── unnecessary-combine-in-source-sample.ts │ │ ├── unnecessary-merge-in-source-guard.ts │ │ ├── correct-combine-in-clock-guard.ts │ │ ├── unnecessary-combine-in-source-guard.ts │ │ ├── unnecessary-merge-in-from-forward.ts │ │ ├── correct-combine-in-from-forward.ts │ │ └── correct.ts │ ├── no-unnecessary-combination.ts.test.js │ └── no-unnecessary-combination.js ├── no-unnecessary-duplication │ ├── no-unnecessary-duplication.md │ └── examples │ │ ├── correct-examples-issue-27.js │ │ └── correct-examples-issue-21.js ├── require-pickup-in-persist │ ├── require-pickup-in-persist.md │ ├── examples │ │ ├── incorrect-unrelated-pickup.js │ │ ├── incorrect-core-package.js │ │ ├── correct-skip-misconfigured.js │ │ ├── incorrect-scoped-package.js │ │ ├── correct-query-package.js │ │ ├── correct-scoped-package.js │ │ ├── correct-other-packages.js │ │ ├── correct-core-package.js │ │ ├── incorrect-complete-config.js │ │ └── correct-complete-config.js │ ├── require-pickup-in-persist.test.js │ └── require-pickup-in-persist.js ├── enforce-gate-naming-convention │ ├── enforce-gate-naming-convention.md │ ├── examples │ │ ├── incorrect-createGate.js │ │ ├── correct-gate-naming.js │ │ ├── correct-gate-naming-from-other-package.js │ │ ├── incorrect-createGate-alias.js │ │ ├── incorrect-gate-naming.ts │ │ ├── correct-gate-naming-in-domain.js │ │ ├── incorrect-createGate-in-domain.js │ │ └── correct-gate-naming.ts │ ├── enforce-gate-naming-convention.ts.test.js │ └── enforce-gate-naming-convention.test.js ├── enforce-store-naming-convention │ ├── enforce-store-naming-convention.md │ ├── prefix │ │ ├── examples │ │ │ ├── correct-issue-139.ts │ │ │ ├── incorrect-createStore.js │ │ │ ├── correct-store-naming-from-other-package.js │ │ │ ├── incorrect-createStore-alias.js │ │ │ ├── incorrect-createStore-postfix.js │ │ │ ├── incorrect-createStore-domain.js │ │ │ ├── incorrect-restore.js │ │ │ ├── correct-factory.js │ │ │ ├── correct-store-naming-in-domain.js │ │ │ ├── incorrect-map.js │ │ │ ├── incorrect-restore-alias.js │ │ │ ├── incorrect-store-naming.ts │ │ │ ├── correct-examples-issue-158.js │ │ │ ├── correct-examples-issue-23.js │ │ │ ├── correct-examples-issue-128.js │ │ │ ├── correct-store-naming-with-handlers.js │ │ │ ├── incorrect-store-naming-with-handlers.js │ │ │ ├── correct-store-naming-in-domain-with-handlers.js │ │ │ ├── incorrect-store-naming-in-domain-with-handlers.js │ │ │ ├── incorrect-combine.js │ │ │ ├── correct-store-naming-property-domain.js │ │ │ ├── incorrect-combine-alias.js │ │ │ ├── correct-examples-issue-150.js │ │ │ ├── correct-examples-issue-136.js │ │ │ ├── correct-store-naming.js │ │ │ └── correct-store-naming.ts │ │ └── enforce-store-naming-convention-prefix.ts.test.js │ └── postfix │ │ ├── examples │ │ ├── incorrect-createStore.js │ │ ├── correct-store-naming-from-other-package.js │ │ ├── incorrect-createStore-prefix.js │ │ ├── incorrect-createStore-alias.js │ │ ├── incorrect-createStore-domain.js │ │ ├── incorrect-restore.js │ │ ├── correct-store-naming-in-domain.js │ │ ├── incorrect-map.js │ │ ├── incorrect-restore-alias.js │ │ ├── incorrect-store-naming.ts │ │ ├── correct-examples-issue-23.js │ │ ├── correct-store-naming-with-handlers.js │ │ ├── incorrect-store-naming-with-handlers.js │ │ ├── correct-store-naming-in-domain-with-handlers.js │ │ ├── incorrect-store-naming-in-domain-with-handlers.js │ │ ├── incorrect-combine.js │ │ ├── incorrect-combine-alias.js │ │ ├── correct-store-naming.js │ │ └── correct-store-naming.ts │ │ ├── enforce-store-naming-convention-postfix.ts.test.js │ │ └── enforce-store-naming-convention-postfix.test.js ├── enforce-effect-naming-convention │ ├── enforce-effect-naming-convention.md │ ├── examples │ │ ├── incorrect-createEffect.js │ │ ├── incorrect-createEffect-alias.js │ │ ├── correct-effect-naming-from-other-package.js │ │ ├── incorrect-createEffect-in-domain.js │ │ ├── incorrect-attach.js │ │ ├── incorrect-attach-alias.js │ │ ├── incorrect-effect-naming.ts │ │ ├── correct-effect-naming-in-domain.js │ │ ├── correct-effect-naming.js │ │ ├── correct-effect-naming.ts │ │ └── correct-examples-issue-24.js │ ├── enforce-effect-naming-convention.ts.test.js │ └── enforce-effect-naming-convention.test.js ├── prefer-sample-over-forward-with-mapping │ └── prefer-sample-over-forward-with-mapping.md ├── no-duplicate-clock-or-source-array-values │ ├── no-duplicate-clock-or-source-array-values.md │ ├── examples │ │ ├── correct-sample.ts │ │ ├── incorrect-sample.ts │ │ └── incorrect-guard.ts │ └── no-duplicate-clock-or-source-array-values.ts.test.js └── tsconfig.json ├── .npmignore ├── config ├── patronum.js ├── scope.js ├── future.js ├── react.js └── recommended.js ├── .gitignore ├── utils ├── node-is-type.js ├── create-link-to-rule.js ├── get-store-name-convention.js ├── traverse-nested-object-node.js ├── traverse-parent-by-type.js ├── get-nested-object-name.js ├── validate-store-name-convention.js ├── extract-imported-from.js ├── builders.js ├── are-nodes-same-in-text.js ├── method.js ├── extract-config.js ├── is.js ├── get-corrected-store-name.js ├── naming.js ├── read-example.js ├── replace-by-sample.js └── node-type-is.js ├── jest.config.js ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── plugin.test.js ├── README.md ├── package.json └── index.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.12 -------------------------------------------------------------------------------- /docs/scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /docs/rules/index.md: -------------------------------------------------------------------------------- 1 | # Rules 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/presets/index.md: -------------------------------------------------------------------------------- 1 | # Presets 2 | 3 | 4 | -------------------------------------------------------------------------------- /rules/no-guard/no-guard.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/no-guard.html 2 | -------------------------------------------------------------------------------- /rules/no-watch/no-watch.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/no-watch.html 2 | -------------------------------------------------------------------------------- /rules/no-forward/no-forward.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/no-forward.html 2 | -------------------------------------------------------------------------------- /rules/no-getState/no-getState.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/no-getState.html 2 | -------------------------------------------------------------------------------- /rules/no-patronum-debug/examples/correct-issue-127.ts: -------------------------------------------------------------------------------- 1 | const Anchor = styled("a")({}); 2 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/prefer-useUnit.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/prefer-useUnit.html 2 | -------------------------------------------------------------------------------- /rules/no-duplicate-on/no-duplicate-on.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/no-duplicate-on.html 2 | -------------------------------------------------------------------------------- /docs/public/comet-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effector/eslint-plugin/HEAD/docs/public/comet-192.png -------------------------------------------------------------------------------- /docs/public/comet-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effector/eslint-plugin/HEAD/docs/public/comet-512.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effector/eslint-plugin/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /rules/keep-options-order/keep-options-order.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/keep-options-order.html 2 | -------------------------------------------------------------------------------- /rules/no-patronum-debug/no-patronum-debug.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/no-patronum-debug.html 2 | -------------------------------------------------------------------------------- /rules/no-useless-methods/no-useless-methods.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/no-useless-methods.html 2 | -------------------------------------------------------------------------------- /rules/no-ambiguity-target/no-ambiguity-target.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/no-ambiguity-target.html 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | scripts 2 | **/examples/** 3 | *.test.* 4 | coverage 5 | *.md 6 | tsconfig.json 7 | pnpm-lock.yaml 8 | docs -------------------------------------------------------------------------------- /config/patronum.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "effector/no-patronum-debug": "error", 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effector/eslint-plugin/HEAD/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/mandatory-scope-binding.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/mandatory-scope-binding.html 2 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/strict-effect-handlers.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/strict-effect-handlers.html 2 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/no-unnecessary-combination.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/no-unnecessary-combination.html 2 | -------------------------------------------------------------------------------- /rules/no-unnecessary-duplication/no-unnecessary-duplication.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/no-unnecessary-duplication.html 2 | -------------------------------------------------------------------------------- /rules/require-pickup-in-persist/require-pickup-in-persist.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/require-pickup-in-persist.html 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_Store 4 | docs/.vitepress/dist 5 | docs/**/__*.md 6 | docs/changelog.md 7 | docs/.vitepress/cache -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from "vitepress/theme"; 2 | import "./custom.css"; 3 | 4 | export default DefaultTheme; 5 | -------------------------------------------------------------------------------- /docs/scripts/prepare-changelog.js: -------------------------------------------------------------------------------- 1 | import { copyFile } from "node:fs/promises"; 2 | 3 | await copyFile("CHANGELOG.md", "docs/changelog.md"); 4 | -------------------------------------------------------------------------------- /rules/enforce-gate-naming-convention/enforce-gate-naming-convention.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/enforce-gate-naming-convention.html 2 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/enforce-store-naming-convention.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/enforce-store-naming-convention.html 2 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/enforce-effect-naming-convention.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/enforce-effect-naming-convention.html 2 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-issue-139.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | const $ = createStore(0); 4 | -------------------------------------------------------------------------------- /rules/keep-options-order/config.js: -------------------------------------------------------------------------------- 1 | const correctOrder = ["clock", "source", "filter", "fn", "target", "greedy"]; 2 | 3 | module.exports = { correctOrder }; 4 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/examples/correct-empty-function.js: -------------------------------------------------------------------------------- 1 | const NoopDecorator = () => () => { 2 | // pass 3 | }; 4 | 5 | export { NoopDecorator }; 6 | -------------------------------------------------------------------------------- /utils/node-is-type.js: -------------------------------------------------------------------------------- 1 | function nodeIsType({ node }) { 2 | return node?.parent?.type === "TSTypeReference"; 3 | } 4 | 5 | module.exports = { nodeIsType }; 6 | -------------------------------------------------------------------------------- /rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/prefer-sample-over-forward-with-mapping.html 2 | -------------------------------------------------------------------------------- /config/scope.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "effector/strict-effect-handlers": "error", 4 | "effector/require-pickup-in-persist": "error", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.md: -------------------------------------------------------------------------------- 1 | https://eslint.effector.dev/rules/no-duplicate-clock-or-source-array-values.html 2 | -------------------------------------------------------------------------------- /utils/create-link-to-rule.js: -------------------------------------------------------------------------------- 1 | function createLinkToRule(name) { 2 | return `https://eslint.effector.dev/rules/${name}`; 3 | } 4 | 5 | module.exports = { createLinkToRule }; 6 | -------------------------------------------------------------------------------- /rules/enforce-gate-naming-convention/examples/incorrect-createGate.js: -------------------------------------------------------------------------------- 1 | import { createGate } from "effector-react"; 2 | 3 | const justGate = createGate(); 4 | 5 | export { justGate }; 6 | -------------------------------------------------------------------------------- /rules/no-getState/examples/correct.ts: -------------------------------------------------------------------------------- 1 | const store = { 2 | getState() { 3 | return {}; 4 | }, 5 | }; 6 | 7 | const value = store.getState(); 8 | 9 | export { value }; 10 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/examples/incorrect-createEffect.js: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | const justEffect = createEffect(); 4 | 5 | export { justEffect }; 6 | -------------------------------------------------------------------------------- /rules/enforce-gate-naming-convention/examples/correct-gate-naming.js: -------------------------------------------------------------------------------- 1 | import { createGate } from "effector-react"; 2 | 3 | // Attcch 4 | const SomeGate = createGate(); 5 | 6 | export { SomeGate }; 7 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/incorrect-createStore.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | const justStore = createStore(null); 4 | 5 | export { justStore }; 6 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/incorrect-createStore.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | const justStore = createStore(null); 4 | 5 | export { justStore }; 6 | -------------------------------------------------------------------------------- /rules/no-useless-methods/examples/incorrect-guard-clock.ts: -------------------------------------------------------------------------------- 1 | import { guard, createEvent } from "effector"; 2 | 3 | const trigger = createEvent(); 4 | 5 | guard({ clock: trigger, filter: Boolean }); 6 | -------------------------------------------------------------------------------- /rules/no-useless-methods/examples/incorrect-sample-clock.ts: -------------------------------------------------------------------------------- 1 | import { sample, createEvent } from "effector"; 2 | 3 | const trigger = createEvent(); 4 | 5 | sample({ clock: trigger, fn: Boolean }); 6 | -------------------------------------------------------------------------------- /rules/no-useless-methods/examples/incorrect-sample-source.ts: -------------------------------------------------------------------------------- 1 | import { sample, createEvent } from "effector"; 2 | 3 | const trigger = createEvent(); 4 | 5 | sample({ source: trigger, fn: Boolean }); 6 | -------------------------------------------------------------------------------- /rules/no-useless-methods/examples/incorrect-guard-source.ts: -------------------------------------------------------------------------------- 1 | import { guard, createEvent } from "effector"; 2 | 3 | const trigger = createEvent(); 4 | 5 | guard({ source: trigger, filter: Boolean }); 6 | -------------------------------------------------------------------------------- /config/future.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "effector/prefer-sample-over-forward-with-mapping": "off", 4 | "effector/no-forward": "warn", 5 | "effector/no-guard": "warn", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /rules/enforce-gate-naming-convention/examples/correct-gate-naming-from-other-package.js: -------------------------------------------------------------------------------- 1 | import { createGate } from "someLibrary"; 2 | 3 | const justOtherGate = createGate(); 4 | 5 | export { justOtherGate }; 6 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/correct-store-naming-from-other-package.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | 3 | const justStore = createStore({}); 4 | 5 | export { justStore }; 6 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/incorrect-createStore-prefix.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | const $justStore = createStore(null); 4 | 5 | export { $justStore }; 6 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-store-naming-from-other-package.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | 3 | const justStore = createStore({}); 4 | 5 | export { justStore }; 6 | -------------------------------------------------------------------------------- /rules/no-watch/examples/incorrect/effect/base.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | const watcher = (x: T) => x; 4 | const watchableFx = createEffect(); 5 | 6 | watchableFx.watch(watcher); 7 | -------------------------------------------------------------------------------- /rules/no-watch/examples/incorrect/event.ts: -------------------------------------------------------------------------------- 1 | import { createEvent } from "effector"; 2 | 3 | const watcher = (x: T) => x; 4 | const watchableEvent = createEvent(); 5 | 6 | watchableEvent.watch(watcher); 7 | -------------------------------------------------------------------------------- /rules/enforce-gate-naming-convention/examples/incorrect-createGate-alias.js: -------------------------------------------------------------------------------- 1 | import { createGate as createSomeGate } from "effector-react"; 2 | 3 | const justGate = createSomeGate(); 4 | 5 | export { justGate }; 6 | -------------------------------------------------------------------------------- /rules/no-getState/examples/incorrect-with-convential-name.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | const $data = createStore(null); 4 | 5 | const value = $data.getState(); 6 | 7 | export { value }; 8 | -------------------------------------------------------------------------------- /rules/no-patronum-debug/examples/incorrect-with-debug.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | import { debug } from "patronum"; 3 | const $store = createStore({ fullname: "John Due" }); 4 | debug($store); 5 | -------------------------------------------------------------------------------- /rules/no-getState/examples/incorrect-with-random-name.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | const randomStore = createStore(null); 4 | 5 | const value = randomStore.getState(); 6 | 7 | export { value }; 8 | -------------------------------------------------------------------------------- /rules/no-watch/examples/incorrect/effect/finally.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | const watcher = (x: T) => x; 4 | const watchableFx = createEffect(); 5 | 6 | watchableFx.finally.watch(watcher); 7 | -------------------------------------------------------------------------------- /config/react.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "effector/enforce-gate-naming-convention": "error", 4 | "effector/mandatory-scope-binding": "error", 5 | "effector/prefer-useUnit": "warn", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /docs/public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "icons": [ 3 | { "src": "/comet-192.png", "type": "image/png", "sizes": "192x192" }, 4 | { "src": "/comet-512.png", "type": "image/png", "sizes": "512x512" } 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/examples/incorrect-createEffect-alias.js: -------------------------------------------------------------------------------- 1 | import { createEffect as createSideEffect } from "effector"; 2 | 3 | const justEffect = createSideEffect(); 4 | 5 | export { justEffect }; 6 | -------------------------------------------------------------------------------- /rules/no-getState/examples/file-for-cross-import.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | const $store = createStore(false); 4 | 5 | const service = { $store }; 6 | 7 | export { service }; 8 | -------------------------------------------------------------------------------- /rules/no-patronum-debug/examples/incorrect-with-import-alias.ts: -------------------------------------------------------------------------------- 1 | import { createEvent } from "effector"; 2 | import { debug as debuggerPatronum } from "patronum"; 3 | const event = createEvent(); 4 | debuggerPatronum(event); 5 | -------------------------------------------------------------------------------- /rules/no-getState/examples/incorrect-with-nested-object.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | const service = { store: createStore(null) }; 4 | 5 | const value = service.store.getState(); 6 | 7 | export { value }; 8 | -------------------------------------------------------------------------------- /rules/no-patronum-debug/examples/correct.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | const debug = (...args) => ({ ...args }); 3 | debug({ test: "debug" }); 4 | const $store = createStore({}); 5 | debug({ store: $store }); 6 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/examples/correct-effect-naming-from-other-package.js: -------------------------------------------------------------------------------- 1 | import { createEffect } from "someLibrary"; 2 | 3 | const justOtherEffect = createEffect(() => null); 4 | 5 | export { justOtherEffect }; 6 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/incorrect-createStore-alias.js: -------------------------------------------------------------------------------- 1 | import { createStore as createEffectorStore } from "effector"; 2 | 3 | const justStore = createEffectorStore(null); 4 | 5 | export { justStore }; 6 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/incorrect-createStore-alias.js: -------------------------------------------------------------------------------- 1 | import { createStore as createEffectorStore } from "effector"; 2 | 3 | const justStore = createEffectorStore(null); 4 | 5 | export { justStore }; 6 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/incorrect-createStore-postfix.js: -------------------------------------------------------------------------------- 1 | import { createStore as createEffectorStore } from "effector"; 2 | 3 | const justStore$ = createEffectorStore(null); 4 | 5 | export { justStore$ }; 6 | -------------------------------------------------------------------------------- /rules/no-watch/examples/incorrect/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | const watcher = (x: T) => x; 4 | const $watchable = createStore(0); 5 | 6 | $watchable.watch(watcher); 7 | $watchable.updates.watch(watcher); 8 | -------------------------------------------------------------------------------- /rules/no-duplicate-on/examples/correct-with-empty-on.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore } from "effector"; 2 | 3 | const $store = createStore(1); 4 | 5 | // Yeah, it's incorrect TS code, but it isn't this plugin's business 6 | $store.on(); 7 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/examples/incorrect-createEffect-in-domain.js: -------------------------------------------------------------------------------- 1 | import { createDomain } from "effector"; 2 | 3 | const service = createDomain(); 4 | 5 | const justEffect = service.createEffect(); 6 | 7 | export { justEffect }; 8 | -------------------------------------------------------------------------------- /rules/no-ambiguity-target/examples/incorrect-sample.ts: -------------------------------------------------------------------------------- 1 | import { sample, createEvent } from "effector"; 2 | 3 | const trigger = createEvent(); 4 | const target = createEvent(); 5 | 6 | const result = sample({ clock: trigger, fn: Boolean, target }); 7 | -------------------------------------------------------------------------------- /utils/get-store-name-convention.js: -------------------------------------------------------------------------------- 1 | function getStoreNameConvention(context) { 2 | // prefix convention is default 3 | return context.settings.effector?.storeNameConvention || "prefix"; 4 | } 5 | 6 | module.exports = { getStoreNameConvention }; 7 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/examples/incorrect-attach.js: -------------------------------------------------------------------------------- 1 | import { createEffect, attach } from "effector"; 2 | 3 | const justEffectFx = createEffect(); 4 | 5 | const attched = attach({ effect: justEffectFx }); 6 | 7 | export { attched }; 8 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/incorrect-createStore-domain.js: -------------------------------------------------------------------------------- 1 | import { createDomain } from "effector"; 2 | 3 | const service = createDomain(); 4 | 5 | const justStore = service.createStore(null); 6 | 7 | export { justStore }; 8 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/incorrect-createStore-domain.js: -------------------------------------------------------------------------------- 1 | import { createDomain } from "effector"; 2 | 3 | const service = createDomain(); 4 | 5 | const justStore = service.createStore(null); 6 | 7 | export { justStore }; 8 | -------------------------------------------------------------------------------- /rules/no-ambiguity-target/examples/incorrect-guard.ts: -------------------------------------------------------------------------------- 1 | import { guard, createEvent } from "effector"; 2 | 3 | const trigger = createEvent(); 4 | const target = createEvent(); 5 | 6 | const result = guard({ clock: trigger, filter: Boolean, target }); 7 | -------------------------------------------------------------------------------- /rules/no-watch/examples/incorrect/effect/done.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | const watcher = (x: T) => x; 4 | const watchableFx = createEffect(); 5 | 6 | watchableFx.done.watch(watcher); 7 | watchableFx.doneData.watch(watcher); 8 | -------------------------------------------------------------------------------- /rules/no-watch/examples/incorrect/effect/fail.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | const watcher = (x: T) => x; 4 | const watchableFx = createEffect(); 5 | 6 | watchableFx.fail.watch(watcher); 7 | watchableFx.failData.watch(watcher); 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testRegex: "./.+\\.test\\.js$", 3 | collectCoverage: false, 4 | collectCoverageFrom: ["rules/**/{!(examples),}/*.js"], 5 | moduleFileExtensions: ["js"], 6 | coverageReporters: ["text-summary", "lcov"], 7 | }; 8 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/examples/unnecessary-merge-in-clock-sample.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, merge, sample } from "effector"; 2 | 3 | const event1 = createEvent(); 4 | const event2 = createEvent(); 5 | 6 | sample({ clock: merge([event1, event2]) }); 7 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/incorrect-restore.js: -------------------------------------------------------------------------------- 1 | import { restore, createEvent } from "effector"; 2 | 3 | const eventForRestore = createEvent(); 4 | const restoredStore = restore(eventForRestore, null); 5 | 6 | export { restoredStore }; 7 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/examples/unnecessary-merge-in-source-sample.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, merge, sample } from "effector"; 2 | 3 | const event1 = createEvent(); 4 | const event2 = createEvent(); 5 | 6 | sample({ source: merge([event1, event2]) }); 7 | -------------------------------------------------------------------------------- /rules/require-pickup-in-persist/examples/incorrect-unrelated-pickup.js: -------------------------------------------------------------------------------- 1 | import { combine } from "effector"; 2 | 3 | import { persist } from "effector-storage/local"; 4 | 5 | persist({ 6 | store: combine({ pickup: true }), 7 | param: { pickup: "yes" }, 8 | }); 9 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/incorrect-restore.js: -------------------------------------------------------------------------------- 1 | import { restore, createEvent } from "effector"; 2 | 3 | const eventForRestore = createEvent(); 4 | const restoredStore = restore(eventForRestore, null); 5 | 6 | export { restoredStore }; 7 | -------------------------------------------------------------------------------- /rules/no-getState/examples/incorrect-with-deep-nested-object.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | const service = { prefix: { body: { store: createStore(null) } } }; 4 | 5 | const value = service.prefix.body.store.getState(); 6 | 7 | export { value }; 8 | -------------------------------------------------------------------------------- /rules/require-pickup-in-persist/examples/incorrect-core-package.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | import { persist } from "effector-storage"; 4 | 5 | const $store = createStore("example"); 6 | 7 | persist({ store: $store, adapter: localAdapter }); 8 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/examples/incorrect-attach-alias.js: -------------------------------------------------------------------------------- 1 | import { createEffect, attach as recreate } from "effector"; 2 | 3 | const justEffectFx = createEffect(); 4 | 5 | const attched = recreate({ effect: justEffectFx }); 6 | 7 | export { attched }; 8 | -------------------------------------------------------------------------------- /rules/enforce-gate-naming-convention/examples/incorrect-gate-naming.ts: -------------------------------------------------------------------------------- 1 | import { createGate } from "effector-react"; 2 | 3 | function createCustomGate() { 4 | return createGate(); 5 | } 6 | 7 | const justGate = createCustomGate(); 8 | 9 | export { justGate }; 10 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-factory.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | function createStoreEx(initial) { 4 | return createStore(initial instanceof Function ? initial() : initial); 5 | } 6 | 7 | export { createStoreEx }; 8 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/examples/correct-combine-in-clock-sample.ts: -------------------------------------------------------------------------------- 1 | import { combine, createStore, sample } from "effector"; 2 | 3 | const $store1 = createStore(null); 4 | const $store2 = createStore(null); 5 | 6 | sample({ clock: combine($store1, $store2) }); 7 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/examples/unnecessary-merge-in-clock-guard.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, guard, merge } from "effector"; 2 | 3 | const event1 = createEvent(); 4 | const event2 = createEvent(); 5 | 6 | guard({ clock: merge([event1, event2]), filter: Boolean }); 7 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/incorrect-event-as-prop.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { clicked } from "./model"; 4 | 5 | const Button: React.FC = () => { 6 | return ; 7 | }; 8 | 9 | export { Button }; 10 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/examples/unnecessary-combine-in-source-sample.ts: -------------------------------------------------------------------------------- 1 | import { combine, createStore, sample } from "effector"; 2 | 3 | const $store1 = createStore(null); 4 | const $store2 = createStore(null); 5 | 6 | sample({ source: combine($store1, $store2) }); 7 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/examples/unnecessary-merge-in-source-guard.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, guard, merge } from "effector"; 2 | 3 | const event1 = createEvent(); 4 | const event2 = createEvent(); 5 | 6 | guard({ source: merge([event1, event2]), filter: Boolean }); 7 | -------------------------------------------------------------------------------- /rules/no-watch/examples/incorrect/guard.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, guard } from "effector"; 2 | 3 | const watcher = (x: T) => x; 4 | const myEvent = createEvent(); 5 | 6 | guard({ 7 | clock: myEvent, 8 | filter: () => Math.random() > 0.5, 9 | }).watch(watcher); 10 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/examples/incorrect-effect-naming.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | function createCustomEffect() { 4 | return createEffect(); 5 | } 6 | 7 | const justEffect = createCustomEffect(); 8 | 9 | export { justEffect }; 10 | -------------------------------------------------------------------------------- /rules/no-ambiguity-target/examples/incorrect-guard-nested.ts: -------------------------------------------------------------------------------- 1 | import { guard, createEvent } from "effector"; 2 | 3 | const trigger = createEvent(); 4 | const target = createEvent(); 5 | 6 | const result = { 7 | something: guard({ clock: trigger, filter: Boolean, target }), 8 | }; 9 | -------------------------------------------------------------------------------- /utils/traverse-nested-object-node.js: -------------------------------------------------------------------------------- 1 | function traverseNestedObjectNode(node) { 2 | if (node.type === "MemberExpression") { 3 | return traverseNestedObjectNode(node.property); 4 | } 5 | 6 | return node; 7 | } 8 | 9 | module.exports = { traverseNestedObjectNode }; 10 | -------------------------------------------------------------------------------- /rules/enforce-gate-naming-convention/examples/correct-gate-naming-in-domain.js: -------------------------------------------------------------------------------- 1 | import { createGate } from "effector-react"; 2 | import { createDomain } from "effector"; 3 | 4 | const domain = createDomain(); 5 | 6 | const SomeGate = createGate({ domain }); 7 | 8 | export { SomeGate }; 9 | -------------------------------------------------------------------------------- /rules/enforce-gate-naming-convention/examples/incorrect-createGate-in-domain.js: -------------------------------------------------------------------------------- 1 | import { createDomain } from "effector"; 2 | import { createGate } from "effector-react"; 3 | 4 | const domain = createDomain(); 5 | 6 | const justGate = createGate({ domain }); 7 | 8 | export { justGate }; 9 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/examples/correct-only-async.js: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | async function f1() {} 4 | async function f2() {} 5 | 6 | const finalFx = createEffect(async () => { 7 | await f1(); 8 | await f2(); 9 | }); 10 | 11 | export { finalFx }; 12 | -------------------------------------------------------------------------------- /rules/keep-options-order/examples/incorrect-guard.js: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore, guard } from "effector"; 2 | 3 | const clock = createEvent(); 4 | const source = createEvent(); 5 | const filter = createStore(); 6 | const target = createEvent(); 7 | 8 | guard({ filter, clock, source, target }); 9 | -------------------------------------------------------------------------------- /rules/no-duplicate-on/examples/incorrect-with-invalid-naming.ts: -------------------------------------------------------------------------------- 1 | import { createStore, createEvent } from "effector"; 2 | 3 | const inc = createEvent(); 4 | const counterStore = createStore(0) 5 | .on(inc, (state) => state + 1) 6 | .on(inc, (state) => state + 2); 7 | 8 | export { counterStore }; 9 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/examples/incorrect-mix-in-simple-function.js: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | async function f1() {} 4 | const oneFx = createEffect(); 5 | 6 | async function justFunc() { 7 | await f1(); 8 | await oneFx(); 9 | } 10 | 11 | export { justFunc }; 12 | -------------------------------------------------------------------------------- /rules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "isolatedModules": true, 4 | "esModuleInterop": true, 5 | "module": "commonjs", 6 | "lib": ["es2017", "es2019"], 7 | "baseUrl": "./" 8 | }, 9 | "include": ["./**/examples/**/*.ts", "./**/examples/**/*.tsx"] 10 | } 11 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/examples/correct-effect-naming-in-domain.js: -------------------------------------------------------------------------------- 1 | import { createDomain } from "effector"; 2 | 3 | const service = createDomain(); 4 | 5 | const effectOneFx = service.createEffect(); 6 | const effectTwoFx = service.effect(); 7 | 8 | export { effectOneFx, effectTwoFx }; 9 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/correct-store-naming-in-domain.js: -------------------------------------------------------------------------------- 1 | import { createDomain } from "effector"; 2 | 3 | const domain = createDomain(); 4 | 5 | const storeOne$ = domain.createStore(null); 6 | const storeTwo$ = domain.store(null); 7 | 8 | export { storeOne$, storeTwo$ }; 9 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/incorrect-map.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | // Just createStore 4 | const justStore$ = createStore(""); 5 | 6 | // Map 7 | const mappedStore = justStore$.map((values) => values.length); 8 | 9 | export { mappedStore }; 10 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/incorrect-restore-alias.js: -------------------------------------------------------------------------------- 1 | import { restore as createStoreFromEvent, createEvent } from "effector"; 2 | 3 | const eventForRestore = createEvent(); 4 | const restoredStore = createStoreFromEvent(eventForRestore, null); 5 | 6 | export { restoredStore }; 7 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-store-naming-in-domain.js: -------------------------------------------------------------------------------- 1 | import { createDomain } from "effector"; 2 | 3 | const domain = createDomain(); 4 | 5 | const $storeOne = domain.createStore(null); 6 | const $storeTwo = domain.store(null); 7 | 8 | export { $storeOne, $storeTwo }; 9 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/incorrect-map.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | // Just createStore 4 | const $justStore = createStore(""); 5 | 6 | // Map 7 | const mappedStore = $justStore.map((values) => values.length); 8 | 9 | export { mappedStore }; 10 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/incorrect-restore-alias.js: -------------------------------------------------------------------------------- 1 | import { restore as createStoreFromEvent, createEvent } from "effector"; 2 | 3 | const eventForRestore = createEvent(); 4 | const restoredStore = createStoreFromEvent(eventForRestore, null); 5 | 6 | export { restoredStore }; 7 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/incorrect-direct-event-call-in-callback.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { clicked } from "./model"; 4 | 5 | const Button: React.FC = () => { 6 | return ; 7 | }; 8 | 9 | export { Button }; 10 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/examples/correct-combine-in-clock-guard.ts: -------------------------------------------------------------------------------- 1 | import { combine, createEvent, createStore, guard, merge } from "effector"; 2 | 3 | const $store1 = createStore(null); 4 | const $store2 = createStore(null); 5 | 6 | guard({ clock: combine($store1, $store2), filter: Boolean }); 7 | -------------------------------------------------------------------------------- /rules/require-pickup-in-persist/examples/correct-skip-misconfigured.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | import { persist } from "effector-storage"; 4 | 5 | const randomCall = () => ({ store: createStore() }); 6 | 7 | persist(); 8 | persist("invalid"); 9 | persist(randomCall()); 10 | -------------------------------------------------------------------------------- /rules/require-pickup-in-persist/examples/incorrect-scoped-package.js: -------------------------------------------------------------------------------- 1 | import { createStore, createEvent } from "effector"; 2 | 3 | import { persist as persistAsync } from "@effector-storage/react-native-async-storage"; 4 | 5 | const $store = createStore("example"); 6 | 7 | persistAsync({ store: $store }); 8 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/examples/incorrect-mix-in-simple-function.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | async function f1() {} 4 | const oneFx = createEffect(); 5 | 6 | async function justFunc() { 7 | await f1(); 8 | await oneFx(1); 9 | } 10 | 11 | export { justFunc }; 12 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/incorrect-store-naming.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | function createCustomStore() { 4 | return createStore(null); 5 | } 6 | 7 | // Just createStore 8 | const justStore = createCustomStore(); 9 | 10 | export { justStore }; 11 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/incorrect-store-naming.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | function createCustomStore() { 4 | return createStore(null); 5 | } 6 | 7 | // Just createStore 8 | const justStore = createCustomStore(); 9 | 10 | export { justStore }; 11 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/incorrect-event-as-hook-callback.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { mounted } from "./model"; 4 | 5 | const Button: React.FC = () => { 6 | React.useEffect(mounted, []); 7 | 8 | return ; 9 | }; 10 | 11 | export { Button }; 12 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/examples/unnecessary-combine-in-source-guard.ts: -------------------------------------------------------------------------------- 1 | import { combine, createEvent, createStore, guard, merge } from "effector"; 2 | 3 | const $store1 = createStore(null); 4 | const $store2 = createStore(null); 5 | 6 | guard({ source: combine($store1, $store2), filter: Boolean }); 7 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/examples/unnecessary-merge-in-from-forward.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, forward, merge } from "effector"; 2 | 3 | const event1 = createEvent(); 4 | const event2 = createEvent(); 5 | 6 | const target = createEvent(); 7 | 8 | forward({ from: merge([event1, event2]), to: target }); 9 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/examples/correct-only-fx.js: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | const oneFx = createEffect(); 4 | const twoFx = createEffect(); 5 | 6 | const finalFx = createEffect(async () => { 7 | await oneFx(); 8 | await twoFx(); 9 | }); 10 | 11 | export { finalFx }; 12 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/examples/incorrect-mix-async-fx.js: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | async function f1() {} 4 | const oneFx = createEffect(); 5 | 6 | const finalFx = createEffect(async () => { 7 | await f1(); 8 | await oneFx(); 9 | }); 10 | 11 | export { finalFx }; 12 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/examples/incorrect-mix-async-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | async function f1() {} 4 | const oneFx = createEffect(); 5 | 6 | const finalFx = createEffect(async () => { 7 | await f1(); 8 | await oneFx(1); 9 | }); 10 | 11 | export { finalFx }; 12 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/examples/correct-effect-naming.js: -------------------------------------------------------------------------------- 1 | import { createEffect, attach } from "effector"; 2 | 3 | // Just createEffect 4 | const baseEffectFx = createEffect(); 5 | 6 | // Attcch 7 | const attachedFx = attach({ effect: baseEffectFx }); 8 | 9 | export { baseEffectFx, attachedFx }; 10 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/examples/incorrect-mix-async-fx-in-func.js: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | async function f1() {} 4 | const oneFx = createEffect(); 5 | 6 | const finalFx = createEffect(async function () { 7 | await f1(); 8 | await oneFx(); 9 | }); 10 | 11 | export { finalFx }; 12 | -------------------------------------------------------------------------------- /rules/require-pickup-in-persist/examples/correct-query-package.js: -------------------------------------------------------------------------------- 1 | import { createStore, createEvent } from "effector"; 2 | 3 | import { persist as persistQuery } from "effector-storage/query"; 4 | 5 | const $store = createStore("example"); 6 | const pickup = createEvent(); 7 | 8 | persistQuery({ store: $store, pickup }); 9 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/examples/incorrect-mix-async-fx-in-func.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | async function f1() {} 4 | const oneFx = createEffect(); 5 | 6 | const finalFx = createEffect(async function () { 7 | await f1(); 8 | await oneFx(1); 9 | }); 10 | 11 | export { finalFx }; 12 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/examples/correct-combine-in-from-forward.ts: -------------------------------------------------------------------------------- 1 | import { combine, createEvent, createStore, forward } from "effector"; 2 | 3 | const $store1 = createStore(null); 4 | const $store2 = createStore(null); 5 | 6 | const target = createEvent(); 7 | 8 | forward({ from: combine($store1, $store2), to: target }); 9 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/examples/incorrect-mix-async-fx-in-named-func.js: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | async function f1() {} 4 | const oneFx = createEffect(); 5 | 6 | const finalFx = createEffect(async function handler() { 7 | await f1(); 8 | await oneFx(); 9 | }); 10 | 11 | export { finalFx }; 12 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/examples/incorrect-mix-async-fx-in-named-func.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | async function f1() {} 4 | const oneFx = createEffect(); 5 | 6 | const finalFx = createEffect(async function handler() { 7 | await f1(); 8 | await oneFx(1); 9 | }); 10 | 11 | export { finalFx }; 12 | -------------------------------------------------------------------------------- /rules/enforce-gate-naming-convention/examples/correct-gate-naming.ts: -------------------------------------------------------------------------------- 1 | import { createGate } from "effector-react"; 2 | 3 | const SomeGate = createGate(); 4 | 5 | // Factory 6 | function createCustomGate() { 7 | return createGate(); 8 | } 9 | 10 | const CustomGate = createCustomGate(); 11 | 12 | export { SomeGate, CustomGate }; 13 | -------------------------------------------------------------------------------- /rules/no-patronum-debug/examples/incorrect-with-debug-fork.ts: -------------------------------------------------------------------------------- 1 | import { fork, createStore } from "effector"; 2 | import { debug } from "patronum"; 3 | const $count = createStore(0); 4 | const scopeA = fork({ values: [[$count, 42]] }); 5 | const scopeB = fork({ values: [[$count, 1337]] }); 6 | debug.registerScope(scopeA, { name: "scope_42" }); 7 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/incorrect-direct-effect-call-in-hook.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { fetchFx } from "./model"; 4 | 5 | const Button: React.FC = () => { 6 | React.useEffect(() => { 7 | fetchFx(); 8 | }, []); 9 | 10 | return ; 11 | }; 12 | 13 | export { Button }; 14 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/incorrect-direct-event-call-in-hook.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { mounted } from "./model"; 4 | 5 | const Button: React.FC = () => { 6 | React.useEffect(() => { 7 | mounted(); 8 | }, []); 9 | 10 | return ; 11 | }; 12 | 13 | export { Button }; 14 | -------------------------------------------------------------------------------- /rules/require-pickup-in-persist/examples/correct-scoped-package.js: -------------------------------------------------------------------------------- 1 | import { createStore, createEvent } from "effector"; 2 | 3 | import { persist as persistAsync } from "@effector-storage/react-native-async-storage"; 4 | 5 | const $store = createStore("example"); 6 | const pickup = createEvent(); 7 | 8 | persistAsync({ store: $store, pickup }); 9 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/correct-examples-issue-23.js: -------------------------------------------------------------------------------- 1 | // Examples were found in production code-base with false-positive detection on 0.1.2 2 | // https://github.com/igorkamyshev/eslint-plugin-effector/issues/23 3 | 4 | forward({ 5 | from: createTransactionFx.doneData.map((t) => [t]), 6 | to: addTransactions, 7 | }); 8 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-examples-issue-158.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | const arrow = ({ store = createStore(0) }) => { 4 | return { store }; 5 | }; 6 | 7 | function declaration({ store = createStore(1) }) { 8 | return { store }; 9 | } 10 | 11 | export { arrow, declaration }; 12 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-examples-issue-23.js: -------------------------------------------------------------------------------- 1 | // Examples were found in production code-base with false-positive detection on 0.1.2 2 | // https://github.com/igorkamyshev/eslint-plugin-effector/issues/23 3 | 4 | forward({ 5 | from: createTransactionFx.doneData.map((t) => [t]), 6 | to: addTransactions, 7 | }); 8 | -------------------------------------------------------------------------------- /rules/no-duplicate-on/examples/correct.ts: -------------------------------------------------------------------------------- 1 | class RandomClass { 2 | on(event: any, callback: () => any) { 3 | return this; 4 | } 5 | } 6 | 7 | const $randomObject = new RandomClass(); 8 | 9 | const randomEvent = {}; 10 | 11 | $randomObject.on(randomEvent, () => null).on(randomEvent, () => null); 12 | 13 | export { $randomObject }; 14 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/incorrect-useEvent-from-react.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEvent } from "react"; 2 | 3 | import { clicked } from "./model"; 4 | 5 | const Button: React.FC = () => { 6 | const clickedEvent = useEvent(clicked); 7 | 8 | return ; 9 | }; 10 | 11 | export { Button }; 12 | -------------------------------------------------------------------------------- /rules/require-pickup-in-persist/examples/correct-other-packages.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "effector"; 2 | 3 | import { persist } from "other-persist"; 4 | import { persist as persistNested } from "other-persist/nested"; 5 | 6 | const $store = createStore("example"); 7 | 8 | persist({ store: $store }); 9 | persistNested({ store: $store }); 10 | -------------------------------------------------------------------------------- /rules/keep-options-order/examples/incorrect-sample.js: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore, sample } from "effector"; 2 | 3 | const clock = createEvent(); 4 | const source = createEvent(); 5 | const filter = createStore(); 6 | const fn = () => null; 7 | const target = createEvent(); 8 | 9 | sample({ source, clock, filter, fn, greedy: true, target }); 10 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/correct-generic.tsx: -------------------------------------------------------------------------------- 1 | import { Effect, fork } from "effector"; 2 | 3 | export function Component() { 4 | const scope = fork({ handlers: new Map, any>([]) }); 5 | 6 | type Somethind = unknown; 7 | 8 | const t: Somethind>> = [] as any; 9 | 10 | return null; 11 | } 12 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-c-brand: darkorange; 3 | --vp-c-brand-light: #ef8319; 4 | --vp-c-brand-dark: #d66a00; 5 | --vp-button-brand-bg: darkorange; 6 | --vp-button-brand-hover-border: #be5e00; 7 | --vp-button-brand-active-border: #a65200; 8 | --vp-button-brand-border: #be5e00; 9 | --vp-button-brand-active-bg: #a65200; 10 | } 11 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/incorrect-direct-event-call-in-hook-cleanup-callback.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { unmounted } from "./model"; 4 | 5 | const Button: React.FC = () => { 6 | React.useEffect(() => { 7 | return () => unmounted(); 8 | }, []); 9 | 10 | return ; 11 | }; 12 | 13 | export { Button }; 14 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-examples-issue-128.js: -------------------------------------------------------------------------------- 1 | // Examples were found in production code-base with exception on 0.10.2 2 | // https://github.com/igorkamyshev/eslint-plugin-effector/issues/128 3 | 4 | import { is, createStore } from "effector"; 5 | 6 | function toStore(value) { 7 | return is.store(value) ? value : createStore(value); 8 | } 9 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/examples/incorrect-useStore.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { createStore } from "effector"; 4 | import { useStore } from "effector-react"; 5 | 6 | const $store = createStore(null); 7 | 8 | function Component() { 9 | const value = useStore($store); 10 | 11 | return ; 12 | } 13 | 14 | export { Component }; 15 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/examples/incorrect-useStore.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { createStore } from "effector"; 4 | import { useStore } from "effector-react"; 5 | 6 | const $store = createStore(null); 7 | 8 | function Component() { 9 | const value = useStore($store); 10 | 11 | return ; 12 | } 13 | 14 | export { Component }; 15 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/correct-event-via-useEvent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useEvent } from "effector-react"; 3 | 4 | import { clicked } from "./model"; 5 | 6 | const Button: React.FC = () => { 7 | const clickedEvent = useEvent(clicked); 8 | 9 | return ; 10 | }; 11 | 12 | export { Button }; 13 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/correct-effect-via-useEvent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useEvent } from "effector-react"; 3 | 4 | import { fetchFx } from "./model"; 5 | 6 | const Button: React.FC = () => { 7 | const clickedEffect = useEvent(fetchFx); 8 | 9 | return ; 10 | }; 11 | 12 | export { Button }; 13 | -------------------------------------------------------------------------------- /rules/no-watch/examples/incorrect/sample.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, sample } from "effector"; 2 | 3 | const watcher = (x: T) => x; 4 | const myEvent = createEvent(); 5 | 6 | sample({ 7 | clock: myEvent, 8 | fn: () => true, 9 | }).watch(watcher); 10 | 11 | const newEvent = sample({ 12 | clock: myEvent, 13 | fn: () => true, 14 | }); 15 | 16 | newEvent.watch(watcher); 17 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-store-naming-with-handlers.js: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore } from "effector"; 2 | 3 | const add = createEvent(); 4 | const sub = createEvent(); 5 | const reset = createEvent(); 6 | 7 | const $sum = createStore(0) 8 | .on(add, (s) => s + 1) 9 | .on(sub, (s) => s - 1) 10 | .reset(reset); 11 | 12 | export { $sum }; 13 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/incorrect-store-naming-with-handlers.js: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore } from "effector"; 2 | 3 | const add = createEvent(); 4 | const sub = createEvent(); 5 | const reset = createEvent(); 6 | 7 | const sum = createStore(0) 8 | .on(add, (s) => s + 1) 9 | .on(sub, (s) => s - 1) 10 | .reset(reset); 11 | 12 | export { sum }; 13 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/incorrect-class-extends.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | import React from "react"; 3 | 4 | const somethingHasppenedFx = createEffect(); 5 | 6 | export class SomeComponent extends React.Component { 7 | componentDidMount() { 8 | somethingHasppenedFx({}); 9 | } 10 | 11 | render() { 12 | return null; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /rules/require-pickup-in-persist/examples/correct-core-package.js: -------------------------------------------------------------------------------- 1 | import { createStore, createEvent } from "effector"; 2 | 3 | import { persist } from "effector-storage"; 4 | import { local as localAdapter } from "effector-storage/local"; 5 | 6 | const $store = createStore("example"); 7 | const pickup = createEvent(); 8 | 9 | persist({ store: $store, pickup, adapter: localAdapter }); 10 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/correct-store-naming-with-handlers.js: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore } from "effector"; 2 | 3 | const add = createEvent(); 4 | const sub = createEvent(); 5 | const reset = createEvent(); 6 | 7 | const sum$ = createStore(0) 8 | .on(add, (s) => s + 1) 9 | .on(sub, (s) => s - 1) 10 | .reset(reset); 11 | 12 | export { sum$ }; 13 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/incorrect-store-naming-with-handlers.js: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore } from "effector"; 2 | 3 | const add = createEvent(); 4 | const sub = createEvent(); 5 | const reset = createEvent(); 6 | 7 | const sum = createStore(0) 8 | .on(add, (s) => s + 1) 9 | .on(sub, (s) => s - 1) 10 | .reset(reset); 11 | 12 | export { sum }; 13 | -------------------------------------------------------------------------------- /rules/require-pickup-in-persist/examples/incorrect-complete-config.js: -------------------------------------------------------------------------------- 1 | import { createStore, createEvent } from "effector"; 2 | 3 | import { persist } from "effector-storage/local"; 4 | 5 | const $store = createStore("example"); 6 | const updated = createEvent(); 7 | 8 | persist({ 9 | source: $store, 10 | target: updated, 11 | 12 | key: "store", 13 | keyPrefix: "local", 14 | }); 15 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/correct-useEvent-aliased.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useEvent as useEffectorEvent } from "effector-react"; 3 | 4 | import { clicked } from "./model"; 5 | 6 | const Button: React.FC = () => { 7 | const clickedEvent = useEffectorEvent(clicked); 8 | 9 | return ; 10 | }; 11 | 12 | export { Button }; 13 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/correct-useEvent-as-property.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as effectorHooks from "effector-react"; 3 | 4 | import { clicked } from "./model"; 5 | 6 | const Button: React.FC = () => { 7 | const clickedEvent = effectorHooks.useEvent(clicked); 8 | 9 | return ; 10 | }; 11 | 12 | export { Button }; 13 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/examples/incorrect-useEvent-with-event.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { createEvent } from "effector"; 4 | import { useEvent } from "effector-react"; 5 | 6 | const event = createEvent(); 7 | 8 | function Component() { 9 | const eventFn = useEvent(event); 10 | 11 | return ; 12 | } 13 | 14 | export { Component }; 15 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/examples/incorrect-useEvent-with-event.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { createEvent } from "effector"; 4 | import { useEvent } from "effector-react"; 5 | 6 | const event = createEvent(); 7 | 8 | function Component() { 9 | const eventFn = useEvent(event); 10 | 11 | return ; 12 | } 13 | 14 | export { Component }; 15 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/examples/incorrect-useEvent-with-effect.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { createEffect } from "effector"; 4 | import { useEvent } from "effector-react"; 5 | 6 | const effectFx = createEffect(); 7 | 8 | function Component() { 9 | const effectFn = useEvent(effectFx); 10 | 11 | return ; 12 | } 13 | 14 | export { Component }; 15 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/examples/incorrect-useEvent-with-effect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { createEffect } from "effector"; 4 | import { useEvent } from "effector-react"; 5 | 6 | const effectFx = createEffect(); 7 | 8 | function Component() { 9 | const effectFn = useEvent(effectFx); 10 | 11 | return ; 12 | } 13 | 14 | export { Component }; 15 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/model.ts: -------------------------------------------------------------------------------- 1 | import { createEffect, createEvent } from "effector"; 2 | 3 | export const clicked = createEvent(); 4 | export const mounted = createEvent(); 5 | export const unmounted = createEvent(); 6 | export const fetchFx = createEffect(() => {}); 7 | 8 | export const deepNestedModel = { 9 | context: { 10 | outputs: { 11 | mounted, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /utils/traverse-parent-by-type.js: -------------------------------------------------------------------------------- 1 | function traverseParentByType(node, type, config) { 2 | const stopOnTypes = config?.stopOnTypes ?? []; 3 | 4 | if (!node || stopOnTypes.includes(node.type)) { 5 | return null; 6 | } 7 | 8 | if (node.type === type) { 9 | return node; 10 | } 11 | 12 | return traverseParentByType(node.parent, type, config); 13 | } 14 | 15 | module.exports = { traverseParentByType }; 16 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/correct-effect-store-via-useStore.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useEvent, useStore } from "effector-react"; 3 | 4 | import { fetchFx } from "./model"; 5 | 6 | const Button: React.FC = () => { 7 | const loading = useStore(fetchFx.pending); 8 | 9 | if (loading) { 10 | return null; 11 | } 12 | 13 | return ; 14 | }; 15 | 16 | export { Button }; 17 | -------------------------------------------------------------------------------- /docs/rules/no-ambiguity-target.md: -------------------------------------------------------------------------------- 1 | # effector/no-ambiguity-target 2 | 3 | Call of `guard`/`sample` with `target` and variable assignment is ambiguity. One of them should be omitted from source code. 4 | 5 | ```ts 6 | // 👎 should be rewritten 7 | const result = guard({ clock: trigger, filter: Boolean, target }); 8 | 9 | // 👍 makes sense 10 | guard({ clock: trigger, filter: Boolean, target }); 11 | const result = target; 12 | ``` 13 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/correct-store-naming-in-domain-with-handlers.js: -------------------------------------------------------------------------------- 1 | import { createDomain } from "effector"; 2 | 3 | const d = createDomain(); 4 | 5 | const add = d.createEvent(); 6 | const sub = d.createEvent(); 7 | const reset = d.createEvent(); 8 | 9 | const sum$ = d 10 | .createStore(0) 11 | .on(add, (s) => s + 1) 12 | .on(sub, (s) => s - 1) 13 | .reset(reset); 14 | 15 | export { sum$ }; 16 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/incorrect-store-naming-in-domain-with-handlers.js: -------------------------------------------------------------------------------- 1 | import { createDomain } from "effector"; 2 | 3 | const d = createDomain(); 4 | 5 | const add = d.createEvent(); 6 | const sub = d.createEvent(); 7 | const reset = d.createEvent(); 8 | 9 | const sum = d 10 | .createStore(0) 11 | .on(add, (s) => s + 1) 12 | .on(sub, (s) => s - 1) 13 | .reset(reset); 14 | 15 | export { sum }; 16 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-store-naming-in-domain-with-handlers.js: -------------------------------------------------------------------------------- 1 | import { createDomain } from "effector"; 2 | 3 | const d = createDomain(); 4 | 5 | const add = d.createEvent(); 6 | const sub = d.createEvent(); 7 | const reset = d.createEvent(); 8 | 9 | const $sum = d 10 | .createStore(0) 11 | .on(add, (s) => s + 1) 12 | .on(sub, (s) => s - 1) 13 | .reset(reset); 14 | 15 | export { $sum }; 16 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/incorrect-store-naming-in-domain-with-handlers.js: -------------------------------------------------------------------------------- 1 | import { createDomain } from "effector"; 2 | 3 | const d = createDomain(); 4 | 5 | const add = d.createEvent(); 6 | const sub = d.createEvent(); 7 | const reset = d.createEvent(); 8 | 9 | const sum = d 10 | .createStore(0) 11 | .on(add, (s) => s + 1) 12 | .on(sub, (s) => s - 1) 13 | .reset(reset); 14 | 15 | export { sum }; 16 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/incorrect-combine.js: -------------------------------------------------------------------------------- 1 | import { createStore, restore, createEvent, combine } from "effector"; 2 | 3 | // Just createStore 4 | const justStore$ = createStore(null); 5 | 6 | // Restore 7 | const eventForRestore = createEvent(); 8 | const restoredStore$ = restore(eventForRestore, null); 9 | 10 | // Combine 11 | const combinedStore = combine(justStore$, restoredStore$); 12 | 13 | export { combinedStore }; 14 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/incorrect-combine.js: -------------------------------------------------------------------------------- 1 | import { createStore, restore, createEvent, combine } from "effector"; 2 | 3 | // Just createStore 4 | const $justStore = createStore(null); 5 | 6 | // Restore 7 | const eventForRestore = createEvent(); 8 | const $restoredStore = restore(eventForRestore, null); 9 | 10 | // Combine 11 | const combinedStore = combine($justStore, $restoredStore); 12 | 13 | export { combinedStore }; 14 | -------------------------------------------------------------------------------- /docs/rules/no-useless-methods.md: -------------------------------------------------------------------------------- 1 | # effector/no-useless-methods 2 | 3 | Call of `gaurd`/`sample` without `target` or variable assignment is useless. It can be omitted from source code. 4 | 5 | ```ts 6 | // 👎 can be omitted 7 | guard({ clock: trigger, filter: Boolean }); 8 | 9 | // 👍 makes sense 10 | const target1 = guard({ clock: trigger, filter: Boolean }); 11 | 12 | // 👍 make sense too 13 | guard({ clock: trigger, filter: Boolean, target: target2 }); 14 | ``` 15 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/examples/correct-effect-naming.ts: -------------------------------------------------------------------------------- 1 | import { createEffect, attach } from "effector"; 2 | 3 | // Just createEffect 4 | const baseEffectFx = createEffect(); 5 | 6 | // Attach 7 | const attachedFx = attach({ effect: baseEffectFx }); 8 | 9 | // Factory 10 | function createCustomEffect() { 11 | return createEffect(); 12 | } 13 | 14 | const customFx = createCustomEffect(); 15 | 16 | export { baseEffectFx, attachedFx, customFx }; 17 | -------------------------------------------------------------------------------- /utils/get-nested-object-name.js: -------------------------------------------------------------------------------- 1 | function getNestedObjectName(node) { 2 | let root = node; 3 | let name = ""; 4 | 5 | while (root.type === "MemberExpression") { 6 | name = `${root.property.name}.${name}`; 7 | root = root.object; 8 | } 9 | 10 | if (root.type === "Identifier") { 11 | name = `${root.name}.${name}`; 12 | } 13 | 14 | // Remove last dot 15 | return name.slice(0, -1); 16 | } 17 | 18 | module.exports = { getNestedObjectName }; 19 | -------------------------------------------------------------------------------- /rules/require-pickup-in-persist/examples/correct-complete-config.js: -------------------------------------------------------------------------------- 1 | import { createStore, createEvent } from "effector"; 2 | 3 | import { persist } from "effector-storage/local"; 4 | 5 | const $store = createStore("example"); 6 | const updated = createEvent(); 7 | 8 | const appStarted = createEvent(); 9 | 10 | persist({ 11 | source: $store.updates, 12 | target: updated, 13 | 14 | pickup: appStarted, 15 | 16 | key: "store", 17 | keyPrefix: "local", 18 | }); 19 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-store-naming-property-domain.js: -------------------------------------------------------------------------------- 1 | import { createDomain } from "effector"; 2 | import { attach, createEffect } from "effector"; 3 | 4 | const domain = createDomain(); 5 | const effect = createEffect(); 6 | 7 | const storeFx = attach({ 8 | source: domain.store(0), 9 | effect, 10 | }); 11 | 12 | const createStoreFx = attach({ 13 | source: domain.createStore(0), 14 | effect, 15 | }); 16 | 17 | export { storeFx, createDomain }; 18 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/examples/correct-examples-issue-24.js: -------------------------------------------------------------------------------- 1 | import { combine } from "effector"; 2 | 3 | // Examples were found in production code-base with false-positive detection on 0.1.2 4 | // https://github.com/igorkamyshev/eslint-plugin-effector/issues/24 5 | 6 | const $sourceOrDefault = combine($source, $allSources, (source, allSources) => { 7 | if (!source || source.length === 0) { 8 | return head(allSources) ?? "Freelance"; 9 | } 10 | 11 | return source; 12 | }); 13 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/correct-event-as-object-prop.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useEvent } from "effector-react"; 3 | 4 | import * as model from "./model"; 5 | 6 | const Button: React.FC = () => { 7 | const clickedEvent = useEvent(model.clicked); 8 | const mounted = useEvent(model.deepNestedModel.context.outputs.mounted); 9 | 10 | React.useEffect(mounted, []); 11 | 12 | return ; 13 | }; 14 | 15 | export { Button }; 16 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/incorrect-combine-alias.js: -------------------------------------------------------------------------------- 1 | import { 2 | createStore, 3 | restore, 4 | createEvent, 5 | combine as mergeStores, 6 | } from "effector"; 7 | 8 | // Just createStore 9 | const $justStore = createStore(null); 10 | 11 | // Restore 12 | const eventForRestore = createEvent(); 13 | const $restoredStore = restore(eventForRestore, null); 14 | 15 | // Combine 16 | const combinedStore = mergeStores($justStore, $restoredStore); 17 | 18 | export { combinedStore }; 19 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/correct-class-no-extends.ts: -------------------------------------------------------------------------------- 1 | import { allSettled, createEffect, fork } from "effector"; 2 | 3 | const somethingHasppenedFx = createEffect(); 4 | 5 | export function CreateController() { 6 | class SomeController { 7 | private async handleHttp() { 8 | const scope = fork({ handlers: [[somethingHasppenedFx, () => null]] }); 9 | 10 | await allSettled(somethingHasppenedFx, { scope, params: {} }); 11 | } 12 | } 13 | 14 | return SomeController; 15 | } 16 | -------------------------------------------------------------------------------- /docs/presets/recommended.md: -------------------------------------------------------------------------------- 1 | # plugin:effector/recommended 2 | 3 | This preset is recommended for most projects. 4 | 5 | 6 | 7 | ## Configuration 8 | 9 | Add `effector` to the plugin section of your `.eslintrc` configuration file, and add `plugin:effector/recommended` to the extends section. 10 | 11 | ```json 12 | { 13 | "plugins": ["effector"], 14 | "extends": ["plugin:effector/recommended"] 15 | } 16 | ``` 17 | 18 | ## Rules 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/rules/no-duplicate-on.md: -------------------------------------------------------------------------------- 1 | # effector/no-duplicate-on 2 | 3 | Disallow duplicates `on`-handlers on particular store. 4 | 5 | ```ts 6 | const increment = createEvent(); 7 | 8 | // 👍 all explicitly 9 | const $goodCounter = createStore(0).on(increment, (counter) => counter + 1); 10 | 11 | // 👎 so, which handler should we choose? 12 | // it's better to remove one of them 13 | const $badCounter = createStore(0) 14 | .on(increment, (counter) => counter + 1) 15 | .on(increment, (counter) => counter + 2); 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/shared/install.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | ::: tip 4 | 5 | This plugin uses TypeScript for more precise results, but JavaScript is supported too. 6 | 7 | ::: 8 | 9 | First, you need to install ESLint and plugin: 10 | 11 | with `pnpm` 12 | 13 | ```sh 14 | pnpm install --dev eslint-plugin-effector eslint 15 | ``` 16 | 17 | with `yarn` 18 | 19 | ```sh 20 | yarn add --dev eslint-plugin-effector eslint 21 | ``` 22 | 23 | with `npm` 24 | 25 | ```sh 26 | npm install --dev eslint-plugin-effector eslint 27 | ``` 28 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/incorrect-combine-alias.js: -------------------------------------------------------------------------------- 1 | import { 2 | createStore, 3 | restore, 4 | createEvent, 5 | combine as mergeStores, 6 | } from "effector"; 7 | 8 | // Just createStore 9 | const justStore$ = createStore(null); 10 | 11 | // Restore 12 | const eventForRestore = createEvent(); 13 | const restoredStore$ = restore(eventForRestore, null); 14 | 15 | // Combine 16 | const combinedStore = mergeStores(justStore$, restoredStore$); 17 | 18 | export { combinedStore }; 19 | -------------------------------------------------------------------------------- /docs/presets/future.md: -------------------------------------------------------------------------------- 1 | # plugin:effector/future 2 | 3 | This preset contains rules, which enforce _future-effector_ code-style. 4 | 5 | 6 | 7 | ## Configuration 8 | 9 | Add `effector` to the plugin section of your `.eslintrc` configuration file, and add `plugin:effector/future` to the extends section. 10 | 11 | ```json 12 | { 13 | "plugins": ["effector"], 14 | "extends": ["plugin:effector/future"] 15 | } 16 | ``` 17 | 18 | ## Rules 19 | 20 | 21 | -------------------------------------------------------------------------------- /rules/no-duplicate-on/examples/correct-with-scopes.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore } from "effector"; 2 | 3 | function createFactory1() { 4 | const event = createEvent(); 5 | 6 | const $store = createStore("").on(event, () => null); 7 | 8 | return { $store, event }; 9 | } 10 | 11 | function createFactory2() { 12 | const event = createEvent(); 13 | 14 | const $store = createStore("").on(event, () => "TEST"); 15 | 16 | return { $store, event }; 17 | } 18 | 19 | export { createFactory1, createFactory2 }; 20 | -------------------------------------------------------------------------------- /docs/presets/react.md: -------------------------------------------------------------------------------- 1 | # plugin:effector/react 2 | 3 | This preset is recommended for projects that use [React](https://reactjs.org) with Effector. 4 | 5 | 6 | 7 | ## Configuration 8 | 9 | Add `effector` to the plugin section of your `.eslintrc` configuration file, and add `plugin:effector/react` to the extends section. 10 | 11 | ```json 12 | { 13 | "plugins": ["effector"], 14 | "extends": ["plugin:effector/react"] 15 | } 16 | ``` 17 | 18 | ## Rules 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/presets/patronum.md: -------------------------------------------------------------------------------- 1 | # plugin:effector/patronum 2 | 3 | This preset is recommended for projects that use [Patronum](https://patronum.effector.dev/). 4 | 5 | 6 | 7 | ## Configuration 8 | 9 | Add `effector` to the plugin section of your `.eslintrc` configuration file, and add `plugin:effector/patronum` to the extends section. 10 | 11 | ```json 12 | { 13 | "plugins": ["effector"], 14 | "extends": ["plugin:effector/patronum"] 15 | } 16 | ``` 17 | 18 | ## Rules 19 | 20 | 21 | -------------------------------------------------------------------------------- /rules/no-useless-methods/examples/correct-nested.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore, guard, sample } from "effector"; 2 | 3 | const emailValidationFired = createEvent(); 4 | const $isEmailFromGuestiaExist = createStore(false); 5 | const $email = createStore(""); 6 | const $emailError = createStore(false); 7 | 8 | sample({ 9 | clock: guard({ 10 | clock: emailValidationFired, 11 | source: $isEmailFromGuestiaExist, 12 | filter: (isExist) => !isExist, 13 | }), 14 | source: $email, 15 | fn: Boolean, 16 | target: $emailError, 17 | }); 18 | -------------------------------------------------------------------------------- /rules/no-unnecessary-duplication/examples/correct-examples-issue-27.js: -------------------------------------------------------------------------------- 1 | import { sample } from "effector"; 2 | 3 | // Examples were found in production code-base with false-positive detection on 0.1.3 4 | // https://github.com/igorkamyshev/eslint-plugin-effector/issues/27 5 | 6 | sample({ 7 | source: { list: $currencyList, active: $currency }, 8 | clock: activateNextCurrency, 9 | fn: ({ list, active }) => { 10 | const index = list.findIndex((v) => v === active); 11 | 12 | return list[index + 1] ?? list[0]; 13 | }, 14 | target: activateCurrency, 15 | }); 16 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-examples-issue-150.js: -------------------------------------------------------------------------------- 1 | import { attach, combine, createStore, createEffect } from "effector"; 2 | 3 | const testFx = attach({ 4 | source: createStore(0), 5 | effect: createEffect(), 6 | }); 7 | 8 | const combineFx = attach({ 9 | source: { 10 | value: combine(), 11 | }, 12 | effect: createEffect(), 13 | }); 14 | 15 | const createStoreFx = attach({ 16 | source: { 17 | value: createStore(0), 18 | }, 19 | effect: createEffect(), 20 | }); 21 | 22 | export { testFx, combineFx, createStoreFx }; 23 | -------------------------------------------------------------------------------- /utils/validate-store-name-convention.js: -------------------------------------------------------------------------------- 1 | const { getStoreNameConvention } = require("./get-store-name-convention"); 2 | 3 | function validateStoreNameConvention(context) { 4 | const storeNameConvention = getStoreNameConvention(context); 5 | 6 | if (storeNameConvention !== "prefix" && storeNameConvention !== "postfix") { 7 | throw new Error( 8 | "Invalid Configuration of effector-plugin-eslint/enforce-store-naming-convention. The value should be equal to prefix or postfix." 9 | ); 10 | } 11 | } 12 | 13 | module.exports = { validateStoreNameConvention }; 14 | -------------------------------------------------------------------------------- /rules/no-duplicate-on/examples/correct-with-nesting.ts: -------------------------------------------------------------------------------- 1 | // Examples were found in production code-base with false-positive detection on 0.5.2 2 | // https://github.com/effector/eslint-plugin/issues/85 3 | 4 | import { createEvent, createStore } from "effector"; 5 | 6 | const $first = createStore(""); 7 | const $second = createStore(""); 8 | 9 | const change = createEvent(); 10 | 11 | const service = { 12 | inputs: { $first, $second }, 13 | }; 14 | 15 | service.inputs.$first.on(change, () => "HI"); 16 | service.inputs.$second.on(change, () => "BYE"); 17 | 18 | export { service }; 19 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/correct-store-naming.js: -------------------------------------------------------------------------------- 1 | import { createStore, restore, createEvent, combine } from "effector"; 2 | 3 | // Just createStore 4 | const justStore$ = createStore(null); 5 | 6 | // Restore 7 | const eventForRestore = createEvent(); 8 | const restoredStore$ = restore(eventForRestore, null); 9 | 10 | // Combine 11 | const combinedStore$ = combine($justStore, $restoredStore); 12 | 13 | // Map 14 | const mappedStore$ = $combinedStore.map((values) => values.length); 15 | 16 | export { justStore$, restoredStore$, combinedStore$, mappedStore$ }; 17 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/examples/correct-store-naming.ts: -------------------------------------------------------------------------------- 1 | import { createStore, restore, createEvent, combine } from "effector"; 2 | 3 | // Just createStore 4 | const justStore$ = createStore(null); 5 | 6 | // Restore 7 | const eventForRestore = createEvent(); 8 | const restoredStore$ = restore(eventForRestore, null); 9 | 10 | // Combine 11 | const combinedStore$ = combine(justStore$, restoredStore$); 12 | 13 | // Map 14 | const mappedStore$ = combinedStore$.map((values) => values.length); 15 | 16 | export { justStore$, restoredStore$, combinedStore$, mappedStore$ }; 17 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-examples-issue-136.js: -------------------------------------------------------------------------------- 1 | // Examples were found in production code-base with exception on 0.10.3 2 | // https://github.com/igorkamyshev/eslint-plugin-effector/issues/136 3 | 4 | import { combine, sample } from "effector"; 5 | import { modelFactory } from "effector-factorio"; 6 | 7 | export const factory = modelFactory(() => { 8 | sample({ 9 | clock: combine([createStore("")]), 10 | fn: ([fieldType, customFieldId]) => customFieldId || fieldType, 11 | target: createStore(""), 12 | }); 13 | 14 | return {}; 15 | }); 16 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-store-naming.js: -------------------------------------------------------------------------------- 1 | import { createStore, restore, createEvent, combine } from "effector"; 2 | 3 | // Just createStore 4 | const $justStore = createStore(null); 5 | 6 | // Restore 7 | const eventForRestore = createEvent(); 8 | const $restoredStore = restore(eventForRestore, null); 9 | 10 | // Combine 11 | const $combinedStore = combine($justStore, $restoredStore); 12 | 13 | // Map 14 | const $mappedStore = $combinedStore.map((values) => values.length); 15 | 16 | export { $justStore, $restoredStore, $combinedStore, $mappedStore }; 17 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/examples/correct-store-naming.ts: -------------------------------------------------------------------------------- 1 | import { createStore, restore, createEvent, combine } from "effector"; 2 | 3 | // Just createStore 4 | const $justStore = createStore(null); 5 | 6 | // Restore 7 | const eventForRestore = createEvent(); 8 | const $restoredStore = restore(eventForRestore, null); 9 | 10 | // Combine 11 | const $combinedStore = combine($justStore, $restoredStore); 12 | 13 | // Map 14 | const $mappedStore = $combinedStore.map((values) => values.length); 15 | 16 | export { $justStore, $restoredStore, $combinedStore, $mappedStore }; 17 | -------------------------------------------------------------------------------- /utils/extract-imported-from.js: -------------------------------------------------------------------------------- 1 | function extractImportedFrom({ importMap, nodeMap, node, packageName }) { 2 | const normalizePackageName = Array.isArray(packageName) 3 | ? packageName 4 | : [packageName]; 5 | 6 | if (normalizePackageName.includes(node.source.value)) { 7 | for (const s of node.specifiers) { 8 | if (s.type === "ImportDefaultSpecifier") { 9 | continue; 10 | } 11 | 12 | importMap.set(s.imported.name, s.local.name); 13 | nodeMap?.set(s.imported.name, s); 14 | } 15 | } 16 | } 17 | 18 | module.exports = { extractImportedFrom }; 19 | -------------------------------------------------------------------------------- /rules/no-watch/no-watch.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("eslint"); 2 | 3 | const rule = require("./no-watch.js"); 4 | 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 2020, 8 | sourceType: "module", 9 | }, 10 | }); 11 | 12 | ruleTester.run("effector/no-watch.test", rule, { 13 | valid: [ 14 | "myFx.finally.watch(myEvent);", 15 | "myEvent.watch((payload) => {if (Boolean(payload)) {myFx(payload);}});", 16 | "$awesome.updates.watch((data) => {myEvent(identity(data));});", 17 | ].map((code) => ({ code })), 18 | 19 | invalid: [], 20 | }); 21 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/examples/correct-single-useUnit.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { createEffect, createEvent, createStore } from "effector"; 4 | import { useUnit } from "effector-react"; 5 | 6 | const $store = createStore(null); 7 | const event = createEvent(); 8 | const effectFx = createEffect(); 9 | 10 | function Component() { 11 | const [value, eventFn, effectFn] = useUnit($store, event, effectFx); 12 | 13 | return ( 14 | 17 | ); 18 | } 19 | 20 | export { Component }; 21 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/examples/correct-single-useUnit.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { createEffect, createEvent, createStore } from "effector"; 4 | import { useUnit } from "effector-react"; 5 | 6 | const $store = createStore(null); 7 | const event = createEvent(); 8 | const effectFx = createEffect(); 9 | 10 | function Component() { 11 | const [value, eventFn, effectFn] = useUnit($store, event, effectFx); 12 | 13 | return ( 14 | 17 | ); 18 | } 19 | 20 | export { Component }; 21 | -------------------------------------------------------------------------------- /utils/builders.js: -------------------------------------------------------------------------------- 1 | const buildObjectInText = { 2 | fromArrayOfNodes({ properties, context }) { 3 | const content = properties 4 | .map((property) => context.getSourceCode().getText(property)) 5 | .join(", "); 6 | 7 | return `{ ${content} }`; 8 | }, 9 | fromMapOfNodes({ properties, context }) { 10 | const content = Object.entries(properties) 11 | .filter(([_, node]) => Boolean(node)) 12 | .map(([key, node]) => `${key}: ${context.getSourceCode().getText(node)}`) 13 | .join(", "); 14 | 15 | return `{ ${content} }`; 16 | }, 17 | }; 18 | 19 | module.exports = { buildObjectInText }; 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | checks: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [16.x, 17.x, 18.x, 19.x, 20.x, 21.x, 22.x, 23.x, 24.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: pnpm/action-setup@v2 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: "pnpm" 24 | - run: pnpm install --frozen-lockfile 25 | - run: pnpm test 26 | -------------------------------------------------------------------------------- /config/recommended.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "effector/enforce-store-naming-convention": "error", 4 | "effector/enforce-effect-naming-convention": "error", 5 | "effector/no-getState": "error", 6 | "effector/no-useless-methods": "error", 7 | "effector/no-unnecessary-duplication": "warn", 8 | "effector/prefer-sample-over-forward-with-mapping": "warn", 9 | "effector/no-ambiguity-target": "warn", 10 | "effector/no-watch": "warn", 11 | "effector/no-unnecessary-combination": "warn", 12 | "effector/no-duplicate-on": "error", 13 | "effector/keep-options-order": "warn", 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /rules/no-ambiguity-target/examples/correct-example-issue-133.js: -------------------------------------------------------------------------------- 1 | // Examples were found in production code-base with exception on 0.10.3 2 | // https://github.com/igorkamyshev/eslint-plugin-effector/issues/133 3 | 4 | import { createStore, createEvent, sample } from "effector"; 5 | 6 | const obj = { 7 | fn: () => { 8 | const $store = createStore(0); 9 | const event = createEvent(); 10 | // warning Method `sample` returns `target` and assigns the result to a variable. Consider removing one of them effector/no-ambiguity-target 11 | sample({ 12 | source: event, 13 | target: $store, 14 | }); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /rules/no-duplicate-clock-or-source-array-values/examples/correct-sample.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore, sample } from "effector"; 2 | import { createEffect } from "effector"; 3 | 4 | const currentOrderUpdated = createEvent(); 5 | 6 | const setUnloadDeliveryDateFx = createEffect(); 7 | const setLoadDeliveryDateFx = createEffect(); 8 | 9 | const $store = createStore(null); 10 | const clickOnBtn = createEvent(); 11 | 12 | sample({ 13 | clock: [ 14 | setUnloadDeliveryDateFx.doneData, 15 | setLoadDeliveryDateFx, 16 | $store, 17 | clickOnBtn, 18 | ], 19 | filter: Boolean, 20 | target: currentOrderUpdated, 21 | }); 22 | -------------------------------------------------------------------------------- /utils/are-nodes-same-in-text.js: -------------------------------------------------------------------------------- 1 | const prettier = require("prettier"); 2 | 3 | function areNodesSameInText({ context, nodes }) { 4 | const texts = nodes.map((node) => { 5 | let sourceText = context.getSourceCode().getText(node); 6 | 7 | const shouldBeWrapped = 8 | sourceText.startsWith("{") && sourceText.endsWith("}"); 9 | 10 | if (shouldBeWrapped) { 11 | sourceText = `(${sourceText})`; 12 | } 13 | 14 | return prettier.format(sourceText, { 15 | parser: "babel-ts", 16 | }); 17 | }); 18 | 19 | return texts.every((text) => text === texts[0]); 20 | } 21 | 22 | module.exports = { areNodesSameInText }; 23 | -------------------------------------------------------------------------------- /docs/presets/scope.md: -------------------------------------------------------------------------------- 1 | # plugin:effector/scope 2 | 3 | This preset is recommended for projects that use [Fork API](https://effector.dev/docs/api/effector/scope). You can read more about Fork API in [an article](https://dev.to/effector/the-best-part-of-effector-4c27). 4 | 5 | 6 | 7 | ## Configuration 8 | 9 | Add `effector` to the plugin section of your `.eslintrc` configuration file, and add `plugin:effector/scope` to the extends section. 10 | 11 | ```json 12 | { 13 | "plugins": ["effector"], 14 | "extends": ["plugin:effector/scope"] 15 | } 16 | ``` 17 | 18 | ## Rules 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/rules/enforce-gate-naming-convention.md: -------------------------------------------------------------------------------- 1 | # effector/enforce-gate-naming-convention 2 | 3 | Enforcing naming conventions helps keep the codebase consistent, and reduces overhead when thinking about how to name a variable with gate. Every gate is a React-component, so it should be named as regular React-component. 4 | 5 | ## Configuration 6 | 7 | ```json 8 | { 9 | "rules": { 10 | "effector/enforce-gate-naming-convention": "error" 11 | } 12 | } 13 | ``` 14 | 15 | ## Examples 16 | 17 | ```ts 18 | // 👍 nice name 19 | const MyFavoritePageGate = createGate(); 20 | 21 | // 👎 bad name 22 | const otherFavoritePageGate = createGate(); 23 | ``` 24 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/examples/correct-multiple-useUnit.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { createEffect, createEvent, createStore } from "effector"; 4 | import { useUnit } from "effector-react"; 5 | 6 | const $store = createStore(null); 7 | const event = createEvent(); 8 | const effectFx = createEffect(); 9 | 10 | function Component() { 11 | const value = useUnit($store); 12 | const eventFn = useUnit(event); 13 | const effectFn = useUnit(effectFx); 14 | 15 | return ( 16 | 19 | ); 20 | } 21 | 22 | export { Component }; 23 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/examples/correct-multiple-useUnit.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { createEffect, createEvent, createStore } from "effector"; 4 | import { useUnit } from "effector-react"; 5 | 6 | const $store = createStore(null); 7 | const event = createEvent(); 8 | const effectFx = createEffect(); 9 | 10 | function Component() { 11 | const value = useUnit($store); 12 | const eventFn = useUnit(event); 13 | const effectFn = useUnit(effectFx); 14 | 15 | return ( 16 | 19 | ); 20 | } 21 | 22 | export { Component }; 23 | -------------------------------------------------------------------------------- /utils/method.js: -------------------------------------------------------------------------------- 1 | function isSomeMethod(methodName, { node, importMap }) { 2 | const normalizedMethodNames = Array.isArray(methodName) 3 | ? methodName 4 | : [methodName]; 5 | 6 | return normalizedMethodNames.some((method) => { 7 | const localMethod = importMap.get(method); 8 | if (!localMethod) { 9 | return false; 10 | } 11 | 12 | const isEffectorMethod = node?.callee?.name === localMethod; 13 | 14 | return isEffectorMethod; 15 | }); 16 | } 17 | 18 | const method = { 19 | is: (...args) => isSomeMethod(...args), 20 | isNot: (...args) => !isSomeMethod(...args), 21 | }; 22 | 23 | module.exports = { method }; 24 | -------------------------------------------------------------------------------- /rules/no-useless-methods/no-useless-methods.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("eslint"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../utils/read-example"); 5 | const rule = require("./no-useless-methods"); 6 | 7 | const ruleTester = new RuleTester({ 8 | parserOptions: { 9 | ecmaVersion: 2020, 10 | sourceType: "module", 11 | }, 12 | }); 13 | 14 | const readExampleForTheRule = (name) => ({ 15 | code: readExample(__dirname, name), 16 | }); 17 | 18 | ruleTester.run("effector/no-useless-methods.test", rule, { 19 | valid: ["correct-examples-issue-74.js"].map(readExampleForTheRule), 20 | 21 | invalid: [], 22 | }); 23 | -------------------------------------------------------------------------------- /rules/keep-options-order/examples/correct-guard.js: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore, guard } from "effector"; 2 | 3 | const clock = createEvent(); 4 | const source = createEvent(); 5 | const filter = createStore(); 6 | const target = createEvent(); 7 | 8 | guard({ clock, source, filter, target }); 9 | 10 | guard({ clock, source, filter }); 11 | guard({ clock, source, filter, target }); 12 | guard({ clock, source, target }); 13 | guard({ clock, filter, target }); 14 | guard({ source, filter, target }); 15 | 16 | guard({ clock, source }); 17 | 18 | guard({ clock, filter }); 19 | guard({ clock, filter, target }); 20 | 21 | guard({ filter, target }); 22 | guard({ filter }); 23 | -------------------------------------------------------------------------------- /docs/rules/enforce-effect-naming-convention.md: -------------------------------------------------------------------------------- 1 | # effector/enforce-effect-naming-convention 2 | 3 | Enforcing naming conventions helps keep the codebase consistent, and reduces overhead when thinking about how to name a variable with effect. Your effect should be distinguished by a suffix `Fx`. For example, `fetchUserInfoFx` is an effect, `fetchUserInfo` is not. 4 | 5 | ## Configuration 6 | 7 | ```json 8 | { 9 | "rules": { 10 | "effector/enforce-effect-naming-convention": "error" 11 | } 12 | } 13 | ``` 14 | 15 | ## Examples 16 | 17 | ```ts 18 | // 👍 nice name 19 | const fetchNameFx = createEffect(); 20 | 21 | // 👎 bad name 22 | const fetchName = createEffect(); 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/rules/mandatory-scope-binding.md: -------------------------------------------------------------------------------- 1 | # effector/mandatory-scope-binding 2 | 3 | Forbids `Event` and `Effect` usage without `useUnit` in React components. 4 | This ensures `Fork API` compatibility and allows writing isomorphic code for SSR apps. 5 | 6 | ```tsx 7 | const increment = createEvent(); 8 | 9 | // 👍 Event usage is wrapped with `useUnit` 10 | const GoodButton = () => { 11 | const incrementEvent = useUnit(increment); 12 | 13 | return ; 14 | }; 15 | 16 | // 👎 Event is not wrapped with `useUnit` - component is not suitable for isomorphic SSR app 17 | const BadButton = () => { 18 | return ; 19 | }; 20 | ``` 21 | -------------------------------------------------------------------------------- /rules/no-ambiguity-target/examples/correct.ts: -------------------------------------------------------------------------------- 1 | import { sample, guard, createEvent } from "effector"; 2 | 3 | const trigger = createEvent(); 4 | const target = createEvent(); 5 | 6 | // with target 7 | sample({ clock: trigger, fn: Boolean, target }); 8 | sample({ source: trigger, fn: Boolean, target }); 9 | 10 | guard({ clock: trigger, filter: Boolean, target }); 11 | guard({ source: trigger, filter: Boolean, target }); 12 | 13 | // with assign 14 | const result1 = sample({ clock: trigger, fn: Boolean }); 15 | const result2 = sample({ source: trigger, fn: Boolean }); 16 | 17 | const result3 = guard({ clock: trigger, filter: Boolean }); 18 | const result4 = guard({ source: trigger, filter: Boolean }); 19 | -------------------------------------------------------------------------------- /rules/no-useless-methods/examples/correct-examples-issue-74.js: -------------------------------------------------------------------------------- 1 | import { sample, guard, createStore, createEvent } from "effector"; 2 | 3 | const $name = createStore(""); 4 | const $shouldSyncRealName = createStore(false); 5 | const $nameValue = createStore(""); 6 | 7 | const nameInputChanged = createEvent(); 8 | 9 | // Examples were found in production code-base with false-positive detection on 0.4.1 10 | // https://github.com/igorkamyshev/eslint-plugin-effector/issues/74 11 | 12 | $nameValue.on( 13 | guard({ 14 | clock: $name, 15 | filter: $shouldSyncRealName, 16 | }), 17 | (prev, newRealName) => newRealName 18 | ); 19 | 20 | sample($nameInput, nameInputChanged).watch(console.log); 21 | -------------------------------------------------------------------------------- /docs/rules/require-pickup-in-persist.md: -------------------------------------------------------------------------------- 1 | # effector/require-pickup-in-persist 2 | 3 | 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`. 4 | 5 | ```ts 6 | import { persist } from "effector-storage/query"; 7 | 8 | const $store = createStore("example"); 9 | 10 | // 👎 no pickup, does not work with Scope 11 | persist({ store: $store }); 12 | ``` 13 | 14 | ```ts 15 | const pickup = createEvent(); 16 | 17 | // 👍 pickup is specified 18 | persist({ store: $store, pickup }); 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/rules/no-unnecessary-duplication.md: -------------------------------------------------------------------------------- 1 | # effector/no-unnecessary-duplication 2 | 3 | Same `clock`/`source` in `sample` and `guard` don't make sense, any of these fields can be omitted in this case. 4 | 5 | ```ts 6 | const $data = createStore(null); 7 | 8 | // 👎 can be simplified 9 | const target1 = sample({ 10 | source: $data, 11 | clock: $data, 12 | fn(data) { 13 | return data.length; 14 | }, 15 | }); 16 | 17 | // 👍 better 18 | const target2 = sample({ 19 | source: $data, 20 | fn(data) { 21 | return data.length; 22 | }, 23 | }); 24 | 25 | // 👍 also nice solution 26 | const target3 = sample({ 27 | clock: $data, 28 | fn(data) { 29 | return data.length; 30 | }, 31 | }); 32 | ``` 33 | -------------------------------------------------------------------------------- /rules/no-duplicate-clock-or-source-array-values/examples/incorrect-sample.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore, sample } from "effector"; 2 | import { createEffect } from "effector"; 3 | 4 | const currentOrderUpdated = createEvent(); 5 | 6 | const setUnloadDeliveryDateFx = createEffect(); 7 | 8 | const $$order = { 9 | setUnloadDeliveryDateFx, 10 | }; 11 | 12 | const $store = createStore(null); 13 | const clickOnBtn = createEvent(); 14 | 15 | sample({ 16 | source: [$store, $store], 17 | clock: [ 18 | setUnloadDeliveryDateFx.doneData, 19 | $$order.setUnloadDeliveryDateFx.doneData, 20 | setUnloadDeliveryDateFx.doneData, 21 | ], 22 | filter: Boolean, 23 | target: currentOrderUpdated, 24 | }); 25 | -------------------------------------------------------------------------------- /docs/rules/prefer-sample-over-forward-with-mapping.md: -------------------------------------------------------------------------------- 1 | # effector/prefer-sample-over-forward-with-mapping 2 | 3 | Prefer `sample` over `forward` with `.map`/`.prepend`. 4 | 5 | ```js 6 | const eventOne = createEvent(); 7 | const eventTwo = createEvent(); 8 | 9 | // 👎 looks weird 10 | forward({ 11 | from: eventOne.map((items) => items.length), 12 | to: eventTwo, 13 | }); 14 | 15 | // 👎 weird too 16 | forward({ 17 | from: eventOne, 18 | to: eventTwo.prepend((items) => items.length), 19 | }); 20 | 21 | // 👍 better 22 | sample({ 23 | source: eventOne, 24 | fn: (items) => items.length, 25 | target: eventTwo, 26 | }); 27 | ``` 28 | 29 | 💡 Tip: It could be superseded by [no-forward](/rules/no-forward.md). 30 | -------------------------------------------------------------------------------- /docs/rules/no-unnecessary-combination.md: -------------------------------------------------------------------------------- 1 | # effector/no-unnecessary-combination 2 | 3 | Call of `combine`/`merge` in `clock`/`source` is unnecessary. It can be omitted from source code. 4 | 5 | ```ts 6 | // 👎 can be simplified 7 | const badEventOne = guard({ 8 | clock: combine($store1, $store2), 9 | filter: $filter, 10 | }); 11 | const badEventOne = guard({ 12 | clock: combine($store1, $store2, (store1, store2) => ({ 13 | x: store1, 14 | y: store2, 15 | })), 16 | filter: $filter, 17 | }); 18 | 19 | // 👍 better 20 | const goodEventOne = guard({ clock: [$store1, $store2], filter: $filter }); 21 | const goodEventTwo = guard({ 22 | clock: { x: $store1, x: $store2 }, 23 | filter: $filter, 24 | }); 25 | ``` 26 | -------------------------------------------------------------------------------- /rules/no-ambiguity-target/no-ambiguity-target.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("eslint"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../utils/read-example"); 5 | const rule = require("./no-ambiguity-target"); 6 | 7 | const ruleTester = new RuleTester({ 8 | parserOptions: { 9 | ecmaVersion: 2020, 10 | sourceType: "module", 11 | }, 12 | }); 13 | 14 | const readExampleForTheRule = (name) => ({ 15 | code: readExample(__dirname, name), 16 | filename: join(__dirname, "examples", name), 17 | }); 18 | 19 | ruleTester.run("effector/no-ambiguity-target.js.test", rule, { 20 | valid: ["correct-example-issue-133.js"].map(readExampleForTheRule), 21 | 22 | invalid: [], 23 | }); 24 | -------------------------------------------------------------------------------- /docs/rules/no-getState.md: -------------------------------------------------------------------------------- 1 | # effector/no-getState 2 | 3 | `.getState` gives rise too difficult to debug imperative code and kind of race condition. Prefer declarative `sample` to pass data from store and `attach` for effects. 4 | 5 | ```ts 6 | const $username = createStore(null); 7 | const userLoggedIn = createEvent(); 8 | 9 | // 👍 good solution 10 | const fetchUserCommentsFx = createEffect((name) => /* ... */); 11 | sample({ source: $username, clock: userLoggedIn, target: fetchUserCommentsFx }); 12 | 13 | // 👎 bad solution 14 | const fetchUserCommentsInBadWayFx = createEffect(() => { 15 | const name = $username.getState(); 16 | 17 | /* ... */ 18 | }); 19 | forward({ from: userLoggedIn, to: fetchUserCommentsInBadWayFx }); 20 | ``` 21 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/correct-scope-import.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useEvent } from "effector-react/scope"; 3 | 4 | import { clicked, mounted, fetchFx, unmounted } from "./model"; 5 | 6 | const Button: React.FC = () => { 7 | const { clickedEvent, mountedEvent, unmountedEvent, fetch } = useEvent({ 8 | clickedEvent: clicked, 9 | mountedEvent: mounted, 10 | unmountedEvent: unmounted, 11 | fetch: fetchFx, 12 | }); 13 | 14 | React.useEffect(() => { 15 | mountedEvent(); 16 | fetch(); 17 | 18 | return () => { 19 | unmountedEvent(); 20 | }; 21 | }, []); 22 | 23 | return ; 24 | }; 25 | 26 | export { Button }; 27 | -------------------------------------------------------------------------------- /rules/no-duplicate-clock-or-source-array-values/examples/incorrect-guard.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore, guard } from "effector"; 2 | import { createEffect } from "effector"; 3 | 4 | const currentOrderUpdated = createEvent(); 5 | 6 | const setUnloadDeliveryDateFx = createEffect(); 7 | 8 | const $$order = { 9 | setUnloadDeliveryDateFx, 10 | }; 11 | 12 | const $store = createStore(null); 13 | const clickOnBtn = createEvent(); 14 | 15 | guard({ 16 | source: [$store], 17 | clock: [ 18 | setUnloadDeliveryDateFx.doneData, 19 | $$order.setUnloadDeliveryDateFx.doneData, 20 | clickOnBtn, 21 | setUnloadDeliveryDateFx.doneData, 22 | clickOnBtn, 23 | ], 24 | filter: Boolean, 25 | target: currentOrderUpdated, 26 | }); 27 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/correct-object-shape-via-useEvent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useEvent } from "effector-react"; 3 | 4 | import { clicked, mounted, fetchFx, unmounted } from "./model"; 5 | 6 | const Button: React.FC = () => { 7 | const { clickedEvent, mountedEvent, unmountedEvent, fetch } = useEvent({ 8 | clickedEvent: clicked, 9 | mountedEvent: mounted, 10 | unmountedEvent: unmounted, 11 | fetch: fetchFx, 12 | }); 13 | 14 | React.useEffect(() => { 15 | mountedEvent(); 16 | fetch(); 17 | 18 | return () => { 19 | unmountedEvent(); 20 | }; 21 | }, []); 22 | 23 | return ; 24 | }; 25 | 26 | export { Button }; 27 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/examples/correct.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore, forward, guard, sample } from "effector"; 2 | 3 | const event1 = createEvent(); 4 | const event2 = createEvent(); 5 | const $store1 = createStore(null); 6 | const $store2 = createStore(null); 7 | 8 | sample({ clock: [event1, event2], source: [$store1, $store2] }); 9 | 10 | sample({ clock: event1, source: [$store1, $store2] }); 11 | 12 | sample({ clock: event1, source: { a: $store1, b: $store2 } }); 13 | 14 | guard({ clock: [event1, event2], source: $store1, filter: Boolean }); 15 | 16 | guard({ clock: event1, source: { a: $store1 }, filter: Boolean }); 17 | 18 | const otherEvent = createEvent(); 19 | 20 | forward({ from: [event1, event2], to: otherEvent }); 21 | -------------------------------------------------------------------------------- /docs/rules/keep-options-order.md: -------------------------------------------------------------------------------- 1 | # effector/keep-options-order 2 | 3 | Some of Effector-methods (e.g., `sample` and `guard`) accept config in object form. This form can be read as "when `clock` is triggered, take data from `source` pass it through `filter`/`fn` and send to `target`". So, it is better to use semantic order of configuration properties — `clock -> source -> filter/fn -> target`. The rule enforces this order for any case. 4 | 5 | ```ts 6 | // 👍 great 7 | sample({ 8 | clock: formSubmit, 9 | source: $formData, 10 | fn: prepareData, 11 | target: sendFormToServerFx, 12 | }); 13 | 14 | // 👎 weird 15 | sample({ 16 | fn: prepareData, 17 | target: sendFormToServerFx, 18 | clock: formSubmit, 19 | source: $formData, 20 | }); 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/scripts/prepare-presets.js: -------------------------------------------------------------------------------- 1 | import { writeFile } from "node:fs/promises"; 2 | 3 | import plugin from "../../index.js"; 4 | 5 | const presets = Object.entries(plugin.configs).map(([name, config]) => [ 6 | name, 7 | Object.entries(config.rules) 8 | .filter(([_, ruleValue]) => ruleValue !== "off") 9 | .map(([ruleName]) => ruleName.replace("effector/", "")), 10 | ]); 11 | 12 | for (const [presetName, rules] of presets) { 13 | const rulesListMD = rules.map(ruleLink).join("\n"); 14 | 15 | await writeFile(`docs/presets/__${presetName}.md`, rulesListMD); 16 | } 17 | 18 | // utils 19 | 20 | function ruleLink(name) { 21 | return `- [${name}](${ruleFile(name)})`; 22 | } 23 | 24 | function ruleFile(name) { 25 | return `/rules/${name}.md`; 26 | } 27 | -------------------------------------------------------------------------------- /docs/rules/no-forward.md: -------------------------------------------------------------------------------- 1 | # effector/no-forward 2 | 3 | Any `forward` call could be replaced with `sample` call. 4 | 5 | ```ts 6 | // 👎 could be replaced 7 | forward({ from: trigger, to: reaction }); 8 | 9 | // 👍 makes sense 10 | sample({ clock: trigger, target: reaction }); 11 | ``` 12 | 13 | Nice bonus: `sample` is extendable. You can add transformation by `fn` and filtering by `filter`. 14 | 15 | ```ts 16 | // 👎 could be replaced 17 | forward({ from: trigger.map((value) => value.length), to: reaction }); 18 | 19 | // 👍 makes sense 20 | sample({ clock: trigger, fn: (value) => value.length, target: reaction }); 21 | ``` 22 | 23 | 💡 Tip: [prefer-sample-over-forward-with-mapping](/rules/prefer-sample-over-forward-with-mapping.md) could be superseded by this rule. 24 | -------------------------------------------------------------------------------- /docs/rules/no-patronum-debug.md: -------------------------------------------------------------------------------- 1 | # effector/no-patronum-debug 2 | 3 | This rule will help to catch the forgotten `debug` and will automatically delete from code 4 | 5 | ```js 6 | // from 7 | import { createStore, createEvent } from "effector"; 8 | import { debug } from "patronum"; 9 | 10 | const increment = createEvent(); 11 | const $counter = createStore(0).on(increment, (count) => count + 1); 12 | 13 | debug($counter); 14 | ``` 15 | 16 | ```js 17 | // to 18 | import { createStore, createEvent } from "effector"; 19 | 20 | const increment = createEvent(); 21 | const $counter = createStore(0).on(increment, (count) => count + 1); 22 | ``` 23 | 24 | `debug` are considered to be intended for debugging units from effector and therefore not suitable for sending to the client. 25 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/examples/correct.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from "effector"; 2 | 3 | const oneFx = createEffect(); 4 | const twoFx = createEffect(); 5 | 6 | const onlyEffectsFx = createEffect(async () => { 7 | await oneFx(1); 8 | await twoFx(1); 9 | }); 10 | 11 | async function f1() {} 12 | async function f2() {} 13 | 14 | const onlyFunctionsFx = createEffect(async () => { 15 | await f1(); 16 | await f2(); 17 | }); 18 | 19 | async function justFunctionWithEffects() { 20 | await oneFx(1); 21 | await twoFx(1); 22 | } 23 | 24 | async function justFunctionWithFunctions() { 25 | await f1(); 26 | await f2(); 27 | } 28 | 29 | export { 30 | onlyEffectsFx, 31 | onlyFunctionsFx, 32 | justFunctionWithEffects, 33 | justFunctionWithFunctions, 34 | }; 35 | -------------------------------------------------------------------------------- /docs/rules/no-guard.md: -------------------------------------------------------------------------------- 1 | # effector/no-guard 2 | 3 | Any `guard` call could be replaced with `sample` call. 4 | 5 | ```ts 6 | // 👎 could be replaced 7 | guard({ clock: trigger, source: $data, filter: Boolean, target: reaction }); 8 | 9 | // 👍 makes sense 10 | sample({ clock: trigger, source: $data, filter: Boolean, target: reaction }); 11 | ``` 12 | 13 | Nice bonus: `sample` is extendable. You can add transformation by `fn`. 14 | 15 | ```ts 16 | // 👎 could be replaced 17 | guard({ 18 | clock: trigger, 19 | source: $data.map((data) => data.length), 20 | filter: Boolean, 21 | target: reaction, 22 | }); 23 | 24 | // 👍 makes sense 25 | sample({ 26 | clock: trigger, 27 | source: $data, 28 | filter: Boolean, 29 | fn: (data) => data.length, 30 | target: reaction, 31 | }); 32 | ``` 33 | -------------------------------------------------------------------------------- /utils/extract-config.js: -------------------------------------------------------------------------------- 1 | function extractConfig(fields, { node }) { 2 | const config = {}; 3 | 4 | if (isObject(node.arguments?.[0])) { 5 | extractConfigFromObject(config, fields, node.arguments?.[0]); 6 | } else if (isObject(node.arguments?.[1])) { 7 | extractConfigFromObject(config, fields, node.arguments?.[1]); 8 | if (fields.includes("clock")) { 9 | config.clock = { value: node.arguments?.[0] }; 10 | } 11 | } 12 | 13 | return config; 14 | } 15 | 16 | function isObject(node) { 17 | return Boolean(node?.properties); 18 | } 19 | 20 | function extractConfigFromObject(config, fields, node) { 21 | fields.forEach((field) => { 22 | config[field] = node?.properties?.find((n) => n.key?.name === field); 23 | }); 24 | } 25 | 26 | module.exports = { extractConfig }; 27 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/examples/correct-array-shape-via-useEvent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useEvent } from "effector-react"; 3 | import { createEvent, createEffect } from "effector"; 4 | 5 | const clicked = createEvent(); 6 | const mounted = createEvent(); 7 | const unmounted = createEvent(); 8 | const fetchFx = createEffect(() => {}); 9 | 10 | const Button: React.FC = () => { 11 | const [clickedEvent, mountedEvent, unmountedEvent, fetch] = useEvent([ 12 | clicked, 13 | mounted, 14 | unmounted, 15 | fetchFx, 16 | ]); 17 | 18 | React.useEffect(() => { 19 | mountedEvent(); 20 | fetch(); 21 | 22 | return () => { 23 | unmountedEvent(); 24 | }; 25 | }, []); 26 | 27 | return ; 28 | }; 29 | 30 | export { Button }; 31 | -------------------------------------------------------------------------------- /rules/no-getState/no-getState.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("eslint"); 2 | 3 | const rule = require("./no-getState"); 4 | 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 2020, 8 | sourceType: "module", 9 | }, 10 | }); 11 | 12 | ruleTester.run("effector/no-getState.test", rule, { 13 | valid: [ 14 | "sample({ source: $store, target: event });", 15 | "forward({ from: $store, to: event });", 16 | "guard({ clock: $store, filter: Boolean, target: event })", 17 | "someObject.getState();", 18 | ].map((code) => ({ code })), 19 | 20 | invalid: [ 21 | { 22 | code: "$store.getState();", 23 | errors: [ 24 | { 25 | messageId: "abusiveCall", 26 | type: "CallExpression", 27 | data: { storeName: "$store" }, 28 | }, 29 | ], 30 | }, 31 | ], 32 | }); 33 | -------------------------------------------------------------------------------- /rules/no-unnecessary-duplication/examples/correct-examples-issue-21.js: -------------------------------------------------------------------------------- 1 | import { guard } from "effector"; 2 | 3 | // Examples were found in production code-base with false-positive detection on 0.1.2 4 | // https://github.com/igorkamyshev/eslint-plugin-effector/issues/21 5 | 6 | guard({ 7 | source: someService.$aviaForm, 8 | clock: someService.submitted, 9 | filter: (state) => 10 | Boolean(state && isValidDirection(state.origin, state.destination)), 11 | target: drawAttention, 12 | }); 13 | 14 | guard({ 15 | source: promocodeModel.$verifyError, 16 | clock: [promocodeChanged, promocodeInputBlurred], 17 | filter: Boolean, 18 | target: promocodeModel.verifyErrorReset, 19 | }); 20 | 21 | guard({ 22 | source: [someService.$aviaForm, $backendFilters, currencyService.$currency], 23 | clock: [$backendFilters, publicApi.allPricesShown], 24 | filter: $isValid, 25 | target: requestPricesFx, 26 | }); 27 | -------------------------------------------------------------------------------- /docs/rules/no-watch.md: -------------------------------------------------------------------------------- 1 | # effector/no-watch 2 | 3 | Method `.watch` leads to imperative code. Try replacing it with operator (`sample`) or use the `target` parameter of the operator. 4 | 5 | > Caution! This rule only works on projects using TypeScript. 6 | 7 | ```ts 8 | const myFx = createEffect(); 9 | const myEvent = createEvent(); 10 | const $awesome = createStore(); 11 | 12 | // 👍 good solutions 13 | sample({ 14 | clock: myFx.finally, 15 | target: myEvent, 16 | }); 17 | 18 | sample({ 19 | clock: myEvent, 20 | filter: Boolean, 21 | target: myFx, 22 | }); 23 | 24 | sample({ 25 | clock: $awesome.updates, 26 | fn: identity, 27 | target: myEvent, 28 | }); 29 | 30 | // 👎 bad solutions 31 | myFx.finally.watch(myEvent); 32 | 33 | myEvent.watch((payload) => { 34 | if (Boolean(payload)) { 35 | myFx(payload); 36 | } 37 | }); 38 | 39 | $awesome.updates.watch((data) => { 40 | myEvent(identity(data)); 41 | }); 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: ESLint plugin 6 | text: Effector's family 7 | tagline: Enforcing best practices for Effector 8 | image: 9 | src: /comet.svg 10 | alt: ESLint plugin for Effector 11 | actions: 12 | - theme: brand 13 | text: All Rules 14 | link: /rules/ 15 | - theme: alt 16 | text: All Presets 17 | link: /presets/ 18 | 19 | features: 20 | - icon: ☄️ 21 | title: Recommended preset 22 | details: Config preset that is recommended for all projects using Effector 23 | link: /presets/recommended 24 | - icon: 💨 25 | title: Future preset 26 | details: Effector is evolving, this preset enforces best-practices for future releases of Effector 27 | link: /presets/future 28 | - icon: ⚛️ 29 | title: React preset 30 | details: This preset is recommended for projects that use React with Effector. 31 | link: /presets/react 32 | --- 33 | -------------------------------------------------------------------------------- /rules/keep-options-order/examples/correct-sample.js: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore, sample } from "effector"; 2 | 3 | const clock = createEvent(); 4 | const source = createEvent(); 5 | const filter = createStore(); 6 | const fn = () => null; 7 | const target = createEvent(); 8 | 9 | sample({ clock, source, filter, fn, target }); 10 | 11 | sample({ clock, source, filter, fn }); 12 | sample({ clock, source, filter, target }); 13 | sample({ clock, source, fn, target }); 14 | sample({ clock, filter, fn, target }); 15 | sample({ source, filter, fn, target }); 16 | 17 | sample({ clock, source, filter }); 18 | sample({ clock, source, fn }); 19 | sample({ clock, source, target }); 20 | 21 | sample({ clock, filter, fn }); 22 | sample({ clock, filter, target }); 23 | 24 | sample({ source, filter, fn }); 25 | sample({ source, filter, target }); 26 | 27 | sample({ filter, fn, target }); 28 | sample({ filter, fn }); 29 | 30 | sample({ filter, fn, greedy }); 31 | -------------------------------------------------------------------------------- /docs/scripts/prepare-toc.js: -------------------------------------------------------------------------------- 1 | import { readdir, writeFile } from "node:fs/promises"; 2 | import { join } from "node:path"; 3 | 4 | // rules 5 | const RULES_DIR = "docs/rules"; 6 | 7 | const rulesDocsDir = await readdir(RULES_DIR); 8 | 9 | const rulesListMD = rulesDocsDir.filter(onlyRule).map(listElement).join("\n"); 10 | 11 | await writeFile(join(RULES_DIR, "__index.md"), rulesListMD); 12 | 13 | // presets 14 | 15 | const PRESETS_DIR = "docs/presets"; 16 | 17 | const presetDocsDir = await readdir(PRESETS_DIR); 18 | 19 | const presetsListMD = presetDocsDir 20 | .filter(onlyRule) 21 | .map(listElement) 22 | .join("\n"); 23 | 24 | await writeFile(join(PRESETS_DIR, "__index.md"), presetsListMD); 25 | 26 | // utils 27 | 28 | function listElement(file) { 29 | return `- [${title(file)}](./${file})`; 30 | } 31 | 32 | function title(file) { 33 | return file.replace(".md", ""); 34 | } 35 | 36 | function onlyRule(file) { 37 | return file !== "index.md" && !file.startsWith("__"); 38 | } 39 | -------------------------------------------------------------------------------- /rules/no-getState/no-getState.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../utils/read-example"); 5 | const rule = require("./no-getState"); 6 | 7 | const ruleTester = new RuleTester({ 8 | languageOptions: { 9 | parserOptions: { 10 | projectService: true, 11 | }, 12 | }, 13 | }); 14 | 15 | const readExampleForTheRule = (name) => ({ 16 | code: readExample(__dirname, name), 17 | filename: join(__dirname, "examples", name), 18 | }); 19 | 20 | ruleTester.run("no-getState.ts.test", rule, { 21 | valid: ["correct.ts"].map(readExampleForTheRule), 22 | 23 | invalid: [ 24 | "incorrect-with-convential-name.ts", 25 | "incorrect-with-random-name.ts", 26 | "incorrect-with-nested-object.ts", 27 | ] 28 | .map(readExampleForTheRule) 29 | .map((result) => ({ 30 | ...result, 31 | errors: [ 32 | { 33 | messageId: "abusiveCall", 34 | type: "CallExpression", 35 | }, 36 | ], 37 | })), 38 | }); 39 | -------------------------------------------------------------------------------- /rules/no-useless-methods/examples/correct.ts: -------------------------------------------------------------------------------- 1 | import { sample, guard, createEvent } from "effector"; 2 | 3 | const trigger = createEvent(); 4 | const target = createEvent(); 5 | 6 | // with target 7 | sample({ clock: trigger, fn: Boolean, target }); 8 | sample({ source: trigger, fn: Boolean, target }); 9 | 10 | guard({ clock: trigger, filter: Boolean, target }); 11 | guard({ source: trigger, filter: Boolean, target }); 12 | 13 | // with simple assign 14 | const result1 = sample({ clock: trigger, fn: Boolean }); 15 | const result2 = sample({ source: trigger, fn: Boolean }); 16 | 17 | const result3 = guard({ clock: trigger, filter: Boolean }); 18 | const result4 = guard({ source: trigger, filter: Boolean }); 19 | 20 | // with complex assign 21 | 22 | const somplexResult = { 23 | target: guard({ source: trigger, filter: Boolean }), 24 | }; 25 | 26 | function createSomething() { 27 | const otherTrigger = createEvent(); 28 | 29 | return guard({ source: otherTrigger, filter: Boolean }); 30 | } 31 | 32 | const createBooleanGuard = (otherTrigger) => 33 | guard({ source: otherTrigger, filter: Boolean }); 34 | -------------------------------------------------------------------------------- /rules/require-pickup-in-persist/require-pickup-in-persist.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("eslint"); 2 | const { join } = require("path"); 3 | 4 | const { 5 | readExample, 6 | getCorrectExamples, 7 | getIncorrectExamples, 8 | } = require("../../utils/read-example"); 9 | 10 | const rule = require("./require-pickup-in-persist"); 11 | 12 | const ruleTester = new RuleTester({ 13 | parserOptions: { 14 | ecmaVersion: 2020, 15 | sourceType: "module", 16 | }, 17 | }); 18 | 19 | const readExampleForTheRule = (name) => ({ 20 | code: readExample(__dirname, name), 21 | filename: join(__dirname, "examples", name), 22 | }); 23 | 24 | ruleTester.run("effector/require-pickup-in-persist.js.test", rule, { 25 | valid: getCorrectExamples(__dirname).map(readExampleForTheRule), 26 | 27 | invalid: 28 | // Errors 29 | getIncorrectExamples(__dirname) 30 | .map(readExampleForTheRule) 31 | .map((result) => ({ 32 | ...result, 33 | errors: [ 34 | { 35 | messageId: "pickupMissing", 36 | type: "CallExpression", 37 | }, 38 | ], 39 | })), 40 | }); 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Igor Kamyshev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rules/no-watch/examples/correct.ts: -------------------------------------------------------------------------------- 1 | const event = { 2 | watch() { 3 | return {}; 4 | }, 5 | }; 6 | const effect = { 7 | done: { 8 | watch() { 9 | return {}; 10 | }, 11 | }, 12 | fail: { 13 | watch() { 14 | return {}; 15 | }, 16 | }, 17 | doneData: { 18 | watch() { 19 | return {}; 20 | }, 21 | }, 22 | failData: { 23 | watch() { 24 | return {}; 25 | }, 26 | }, 27 | finally: { 28 | watch() { 29 | return {}; 30 | }, 31 | }, 32 | watch() { 33 | return {}; 34 | }, 35 | }; 36 | const $ = { 37 | updates: { 38 | watch() { 39 | return {}; 40 | }, 41 | }, 42 | watch() { 43 | return {}; 44 | }, 45 | }; 46 | const sample = { 47 | watch() { 48 | return {}; 49 | }, 50 | }; 51 | const guard = { 52 | watch() { 53 | return {}; 54 | }, 55 | }; 56 | 57 | event.watch(); 58 | effect.done.watch(); 59 | effect.fail.watch(); 60 | effect.doneData.watch(); 61 | effect.failData.watch(); 62 | effect.finally.watch(); 63 | $.watch(); 64 | $.updates.watch(); 65 | sample.watch(); 66 | guard.watch(); 67 | 68 | export const T = () => true; 69 | -------------------------------------------------------------------------------- /utils/is.js: -------------------------------------------------------------------------------- 1 | const { ESLintUtils } = require("@typescript-eslint/utils"); 2 | 3 | const { nodeTypeIs } = require("./node-type-is"); 4 | const { namingOf } = require("./naming"); 5 | 6 | function isSomething({ isValidNaming, isTypeCorrect }) { 7 | return ({ node, context }) => { 8 | let parserServices; 9 | try { 10 | parserServices = ESLintUtils.getParserServices(context); 11 | } catch (e) { 12 | // no types info 13 | } 14 | 15 | if (parserServices?.program) { 16 | return isTypeCorrect({ node, context }); 17 | } 18 | 19 | return isValidNaming({ name: node?.name ?? node?.id?.name, context }); 20 | }; 21 | } 22 | 23 | const isStore = isSomething({ 24 | isTypeCorrect: nodeTypeIs.store, 25 | isValidNaming: namingOf.store.isValid, 26 | }); 27 | 28 | const isEffect = isSomething({ 29 | isTypeCorrect: nodeTypeIs.effect, 30 | isValidNaming: namingOf.effect.isValid, 31 | }); 32 | 33 | const is = { 34 | store: (opts) => isStore(opts), 35 | effect: (opts) => isEffect(opts), 36 | not: { store: (opts) => !isStore(opts), effect: (opts) => !isEffect(opts) }, 37 | }; 38 | 39 | module.exports = { is }; 40 | -------------------------------------------------------------------------------- /rules/no-duplicate-on/no-duplicate-on.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../utils/read-example"); 5 | const rule = require("./no-duplicate-on"); 6 | 7 | const ruleTester = new RuleTester({ 8 | languageOptions: { 9 | parserOptions: { 10 | projectService: true, 11 | }, 12 | }, 13 | }); 14 | 15 | const readExampleForTheRule = (name) => ({ 16 | code: readExample(__dirname, name), 17 | filename: join(__dirname, "examples", name), 18 | }); 19 | 20 | ruleTester.run("no-duplicate-on.ts.test", rule, { 21 | valid: [ 22 | "correct.ts", 23 | "correct-with-scopes.ts", 24 | "correct-with-nesting.ts", 25 | "correct-with-empty-on.ts", 26 | ].map(readExampleForTheRule), 27 | invalid: ["incorrect-with-invalid-naming.ts"] 28 | .map(readExampleForTheRule) 29 | .map((example) => ({ 30 | ...example, 31 | errors: [ 32 | { 33 | messageId: "duplicateOn", 34 | type: "CallExpression", 35 | data: { storeName: "counterStore", unitName: "inc" }, 36 | }, 37 | ], 38 | })), 39 | }); 40 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/prefer-useUnit.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { 5 | readExample, 6 | getCorrectExamples, 7 | getIncorrectExamples, 8 | } = require("../../utils/read-example"); 9 | 10 | const rule = require("./prefer-useUnit"); 11 | 12 | const ruleTester = new RuleTester({ 13 | languageOptions: { 14 | parserOptions: { 15 | projectService: true, 16 | }, 17 | }, 18 | }); 19 | 20 | const readExampleForTheRule = (name) => ({ 21 | code: readExample(__dirname, name), 22 | filename: join(__dirname, "examples", name), 23 | }); 24 | 25 | ruleTester.run("prefer-useUnit.ts.test", rule, { 26 | valid: getCorrectExamples(__dirname, { ext: ["tsx", "ts"] }).map( 27 | readExampleForTheRule 28 | ), 29 | 30 | invalid: 31 | // Errors 32 | getIncorrectExamples(__dirname, { ext: ["tsx", "ts"] }) 33 | .map(readExampleForTheRule) 34 | .map((result) => ({ 35 | ...result, 36 | errors: [ 37 | { 38 | messageId: "useUnitNeeded", 39 | type: "CallExpression", 40 | }, 41 | ], 42 | })), 43 | }); 44 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/prefer-useUnit.js.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("eslint"); 2 | const { join } = require("path"); 3 | 4 | const { 5 | readExample, 6 | getCorrectExamples, 7 | getIncorrectExamples, 8 | } = require("../../utils/read-example"); 9 | 10 | const rule = require("./prefer-useUnit"); 11 | 12 | const ruleTester = new RuleTester({ 13 | parserOptions: { 14 | ecmaVersion: 2020, 15 | sourceType: "module", 16 | ecmaFeatures: { jsx: true }, 17 | }, 18 | }); 19 | 20 | const readExampleForTheRule = (name) => ({ 21 | code: readExample(__dirname, name), 22 | filename: join(__dirname, "examples", name), 23 | }); 24 | 25 | ruleTester.run("effector/prefer-useUnit.js.test", rule, { 26 | valid: getCorrectExamples(__dirname, { ext: ["jsx", "jx"] }).map( 27 | readExampleForTheRule 28 | ), 29 | 30 | invalid: 31 | // Errors 32 | getIncorrectExamples(__dirname, { ext: ["jsx", "js"] }) 33 | .map(readExampleForTheRule) 34 | .map((result) => ({ 35 | ...result, 36 | errors: [ 37 | { 38 | messageId: "useUnitNeeded", 39 | type: "CallExpression", 40 | }, 41 | ], 42 | })), 43 | }); 44 | -------------------------------------------------------------------------------- /docs/rules/no-duplicate-clock-or-source-array-values.md: -------------------------------------------------------------------------------- 1 | # effector/no-duplicate-clock-or-source-array-values 2 | 3 | This rule forbids unit duplicates on `source` and `clock` with sample and guard methods. 4 | 5 | ```js 6 | // from 7 | import { createEvent, createStore, sample } from "effector"; 8 | import { createEffect } from "effector"; 9 | 10 | const currentOrderUpdated = createEvent(); 11 | const setUnloadDeliveryDateFx = createEffect(); 12 | 13 | const $store = createStore(null); 14 | 15 | sample({ 16 | clock: [ 17 | setUnloadDeliveryDateFx.doneData, 18 | setUnloadDeliveryDateFx.doneData, 19 | $store, 20 | ], 21 | filter: Boolean, 22 | target: currentOrderUpdated, 23 | }); 24 | ``` 25 | 26 | --- 27 | 28 | ```js 29 | // to 30 | import { createEvent, createStore, sample } from "effector"; 31 | import { createEffect } from "effector"; 32 | 33 | const currentOrderUpdated = createEvent(); 34 | const setUnloadDeliveryDateFx = createEffect(); 35 | 36 | const $store = createStore(null); 37 | 38 | sample({ 39 | clock: [ 40 | setUnloadDeliveryDateFx.doneData, // dublicate removed 41 | $store, 42 | ], 43 | filter: Boolean, 44 | target: currentOrderUpdated, 45 | }); 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/rules/strict-effect-handlers.md: -------------------------------------------------------------------------------- 1 | # effector/strict-effect-handlers 2 | 3 | [Related documentation](https://effector.dev/docs/api/effector/scope#imperative-effects-calls-with-scope) 4 | 5 | When effect calls another effects then it should call only effects, not common asynchronous functions and effect calls should have `await`: 6 | 7 | ```ts 8 | // 👍 effect without inner effects: 9 | const delayFx = createEffect(async () => { 10 | await new Promise((rs) => setTimeout(rs, 80)); 11 | }); 12 | ``` 13 | 14 | ```ts 15 | const authUserFx = createEffect(); 16 | const sendMessageFx = createEffect(); 17 | 18 | // 👍 effect with inner effects 19 | const sendWithAuthFx = createEffect(async () => { 20 | await authUserFx(); 21 | await delayFx(); 22 | await sendMessageFx(); 23 | }); 24 | ``` 25 | 26 | ```ts 27 | // 👎 effect with inner effects and common async functions 28 | 29 | const sendWithAuthFx = createEffect(async () => { 30 | await authUserFx(); 31 | //WRONG! wrap that in effect 32 | await new Promise((rs) => setTimeout(rs, 80)); 33 | //context lost 34 | await sendMessageFx(); 35 | }); 36 | ``` 37 | 38 | So, any effect might either call another effects or perform some asynchronous computations but not both. 39 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/mandatory-scope-binding.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { 5 | readExample, 6 | getCorrectExamples, 7 | getIncorrectExamples, 8 | } = require("../../utils/read-example"); 9 | 10 | const rule = require("./mandatory-scope-binding"); 11 | 12 | const ruleTester = new RuleTester({ 13 | languageOptions: { 14 | parserOptions: { 15 | projectService: true, 16 | }, 17 | }, 18 | }); 19 | 20 | const readExampleForTheRule = (name) => ({ 21 | code: readExample(__dirname, name), 22 | filename: join(__dirname, "examples", name), 23 | }); 24 | 25 | ruleTester.run("mandatory-scope-binding.ts.test", rule, { 26 | valid: getCorrectExamples(__dirname, { ext: ["tsx", "ts"] }).map( 27 | readExampleForTheRule 28 | ), 29 | 30 | invalid: 31 | // Errors 32 | getIncorrectExamples(__dirname, { ext: ["tsx", "ts"] }) 33 | .map(readExampleForTheRule) 34 | .map((result) => ({ 35 | ...result, 36 | errors: [ 37 | { 38 | messageId: "useEventNeeded", 39 | type: "Identifier", 40 | }, 41 | ], 42 | })), 43 | }); 44 | -------------------------------------------------------------------------------- /docs/rules/enforce-store-naming-convention.md: -------------------------------------------------------------------------------- 1 | # effector/enforce-store-naming-convention 2 | 3 | Enforcing naming conventions helps keep the codebase consistent, and reduces overhead when thinking about how to name a variable with store. Depending on the configuration your stores should be distinguished by a prefix or a postfix $. Enforces prefix convention by default. 4 | 5 | ## Prefix convention 6 | 7 | When configured as: 8 | 9 | ```js 10 | module.exports = { 11 | rules: { 12 | "effector/enforce-store-naming-convention": "error", 13 | }, 14 | }; 15 | ``` 16 | 17 | Prefix convention will be enforced: 18 | 19 | ```ts 20 | // 👍 nice name 21 | const $name = createStore(null); 22 | 23 | // 👎 bad name 24 | const name = createStore(null); 25 | ``` 26 | 27 | ## Postfix convention 28 | 29 | When configured as: 30 | 31 | ```js 32 | module.exports = { 33 | rules: { 34 | "effector/enforce-store-naming-convention": "error", 35 | }, 36 | settings: { 37 | effector: { 38 | storeNameConvention: "postfix", 39 | }, 40 | }, 41 | }; 42 | ``` 43 | 44 | Postfix convention will be enforced: 45 | 46 | ```ts 47 | // 👍 nice name 48 | const name$ = createStore(null); 49 | 50 | // 👎 bad name 51 | const name = createStrore(null); 52 | ``` 53 | -------------------------------------------------------------------------------- /utils/get-corrected-store-name.js: -------------------------------------------------------------------------------- 1 | const { getStoreNameConvention } = require("./get-store-name-convention"); 2 | 3 | function getCorrectedStoreName(storeName, context) { 4 | const storeNameConvention = getStoreNameConvention(context); 5 | 6 | // handle edge case 7 | if (storeName.startsWith("$") && storeName.endsWith("$")) { 8 | const storeNameWithoutConvention = trimByPattern(storeName, "$"); 9 | return formatStoreName(storeNameWithoutConvention, storeNameConvention); 10 | } 11 | 12 | const correctedStoreName = formatStoreName(storeName, storeNameConvention); 13 | 14 | return correctedStoreName; 15 | } 16 | 17 | function formatStoreName(storeName, convention) { 18 | return convention === "prefix" ? `$${storeName}` : `${storeName}$`; 19 | } 20 | 21 | function trimByPattern(s, template) { 22 | let l = 0, 23 | r = s.length - 1; 24 | 25 | while (l <= r) { 26 | const head = s[l]; 27 | const tail = s[r]; 28 | 29 | if (head === template) { 30 | l++; 31 | } 32 | 33 | if (tail === template) { 34 | r--; 35 | } 36 | 37 | if (head !== template && tail !== template) { 38 | return s.slice(l, r + 1); 39 | } 40 | } 41 | 42 | return s; 43 | } 44 | 45 | module.exports = { getCorrectedStoreName }; 46 | -------------------------------------------------------------------------------- /utils/naming.js: -------------------------------------------------------------------------------- 1 | const { getStoreNameConvention } = require("./get-store-name-convention"); 2 | 3 | function isEffectNameValid({ name }) { 4 | return Boolean(name?.endsWith("Fx")); 5 | } 6 | 7 | function isGateNameValid({ name }) { 8 | const [firstChar] = name.split(""); 9 | 10 | return Boolean(firstChar?.toUpperCase() === firstChar); 11 | } 12 | 13 | function isStoreNameValid({ name, context }) { 14 | const storeNameConvention = getStoreNameConvention(context); 15 | 16 | // validate edge case 17 | if (name?.length > 1 && name?.startsWith("$") && name?.endsWith("$")) { 18 | return false; 19 | } 20 | 21 | if (storeNameConvention === "prefix" && name?.startsWith("$")) { 22 | return true; 23 | } 24 | 25 | if (storeNameConvention === "postfix" && name?.endsWith("$")) { 26 | return true; 27 | } 28 | 29 | return false; 30 | } 31 | 32 | const namingOf = { 33 | effect: { 34 | isValid: (opts) => isEffectNameValid(opts), 35 | isInvalid: (opts) => !isEffectNameValid(opts), 36 | }, 37 | store: { 38 | isValid: (opts) => isStoreNameValid(opts), 39 | isInvalid: (opts) => !isStoreNameValid(opts), 40 | }, 41 | gate: { 42 | isValid: (opts) => isGateNameValid(opts), 43 | isInvalid: (opts) => !isGateNameValid(opts), 44 | }, 45 | }; 46 | 47 | module.exports = { namingOf }; 48 | -------------------------------------------------------------------------------- /rules/enforce-gate-naming-convention/enforce-gate-naming-convention.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../utils/read-example"); 5 | 6 | const rule = require("./enforce-gate-naming-convention"); 7 | 8 | const ruleTester = new RuleTester({ 9 | languageOptions: { 10 | parserOptions: { 11 | projectService: true, 12 | }, 13 | }, 14 | }); 15 | 16 | const readExampleForTheRule = (name) => ({ 17 | code: readExample(__dirname, name), 18 | filename: join(__dirname, "examples", name), 19 | }); 20 | 21 | ruleTester.run("enforce-gate-naming-convention.ts.test", rule, { 22 | valid: ["correct-gate-naming.ts"].map(readExampleForTheRule), 23 | 24 | invalid: [ 25 | // Errors 26 | ...["incorrect-gate-naming.ts"] 27 | .map(readExampleForTheRule) 28 | .map((result) => ({ 29 | ...result, 30 | errors: [ 31 | { 32 | messageId: "invalidName", 33 | type: "VariableDeclarator", 34 | suggestions: [ 35 | { 36 | messageId: "renameGate", 37 | output: result.code.replace("justGate", "JustGate"), 38 | }, 39 | ], 40 | }, 41 | ], 42 | })), 43 | ], 44 | }); 45 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/enforce-effect-naming-convention.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../utils/read-example"); 5 | 6 | const rule = require("./enforce-effect-naming-convention"); 7 | 8 | const ruleTester = new RuleTester({ 9 | languageOptions: { 10 | parserOptions: { 11 | projectService: true, 12 | }, 13 | }, 14 | }); 15 | 16 | const readExampleForTheRule = (name) => ({ 17 | code: readExample(__dirname, name), 18 | filename: join(__dirname, "examples", name), 19 | }); 20 | 21 | ruleTester.run("enforce-effect-naming-convention.ts.test", rule, { 22 | valid: ["correct-effect-naming.ts"].map(readExampleForTheRule), 23 | 24 | invalid: [ 25 | // Errors 26 | ...["incorrect-effect-naming.ts"] 27 | .map(readExampleForTheRule) 28 | .map((result) => ({ 29 | ...result, 30 | errors: [ 31 | { 32 | messageId: "invalidName", 33 | type: "VariableDeclarator", 34 | suggestions: [ 35 | { 36 | messageId: "renameEffect", 37 | output: result.code.replace("justEffect", "justEffectFx"), 38 | }, 39 | ], 40 | }, 41 | ], 42 | })), 43 | ], 44 | }); 45 | -------------------------------------------------------------------------------- /rules/no-ambiguity-target/examples/correct-examples-issue-49.ts: -------------------------------------------------------------------------------- 1 | import { createEffect, createEvent, createStore, guard } from "effector"; 2 | 3 | // Examples were found in production code-base with false-positive detection on 0.3.0 4 | // https://github.com/effector/eslint-plugin/issues/49 5 | 6 | interface IFilters { 7 | first: string; 8 | second: string; 9 | } 10 | 11 | interface IResponseData { 12 | data: string; 13 | } 14 | 15 | const createFactory = () => { 16 | const { applyFilters, getDataFx } = createHandlers(); 17 | 18 | const $data = createStore({ data: "" }); 19 | 20 | const $filters = createStore({ 21 | first: "", 22 | second: "", 23 | }); 24 | 25 | const $hasValidFilters = $filters.map((filters) => 26 | Object.values(filters).every(Boolean) 27 | ); 28 | 29 | guard({ 30 | clock: applyFilters, 31 | source: $filters, 32 | filter: $hasValidFilters, 33 | target: getDataFx, 34 | }); 35 | 36 | return { 37 | $data, 38 | $filters, 39 | $hasValidFilters, 40 | applyFilters, 41 | getDataFx, 42 | }; 43 | }; 44 | 45 | const createHandlers = () => { 46 | const applyFilters = createEvent(); 47 | 48 | const getDataFx = createEffect(); 49 | 50 | return { 51 | applyFilters, 52 | getDataFx, 53 | }; 54 | }; 55 | 56 | export { createFactory }; 57 | -------------------------------------------------------------------------------- /docs/rules/prefer-useUnit.md: -------------------------------------------------------------------------------- 1 | # effector/prefer-useUnit 2 | 3 | `useUnit` is a brand-new hook which allows you to bind stores, events, and effects to React render cycle. 4 | 5 | It is preferable than `useStore`/`useEvents`: 6 | 7 | - it enables store updates batching, which can significantly increase React performance 8 | - it does not have name collision with future React `useEvent` internal hook 9 | - it has more explicit naming, old `useEvent` accepts events and effect, so its name is a bit implicit, new `useUnit` accepts any unit 10 | 11 | You can replace `useStore`/`useEvent` by `useUnit` as is or replace all old hooks with only one `useUnit` 12 | 13 | ```tsx 14 | // 👎 old approach 15 | function OldComponent() { 16 | const value = useStore($value); 17 | const eventFn = useEvent(event); 18 | const effectFn = useEvent(effectFx); 19 | 20 | return ( 21 | <> 22 |

{value}

23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | // 👍 new approach 30 | function NewComponent() { 31 | const [value, eventFn, effectFn] = useUnit($value, event, effectFx); 32 | 33 | return ( 34 | <> 35 |

{value}

36 | 37 | 38 | 39 | ); 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /plugin.test.js: -------------------------------------------------------------------------------- 1 | const { readdir } = require("fs/promises"); 2 | const { join } = require("path"); 3 | 4 | const plugin = require("./index"); 5 | const { createLinkToRule } = require("./utils/create-link-to-rule"); 6 | 7 | describe("plugin", () => { 8 | test("any rule should have a link to docs", () => { 9 | const { rules } = plugin; 10 | 11 | Object.entries(rules).forEach(([name, rule]) => { 12 | expect(rule.meta.docs.url).toBe(createLinkToRule(name)); 13 | }); 14 | }); 15 | 16 | test("any rule should be exported", async () => { 17 | const rulesDirContent = await readdir(join(__dirname, "rules")); 18 | 19 | const allRules = rulesDirContent 20 | .filter((fileName) => !fileName.includes(".json")) 21 | .sort(); 22 | 23 | const exportedRules = Object.entries(plugin.rules) 24 | .map(([name]) => name) 25 | .sort(); 26 | 27 | expect(exportedRules).toEqual(allRules); 28 | }); 29 | 30 | test("any config should be exported", async () => { 31 | const configsDirContent = await readdir(join(__dirname, "config")); 32 | 33 | const allConfigs = configsDirContent 34 | .map((fileName) => fileName.replace(".js", "")) 35 | .sort(); 36 | 37 | const exportedConfigs = Object.entries(plugin.configs) 38 | .map(([name]) => name) 39 | .sort(); 40 | 41 | expect(exportedConfigs).toEqual(allConfigs); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /rules/no-getState/no-getState.js: -------------------------------------------------------------------------------- 1 | const { 2 | traverseNestedObjectNode, 3 | } = require("../../utils/traverse-nested-object-node"); 4 | const { createLinkToRule } = require("../../utils/create-link-to-rule"); 5 | const { is } = require("../../utils/is"); 6 | 7 | module.exports = { 8 | meta: { 9 | type: "problem", 10 | docs: { 11 | description: "Forbids `.getState` calls on any Effector store", 12 | category: "Quality", 13 | recommended: true, 14 | url: createLinkToRule("no-getState"), 15 | }, 16 | messages: { 17 | abusiveCall: 18 | "Method `.getState` called on store `{{ storeName }}` can lead to race conditions. Replace it with `sample` or `guard`.", 19 | }, 20 | schema: [], 21 | }, 22 | create(context) { 23 | return { 24 | 'CallExpression[callee.property.name="getState"]'(node) { 25 | const storeNode = traverseNestedObjectNode(node.callee?.object); 26 | 27 | const isEffectorStore = is.store({ 28 | context, 29 | node: storeNode, 30 | }); 31 | 32 | if (!isEffectorStore) { 33 | return; 34 | } 35 | 36 | reportGetStateCall({ context, node, storeName: storeNode.name }); 37 | }, 38 | }; 39 | }, 40 | }; 41 | 42 | function reportGetStateCall({ context, node, storeName }) { 43 | context.report({ 44 | node, 45 | messageId: "abusiveCall", 46 | data: { 47 | storeName, 48 | }, 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /rules/no-ambiguity-target/no-ambiguity-target.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../utils/read-example"); 5 | const rule = require("./no-ambiguity-target"); 6 | 7 | const ruleTester = new RuleTester({ 8 | languageOptions: { 9 | parserOptions: { 10 | projectService: true, 11 | }, 12 | }, 13 | }); 14 | 15 | const readExampleForTheRule = (name) => ({ 16 | code: readExample(__dirname, name), 17 | filename: join(__dirname, "examples", name), 18 | }); 19 | 20 | ruleTester.run("no-ambiguity-target.ts.test", rule, { 21 | valid: ["correct.ts", "correct-examples-issue-49.ts"].map( 22 | readExampleForTheRule 23 | ), 24 | 25 | invalid: [ 26 | ...["incorrect-sample.ts"].map(readExampleForTheRule).map((result) => ({ 27 | ...result, 28 | errors: [ 29 | { 30 | messageId: "ambiguityTarget", 31 | type: "CallExpression", 32 | data: { methodName: "sample" }, 33 | }, 34 | ], 35 | })), 36 | ...["incorrect-guard.ts", "incorrect-guard-nested.ts"] 37 | .map(readExampleForTheRule) 38 | .map((result) => ({ 39 | ...result, 40 | errors: [ 41 | { 42 | messageId: "ambiguityTarget", 43 | type: "CallExpression", 44 | data: { methodName: "guard" }, 45 | }, 46 | ], 47 | })), 48 | ], 49 | }); 50 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/prefix/enforce-store-naming-convention-prefix.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../../utils/read-example"); 5 | 6 | const rule = require("../enforce-store-naming-convention"); 7 | 8 | const ruleTester = new RuleTester({ 9 | languageOptions: { 10 | parserOptions: { 11 | projectService: true, 12 | }, 13 | }, 14 | }); 15 | 16 | const readExampleForTheRule = (name) => ({ 17 | code: readExample(__dirname, name), 18 | filename: join(__dirname, "examples", name), 19 | }); 20 | 21 | ruleTester.run( 22 | "enforce-store-naming-convention-prefix.ts.test", 23 | rule, 24 | { 25 | valid: ["correct-store-naming.ts", "correct-issue-139.ts"].map( 26 | readExampleForTheRule 27 | ), 28 | 29 | invalid: [ 30 | // Errors 31 | ...["incorrect-store-naming.ts"] 32 | .map(readExampleForTheRule) 33 | .map((result) => ({ 34 | ...result, 35 | errors: [ 36 | { 37 | messageId: "invalidName", 38 | type: "VariableDeclarator", 39 | suggestions: [ 40 | { 41 | messageId: "renameStore", 42 | output: result.code.replace("justStore", "$justStore"), 43 | }, 44 | ], 45 | }, 46 | ], 47 | })), 48 | ], 49 | } 50 | ); 51 | -------------------------------------------------------------------------------- /rules/no-useless-methods/no-useless-methods.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../utils/read-example"); 5 | const rule = require("./no-useless-methods"); 6 | 7 | const ruleTester = new RuleTester({ 8 | languageOptions: { 9 | parserOptions: { 10 | projectService: true, 11 | }, 12 | }, 13 | }); 14 | 15 | const readExampleForTheRule = (name) => ({ 16 | code: readExample(__dirname, name), 17 | filename: join(__dirname, "examples", name), 18 | }); 19 | 20 | ruleTester.run("no-useless-methods.ts.test", rule, { 21 | valid: ["correct.ts", "correct-nested.ts"].map(readExampleForTheRule), 22 | 23 | invalid: [ 24 | ...["incorrect-sample-clock.ts", "incorrect-sample-source.ts"] 25 | .map(readExampleForTheRule) 26 | .map((result) => ({ 27 | ...result, 28 | errors: [ 29 | { 30 | messageId: "uselessMethod", 31 | type: "CallExpression", 32 | data: { methodName: "sample" }, 33 | }, 34 | ], 35 | })), 36 | ...["incorrect-guard-clock.ts", "incorrect-guard-source.ts"] 37 | .map(readExampleForTheRule) 38 | .map((result) => ({ 39 | ...result, 40 | errors: [ 41 | { 42 | messageId: "uselessMethod", 43 | type: "CallExpression", 44 | data: { methodName: "guard" }, 45 | }, 46 | ], 47 | })), 48 | ], 49 | }); 50 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/enforce-store-naming-convention-postfix.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../../utils/read-example"); 5 | 6 | const rule = require("../enforce-store-naming-convention"); 7 | 8 | const ruleTester = new RuleTester({ 9 | languageOptions: { 10 | parserOptions: { 11 | projectService: true, 12 | }, 13 | }, 14 | }); 15 | 16 | const readExampleForTheRule = (name) => ({ 17 | code: readExample(__dirname, name), 18 | filename: join(__dirname, "examples", name), 19 | settings: { 20 | effector: { 21 | storeNameConvention: "postfix", 22 | }, 23 | }, 24 | }); 25 | 26 | ruleTester.run( 27 | "enforce-store-naming-convention-postfix.ts.test", 28 | rule, 29 | { 30 | valid: ["correct-store-naming.ts"].map(readExampleForTheRule), 31 | 32 | invalid: [ 33 | // Errors 34 | ...["incorrect-store-naming.ts"] 35 | .map(readExampleForTheRule) 36 | .map((result) => ({ 37 | ...result, 38 | errors: [ 39 | { 40 | messageId: "invalidName", 41 | type: "VariableDeclarator", 42 | suggestions: [ 43 | { 44 | messageId: "renameStore", 45 | output: result.code.replace("justStore", "justStore$"), 46 | }, 47 | ], 48 | }, 49 | ], 50 | })), 51 | ], 52 | } 53 | ); 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-effector 2 | 3 | Enforcing best practices for [Effector](http://effector.dev/). Documentation available at [eslint.effector.dev](https://eslint.effector.dev/). 4 | 5 | > This plugin uses TypeScript for more precise results, but JavaScript is supported too. 6 | 7 | ## Installation 8 | 9 | Install [ESLint](http://eslint.org) and `eslint-plugin-effector`: 10 | 11 | ### pnpm 12 | 13 | ``` 14 | $ pnpm install --dev eslint 15 | $ pnpm install --dev eslint-plugin-effector 16 | ``` 17 | 18 | ### yarn 19 | 20 | ``` 21 | $ yarn add --dev eslint 22 | $ yarn add --dev eslint-plugin-effector 23 | ``` 24 | 25 | ### npm 26 | 27 | ``` 28 | $ npm install --dev eslint 29 | $ npm install --dev eslint-plugin-effector 30 | ``` 31 | 32 | ## Usage 33 | 34 | Add `effector` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: 35 | 36 | ```json 37 | { 38 | "plugins": ["effector"], 39 | "extends": ["plugin:effector/recommended", "plugin:effector/scope"] 40 | } 41 | ``` 42 | 43 | Read more detailed docs on [eslint.effector.dev](https://eslint.effector.dev/) 44 | 45 | ## Maintenance 46 | 47 | ### Release flow 48 | 49 | 1. Bump `version` in [package.json](package.json) 50 | 2. Fill [CHANGELOG.md](CHANGELOG.md) 51 | 3. Commit changes by `git commit -m "Release X.X.X"` 52 | 4. Create git tag for release by `git tag -a vX.X.X -m "vX.X.X"` 53 | 5. Push changes to remote by `git push --follow-tags` 54 | 6. Release package to registry by `pnpm clean-publish` 55 | 7. Fill release page with changelog on GitHub 56 | -------------------------------------------------------------------------------- /rules/require-pickup-in-persist/require-pickup-in-persist.js: -------------------------------------------------------------------------------- 1 | const { createLinkToRule } = require("../../utils/create-link-to-rule"); 2 | 3 | module.exports = { 4 | meta: { 5 | type: "problem", 6 | docs: { 7 | category: "Quality", 8 | url: createLinkToRule("require-pickup-in-persist"), 9 | }, 10 | messages: { 11 | pickupMissing: 12 | "This `persist` call does not specify a `pickup` event that is required for scoped usage of `effector-storage`.", 13 | }, 14 | schema: [], 15 | }, 16 | create(context) { 17 | const pickupImports = new Set(); 18 | 19 | /** 20 | * Finds `effector-storage` packages, scoped and unscoped, including 21 | * contents of these packages. See examples for a full list. 22 | */ 23 | const PACKAGE_NAME = /^@?effector-storage(\u002F[\w-]+)*$/; 24 | 25 | const declarationSelector = `ImportDeclaration[source.value=${PACKAGE_NAME}]`; 26 | const persistImportSelector = `ImportSpecifier[imported.name="persist"]`; 27 | 28 | const configSelector = `[arguments.length=1][arguments.0.type="ObjectExpression"]`; 29 | const callSelector = `[callee.type="Identifier"]`; 30 | 31 | return { 32 | [`${declarationSelector} > ${persistImportSelector}`](node) { 33 | pickupImports.add(node.local.name); 34 | }, 35 | [`CallExpression${configSelector}${callSelector}`](node) { 36 | if (!pickupImports.has(node.callee.name)) return; 37 | 38 | const config = node.arguments[0]; 39 | 40 | if (config.properties.some((prop) => prop.key?.name === "pickup")) 41 | return; 42 | 43 | context.report({ node, messageId: "pickupMissing" }); 44 | }, 45 | }; 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /utils/read-example.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require("fs"); 2 | const { join } = require("path"); 3 | const glob = require("glob"); 4 | 5 | function readExample(dirname, exampleName) { 6 | return readFileSync(join(dirname, "examples", exampleName)).toString(); 7 | } 8 | 9 | function getCorrectExamples(dirname, config = {}) { 10 | const { ext, namesOnly = true } = config; 11 | const pattern = `correct-*.${resolveExtension(ext)}`; 12 | const correct = glob.sync(join(dirname, "examples", pattern)); 13 | 14 | let result = correct; 15 | 16 | if (namesOnly) { 17 | result = result.map((path) => { 18 | const rightSlashIdx = path.lastIndexOf("/"); 19 | 20 | return path.slice(rightSlashIdx + 1); 21 | }); 22 | } 23 | 24 | return result; 25 | } 26 | 27 | function resolveExtension(ext) { 28 | const DEFAULT_EXT = "js"; 29 | 30 | if (Array.isArray(ext)) { 31 | if (ext.length === 0) { 32 | return DEFAULT_EXT; 33 | } 34 | 35 | if (ext.length === 1) { 36 | return ext[0]; 37 | } 38 | 39 | return `{${ext.join(",")}}`; 40 | } 41 | 42 | return ext ?? DEFAULT_EXT; 43 | } 44 | 45 | function getIncorrectExamples(dirname, config = {}) { 46 | const { ext, namesOnly = true } = config; 47 | const pattern = `incorrect-*.${resolveExtension(ext)}`; 48 | const incorrect = glob.sync(join(dirname, "examples", pattern)); 49 | 50 | let result = incorrect; 51 | 52 | if (namesOnly) { 53 | result = result.map((path) => { 54 | const rightSlashIdx = path.lastIndexOf("/"); 55 | 56 | return path.slice(rightSlashIdx + 1); 57 | }); 58 | } 59 | 60 | return result; 61 | } 62 | 63 | module.exports = { readExample, getCorrectExamples, getIncorrectExamples }; 64 | -------------------------------------------------------------------------------- /rules/prefer-useUnit/prefer-useUnit.js: -------------------------------------------------------------------------------- 1 | const { createLinkToRule } = require("../../utils/create-link-to-rule"); 2 | const { extractImportedFrom } = require("../../utils/extract-imported-from"); 3 | 4 | module.exports = { 5 | meta: { 6 | type: "problem", 7 | docs: { 8 | description: 9 | "Suggests to replace old hooks `useStore`/`useEvent` by the new one `useUnit`", 10 | category: "Quality", 11 | recommended: true, 12 | url: createLinkToRule("prefer-useUnit"), 13 | }, 14 | messages: { 15 | useUnitNeeded: "`{{ hookName }}` could be replaced by `useUnit`", 16 | }, 17 | schema: [], 18 | }, 19 | create(context) { 20 | const importedFromEffectorReact = new Map(); 21 | 22 | return { 23 | ImportDeclaration(node) { 24 | extractImportedFrom({ 25 | importMap: importedFromEffectorReact, 26 | packageName: "effector-react", 27 | node, 28 | }); 29 | }, 30 | CallExpression(node) { 31 | const OLD_HOOKS = ["useStore", "useEvent"]; 32 | const NEW_HOOK = ["useUnit"]; 33 | 34 | for (const oldHookName of OLD_HOOKS) { 35 | const localOldHookName = importedFromEffectorReact.get(oldHookName); 36 | if (!localOldHookName) { 37 | continue; 38 | } 39 | 40 | const isOldHook = node.callee.name === localOldHookName; 41 | if (!isOldHook) { 42 | continue; 43 | } 44 | 45 | context.report({ 46 | node, 47 | messageId: "useUnitNeeded", 48 | data: { 49 | hookName: oldHookName, 50 | }, 51 | }); 52 | } 53 | }, 54 | }; 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /docs/.vitepress/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lang: "en-US", 3 | title: "ESLint plugin for Effector", 4 | description: "Enforcing best practices for Effector", 5 | lastUpdated: true, 6 | head: [ 7 | ["link", { rel: "icon", href: "/favicon.ico", sizes: "any" }], 8 | ["link", { rel: "icon", href: "/comet.svg", type: "image/svg+xml" }], 9 | ["link", { rel: "apple-touch-icon", href: "/apple-touch-icon.png" }], 10 | ["link", { rel: "manifest", href: "/manifest.webmanifest" }], 11 | ], 12 | themeConfig: { 13 | siteTitle: "ESLint plugin for Effector", 14 | logo: "/comet.svg", 15 | footer: { 16 | message: "Released under the MIT License.", 17 | }, 18 | socialLinks: [ 19 | { icon: "github", link: "https://github.com/effector/eslint-plugin" }, 20 | { icon: "twitter", link: "https://twitter.com/effectorjs" }, 21 | ], 22 | algolia: { 23 | appId: "NW4641ANOK", 24 | apiKey: "82a2fcdc603a1de18478e1c461450168", 25 | indexName: "eslint-effector", 26 | }, 27 | editLink: { 28 | pattern: 29 | "https://github.com/effector/eslint-plugin/edit/master/docs/:path", 30 | }, 31 | nav: [ 32 | { 33 | text: "Presets", 34 | link: "/presets/", 35 | activeMatch: "^/presets/", 36 | }, 37 | { 38 | text: "Rules", 39 | link: "/rules/", 40 | activeMatch: "^/rules/", 41 | }, 42 | { 43 | text: "More", 44 | items: [ 45 | { 46 | text: "Changelog", 47 | link: "/changelog", 48 | }, 49 | { 50 | text: "Effector", 51 | link: "https://effector.dev", 52 | }, 53 | ], 54 | }, 55 | ], 56 | sidebar: {}, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-effector", 3 | "version": "0.16.0", 4 | "description": "Enforcing best practices for Effector", 5 | "keywords": [ 6 | "eslint", 7 | "eslint-plugin", 8 | "eslintplugin", 9 | "effector" 10 | ], 11 | "repository": "effector/eslint-plugin", 12 | "main": "index.js", 13 | "author": "Igor Kamyshev ", 14 | "license": "MIT", 15 | "packageManager": "pnpm@8.10.5", 16 | "scripts": { 17 | "test": "jest", 18 | "docs:prepare": "node ./docs/scripts/prepare-toc.js && node ./docs/scripts/prepare-presets.js && node ./docs/scripts/prepare-changelog.js", 19 | "docs:build": "pnpm docs:prepare && vitepress build docs", 20 | "docs:dev": "pnpm docs:prepare && vitepress dev docs" 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "engines": { 26 | "node": ">=16 <25" 27 | }, 28 | "devDependencies": { 29 | "@types/react": "^17.0.37", 30 | "@typescript-eslint/parser": "^8.0.0", 31 | "@typescript-eslint/rule-tester": "^8.0.0", 32 | "@typescript-eslint/utils": "^8.0.0", 33 | "clean-publish": "^3.2.0", 34 | "effector": "^23", 35 | "effector-react": "^23", 36 | "eslint": "^8.57.0", 37 | "glob": "^8.0.3", 38 | "jest": "^29.0.0", 39 | "nano-staged": "^0.5.0", 40 | "react": "^17.0.2", 41 | "simple-git-hooks": "^2.7.0", 42 | "typescript": "^5.0.0", 43 | "vitepress": "1.0.0-alpha.65", 44 | "vue": "^3.2.45" 45 | }, 46 | "peerDependencies": { 47 | "effector": "^23", 48 | "eslint": "^8.57.0 || ^9.0.0" 49 | }, 50 | "dependencies": { 51 | "prettier": "^2.3.2" 52 | }, 53 | "simple-git-hooks": { 54 | "pre-commit": "pnpm nano-staged" 55 | }, 56 | "nano-staged": { 57 | "*.{js,ts,md}": "prettier --write" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /rules/no-watch/no-watch.js: -------------------------------------------------------------------------------- 1 | const { ESLintUtils } = require("@typescript-eslint/utils"); 2 | 3 | const { 4 | traverseNestedObjectNode, 5 | } = require("../../utils/traverse-nested-object-node"); 6 | const { createLinkToRule } = require("../../utils/create-link-to-rule"); 7 | const { nodeTypeIs } = require("../../utils/node-type-is"); 8 | 9 | module.exports = { 10 | meta: { 11 | type: "suggestion", 12 | docs: { 13 | description: "Avoid `.watch` calls on any Effector unit or operator", 14 | category: "Quality", 15 | recommended: true, 16 | url: createLinkToRule("no-watch"), 17 | }, 18 | messages: { 19 | abusiveCall: 20 | "Method `.watch` leads to imperative code. Try to replace it with operator (`sample`) or use the `target` parameter of the operator.", 21 | }, 22 | schema: [], 23 | }, 24 | create(context) { 25 | let parserServices; 26 | try { 27 | parserServices = ESLintUtils.getParserServices(context); 28 | } catch (err) { 29 | // no types information 30 | } 31 | 32 | if (!parserServices?.program) { 33 | // JavaScript-way https://github.com/effector/eslint-plugin/issues/48#issuecomment-931107829 34 | return {}; 35 | } 36 | 37 | return { 38 | 'CallExpression[callee.property.name="watch"]'(node) { 39 | const object = traverseNestedObjectNode(node.callee?.object); 40 | 41 | const isEffectorUnit = nodeTypeIs.unit({ 42 | node: object, 43 | context, 44 | }); 45 | 46 | if (!isEffectorUnit) { 47 | return; 48 | } 49 | 50 | reportWatchCall({ context, node }); 51 | }, 52 | }; 53 | }, 54 | }; 55 | 56 | function reportWatchCall({ context, node }) { 57 | context.report({ 58 | node, 59 | messageId: "abusiveCall", 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /rules/enforce-gate-naming-convention/enforce-gate-naming-convention.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("eslint"); 2 | 3 | const { readExample } = require("../../utils/read-example"); 4 | 5 | const rule = require("./enforce-gate-naming-convention"); 6 | 7 | const ruleTester = new RuleTester({ 8 | parserOptions: { 9 | ecmaVersion: 2020, 10 | sourceType: "module", 11 | }, 12 | }); 13 | 14 | const readExampleForTheRule = (name) => readExample(__dirname, name); 15 | 16 | ruleTester.run("effector/enforce-gate-naming-convention.test", rule, { 17 | valid: [ 18 | "correct-gate-naming.js", 19 | "correct-gate-naming-from-other-package.js", 20 | "correct-gate-naming-in-domain.js", 21 | ] 22 | .map(readExampleForTheRule) 23 | .map((code) => ({ code })), 24 | 25 | invalid: [ 26 | // Errors 27 | ...[ 28 | "incorrect-createGate.js", 29 | "incorrect-createGate-alias.js", 30 | "incorrect-createGate-in-domain.js", 31 | ] 32 | .map(readExampleForTheRule) 33 | .map((code) => ({ 34 | code, 35 | errors: [ 36 | { 37 | messageId: "invalidName", 38 | type: "VariableDeclarator", 39 | }, 40 | ], 41 | })), 42 | // Suggestions 43 | { 44 | code: ` 45 | import {createGate} from 'effector-react'; 46 | const someGate = createGate(); 47 | `, 48 | errors: [ 49 | { 50 | messageId: "invalidName", 51 | suggestions: [ 52 | { 53 | messageId: "renameGate", 54 | data: { gateName: "someGate", correctedGateName: "SomeGate" }, 55 | output: ` 56 | import {createGate} from 'effector-react'; 57 | const SomeGate = createGate(); 58 | `, 59 | }, 60 | ], 61 | }, 62 | ], 63 | }, 64 | ], 65 | }); 66 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/strict-effect-handlers.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("eslint"); 2 | 3 | const { readExample } = require("../../utils/read-example"); 4 | const rule = require("./strict-effect-handlers"); 5 | 6 | const ruleTester = new RuleTester({ 7 | parserOptions: { 8 | ecmaVersion: 2020, 9 | sourceType: "module", 10 | }, 11 | }); 12 | 13 | const readExampleForTheRule = (name) => ({ 14 | code: readExample(__dirname, name), 15 | }); 16 | 17 | ruleTester.run("effector/strict-effect-handlers.test", rule, { 18 | valid: [ 19 | "correct-only-async.js", 20 | "correct-only-fx.js", 21 | "correct-empty-function.js", 22 | ].map(readExampleForTheRule), 23 | 24 | invalid: [ 25 | { 26 | ...readExampleForTheRule("incorrect-mix-async-fx.js"), 27 | errors: [ 28 | { 29 | messageId: "mixedCallsInHandler", 30 | type: "CallExpression", 31 | data: { effectName: "finalFx" }, 32 | }, 33 | ], 34 | }, 35 | { 36 | ...readExampleForTheRule("incorrect-mix-async-fx-in-func.js"), 37 | errors: [ 38 | { 39 | messageId: "mixedCallsInHandler", 40 | type: "CallExpression", 41 | data: { effectName: "finalFx" }, 42 | }, 43 | ], 44 | }, 45 | { 46 | ...readExampleForTheRule("incorrect-mix-async-fx-in-named-func.js"), 47 | errors: [ 48 | { 49 | messageId: "mixedCallsInHandler", 50 | type: "CallExpression", 51 | data: { effectName: "finalFx" }, 52 | }, 53 | ], 54 | }, 55 | { 56 | ...readExampleForTheRule("incorrect-mix-in-simple-function.js"), 57 | errors: [ 58 | { 59 | messageId: "mixedCallsInFunction", 60 | type: "FunctionDeclaration", 61 | data: { functionName: "justFunc" }, 62 | }, 63 | ], 64 | }, 65 | ], 66 | }); 67 | -------------------------------------------------------------------------------- /rules/enforce-effect-naming-convention/enforce-effect-naming-convention.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("eslint"); 2 | 3 | const { readExample } = require("../../utils/read-example"); 4 | 5 | const rule = require("./enforce-effect-naming-convention"); 6 | 7 | const ruleTester = new RuleTester({ 8 | parserOptions: { 9 | ecmaVersion: 2020, 10 | sourceType: "module", 11 | }, 12 | }); 13 | 14 | const readExampleForTheRule = (name) => readExample(__dirname, name); 15 | 16 | ruleTester.run("effector/enforce-effect-naming-convention.test", rule, { 17 | valid: [ 18 | "correct-effect-naming.js", 19 | "correct-effect-naming-from-other-package.js", 20 | "correct-effect-naming-in-domain.js", 21 | "correct-examples-issue-24.js", 22 | ] 23 | .map(readExampleForTheRule) 24 | .map((code) => ({ code })), 25 | 26 | invalid: [ 27 | // Errors 28 | ...[ 29 | "incorrect-createEffect.js", 30 | "incorrect-createEffect-alias.js", 31 | "incorrect-createEffect-in-domain.js", 32 | "incorrect-attach.js", 33 | "incorrect-attach-alias.js", 34 | ] 35 | .map(readExampleForTheRule) 36 | .map((code) => ({ 37 | code, 38 | errors: [ 39 | { 40 | messageId: "invalidName", 41 | type: "VariableDeclarator", 42 | }, 43 | ], 44 | })), 45 | // Suggestions 46 | { 47 | code: ` 48 | import {createEffect} from 'effector'; 49 | const effect = createEffect(); 50 | `, 51 | errors: [ 52 | { 53 | messageId: "invalidName", 54 | suggestions: [ 55 | { 56 | messageId: "renameEffect", 57 | data: { effectName: "effect" }, 58 | output: ` 59 | import {createEffect} from 'effector'; 60 | const effectFx = createEffect(); 61 | `, 62 | }, 63 | ], 64 | }, 65 | ], 66 | }, 67 | ], 68 | }); 69 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/strict-effect-handlers.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../utils/read-example"); 5 | const rule = require("./strict-effect-handlers"); 6 | 7 | const ruleTester = new RuleTester({ 8 | languageOptions: { 9 | parserOptions: { 10 | projectService: true, 11 | }, 12 | }, 13 | }); 14 | 15 | const readExampleForTheRule = (name) => ({ 16 | code: readExample(__dirname, name), 17 | filename: join(__dirname, "examples", name), 18 | }); 19 | 20 | ruleTester.run("strict-effect-handlers.ts.test", rule, { 21 | valid: ["correct.ts"].map(readExampleForTheRule), 22 | 23 | invalid: [ 24 | { 25 | ...readExampleForTheRule("incorrect-mix-async-fx.ts"), 26 | errors: [ 27 | { 28 | messageId: "mixedCallsInHandler", 29 | type: "CallExpression", 30 | data: { effectName: "finalFx" }, 31 | }, 32 | ], 33 | }, 34 | { 35 | ...readExampleForTheRule("incorrect-mix-async-fx-in-func.ts"), 36 | errors: [ 37 | { 38 | messageId: "mixedCallsInHandler", 39 | type: "CallExpression", 40 | data: { effectName: "finalFx" }, 41 | }, 42 | ], 43 | }, 44 | { 45 | ...readExampleForTheRule("incorrect-mix-async-fx-in-named-func.ts"), 46 | errors: [ 47 | { 48 | messageId: "mixedCallsInHandler", 49 | type: "CallExpression", 50 | data: { effectName: "finalFx" }, 51 | }, 52 | ], 53 | }, 54 | { 55 | ...readExampleForTheRule("incorrect-mix-in-simple-function.ts"), 56 | errors: [ 57 | { 58 | messageId: "mixedCallsInFunction", 59 | type: "FunctionDeclaration", 60 | data: { functionName: "justFunc" }, 61 | }, 62 | ], 63 | }, 64 | ], 65 | }); 66 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/no-unnecessary-combination.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../utils/read-example"); 5 | const rule = require("./no-unnecessary-combination"); 6 | 7 | const ruleTester = new RuleTester({ 8 | languageOptions: { 9 | parserOptions: { 10 | projectService: true, 11 | }, 12 | }, 13 | }); 14 | 15 | const readExampleForTheRule = (name) => ({ 16 | code: readExample(__dirname, name), 17 | filename: join(__dirname, "examples", name), 18 | }); 19 | 20 | ruleTester.run("no-unnecessary-combination.ts.test", rule, { 21 | valid: [ 22 | "correct.ts", 23 | "correct-combine-in-clock-guard.ts", 24 | "correct-combine-in-clock-sample.ts", 25 | "correct-combine-in-from-forward.ts", 26 | ].map(readExampleForTheRule), 27 | 28 | invalid: [ 29 | ...[ 30 | "unnecessary-merge-in-source-guard.ts", 31 | "unnecessary-merge-in-clock-guard.ts", 32 | "unnecessary-merge-in-source-sample.ts", 33 | "unnecessary-merge-in-clock-sample.ts", 34 | "unnecessary-merge-in-from-forward.ts", 35 | ] 36 | .map(readExampleForTheRule) 37 | .map((result) => ({ 38 | ...result, 39 | errors: [ 40 | { 41 | messageId: "unnecessaryCombination", 42 | type: "CallExpression", 43 | data: { methodName: "merge" }, 44 | }, 45 | ], 46 | })), 47 | ...[ 48 | "unnecessary-combine-in-source-guard.ts", 49 | "unnecessary-combine-in-source-sample.ts", 50 | ] 51 | .map(readExampleForTheRule) 52 | .map((result) => ({ 53 | ...result, 54 | errors: [ 55 | { 56 | messageId: "unnecessaryCombination", 57 | type: "CallExpression", 58 | data: { methodName: "combine" }, 59 | }, 60 | ], 61 | })), 62 | ], 63 | }); 64 | -------------------------------------------------------------------------------- /rules/no-forward/no-forward.js: -------------------------------------------------------------------------------- 1 | const { extractImportedFrom } = require("../../utils/extract-imported-from"); 2 | const { createLinkToRule } = require("../../utils/create-link-to-rule"); 3 | const { method } = require("../../utils/method"); 4 | const { replaceForwardBySample } = require("../../utils/replace-by-sample"); 5 | const { extractConfig } = require("../../utils/extract-config"); 6 | 7 | module.exports = { 8 | meta: { 9 | type: "problem", 10 | docs: { 11 | description: "Prefer `sample` over `forward`", 12 | category: "Quality", 13 | recommended: true, 14 | url: createLinkToRule("no-forward"), 15 | }, 16 | messages: { 17 | noForward: 18 | "Instead of `forward` you can use `sample`, it is more extendable.", 19 | replaceWithSample: "Replace `forward` with `sample`.", 20 | }, 21 | schema: [], 22 | hasSuggestions: true, 23 | }, 24 | create(context) { 25 | const importNodes = new Map(); 26 | const importedFromEffector = new Map(); 27 | 28 | return { 29 | ImportDeclaration(node) { 30 | extractImportedFrom({ 31 | importMap: importedFromEffector, 32 | nodeMap: importNodes, 33 | node, 34 | packageName: "effector", 35 | }); 36 | }, 37 | CallExpression(node) { 38 | if ( 39 | method.isNot("forward", { 40 | node, 41 | importMap: importedFromEffector, 42 | }) 43 | ) { 44 | return; 45 | } 46 | 47 | const forwardConfig = extractConfig(["from", "to"], { node }); 48 | 49 | if (!forwardConfig.from || !forwardConfig.to) { 50 | return; 51 | } 52 | 53 | context.report({ 54 | messageId: "noForward", 55 | node, 56 | suggest: [ 57 | { 58 | messageId: "replaceWithSample", 59 | *fix(fixer) { 60 | yield* replaceForwardBySample(forwardConfig, { 61 | fixer, 62 | node, 63 | context, 64 | importNodes, 65 | }); 66 | }, 67 | }, 68 | ], 69 | }); 70 | }, 71 | }; 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "enforce-store-naming-convention": require("./rules/enforce-store-naming-convention/enforce-store-naming-convention"), 4 | "enforce-effect-naming-convention": require("./rules/enforce-effect-naming-convention/enforce-effect-naming-convention"), 5 | "no-getState": require("./rules/no-getState/no-getState"), 6 | "no-unnecessary-duplication": require("./rules/no-unnecessary-duplication/no-unnecessary-duplication"), 7 | "prefer-sample-over-forward-with-mapping": require("./rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping"), 8 | "no-duplicate-clock-or-source-array-values": require("./rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values"), 9 | "no-useless-methods": require("./rules/no-useless-methods/no-useless-methods"), 10 | "no-ambiguity-target": require("./rules/no-ambiguity-target/no-ambiguity-target"), 11 | "no-watch": require("./rules/no-watch/no-watch"), 12 | "no-unnecessary-combination": require("./rules/no-unnecessary-combination/no-unnecessary-combination"), 13 | "no-duplicate-on": require("./rules/no-duplicate-on/no-duplicate-on"), 14 | "strict-effect-handlers": require("./rules/strict-effect-handlers/strict-effect-handlers"), 15 | "enforce-gate-naming-convention": require("./rules/enforce-gate-naming-convention/enforce-gate-naming-convention"), 16 | "keep-options-order": require("./rules/keep-options-order/keep-options-order"), 17 | "no-forward": require("./rules/no-forward/no-forward"), 18 | "no-guard": require("./rules/no-guard/no-guard"), 19 | "mandatory-scope-binding": require("./rules/mandatory-scope-binding/mandatory-scope-binding"), 20 | "prefer-useUnit": require("./rules/prefer-useUnit/prefer-useUnit"), 21 | "require-pickup-in-persist": require("./rules/require-pickup-in-persist/require-pickup-in-persist"), 22 | "no-patronum-debug": require("./rules/no-patronum-debug/no-patronum-debug"), 23 | }, 24 | configs: { 25 | recommended: require("./config/recommended"), 26 | scope: require("./config/scope"), 27 | react: require("./config/react"), 28 | future: require("./config/future"), 29 | patronum: require("./config/patronum"), 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /rules/no-guard/no-guard.js: -------------------------------------------------------------------------------- 1 | const { extractImportedFrom } = require("../../utils/extract-imported-from"); 2 | const { createLinkToRule } = require("../../utils/create-link-to-rule"); 3 | const { method } = require("../../utils/method"); 4 | const { replaceGuardBySample } = require("../../utils/replace-by-sample"); 5 | const { extractConfig } = require("../../utils/extract-config"); 6 | 7 | module.exports = { 8 | meta: { 9 | type: "problem", 10 | docs: { 11 | description: "Prefer `sample` over `guard`", 12 | category: "Quality", 13 | recommended: true, 14 | url: createLinkToRule("no-guard"), 15 | }, 16 | messages: { 17 | noGuard: 18 | "Instead of `guard` you can use `sample`, it is more extendable.", 19 | replaceWithSample: "Replace `guard` with `sample`.", 20 | }, 21 | schema: [], 22 | hasSuggestions: true, 23 | }, 24 | create(context) { 25 | const importNodes = new Map(); 26 | const importedFromEffector = new Map(); 27 | 28 | return { 29 | ImportDeclaration(node) { 30 | extractImportedFrom({ 31 | importMap: importedFromEffector, 32 | nodeMap: importNodes, 33 | node, 34 | packageName: "effector", 35 | }); 36 | }, 37 | CallExpression(node) { 38 | if ( 39 | method.isNot("guard", { 40 | node, 41 | importMap: importedFromEffector, 42 | }) 43 | ) { 44 | return; 45 | } 46 | 47 | const guardConfig = extractConfig( 48 | ["source", "clock", "target", "filter"], 49 | { 50 | node, 51 | } 52 | ); 53 | 54 | if (!guardConfig.clock || !guardConfig.filter) { 55 | return; 56 | } 57 | 58 | context.report({ 59 | messageId: "noGuard", 60 | node, 61 | suggest: [ 62 | { 63 | messageId: "replaceWithSample", 64 | *fix(fixer) { 65 | yield* replaceGuardBySample(guardConfig, { 66 | fixer, 67 | node, 68 | context, 69 | importNodes, 70 | }); 71 | }, 72 | }, 73 | ], 74 | }); 75 | }, 76 | }; 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /rules/no-ambiguity-target/no-ambiguity-target.js: -------------------------------------------------------------------------------- 1 | const { extractImportedFrom } = require("../../utils/extract-imported-from"); 2 | const { traverseParentByType } = require("../../utils/traverse-parent-by-type"); 3 | const { createLinkToRule } = require("../../utils/create-link-to-rule"); 4 | const { method } = require("../../utils/method"); 5 | 6 | module.exports = { 7 | meta: { 8 | type: "problem", 9 | docs: { 10 | description: "Forbids ambiguity targets in `sample` and `guard`", 11 | category: "Quality", 12 | recommended: true, 13 | url: createLinkToRule("no-ambiguity-target"), 14 | }, 15 | messages: { 16 | ambiguityTarget: 17 | "Method `{{ methodName }}` returns `target` and assigns the result to a variable. Consider removing one of them.", 18 | }, 19 | schema: [], 20 | }, 21 | create(context) { 22 | const importedFromEffector = new Map(); 23 | 24 | return { 25 | ImportDeclaration(node) { 26 | extractImportedFrom({ 27 | importMap: importedFromEffector, 28 | node, 29 | packageName: "effector", 30 | }); 31 | }, 32 | CallExpression(node) { 33 | if ( 34 | method.isNot(["sample", "guard"], { 35 | node, 36 | importMap: importedFromEffector, 37 | }) 38 | ) { 39 | return; 40 | } 41 | 42 | const configHasTarget = node?.arguments?.[0]?.properties?.some( 43 | (prop) => prop?.key.name === "target" 44 | ); 45 | if (!configHasTarget) { 46 | return; 47 | } 48 | 49 | const resultAssignedInVariable = traverseParentByType( 50 | node, 51 | "VariableDeclarator", 52 | { stopOnTypes: ["BlockStatement"] } 53 | ); 54 | const resultPartOfChain = traverseParentByType( 55 | node, 56 | "ObjectExpression", 57 | { stopOnTypes: ["BlockStatement"] } 58 | ); 59 | 60 | if (resultAssignedInVariable || resultPartOfChain) { 61 | context.report({ 62 | node, 63 | messageId: "ambiguityTarget", 64 | data: { 65 | methodName: node?.callee?.name, 66 | }, 67 | }); 68 | 69 | return; 70 | } 71 | }, 72 | }; 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /rules/no-duplicate-on/no-duplicate-on.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("eslint"); 2 | 3 | const rule = require("./no-duplicate-on"); 4 | 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 2020, 8 | sourceType: "module", 9 | }, 10 | }); 11 | 12 | ruleTester.run("effector/no-duplicate-on.test", rule, { 13 | valid: [ 14 | "$store.on(first, () => null).on(second, () => null);", 15 | "$store.on([first, second], () => null);", 16 | "$store.on(first, () => null);", 17 | "$store.on(firstFx, () => null).on(firstFx.doneData, () => null);", 18 | ].map((code) => ({ code })), 19 | 20 | invalid: [ 21 | { 22 | code: "$store.on(first, () => null).on(first, () => null);", 23 | errors: [ 24 | { 25 | messageId: "duplicateOn", 26 | type: "CallExpression", 27 | data: { storeName: "$store", unitName: "first" }, 28 | }, 29 | ], 30 | }, 31 | { 32 | code: ` 33 | $store.on(first, () => null); 34 | $store.on(first, () => null); 35 | `, 36 | errors: [ 37 | { 38 | messageId: "duplicateOn", 39 | type: "CallExpression", 40 | data: { storeName: "$store", unitName: "first" }, 41 | }, 42 | ], 43 | }, 44 | { 45 | code: ` 46 | $store.on(first, () => null); 47 | $store.on([first, second], () => null); 48 | `, 49 | errors: [ 50 | { 51 | messageId: "duplicateOn", 52 | type: "CallExpression", 53 | data: { storeName: "$store", unitName: "first" }, 54 | }, 55 | ], 56 | }, 57 | { 58 | code: "$store.on(firstFx.doneData, () => null).on(firstFx.doneData, () => null);", 59 | errors: [ 60 | { 61 | messageId: "duplicateOn", 62 | type: "CallExpression", 63 | data: { storeName: "$store", unitName: "firstFx.doneData" }, 64 | }, 65 | ], 66 | }, 67 | { 68 | code: "$store.on(service.firstFx.doneData, () => null).on(service.firstFx.doneData, () => null);", 69 | errors: [ 70 | { 71 | messageId: "duplicateOn", 72 | type: "CallExpression", 73 | data: { storeName: "$store", unitName: "service.firstFx.doneData" }, 74 | }, 75 | ], 76 | }, 77 | ], 78 | }); 79 | -------------------------------------------------------------------------------- /rules/strict-effect-handlers/strict-effect-handlers.js: -------------------------------------------------------------------------------- 1 | const { createLinkToRule } = require("../../utils/create-link-to-rule"); 2 | const { is } = require("../../utils/is"); 3 | 4 | module.exports = { 5 | meta: { 6 | type: "problem", 7 | docs: { 8 | description: 9 | "Forbids mix of async functions and effects calls in effect handlers.", 10 | category: "Quality", 11 | recommended: true, 12 | url: createLinkToRule("strict-effect-handlers"), 13 | }, 14 | messages: { 15 | mixedCallsInHandler: 16 | "Handler of effect `{{ effectName }}` can lead to scope loosing in Fork API.", 17 | mixedCallsInFunction: 18 | "Function `{{ functionName }}` can lead to scope loosing in Fork API.", 19 | }, 20 | schema: [], 21 | }, 22 | create(context) { 23 | function onEffectHandler(node) { 24 | const functionBody = node.body?.body; 25 | 26 | if (!Array.isArray(functionBody)) { 27 | return; 28 | } 29 | 30 | const calledNodes = functionBody 31 | .filter((bodyNode) => bodyNode.expression?.type === "AwaitExpression") 32 | .map((awaitNode) => ({ 33 | node: awaitNode.expression.argument.callee, 34 | context, 35 | })); 36 | 37 | const hasEffects = calledNodes.some(is.effect); 38 | const hasRegularAsyncFunctions = calledNodes.some(is.not.effect); 39 | 40 | const hasError = hasEffects && hasRegularAsyncFunctions; 41 | 42 | if (!hasError) { 43 | return; 44 | } 45 | 46 | const isEffectHandler = is.effect({ 47 | node: node.parent?.parent, 48 | context, 49 | }); 50 | 51 | if (isEffectHandler) { 52 | const effectName = node.parent?.parent?.id?.name ?? "Unknown"; 53 | 54 | context.report({ 55 | node: node.parent, 56 | messageId: "mixedCallsInHandler", 57 | data: { effectName }, 58 | }); 59 | } else { 60 | const functionName = node.id?.name ?? "Unknown"; 61 | 62 | context.report({ 63 | node, 64 | messageId: "mixedCallsInFunction", 65 | data: { functionName }, 66 | }); 67 | } 68 | } 69 | 70 | return { 71 | ArrowFunctionExpression: onEffectHandler, 72 | FunctionExpression: onEffectHandler, 73 | FunctionDeclaration: onEffectHandler, 74 | }; 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../utils/read-example"); 5 | const rule = require("./no-duplicate-clock-or-source-array-values"); 6 | 7 | const ruleTester = new RuleTester({ 8 | languageOptions: { 9 | parserOptions: { 10 | projectService: true, 11 | }, 12 | }, 13 | }); 14 | 15 | const readExampleForTheRule = (name) => ({ 16 | code: readExample(__dirname, name), 17 | filename: join(__dirname, "examples", name), 18 | }); 19 | 20 | ruleTester.run( 21 | "no-duplicate-clock-or-source-array-values.ts.test", 22 | rule, 23 | { 24 | valid: ["correct-sample.ts"].map(readExampleForTheRule), 25 | 26 | invalid: [ 27 | ...["incorrect-sample.ts"].map(readExampleForTheRule).map((result) => ({ 28 | ...result, 29 | errors: [ 30 | { 31 | messageId: "duplicatesInSource", 32 | type: "Identifier", 33 | suggestions: [ 34 | { 35 | messageId: "removeDuplicate", 36 | output: result.code.replace("$store, $store", "$store, "), // ESLint removes node but leaves trailing comma 37 | }, 38 | ], 39 | }, 40 | { 41 | messageId: "duplicatesInClock", 42 | type: "MemberExpression", 43 | suggestions: [ 44 | { 45 | messageId: "removeDuplicate", 46 | output: result.code.replace("setUnloadDeliveryDateFx.doneData,\n ],", ",\n ],"), // Remove duplicate but leave comma 47 | }, 48 | ], 49 | }, 50 | ], 51 | })), 52 | ...["incorrect-guard.ts"].map(readExampleForTheRule).map((result) => ({ 53 | ...result, 54 | errors: [ 55 | { 56 | messageId: "duplicatesInClock", 57 | type: "MemberExpression", 58 | suggestions: [ 59 | { 60 | messageId: "removeDuplicate", 61 | output: result.code.replace(" clickOnBtn,\n setUnloadDeliveryDateFx.doneData,", " clickOnBtn,\n ,"), // Remove duplicate, leave comma 62 | }, 63 | ], 64 | }, 65 | ], 66 | })), 67 | ], 68 | } 69 | ); 70 | -------------------------------------------------------------------------------- /rules/mandatory-scope-binding/mandatory-scope-binding.js: -------------------------------------------------------------------------------- 1 | const { ESLintUtils } = require("@typescript-eslint/utils"); 2 | 3 | const { createLinkToRule } = require("../../utils/create-link-to-rule"); 4 | const { isInsideReactComponent } = require("../../utils/react"); 5 | const { nodeTypeIs } = require("../../utils/node-type-is"); 6 | const { traverseParentByType } = require("../../utils/traverse-parent-by-type"); 7 | const { nodeIsType } = require("../../utils/node-is-type"); 8 | 9 | module.exports = { 10 | meta: { 11 | type: "problem", 12 | docs: { 13 | description: 14 | "Forbids `Event` and `Effect` usage without `useUnit` in React components.", 15 | category: "Quality", 16 | recommended: true, 17 | url: createLinkToRule("mandatory-scope-binding"), 18 | }, 19 | messages: { 20 | useEventNeeded: 21 | "{{ unitName }} must be wrapped with `useUnit` from `effector-react` before usage inside React components", 22 | }, 23 | schema: [], 24 | }, 25 | create(context) { 26 | let parserServices; 27 | try { 28 | parserServices = ESLintUtils.getParserServices(context); 29 | } catch (err) { 30 | // no types information 31 | } 32 | 33 | // TypeScript-only rule, since units can be imported from anywhere 34 | if (!parserServices?.program) { 35 | return {}; 36 | } 37 | 38 | return { 39 | Identifier(node) { 40 | if (!isInsideReactComponent(node)) { 41 | return; 42 | } 43 | 44 | if (nodeIsType({ node })) { 45 | return; 46 | } 47 | 48 | if ( 49 | nodeTypeIs.not.effect({ node, context }) && 50 | nodeTypeIs.not.event({ node, context }) 51 | ) { 52 | return; 53 | } 54 | 55 | if (isInsideEffectorHook({ node, context })) { 56 | return; 57 | } 58 | 59 | context.report({ 60 | node, 61 | messageId: "useEventNeeded", 62 | data: { 63 | unitName: node.name, 64 | }, 65 | }); 66 | }, 67 | }; 68 | }, 69 | }; 70 | 71 | function isInsideEffectorHook({ node, context }) { 72 | const calleeParentNode = traverseParentByType(node.parent, "CallExpression"); 73 | 74 | if (!calleeParentNode?.callee) return false; 75 | 76 | return nodeTypeIs.effectorReactHook({ 77 | node: calleeParentNode.callee, 78 | context, 79 | hook: ["useEvent", "useUnit", "useStore"], 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /rules/no-patronum-debug/no-patronum-debug.ts.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("@typescript-eslint/rule-tester"); 2 | const { join } = require("path"); 3 | 4 | const { readExample } = require("../../utils/read-example"); 5 | const rule = require("./no-patronum-debug"); 6 | 7 | const ruleTester = new RuleTester({ 8 | languageOptions: { 9 | parserOptions: { 10 | projectService: true, 11 | }, 12 | }, 13 | }); 14 | 15 | const readExampleForTheRule = (name) => ({ 16 | code: readExample(__dirname, name), 17 | filename: join(__dirname, "examples", name), 18 | }); 19 | 20 | ruleTester.run("no-patronum-debug", rule, { 21 | valid: ["correct.ts", "correct-issue-127.ts"].map(readExampleForTheRule), 22 | invalid: [ 23 | ...["incorrect-with-debug.ts"] 24 | .map(readExampleForTheRule) 25 | .map((example) => ({ 26 | ...example, 27 | errors: [ 28 | { 29 | messageId: "noPatronumDebug", 30 | type: "CallExpression", 31 | suggestions: [ 32 | { 33 | messageId: "removePatronumDebug", 34 | output: `import { createStore } from "effector"; 35 | const $store = createStore({ fullname: "John Due" }); 36 | `, 37 | }, 38 | ], 39 | }, 40 | ], 41 | })), 42 | ...["incorrect-with-import-alias.ts"] 43 | .map(readExampleForTheRule) 44 | .map((example) => ({ 45 | ...example, 46 | errors: [ 47 | { 48 | messageId: "noPatronumDebug", 49 | type: "CallExpression", 50 | suggestions: [ 51 | { 52 | messageId: "removePatronumDebug", 53 | output: `import { createEvent } from "effector"; 54 | const event = createEvent(); 55 | `, 56 | }, 57 | ], 58 | }, 59 | ], 60 | })), 61 | ...["incorrect-with-debug-fork.ts"] 62 | .map(readExampleForTheRule) 63 | .map((example) => ({ 64 | ...example, 65 | errors: [ 66 | { 67 | messageId: "noPatronumDebug", 68 | type: "CallExpression", 69 | suggestions: [ 70 | { 71 | messageId: "removePatronumDebug", 72 | output: `import { fork, createStore } from "effector"; 73 | const $count = createStore(0); 74 | const scopeA = fork({ values: [[$count, 42]] }); 75 | const scopeB = fork({ values: [[$count, 1337]] }); 76 | `, 77 | }, 78 | ], 79 | }, 80 | ], 81 | })), 82 | ], 83 | }); 84 | -------------------------------------------------------------------------------- /rules/enforce-store-naming-convention/postfix/enforce-store-naming-convention-postfix.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("eslint"); 2 | 3 | const { readExample } = require("../../../utils/read-example"); 4 | 5 | const rule = require("../enforce-store-naming-convention"); 6 | 7 | const ruleTester = new RuleTester({ 8 | parserOptions: { 9 | ecmaVersion: 2020, 10 | sourceType: "module", 11 | }, 12 | settings: { 13 | effector: { 14 | storeNameConvention: "postfix", 15 | }, 16 | }, 17 | }); 18 | 19 | const readExampleForTheRule = (name) => readExample(__dirname, name); 20 | 21 | ruleTester.run("effector/enforce-store-naming-convention-postfix.test", rule, { 22 | valid: [ 23 | "correct-store-naming.js", 24 | "correct-store-naming-from-other-package.js", 25 | "correct-store-naming-in-domain.js", 26 | "correct-examples-issue-23.js", 27 | "correct-store-naming-with-handlers.js", 28 | "correct-store-naming-in-domain-with-handlers.js", 29 | ] 30 | .map(readExampleForTheRule) 31 | .map((code) => ({ 32 | code, 33 | })), 34 | 35 | invalid: [ 36 | // Errors 37 | ...[ 38 | "incorrect-createStore.js", 39 | "incorrect-createStore-alias.js", 40 | "incorrect-createStore-prefix.js", 41 | "incorrect-restore.js", 42 | "incorrect-restore-alias.js", 43 | "incorrect-combine.js", 44 | "incorrect-combine-alias.js", 45 | "incorrect-map.js", 46 | "incorrect-createStore-domain.js", 47 | "incorrect-store-naming-with-handlers.js", 48 | "incorrect-store-naming-in-domain-with-handlers.js", 49 | ] 50 | .map(readExampleForTheRule) 51 | .map((code) => ({ 52 | code, 53 | settings: { 54 | effector: { 55 | storeNameConvention: "postfix", 56 | }, 57 | }, 58 | errors: [ 59 | { 60 | messageId: "invalidName", 61 | type: "VariableDeclarator", 62 | }, 63 | ], 64 | })), 65 | // Suggestions 66 | { 67 | code: ` 68 | import {createStore} from 'effector'; 69 | const store = createStore(null); 70 | `, 71 | errors: [ 72 | { 73 | messageId: "invalidName", 74 | suggestions: [ 75 | { 76 | messageId: "renameStore", 77 | data: { storeName: "store", correctedStoreName: "store$" }, 78 | output: ` 79 | import {createStore} from 'effector'; 80 | const store$ = createStore(null); 81 | `, 82 | }, 83 | ], 84 | }, 85 | ], 86 | }, 87 | ], 88 | }); 89 | -------------------------------------------------------------------------------- /rules/no-useless-methods/no-useless-methods.js: -------------------------------------------------------------------------------- 1 | const { extractImportedFrom } = require("../../utils/extract-imported-from"); 2 | const { traverseParentByType } = require("../../utils/traverse-parent-by-type"); 3 | const { createLinkToRule } = require("../../utils/create-link-to-rule"); 4 | const { method } = require("../../utils/method"); 5 | 6 | module.exports = { 7 | meta: { 8 | type: "problem", 9 | docs: { 10 | description: "Forbids useless calls of `sample` and `guard`", 11 | category: "Quality", 12 | recommended: true, 13 | url: createLinkToRule("no-useless-methods"), 14 | }, 15 | messages: { 16 | uselessMethod: 17 | "Method `{{ methodName }}` does nothing in this case. You should assign the result to variable or pass `target` to it.", 18 | }, 19 | schema: [], 20 | }, 21 | create(context) { 22 | const importedFromEffector = new Map(); 23 | 24 | return { 25 | ImportDeclaration(node) { 26 | extractImportedFrom({ 27 | importMap: importedFromEffector, 28 | node, 29 | packageName: "effector", 30 | }); 31 | }, 32 | CallExpression(node) { 33 | if ( 34 | method.isNot(["sample", "guard"], { 35 | node, 36 | importMap: importedFromEffector, 37 | }) 38 | ) { 39 | return; 40 | } 41 | 42 | const resultAssignedInVariable = traverseParentByType( 43 | node, 44 | "VariableDeclarator" 45 | ); 46 | if (resultAssignedInVariable) { 47 | return; 48 | } 49 | 50 | const resultReturnedFromFactory = traverseParentByType( 51 | node, 52 | "ReturnStatement" 53 | ); 54 | if (resultReturnedFromFactory) { 55 | return; 56 | } 57 | 58 | const resultPartOfChain = traverseParentByType( 59 | node, 60 | "ObjectExpression" 61 | ); 62 | if (resultPartOfChain) { 63 | return; 64 | } 65 | 66 | const configHasTarget = node?.arguments?.[0]?.properties?.some( 67 | (prop) => prop?.key.name === "target" 68 | ); 69 | if (configHasTarget) { 70 | return; 71 | } 72 | 73 | const resultIsWatched = node?.parent?.property?.name === "watch"; 74 | if (resultIsWatched) { 75 | return; 76 | } 77 | 78 | const resultIsArgument = node?.parent?.type === "CallExpression"; 79 | if (resultIsArgument) { 80 | return; 81 | } 82 | 83 | context.report({ 84 | node, 85 | messageId: "uselessMethod", 86 | data: { 87 | methodName: node?.callee?.name, 88 | }, 89 | }); 90 | }, 91 | }; 92 | }, 93 | }; 94 | -------------------------------------------------------------------------------- /rules/no-unnecessary-combination/no-unnecessary-combination.js: -------------------------------------------------------------------------------- 1 | const { extractImportedFrom } = require("../../utils/extract-imported-from"); 2 | const { createLinkToRule } = require("../../utils/create-link-to-rule"); 3 | const { method } = require("../../utils/method"); 4 | 5 | module.exports = { 6 | meta: { 7 | type: "problem", 8 | docs: { 9 | description: 10 | "Forbids unnecessary combinations in `clock`, `source` and `forward`", 11 | category: "Quality", 12 | recommended: true, 13 | url: createLinkToRule("no-unnecessary-combination"), 14 | }, 15 | messages: { 16 | unnecessaryCombination: 17 | "Method {{ methodName }} is used under the hood, you can omit it.", 18 | }, 19 | schema: [], 20 | }, 21 | create(context) { 22 | const importedFromEffector = new Map(); 23 | 24 | return { 25 | ImportDeclaration(node) { 26 | extractImportedFrom({ 27 | importMap: importedFromEffector, 28 | node, 29 | packageName: "effector", 30 | }); 31 | }, 32 | CallExpression(node) { 33 | const CONFIG_ARG_PROPERTIES = ["source", "clock", "from"]; 34 | 35 | function toLocalMethod(methodName) { 36 | return importedFromEffector.get(methodName); 37 | } 38 | 39 | const UNNECESSARY_METHODS = { 40 | source: ["combine", "merge"].map(toLocalMethod).filter(Boolean), 41 | clock: ["merge"].map(toLocalMethod).filter(Boolean), 42 | from: ["merge"].map(toLocalMethod).filter(Boolean), 43 | }; 44 | 45 | if ( 46 | method.isNot(["sample", "guard", "forward"], { 47 | node, 48 | importMap: importedFromEffector, 49 | }) 50 | ) { 51 | return; 52 | } 53 | 54 | const candidates = 55 | node?.arguments?.[0]?.properties?.filter((n) => 56 | CONFIG_ARG_PROPERTIES.includes(n.key.name) 57 | ) ?? []; 58 | 59 | if (candidates.length === 0) { 60 | return; 61 | } 62 | 63 | for (const candidate of candidates) { 64 | const candidateName = candidate?.value?.callee?.name; 65 | const argProp = candidate?.key?.name; 66 | if (!candidateName || !argProp) { 67 | continue; 68 | } 69 | 70 | const localUnnecessaryMethods = UNNECESSARY_METHODS[argProp]; 71 | 72 | const UnnecessaryMethodIsEffectorMethod = 73 | localUnnecessaryMethods.some((m) => m === candidateName); 74 | 75 | if (!UnnecessaryMethodIsEffectorMethod) { 76 | continue; 77 | } 78 | 79 | context.report({ 80 | node: candidate?.value, 81 | messageId: "unnecessaryCombination", 82 | data: { methodName: candidateName }, 83 | }); 84 | } 85 | }, 86 | }; 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /utils/replace-by-sample.js: -------------------------------------------------------------------------------- 1 | const { buildObjectInText } = require("./builders"); 2 | 3 | function* replaceGuardBySample( 4 | guardConfig, 5 | { fixer, node, context, importNodes } 6 | ) { 7 | let mapperFunctionNode = null; 8 | 9 | let clockNode = guardConfig.clock?.value; 10 | let targetNode = guardConfig.target?.value; 11 | let sourceNode = guardConfig.source?.value; 12 | let filterNode = guardConfig.filter?.value; 13 | 14 | if ( 15 | targetNode?.type === "CallExpression" && 16 | targetNode?.callee?.property?.name === "prepend" 17 | ) { 18 | mapperFunctionNode = targetNode?.arguments?.[0]; 19 | targetNode = targetNode.callee.object; 20 | targetMapperUsed = true; 21 | } 22 | 23 | yield* replaceBySample( 24 | { clockNode, sourceNode, filterNode, mapperFunctionNode, targetNode }, 25 | { node, fixer, context, importNodes, methodName: "guard" } 26 | ); 27 | } 28 | 29 | function* replaceForwardBySample( 30 | forwardConfig, 31 | { fixer, node, context, importNodes } 32 | ) { 33 | let mapperFunctionNode = null; 34 | 35 | let clockMapperUsed = false; 36 | let targetMapperUsed = false; 37 | 38 | let clockNode = forwardConfig.from.value; 39 | let targetNode = forwardConfig.to.value; 40 | 41 | if ( 42 | clockNode?.type === "CallExpression" && 43 | clockNode?.callee?.property?.name === "map" 44 | ) { 45 | mapperFunctionNode = clockNode?.arguments?.[0]; 46 | clockNode = clockNode.callee.object; 47 | clockMapperUsed = true; 48 | } 49 | 50 | if ( 51 | targetNode?.type === "CallExpression" && 52 | targetNode?.callee?.property?.name === "prepend" 53 | ) { 54 | mapperFunctionNode = targetNode?.arguments?.[0]; 55 | targetNode = targetNode.callee.object; 56 | targetMapperUsed = true; 57 | } 58 | 59 | // We cannot apply two mappers in one sample 60 | // Let's revert mappers and use .map + .prepend 61 | if (clockMapperUsed && targetMapperUsed) { 62 | mapperFunctionNode = null; 63 | clockNode = forwardConfig.from.value; 64 | targetNode = forwardConfig.to.value; 65 | } 66 | 67 | yield* replaceBySample( 68 | { clockNode, mapperFunctionNode, targetNode }, 69 | { node, fixer, context, importNodes, methodName: "forward" } 70 | ); 71 | } 72 | 73 | function* replaceBySample( 74 | { clockNode, sourceNode, filterNode, mapperFunctionNode, targetNode }, 75 | { node, fixer, context, importNodes, methodName } 76 | ) { 77 | yield fixer.replaceText( 78 | node, 79 | `sample(${buildObjectInText.fromMapOfNodes({ 80 | properties: { 81 | clock: clockNode, 82 | source: sourceNode, 83 | filter: filterNode, 84 | fn: mapperFunctionNode, 85 | target: targetNode, 86 | }, 87 | context, 88 | })})` 89 | ); 90 | 91 | const importNode = importNodes.get(methodName); 92 | 93 | if (!importNodes.has("sample")) { 94 | yield fixer.insertTextAfter(importNode, ", sample"); 95 | } 96 | } 97 | 98 | module.exports = { replaceForwardBySample, replaceGuardBySample }; 99 | -------------------------------------------------------------------------------- /utils/node-type-is.js: -------------------------------------------------------------------------------- 1 | const { ESLintUtils } = require("@typescript-eslint/utils"); 2 | 3 | function hasType({ node, possibleTypes, context, from }) { 4 | try { 5 | const parserServices = ESLintUtils.getParserServices(context); 6 | const checker = parserServices.program.getTypeChecker(); 7 | const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); 8 | const type = checker.getTypeAtLocation( 9 | originalNode?.initializer ?? originalNode 10 | ); 11 | 12 | const symbol = type?.symbol ?? type?.aliasSymbol; 13 | 14 | return ( 15 | possibleTypes.includes(symbol?.escapedName) && 16 | Boolean(symbol?.parent?.escapedName?.includes(from)) 17 | ); 18 | } catch (e) { 19 | return false; 20 | } 21 | } 22 | 23 | const nodeTypeIs = { 24 | effect: (opts) => 25 | hasType({ ...opts, possibleTypes: ["Effect"], from: "effector" }), 26 | store: (opts) => 27 | hasType({ 28 | ...opts, 29 | possibleTypes: ["Store", "StoreWritable"], 30 | from: "effector", 31 | }), 32 | event: (opts) => 33 | hasType({ 34 | ...opts, 35 | possibleTypes: ["Event", "EventCallable"], 36 | from: "effector", 37 | }), 38 | unit: (opts) => 39 | hasType({ 40 | ...opts, 41 | possibleTypes: [ 42 | "Effect", 43 | "Store", 44 | "Event", 45 | "EventCallable", 46 | "StoreWritable", 47 | ], 48 | from: "effector", 49 | }), 50 | gate: (opts) => 51 | hasType({ ...opts, possibleTypes: ["Gate"], from: "effector-react" }), 52 | effectorReactHook: (opts) => 53 | hasType({ 54 | ...opts, 55 | possibleTypes: opts.hook 56 | ? [].concat(opts.hook) 57 | : [ 58 | "useStore", 59 | "useStoreMap", 60 | "useList", 61 | "useEvent", 62 | "useGate", 63 | "useUnit", 64 | ], 65 | from: "effector-react", 66 | }), 67 | not: { 68 | effect: (opts) => 69 | !hasType({ 70 | ...opts, 71 | possibleTypes: ["Effect"], 72 | from: "effector", 73 | }), 74 | store: (opts) => 75 | !hasType({ 76 | ...opts, 77 | possibleTypes: ["Store", "StoreWritable"], 78 | from: "effector", 79 | }), 80 | event: (opts) => 81 | !hasType({ 82 | ...opts, 83 | possibleTypes: ["Event", "EventCallable"], 84 | from: "effector", 85 | }), 86 | unit: (opts) => 87 | !hasType({ 88 | ...opts, 89 | possibleTypes: [ 90 | "Effect", 91 | "Store", 92 | "Event", 93 | "EventCallable", 94 | "StoreWritable", 95 | ], 96 | from: "effector", 97 | }), 98 | gate: (opts) => 99 | !hasType({ ...opts, possibleTypes: ["Gate"], from: "effector-react" }), 100 | effectorReactHook: (opts) => !nodeTypeIs.effectorReactHook(opts), 101 | }, 102 | }; 103 | 104 | module.exports = { 105 | nodeTypeIs, 106 | }; 107 | -------------------------------------------------------------------------------- /rules/keep-options-order/keep-options-order.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require("eslint"); 2 | 3 | const { readExample } = require("../../utils/read-example"); 4 | 5 | const rule = require("./keep-options-order"); 6 | 7 | const ruleTester = new RuleTester({ 8 | parserOptions: { 9 | ecmaVersion: 2020, 10 | sourceType: "module", 11 | }, 12 | }); 13 | 14 | const readExampleForTheRule = (name) => readExample(__dirname, name); 15 | 16 | ruleTester.run("effector/keep-options-order.test", rule, { 17 | valid: ["correct-sample.js", "correct-guard.js"] 18 | .map(readExampleForTheRule) 19 | .map((code) => ({ code })), 20 | 21 | invalid: [ 22 | // Errors 23 | ...["incorrect-sample.js", "incorrect-guard.js"] 24 | .map(readExampleForTheRule) 25 | .map((code) => ({ 26 | code, 27 | errors: [ 28 | { 29 | messageId: "invalidOrder", 30 | type: "CallExpression", 31 | }, 32 | ], 33 | })), 34 | // Suggestions 35 | { 36 | code: ` 37 | import {sample} from 'effector'; 38 | sample({ source, clock, fn, target }); 39 | `, 40 | errors: [ 41 | { 42 | messageId: "invalidOrder", 43 | suggestions: [ 44 | { 45 | messageId: "changeOrder", 46 | output: ` 47 | import {sample} from 'effector'; 48 | sample({ clock, source, fn, target }); 49 | `, 50 | }, 51 | ], 52 | }, 53 | ], 54 | }, 55 | { 56 | code: ` 57 | import {sample} from 'effector'; 58 | sample({ fn() { return null }, clock, target }); 59 | `, 60 | errors: [ 61 | { 62 | messageId: "invalidOrder", 63 | suggestions: [ 64 | { 65 | messageId: "changeOrder", 66 | output: ` 67 | import {sample} from 'effector'; 68 | sample({ clock, fn() { return null }, target }); 69 | `, 70 | }, 71 | ], 72 | }, 73 | ], 74 | }, 75 | { 76 | code: ` 77 | import {sample} from 'effector'; 78 | sample({ fn, clock: event.map(() => null), target }); 79 | `, 80 | errors: [ 81 | { 82 | messageId: "invalidOrder", 83 | suggestions: [ 84 | { 85 | messageId: "changeOrder", 86 | output: ` 87 | import {sample} from 'effector'; 88 | sample({ clock: event.map(() => null), fn, target }); 89 | `, 90 | }, 91 | ], 92 | }, 93 | ], 94 | }, 95 | { 96 | code: ` 97 | import {sample} from 'effector'; 98 | sample({ source: combine({ a: $a }), clock, target }); 99 | `, 100 | errors: [ 101 | { 102 | messageId: "invalidOrder", 103 | suggestions: [ 104 | { 105 | messageId: "changeOrder", 106 | output: ` 107 | import {sample} from 'effector'; 108 | sample({ clock, source: combine({ a: $a }), target }); 109 | `, 110 | }, 111 | ], 112 | }, 113 | ], 114 | }, 115 | ], 116 | }); 117 | --------------------------------------------------------------------------------