Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-foxes-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-effector": minor
---

Improve `enforce-effect-naming-convention` to enforce naming in binding contexts (destructuring & function parameters)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ description: Enforce Fx as a suffix for any Effector Effect

# effector/enforce-effect-naming-convention

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.
Enforces Effector naming conventions to reduce code reading overhead by clearly and consistently marking all `Effect`s with an `Fx` suffix across the codebase.

`Effect`s must be distinguished from other variables by an `Fx` suffix. For example, `fetchUserInfoFx` is an effect, `fetchUserInfo` is not.

## Configuration

Expand All @@ -20,8 +22,10 @@ Enforcing naming conventions helps keep the codebase consistent, and reduces ove

```ts
// 👍 nice name
const fetchNameFx = createEffect()
const fetchUserFx = createEffect()
const { fetchUserFx, fetchPostFx } = useContext(ApiContext)

// 👎 bad name
const fetchName = createEffect()
const fetchUser = createEffect()
const { fetchUser, fetchPost } = useContext(ApiContext)
```
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,76 @@ ruleTester.run("enforce-effect-naming-convention", rule, {
const renamedFx = attachedFx
`,
},
{
name: "effect as destructured argument",
code: ts`
import { type Effect, createEffect } from "effector"

type QueryParams = { runFx: Effect<void, void> }

const alpha = ({ runFx = createEffect() }: QueryParams) => undefined
const beta = ({ runFx }: QueryParams) => undefined
const gamma = (runFx = createEffect()) => undefined
`,
},
{
name: "shape destructuring -> ident",
code: ts`
import { createEffect } from "effector"

const { fooFx } = { fooFx: createEffect() }
const { foo: fooFx } = { foo: createEffect() }
`,
},
{
name: "shape destructuring -> assignment",
code: ts`
import { createEffect } from "effector"

const { foo: fooFx = createEffect() } = { foo: null }
const { bar: barFx = createEffect() } = { bar: createEffect() }
`,
},
{
name: "array destructuring -> ident",
code: ts`
import { createEffect, attach } from "effector"

const baseFx = createEffect()
const [firstFx] = [createEffect()]
const [secondFx, thirdFx] = [attach({ effect: baseFx }), createEffect()]
`,
},
{
name: "array destructuring -> assignment",
code: ts`
import { createEffect } from "effector"

const [firstFx = createEffect()] = [null]
const [secondFx = createEffect()] = [createEffect()]
`,
},
{
name: "mixed nested destructuring -> assignment",
code: ts`
import { createEffect } from "effector"

const {
first: [secondFx = createEffect()],
} = { first: [null] }
`,
},
{
name: "property in argument context",
code: ts`
import { createEffect, attach } from "effector"

const sourceFx = createEffect()

const attachedFx = attach({ effect: sourceFx })
const grouped = { alpha: createEffect(), beta: attach({ effect: sourceFx }) }
`,
},
],
invalid: [
{
Expand Down Expand Up @@ -163,5 +233,186 @@ ruleTester.run("enforce-effect-naming-convention", rule, {
},
],
},
{
name: "variable with type annotation",
code: ts`
import { createEffect, type Effect } from "effector"

const fetch: Effect<void, void> = createEffect()
`,
errors: [{ messageId: "invalid", line: 3, data: { current: "fetch", fixed: "fetchFx" } }],
},
{
name: "shape destructuring",
code: ts`
import { createEffect } from "effector"

const { first } = { first: createEffect() }
const { fooFx: first } = { fooFx: createEffect() }
`,
errors: [
{
messageId: "invalid",
line: 3,
suggestions: [
{
messageId: "rename",
data: { current: "first", fixed: "firstFx" },
output: ts`
import { createEffect } from "effector"

const { first: firstFx } = { first: createEffect() }
const { fooFx: first } = { fooFx: createEffect() }
`,
},
],
},
{
messageId: "invalid",
line: 4,
suggestions: [
{
messageId: "rename",
data: { current: "first", fixed: "firstFx" },
output: ts`
import { createEffect } from "effector"

const { first } = { first: createEffect() }
const { fooFx: firstFx } = { fooFx: createEffect() }
`,
},
],
},
],
},
{
name: "shape destructuring with assignment",
code: ts`
import { createEffect, attach } from "effector"

const sourceFx = createEffect()

const { first = createEffect() } = { first: sourceFx }
const { second: beta = attach({ effect: sourceFx }) } = { second: sourceFx }
`,
errors: [
{
messageId: "invalid",
line: 5,
suggestions: [
{
messageId: "rename",
data: { current: "first", fixed: "firstFx" },
output: ts`
import { createEffect, attach } from "effector"

const sourceFx = createEffect()

const { first: firstFx = createEffect() } = { first: sourceFx }
const { second: beta = attach({ effect: sourceFx }) } = { second: sourceFx }
`,
},
],
},
{
messageId: "invalid",
line: 6,
suggestions: [
{
messageId: "rename",
data: { current: "beta", fixed: "betaFx" },
output: ts`
import { createEffect, attach } from "effector"

const sourceFx = createEffect()

const { first = createEffect() } = { first: sourceFx }
const { second: betaFx = attach({ effect: sourceFx }) } = { second: sourceFx }
`,
},
],
},
],
},
{
name: "array destructuring",
code: ts`
import { createEffect } from "effector"

const [first, second = createEffect()] = [createEffect()]
`,
errors: [
{
messageId: "invalid",
line: 3,
suggestions: [
{
messageId: "rename",
data: { current: "first", fixed: "firstFx" },
output: ts`
import { createEffect } from "effector"

const [firstFx, second = createEffect()] = [createEffect()]
`,
},
],
},
{
messageId: "invalid",
line: 3,
suggestions: [
{
messageId: "rename",
data: { current: "second", fixed: "secondFx" },
output: ts`
import { createEffect } from "effector"

const [first, secondFx = createEffect()] = [createEffect()]
`,
},
],
},
],
},
{
name: "function parameter nested inferred destructuring",
code: ts`
import { type Effect } from "effector"

type Config = { effect: Effect<void, void> }
function test({ config: { effect } }: { config: Config }) {}
`,
errors: [
{
messageId: "invalid",
line: 4,
suggestions: [
{
messageId: "rename",
data: { current: "effect", fixed: "effectFx" },
output: ts`
import { type Effect } from "effector"

type Config = { effect: Effect<void, void> }
function test({ config: { effect: effectFx } }: { config: Config }) {}
`,
},
],
},
],
},
{
name: "function parameter inferred",
code: ts`
import { type Effect } from "effector"

function alpha(effect: Effect<void, void>) {}
const beta = (effect: Effect<void, void>) => {}
`,
errors: [
{ messageId: "invalid", line: 3, data: { current: "effect", fixed: "effectFx" } },
{ messageId: "invalid", line: 4, data: { current: "effect", fixed: "effectFx" } },
],
},
],
})
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { ESLintUtils, type TSESTree as Node, type TSESLint } from "@typescript-eslint/utils"
import { ESLintUtils, TSESTree as Node, AST_NODE_TYPES as NodeType, type TSESLint } from "@typescript-eslint/utils"

import { createRule } from "@/shared/create"
import { isType } from "@/shared/is"

type Suggestion = TSESLint.SuggestionReportDescriptor<"invalid" | "rename">

type ShapeProperty = Node.Property &
({ value: Node.Identifier } | { value: Node.AssignmentPattern & { left: Node.Identifier } })

export default createRule({
name: "enforce-effect-naming-convention",
meta: {
Expand All @@ -11,7 +16,7 @@ export default createRule({
description: "Enforce Fx as a suffix for any Effector Effect.",
},
messages: {
invalid: "Effect `{{ current }}` should be named with suffix, rename it to `{{ fixed }}`",
invalid: 'Effect "{{ current }}" should be named with `Fx` suffix, rename it to "{{ fixed }}"',
rename: 'Rename "{{ current }}" to "{{ fixed }}"',
},
schema: [],
Expand All @@ -21,30 +26,69 @@ export default createRule({
create: (context) => {
const services = ESLintUtils.getParserServices(context)

type VariableDeclarator = Node.VariableDeclarator & { id: Node.Identifier }

return {
[`VariableDeclarator[id.name!=${FxRegex}]`]: (node: VariableDeclarator) => {
const type = services.getTypeAtLocation(node)
[`${selector.variable}, ${selector.array.identifier}, ${selector.array.assignment}, ${selector.function.identifier}, ${selector.function.assignment}`]:
(node: Node.Identifier) => {
const type = services.getTypeAtLocation(node)

const isEffect = isType.effect(type, services.program)
if (!isEffect) return

const data = { current: node.name, fixed: node.name + "Fx" }

// type annotation is included in `range` so we can't reliably replace text without erasing the annotation
if (node.typeAnnotation) return context.report({ node, messageId: "invalid", data })

const suggestion: Suggestion = {
messageId: "rename",
data: { current: node.name, fixed: data.fixed },
fix: (fixer) => fixer.replaceText(node, data.fixed),
}

context.report({ node, messageId: "invalid", data, suggest: [suggestion] })
},

[`${selector.shape.identifier}, ${selector.shape.assignment}`]: (node: ShapeProperty) => {
const type = services.getTypeAtLocation(node.value)
const ident = node.value.type === NodeType.Identifier ? node.value : node.value.left

const isEffect = isType.effect(type, services.program)
if (!isEffect) return

const current = node.id.name
const fixed = current + "Fx"
const data = { current: ident.name, fixed: ident.name + "Fx" }

const data = { current, fixed }
// type annotation is included in `range` so we can't reliably replace text without erasing the annotation
if (ident.typeAnnotation) return context.report({ node: ident, messageId: "invalid", data })

const suggestion = {
messageId: "rename" as const,
data: { current, fixed },
fix: (fixer: TSESLint.RuleFixer) => fixer.replaceText(node.id, fixed),
const suggestion: Suggestion = {
messageId: "rename",
data: { current: ident.name, fixed: data.fixed },
fix: (fixer) =>
node.shorthand
? fixer.insertTextAfter(node.key, `: ${data.fixed}`) // { x } -> { x: xFx }
: fixer.replaceText(ident, data.fixed), // { x: y } -> { x: yFx }
}

context.report({ node: node.id, messageId: "invalid", data, suggest: [suggestion] })
context.report({ node: ident, messageId: "invalid", data, suggest: [suggestion] })
},
}
},
})

const FxRegex = /Fx$/

const selector = {
variable: `VariableDeclarator > Identifier.id[name!=${FxRegex}]`,
array: {
identifier: `ArrayPattern > Identifier.elements[name!=${FxRegex}]`,
assignment: `ArrayPattern > AssignmentPattern > Identifier.left[name!=${FxRegex}]`,
},
shape: {
identifier: `ObjectPattern > Property:has(> Identifier.value[name!=${FxRegex}])`,
assignment: `ObjectPattern > Property:has(> AssignmentPattern:has(> Identifier.left[name!=${FxRegex}]))`,
},
function: {
identifier: `:function > Identifier.params[name!=${FxRegex}]`,
assignment: `:function > AssignmentPattern > Identifier.left[name!=${FxRegex}]`,
},
}
Loading