Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
feat(rule): add new rule for destructured units
  • Loading branch information
Olovyannikov committed Dec 9, 2025
commit d687cb36f45d09f9543b081b3aa0fc89fa5a09f0
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ coverage
docs/.vitepress/dist
docs/**/__*.md
docs/changelog.md
docs/.vitepress/cache
docs/.vitepress/cache
.idea
**/*.xml
1 change: 1 addition & 0 deletions config/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ module.exports = {
"effector/enforce-gate-naming-convention": "error",
"effector/mandatory-scope-binding": "error",
"effector/prefer-useUnit": "warn",
"effector/use-unit-destructuring": "warn",
},
};
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = {
"prefer-useUnit": require("./rules/prefer-useUnit/prefer-useUnit"),
"require-pickup-in-persist": require("./rules/require-pickup-in-persist/require-pickup-in-persist"),
"no-patronum-debug": require("./rules/no-patronum-debug/no-patronum-debug"),
"use-unit-destructuring": require("./rules/use-unit-destructuring/use-unit-destructuring"),
},
configs: {
recommended: require("./config/recommended"),
Expand Down
139 changes: 139 additions & 0 deletions rules/use-unit-destructuring/use-unit-destructuring.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
const { createLinkToRule } = require("../../utils/create-link-to-rule");
module.exports = {
meta: {
type: "problem",
docs: {
description:
"Ensure destructured properties match the passed unit object/array",
category: "Best Practices",
recommended: true,
url: createLinkToRule("use-unit-destructuring"),
},
messages: {
unusedKey: 'Property "{{key}}" is passed but not destructured',
missingKey:
'Property "{{key}}" is destructured but not passed in the unit object',
implicitSubscription:
"Element at index {{index}} ({{name}}) is passed but not destructured, causing implicit subscription",
},
schema: [],
},
create(context) {
return {
CallExpression(node) {
// Search for useUnit
if (
node.callee.type !== "Identifier" ||
node.callee.name !== "useUnit" ||
node.arguments.length === 0
) {
return;
}

const argument = node.arguments[0];
const parent = node.parent;

if (parent.type !== "VariableDeclarator") {
return;
}

// Shape is Object-like
if (
argument.type === "ObjectExpression" &&
parent.id.type === "ObjectPattern"
) {
handleObjectPattern(context, argument, parent.id);
}

// Shape is Array-like
if (
argument.type === "ArrayExpression" &&
parent.id.type === "ArrayPattern"
) {
handleArrayPattern(context, argument, parent.id);
}
},
};
},
};

function handleObjectPattern(context, objectArgument, objectPattern) {
// Collect all keys from argument object
const argumentKeys = new Set(
objectArgument.properties
.filter(
(prop) => prop.type === "Property" && prop.key.type === "Identifier"
)
.map((prop) => prop.key.name)
);

// Collect destructured keys
const destructuredKeys = new Set(
objectPattern.properties
.filter(
(prop) => prop.type === "Property" && prop.key.type === "Identifier"
)
.map((prop) => prop.key.name)
);

// Check unused keys
for (const key of argumentKeys) {
if (!destructuredKeys.has(key)) {
context.report({
node: objectArgument,
messageId: "unusedKey",
data: { key },
});
}
}

// Check missing keys
for (const key of destructuredKeys) {
if (!argumentKeys.has(key)) {
context.report({
node: objectPattern,
messageId: "missingKey",
data: { key },
});
}
}
}

function handleArrayPattern(context, arrayArgument, arrayPattern) {
const argumentElements = arrayArgument.elements;
const destructuredElements = arrayPattern.elements;

// Check all array elements was destructured
const destructuredCount = destructuredElements.filter(
(el) => el !== null
).length;
const argumentCount = argumentElements.filter((el) => el !== null).length;

if (destructuredCount < argumentCount) {
// If undestructured elements exists
for (let i = destructuredCount; i < argumentCount; i++) {
const element = argumentElements[i];
if (element) {
// Get the name of variable for an info message
let name = "unknown";
if (element.type === "Identifier") {
name = element.name;
} else if (element.type === "MemberExpression") {
const sourceCode = context.getSourceCode
? context.getSourceCode()
: context.sourceCode;
name = sourceCode.getText(element);
}

context.report({
node: element,
messageId: "implicitSubscription",
data: {
index: i,
name: name,
},
});
}
}
}
}
1 change: 1 addition & 0 deletions rules/use-unit-destructuring/use-unit-destructuring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://eslint.effector.dev/rules/use-unit-destructuring.html
161 changes: 161 additions & 0 deletions rules/use-unit-destructuring/use-unit-destructuring.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
const { RuleTester } = require("eslint");
const rule = require("./use-unit-destructuring");

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

ruleTester.run("effector/use-unit-destructuring.test", rule, {
valid: [
// All keys were destructured
{
code: `
import { useUnit } from "effector-react";
const { value, setValue } = useUnit({
value: $store,
setValue: event,
});
`,
},
// All keys were destructured
{
code: `
import { useUnit } from "effector-react";
const [value, setValue] = useUnit([$store, event]);
`,
},
// With one element in object-shape
{
code: `
import { useUnit } from "effector-react";
const { value } = useUnit({ value: $store });
`,
},
// With one element in array-shape
{
code: `
import { useUnit } from "effector-react";
const [value] = useUnit([$store]);
`,
},
// Is not useUnit - no check
{
code: `
const { value } = someOtherFunction({
value: $store,
setValue: event,
});
`,
},
],

invalid: [
// Object: not destructured
{
code: `
import { useUnit } from "effector-react";
const { value } = useUnit({
value: $store,
setValue: event,
});
`,
errors: [
{
messageId: "unusedKey",
data: { key: "setValue" },
},
],
},
// Object: destructured, but key does not exist
{
code: `
import { useUnit } from "effector-react";
const { value, setValue, extra } = useUnit({
value: $store,
setValue: event,
});
`,
errors: [
{
messageId: "missingKey",
data: { key: "extra" },
},
],
},
// Array: implicit subscription (not all elements were destructuring)
{
code: `
import { useUnit } from "effector-react";
const [setValue] = useUnit([event, $store]);
`,
errors: [
{
messageId: "implicitSubscription",
data: { index: 1, name: "$store" },
},
],
},
// Array: several implicit subscriptions
{
code: `
import { useUnit } from "effector-react";
const [value] = useUnit([$store, event, $anotherStore]);
`,
errors: [
{
messageId: "implicitSubscription",
data: { index: 1, name: "event" },
},
{
messageId: "implicitSubscription",
data: { index: 2, name: "$anotherStore" },
},
],
},
// Object: several unused keys
{
code: `
import { useUnit } from "effector-react";
const { value } = useUnit({
value: $store,
setValue: event,
reset: resetEvent,
});
`,
errors: [
{
messageId: "unusedKey",
data: { key: "setValue" },
},
{
messageId: "unusedKey",
data: { key: "reset" },
},
],
},
{
code: `
import React, { Fragment } from "react";
import { useUnit } from "effector-react";

const ObjectShapeComponent = () => {
const { value } = useUnit({
value: $store,
setValue: event,
});
return <Fragment>{value}</Fragment>;
};
`,
errors: [
{
messageId: "unusedKey",
data: { key: "setValue" },
},
],
},
],
});