From 5bf5a0742de84c4eb4d112b4d5b906365eeb6ce0 Mon Sep 17 00:00:00 2001 From: Ilya Olovyannikov Date: Tue, 9 Dec 2025 12:56:00 +0300 Subject: [PATCH 1/3] feat(rule): add new rule for destructured units --- .gitignore | 3 + config/react.js | 0 index.js | 0 .../use-unit-destructuring.js | 139 +++++++++++++++ .../use-unit-destructuring.md | 1 + .../use-unit-destructuring.test.js | 161 ++++++++++++++++++ 6 files changed, 304 insertions(+) create mode 100644 config/react.js create mode 100644 index.js create mode 100644 rules/use-unit-destructuring/use-unit-destructuring.js create mode 100644 rules/use-unit-destructuring/use-unit-destructuring.md create mode 100644 rules/use-unit-destructuring/use-unit-destructuring.test.js diff --git a/.gitignore b/.gitignore index 61450cc..91cd5fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.idea +**/*.xml + # Logs logs *.log diff --git a/config/react.js b/config/react.js new file mode 100644 index 0000000..e69de29 diff --git a/index.js b/index.js new file mode 100644 index 0000000..e69de29 diff --git a/rules/use-unit-destructuring/use-unit-destructuring.js b/rules/use-unit-destructuring/use-unit-destructuring.js new file mode 100644 index 0000000..66fb48d --- /dev/null +++ b/rules/use-unit-destructuring/use-unit-destructuring.js @@ -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, + }, + }); + } + } + } +} diff --git a/rules/use-unit-destructuring/use-unit-destructuring.md b/rules/use-unit-destructuring/use-unit-destructuring.md new file mode 100644 index 0000000..6ad7127 --- /dev/null +++ b/rules/use-unit-destructuring/use-unit-destructuring.md @@ -0,0 +1 @@ +https://eslint.effector.dev/rules/use-unit-destructuring.html diff --git a/rules/use-unit-destructuring/use-unit-destructuring.test.js b/rules/use-unit-destructuring/use-unit-destructuring.test.js new file mode 100644 index 0000000..68d62e4 --- /dev/null +++ b/rules/use-unit-destructuring/use-unit-destructuring.test.js @@ -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 {value}; + }; + `, + errors: [ + { + messageId: "unusedKey", + data: { key: "setValue" }, + }, + ], + }, + ], +}); From 6c9cd6960d481f2a89af0f2215c620481b80419a Mon Sep 17 00:00:00 2001 From: Ilya Olovyannikov Date: Tue, 9 Dec 2025 13:01:33 +0300 Subject: [PATCH 2/3] docs(rule): add docs for new rule --- docs/rules/use-unit-destructuring.md | 145 +++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 docs/rules/use-unit-destructuring.md diff --git a/docs/rules/use-unit-destructuring.md b/docs/rules/use-unit-destructuring.md new file mode 100644 index 0000000..88d6d7e --- /dev/null +++ b/docs/rules/use-unit-destructuring.md @@ -0,0 +1,145 @@ +# effector/use-unit-destructuring + +[Related documentation](https://effector.dev/en/api/effector-react/useunit/) + +Ensures that all units passed to useUnit are properly destructured to avoid unused subscriptions and implicit re-renders. + +## Rule Details +This rule enforces that: +- All properties passed in an object to useUnit must be destructured to prevent implicit subscriptions; +- All elements passed in an array to useUnit must be destructured to prevent implicit subscriptions also. + +### Object shape +When using useUnit with an object, you must destructure all keys that you pass. Otherwise, unused units will still create subscriptions and cause unnecessary re-renders. +TypeScript + +```ts +// 👍 correct - all properties are destructured +const { value, setValue } = useUnit({ + value: $store, + setValue: event, +}); +``` + +```ts +// 👎 incorrect - setValue is not destructured but still creates subscription +const { value } = useUnit({ + value: $store, + setValue: event, // unused but subscribed! +}); +``` + +```ts +// 👎 incorrect - extra is destructured but not passed +const { + value, + setValue, + extra // extra is missing - will be undefined +} = useUnit({ + value: $store, + setValue: event, +}); +``` + +### Array shape +When using useUnit with an array, you must destructure all elements. Elements that are not destructured will still create subscriptions, leading to implicit re-renders. +TypeScript + +```ts +// 👍 correct - all elements are destructured +const [value, setValue] = useUnit([$store, event]); +``` + +```ts +// 👎 incorrect - $store is not destructured but creates implicit subscription +const [setValue] = useUnit([event, $store]); +// Component will re-render when $store changes, even though you don't use it! +``` + +```ts +// 👎 incorrect - event and $anotherStore cause implicit subscriptions +const [value] = useUnit([$store, event, $anotherStore]); +// Component re-renders on $store, event, and $anotherStore changes +``` + +## Why is this important? +Implicit subscriptions can lead to: +- Performance issues: unnecessary re-renders when unused stores update +- Hard-to-debug behavior: component re-renders for unclear reasons +- Memory leaks: subscriptions that are never cleaned up properly + +## Examples + +### Real-world example + +```tsx +import React, { Fragment } from "react"; +import { createEvent, createStore } from "effector"; +import { useUnit } from "effector-react"; + +const $store = createStore("Hello World!"); +const event = createEvent(); + +// 👎 incorrect +const BadComponent = () => { + const { value } = useUnit({ + value: $store, + setValue: event, // ❌ not used but subscribed! + }); + + return {value}; +}; + +// 👍 correct +const GoodComponent = () => { + const { value, setValue } = useUnit({ + value: $store, + setValue: event, + }); + + return ; +}; +``` + +```tsx +import React, { Fragment } from "react"; +import { createEvent, createStore } from "effector"; +import { useUnit } from "effector-react"; + +const $store = createStore("Hello World!"); +const event = createEvent(); + +// 👎 incorrect - implicit subscription to $store +const BadComponent = () => { + const [setValue] = useUnit([event, $store]); // ❌ $store not used but subscribed! + + return ; +}; + +// 👍 correct - explicit destructuring +const GoodComponent = () => { + const [value, setValue] = useUnit([$store, event]); + + return ; +}; + +// 👍 also correct - only pass what you need +const AlsoGoodComponent = () => { + const [setValue] = useUnit([event]); // ✅ no implicit subscriptions + + return ; +}; +``` + +### When Not To Use It +If you intentionally want to subscribe to a store without using its value (rare case), you can disable this rule for that line: + +```tsx +// eslint-disable-next-line effector/use-unit-destructuring +const { value } = useUnit({ + value: $store, + trigger: $triggerStore, // intentionally subscribing without using +}); +``` + +However, in most cases, you should refactor your code to avoid implicit subscriptions. \ No newline at end of file From 80edafd92001e7cff9fcf3eae3b68282638f22e1 Mon Sep 17 00:00:00 2001 From: Ilya Olovyannikov Date: Wed, 4 Mar 2026 11:22:30 +0300 Subject: [PATCH 3/3] feat(use-unit-destructuring): update to v17 --- .../use-unit-destructuring.js | 139 ------------------ .../use-unit-destructuring.md | 1 - src/index.ts | 2 + .../use-unit-destructuring.md | 0 .../use-unit-destructuring.test.ts | 22 +-- .../use-unit-destructuring.ts | 138 +++++++++++++++++ src/ruleset.ts | 1 + 7 files changed, 154 insertions(+), 149 deletions(-) delete mode 100644 rules/use-unit-destructuring/use-unit-destructuring.js delete mode 100644 rules/use-unit-destructuring/use-unit-destructuring.md rename {docs/rules => src/rules/use-unit-destructuring}/use-unit-destructuring.md (100%) rename rules/use-unit-destructuring/use-unit-destructuring.test.js => src/rules/use-unit-destructuring/use-unit-destructuring.test.ts (90%) create mode 100644 src/rules/use-unit-destructuring/use-unit-destructuring.ts diff --git a/rules/use-unit-destructuring/use-unit-destructuring.js b/rules/use-unit-destructuring/use-unit-destructuring.js deleted file mode 100644 index 66fb48d..0000000 --- a/rules/use-unit-destructuring/use-unit-destructuring.js +++ /dev/null @@ -1,139 +0,0 @@ -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, - }, - }); - } - } - } -} diff --git a/rules/use-unit-destructuring/use-unit-destructuring.md b/rules/use-unit-destructuring/use-unit-destructuring.md deleted file mode 100644 index 6ad7127..0000000 --- a/rules/use-unit-destructuring/use-unit-destructuring.md +++ /dev/null @@ -1 +0,0 @@ -https://eslint.effector.dev/rules/use-unit-destructuring.html diff --git a/src/index.ts b/src/index.ts index bac8f24..0cee143 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ import noWatch from "./rules/no-watch/no-watch" import preferUseUnit from "./rules/prefer-useUnit/prefer-useUnit" import requirePickupInPersist from "./rules/require-pickup-in-persist/require-pickup-in-persist" import strictEffectHandlers from "./rules/strict-effect-handlers/strict-effect-handlers" +import useUnitDestructuring from "./rules/use-unit-destructuring/use-unit-destructuring" import { ruleset } from "./ruleset" const base = { @@ -45,6 +46,7 @@ const base = { "no-useless-methods": noUselessMethods, "no-watch": noWatch, "prefer-useUnit": preferUseUnit, + "use-unit-destructuring": useUnitDestructuring, "require-pickup-in-persist": requirePickupInPersist, "strict-effect-handlers": strictEffectHandlers, }, diff --git a/docs/rules/use-unit-destructuring.md b/src/rules/use-unit-destructuring/use-unit-destructuring.md similarity index 100% rename from docs/rules/use-unit-destructuring.md rename to src/rules/use-unit-destructuring/use-unit-destructuring.md diff --git a/rules/use-unit-destructuring/use-unit-destructuring.test.js b/src/rules/use-unit-destructuring/use-unit-destructuring.test.ts similarity index 90% rename from rules/use-unit-destructuring/use-unit-destructuring.test.js rename to src/rules/use-unit-destructuring/use-unit-destructuring.test.ts index 68d62e4..de1eb69 100644 --- a/rules/use-unit-destructuring/use-unit-destructuring.test.js +++ b/src/rules/use-unit-destructuring/use-unit-destructuring.test.ts @@ -1,15 +1,18 @@ -const { RuleTester } = require("eslint"); -const rule = require("./use-unit-destructuring"); +import { RuleTester } from "@typescript-eslint/rule-tester" + +import rule from "./use-unit-destructuring" const ruleTester = new RuleTester({ - parserOptions: { - ecmaVersion: 2020, - sourceType: "module", - ecmaFeatures: { jsx: true }, + languageOptions: { + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + ecmaFeatures: { jsx: true }, + }, }, -}); +}) -ruleTester.run("effector/use-unit-destructuring.test", rule, { +ruleTester.run("effector/use-unit-destructuring", rule, { valid: [ // All keys were destructured { @@ -137,6 +140,7 @@ ruleTester.run("effector/use-unit-destructuring.test", rule, { }, ], }, + // JSX component with object-shape { code: ` import React, { Fragment } from "react"; @@ -158,4 +162,4 @@ ruleTester.run("effector/use-unit-destructuring.test", rule, { ], }, ], -}); +}) diff --git a/src/rules/use-unit-destructuring/use-unit-destructuring.ts b/src/rules/use-unit-destructuring/use-unit-destructuring.ts new file mode 100644 index 0000000..3fc6397 --- /dev/null +++ b/src/rules/use-unit-destructuring/use-unit-destructuring.ts @@ -0,0 +1,138 @@ +import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils" + +import { createRule } from "@/shared/create" + +type MessageIds = "unusedKey" | "missingKey" | "implicitSubscription" +type Options = [] + +export default createRule({ + name: "use-unit-destructuring", + meta: { + type: "problem", + docs: { + description: "Ensure destructured properties match the passed unit object/array", + }, + 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: [], + }, + defaultOptions: [], + create(context) { + function handleObjectPattern( + objectArgument: TSESTree.ObjectExpression, + objectPattern: TSESTree.ObjectPattern, + ): void { + // Collect all keys from argument object + const argumentKeys = new Set( + objectArgument.properties + .filter( + (prop): prop is TSESTree.Property => + prop.type === AST_NODE_TYPES.Property && prop.key.type === AST_NODE_TYPES.Identifier, + ) + .map((prop) => (prop.key as TSESTree.Identifier).name), + ) + + // Collect destructured keys + const destructuredKeys = new Set( + objectPattern.properties + .filter( + (prop): prop is TSESTree.Property => + prop.type === AST_NODE_TYPES.Property && prop.key.type === AST_NODE_TYPES.Identifier, + ) + .map((prop) => (prop.key as TSESTree.Identifier).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(arrayArgument: TSESTree.ArrayExpression, arrayPattern: TSESTree.ArrayPattern): void { + const argumentElements = arrayArgument.elements + const destructuredElements = arrayPattern.elements + + const destructuredCount = destructuredElements.filter((el) => el !== null).length + const argumentCount = argumentElements.filter((el) => el !== null).length + + if (destructuredCount >= argumentCount) return + + // If undestructured elements exists + for (let i = destructuredCount; i < argumentCount; i++) { + const element = argumentElements[i] + if (!element || element.type === AST_NODE_TYPES.SpreadElement) continue + + let name = "unknown" + + if (element.type === AST_NODE_TYPES.Identifier) { + name = element.name + } else if (element.type === AST_NODE_TYPES.MemberExpression) { + name = context.sourceCode.getText(element) + } + + context.report({ + node: element, + messageId: "implicitSubscription", + data: { + index: i, + name, + }, + }) + } + } + + return { + CallExpression(node): void { + if ( + node.callee.type !== AST_NODE_TYPES.Identifier || + node.callee.name !== "useUnit" || + node.arguments.length === 0 + ) { + return + } + + const argument = node.arguments[0] + const parent = node.parent + + if ( + !parent || + parent.type !== AST_NODE_TYPES.VariableDeclarator || + argument?.type === AST_NODE_TYPES.SpreadElement + ) { + return + } + + // Shape is Object-like + if (argument?.type === AST_NODE_TYPES.ObjectExpression && parent.id.type === AST_NODE_TYPES.ObjectPattern) { + handleObjectPattern(argument, parent.id) + } + + // Shape is Array-like + if (argument?.type === AST_NODE_TYPES.ArrayExpression && parent.id.type === AST_NODE_TYPES.ArrayPattern) { + handleArrayPattern(argument, parent.id) + } + }, + } + }, +}) diff --git a/src/ruleset.ts b/src/ruleset.ts index b11fbd2..5c57ba4 100644 --- a/src/ruleset.ts +++ b/src/ruleset.ts @@ -28,6 +28,7 @@ const react = { "effector/enforce-gate-naming-convention": "error", "effector/mandatory-scope-binding": "error", "effector/prefer-useUnit": "error", + "effector/use-unit-destructuring": "warn", } satisfies TSESLint.Linter.RulesRecord const future = {