diff --git a/packages/eslint-plugin/.eslintrc.js b/packages/eslint-plugin/.eslintrc.js index a0b2f073c..0377cb80b 100644 --- a/packages/eslint-plugin/.eslintrc.js +++ b/packages/eslint-plugin/.eslintrc.js @@ -1,16 +1,16 @@ -"use strict"; +'use strict' module.exports = { - "parserOptions": { - "ecmaVersion": "latest" + parserOptions: { + ecmaVersion: 'latest', }, root: true, extends: [ - "eslint:recommended", - "plugin:eslint-plugin/recommended", - "plugin:node/recommended", + 'eslint:recommended', + 'plugin:eslint-plugin/recommended', + 'plugin:node/recommended', ], env: { - "es6": true, + es6: true, }, -}; +} diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index e4ae03c59..cc8e2b780 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -1,3 +1,7 @@ +# `@reatom/eslint-plugin` + +Reatom-specific ESLint rules. + ## Installation ```sh @@ -6,7 +10,7 @@ npm i -D @reatom/eslint-plugin ## Usage -You should add `@reatom` to `plugins` and specify `extends` or `rules` into your config. +Add `@reatom` to `plugins` and specify `extends` or `rules` in your config. ```json { @@ -19,17 +23,15 @@ You should add `@reatom` to `plugins` and specify `extends` or `rules` into your { "plugins": ["@reatom"], "rules": { - "@reatom/atom-rule": "error", - "@reatom/action-rule": "error", - "@reatom/reatom-prefix-rule": "error", - "@reatom/atom-postfix-rule": "error" + "@reatom/async-rule": "error", + "@reatom/unit-naming-rule": "error" } } ``` Here is an example of React + TypeScript + Prettier config with Reatom. -> [Eslint setup commit](https://github.com/artalar/reatom-react-ts/commit/3632b01d6a58a35602d1c191e5d6b53a7717e747) +> [ESLint setup commit](https://github.com/artalar/reatom-react-ts/commit/3632b01d6a58a35602d1c191e5d6b53a7717e747) ```json { @@ -61,23 +63,63 @@ Here is an example of React + TypeScript + Prettier config with Reatom. } ``` +## Rules + +### `unit-naming-rule` + +Ensures that all Reatom entities specify the name parameter used for debugging. We assume that Reatom entity factories are `atom`, `action` and all `reatom*` (like `reatomAsync`) functions imported from `@reatom/*` packages. + +The name must be equal to the name of a variable or a property an entity is assigned to, like this: + +```ts +const count = atom(0, 'count') + +const someNamespace = { + count: atom(0, 'count'), +} +``` + +When creating atoms dynamically with factories, you can also specify the "namespace" of the name before the `.` symbol: + +```ts +const reatomUser = (_name: string) => { + const name = atom(_name, 'reatomUser.name') + + return { name } +} +``` + +For private atoms, `_` prefix can be used: + +```ts +const secretState = atom(0, '_secretState') +``` + +You can also ensure that `atom` names have a prefix or a postfix through the configuration, for example: + +```ts +{ + atomPrefix: '', + atomPostfix: 'Atom', +} +``` + +### `async-rule` + +Ensures that asynchronous interactions within Reatom functions are wrapped with `ctx.schedule`. Read [the docs](https://www.reatom.dev/package/core/#ctx-api) for more info. + ## Motivation - Many have asked why not make a Babel plugin for naming, why keep it in source? Our opinion on this has long been formed, keep it: +The primary purpose of this plugin is to automate generation of atom and action names using ESLint autofixes. Many have asked why not make a Babel plugin for naming, why keep it in source, here is our opinion: -- Build tools are different, besides the outdated Babel there are a lot of other tools, and even written in different languages, it is difficult to support all of them; -- The result of the plugin's work is not visible and can lead to unpleasant debugging; -- The plugins and transpilation tools themselves often catch some bugs, partly due to the complexity of JavaScript as a language and partly due to the fragmentation of the ecosystem; -- It is difficult to cover all cases with a single transpilation, factories cause the most problems; -- The name contains a hash - redundant information when debugging; -- The hash is tied to the file and line - it easily changes with the slightest refactoring, causing the user cache to drop, this must be taken into account in automatic migrations (talking about the client persist state). +- Build infrastructure is diverse and divergent - it's hard to support a plenty of tools used today; +- Plugin's output may be unexpected; +- Such plugin is hard to implement because of variety of naming strategies, especially in factories; -This list comes from real practice, for the first Reatom version (2019) there was a plugin and it was already clear that the game was not worth the candle, now the situation is even worse. +These are the problems we faced back in 2019 when the first version of Reatom was released. They made it clear for us that the game is not worth the candle. -On the contrary, writing the name manually in the argument has a lot of advantages: +On the contrary, explicit unit names have multiple advantages: -- Maximum visibility; -- Full control; -- Zero setup, not difficult, really not difficult, in terms of the amount and complexity of code required forces - nothing; -- AI tools, like Copilot, help a lot with the names; -- This ESLint plugin handles most cases perfectly. +- No risk of unexpected plugin behaviour, full control over unit names; +- Requires no build infrastructure and is not that hard to do; +- Writing names is simplified even further by AI coding helpers (i.e. Copilot) and this set of ESLint rules. diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 136cdc600..e52ee5fe4 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -3,7 +3,7 @@ "version": "3.4.3", "private": false, "sideEffects": false, - "description": "Reatom for eslint-plugin", + "description": "Reatom-specific ESLint rules", "source": "src/index.ts", "exports": { "types": "./build/index.d.ts", @@ -20,8 +20,8 @@ "sandbox": "vite", "prepublishOnly": "npm run build && npm run test", "build": "microbundle -f esm,cjs", - "test": "ts-node src/tests/index.ts", - "test:watch": "tsx watch src/**/*.test.ts" + "test": "ts-node src/index.test.ts", + "test:watch": "tsx watch src/index.test.ts" }, "author": "pivaszbs", "maintainers": [ diff --git a/packages/eslint-plugin/src/index.test.ts b/packages/eslint-plugin/src/index.test.ts new file mode 100644 index 000000000..d9a358332 --- /dev/null +++ b/packages/eslint-plugin/src/index.test.ts @@ -0,0 +1,2 @@ +// import './rules/unit-naming-rule.test' +import './rules/async-rule.test' diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts index 7902fa013..8646f0509 100644 --- a/packages/eslint-plugin/src/index.ts +++ b/packages/eslint-plugin/src/index.ts @@ -1,22 +1,21 @@ -import { actionRule } from './rules/action-rule' -import { atomPostfixRule } from './rules/atom-postifx-rule' -import { atomRule } from './rules/atom-rule' -import { reatomPrefixRule } from './rules/reatom-prefix-rule' +import { ESLint } from 'eslint' +import { asyncRule } from './rules/async-rule' +import { unitNamingRule } from './rules/unit-naming-rule' -export const rules = { - 'atom-rule': atomRule, - 'action-rule': actionRule, - 'reatom-prefix-rule': reatomPrefixRule, - 'atom-postfix-rule': atomPostfixRule, +const rules = { + 'unit-naming-rule': unitNamingRule, + 'async-rule': asyncRule, } -export const configs = { - recommended: { - rules: { - '@reatom/atom-rule': 'error', - '@reatom/action-rule': 'error', - '@reatom/reatom-prefix-rule': 'error', - '@reatom/atom-postfix-rule': 'error', +export default { + rules, + configs: { + recommended: { + rules: Object.fromEntries( + Object.keys(rules).map((ruleName) => { + return [`@reatom/${ruleName}`, 'error'] + }), + ), }, }, -} +} satisfies ESLint.Plugin diff --git a/packages/eslint-plugin/src/lib.ts b/packages/eslint-plugin/src/lib.ts deleted file mode 100644 index 89cef7a15..000000000 --- a/packages/eslint-plugin/src/lib.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { Identifier, Literal, Node, ImportDeclaration, VariableDeclarator, TemplateLiteral } from 'estree' - -export function isIdentifier(node: Node): node is Identifier { - return node?.type === 'Identifier' -} - -export function isLiteral(node: any): node is Literal { - return node && 'type' in node && node.type === 'Literal' -} - -export function isTemplateLiteral(node: any): node is TemplateLiteral { - return node && 'type' in node && node.type === 'TemplateLiteral' -} - -export interface ExtractConfig { - from: ImportDeclaration - packageName: string - importsAlias: Map - nodeMap?: Map - filter?: (original: string, local: string) => boolean -} - -export function extractImportDeclaration({ - from, - packageName, - importsAlias, - nodeMap, - filter = () => true, -}: ExtractConfig) { - const imported = from.source.value - - if (typeof imported === 'string' && imported.startsWith(packageName)) { - for (const method of from.specifiers) { - if (method.type === 'ImportDefaultSpecifier') continue - - if ('imported' in method && Boolean(method.imported)) { - const localName = method.local.name - const originalName = method.imported.name - - if (filter(originalName, localName)) { - importsAlias.set(originalName, localName) - nodeMap?.set(originalName, method) - } - } - } - } -} - -export function extractAssignedVariableName(node: Node | null) { - const identifier = extractAssignedVariable(node); - if (!identifier) return null; - - return identifier.name; -} - -export function extractAssignedVariable(node: Node | null) { - if (node?.type === 'VariableDeclarator' && 'name' in node.id) { - return node.id - } - - return node && 'key' in node && node.key?.type === 'Identifier' - ? node.key - : null -} - -export function traverseBy( - field: keyof T, - config: { - node: T - match: Set - exit?: string[] - }, -) { - const { match, exit = ['Program'], node } = config - - const stack = [node] - - while (stack.length > 0) { - const currentNode = stack.pop() - - if ( - !currentNode || - !includeField(currentNode, field) || - exit.includes(currentNode.type) - ) { - return null - } - - if (match.has(currentNode.type)) { - return currentNode - } - - stack.push(currentNode[field] as T) - } - - return null -} - -function includeField(node: Partial, field: keyof T) { - return node && field in node && Boolean(node[field]) -} diff --git a/packages/eslint-plugin/src/rules/action-rule.ts b/packages/eslint-plugin/src/rules/action-rule.ts deleted file mode 100644 index 3cf378b4e..000000000 --- a/packages/eslint-plugin/src/rules/action-rule.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { Rule } from 'eslint' -import type { - CallExpression, - Identifier, - Literal, - ArrowFunctionExpression, -} from 'estree' -import { - extractAssignedVariableName, - extractImportDeclaration, - isLiteral, - traverseBy, -} from '../lib' - -type ActionCallExpression = CallExpression & { - callee: Identifier - arguments: - | [] - | [Literal] - | [ArrowFunctionExpression] - | [ArrowFunctionExpression, Literal] -} - -export const actionRule: Rule.RuleModule = { - meta: { - type: 'suggestion', - docs: { - description: 'Add name for every action call', - }, - messages: { - noname: `action "{{ actionName }}" should has a name inside action() call`, - invalidName: `action "{{ actionName }}" should be named as it's variable name, rename it to "{{ actionName }}"`, - }, - fixable: 'code', - }, - create(context: Rule.RuleContext): Rule.RuleListener { - const importedFromReatom = new Map() - - return { - ImportDeclaration(node) { - extractImportDeclaration({ - from: node, - importsAlias: importedFromReatom, - packageName: '@reatom', - }) - }, - CallExpression(node) { - if (!isActionCallExpression(node, importedFromReatom)) return - - const matchBy = new Set([ - 'VariableDeclarator', - 'PropertyDefinition', - 'Property', - ]) - - const actionVariable = traverseBy('parent', { - match: matchBy, - node, - }) - - const actionName = extractAssignedVariableName(actionVariable) - - if (!actionName) { - return - } - - const amountOfArguments = node.arguments.length - - if (amountOfArguments === 0 && node.arguments) { - const sourceCode = context.getSourceCode() - const parenthesesToken = sourceCode.getTokens(node) - - const [betweenParentheses] = parenthesesToken.filter((token, idx) => { - return ( - token.value === '(' && parenthesesToken[idx + 1]?.value === ')' - ) - }) - - if (!betweenParentheses) return - - context.report({ - node, - messageId: 'noname', - data: { actionName }, - fix(fixer) { - return fixer.insertTextAfter( - betweenParentheses, - `"${actionName}"`, - ) - }, - }) - return - } - - if ( - isLiteral(node.arguments[0]) && - node.arguments[0].value !== actionName - ) { - context.report({ - node, - messageId: 'invalidName', - data: { actionName }, - fix(fixer) { - return fixer.replaceText(node.arguments[0], `"${actionName}"`) - }, - }) - return - } - - if (node.arguments[0].type === 'ArrowFunctionExpression') { - if (amountOfArguments === 1) { - const afterInsert = node.arguments[0] - - context.report({ - node, - messageId: 'noname', - data: { actionName }, - fix(fixer) { - return fixer.insertTextAfter(afterInsert, `, "${actionName}"`) - }, - }) - return - } - - if ( - amountOfArguments === 2 && - node.arguments[1].value !== actionName - ) { - const forReplace = node.arguments[1] - - context.report({ - node, - messageId: 'invalidName', - data: { actionName }, - fix(fixer) { - return fixer.replaceText(forReplace, `"${actionName}"`) - }, - }) - return - } - } - }, - } - }, -} - -function isActionCallExpression( - node: CallExpression, - importedFromReatom: Map, -): node is ActionCallExpression { - return ( - node?.type === 'CallExpression' && - node.callee?.type === 'Identifier' && - importedFromReatom.get('action') === node.callee.name && - (node.arguments.length === 0 || - (node.arguments.length === 1 && node.arguments[0]?.type === 'Literal') || - (node.arguments.length === 1 && - node.arguments[0]?.type === 'ArrowFunctionExpression') || - (node.arguments.length === 2 && - node.arguments[0]?.type === 'ArrowFunctionExpression' && - node.arguments[1]?.type == 'Literal')) - ) -} diff --git a/packages/eslint-plugin/src/rules/async-rule.test.ts b/packages/eslint-plugin/src/rules/async-rule.test.ts new file mode 100644 index 000000000..483ec6454 --- /dev/null +++ b/packages/eslint-plugin/src/rules/async-rule.test.ts @@ -0,0 +1,27 @@ +import { RuleTester } from 'eslint' +import { asyncRule } from './async-rule' + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, +}) + +const ImportReatomAsync = 'import {reatomAsync} from "@reatom/framework"' +const ImportReatomAsyncAlias = + 'import {reatomAsync as createAsync} from "@reatom/framework"' + +tester.run('async-rule', asyncRule, { + valid: [ + `${ImportReatomAsync}; const reatomSome = reatomAsync(async ctx => await ctx.schedule(() => someEffect()))`, + `${ImportReatomAsyncAlias}; const reatomSome = createAsync(async ctx => await ctx.schedule(() => someEffect()))`, + ], + invalid: [ + { + code: `${ImportReatomAsync}; const reatomSome = reatomAsync(async ctx => await someEffect())`, + errors: [{ messageId: 'scheduleMissing' }], + output: `${ImportReatomAsync}; const reatomSome = reatomAsync(async ctx => await ctx.schedule(() => someEffect()))`, + }, + ], +}) diff --git a/packages/eslint-plugin/src/rules/async-rule.ts b/packages/eslint-plugin/src/rules/async-rule.ts new file mode 100644 index 000000000..f8cf3954e --- /dev/null +++ b/packages/eslint-plugin/src/rules/async-rule.ts @@ -0,0 +1,63 @@ +import type { Rule } from 'eslint' +import type * as estree from 'estree' +import { ascend, createImportMap, isReatomFactoryName } from '../shared' + +const ReatomFactoryPrefix = 'reatom' + +export const asyncRule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + recommended: true, + description: + 'Ensures that asynchronous interactions within Reatom functions are wrapped with `ctx.schedule`.', + }, + messages: { + scheduleMissing: + 'Asynchronous interactions within Reatom functions should be wrapped with `ctx.schedule`', + }, + fixable: 'code', + }, + create(context: Rule.RuleContext): Rule.RuleListener { + const imports = createImportMap('@reatom') + + return { + ImportDeclaration: imports.onImportNode, + AwaitExpression(node) { + const fn = ascend(node, 'ArrowFunctionExpression', 'FunctionExpression') + if (!fn) return + + if (fn.parent.type !== 'CallExpression') return + if (fn.parent.callee.type !== 'Identifier') return + if (!isReatomFactoryName(fn.parent.callee.name)) return + + if (isCtxSchedule(node.argument)) return + + context.report({ + node, + messageId: 'scheduleMissing', + fix: (fixer) => wrapScheduleFix(fixer, node), + }) + }, + } + }, +} + +const isCtxSchedule = (node: estree.Node) => { + return ( + node.type === 'CallExpression' && + node.callee.type === 'MemberExpression' && + node.callee.object.type === 'Identifier' && + node.callee.object.name === 'ctx' && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'schedule' + ) +} + +const wrapScheduleFix = ( + fixer: Rule.RuleFixer, + node: estree.AwaitExpression, +) => [ + fixer.insertTextBefore(node.argument, 'ctx.schedule(() => '), + fixer.insertTextAfter(node.argument, ')'), +] diff --git a/packages/eslint-plugin/src/rules/atom-postifx-rule.ts b/packages/eslint-plugin/src/rules/atom-postifx-rule.ts deleted file mode 100644 index 35dbdb74b..000000000 --- a/packages/eslint-plugin/src/rules/atom-postifx-rule.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { Rule } from 'eslint' -import type { CallExpression, Identifier, Literal, Node } from 'estree' -import { - extractAssignedVariable, - extractAssignedVariableName, - extractImportDeclaration, - isLiteral, - traverseBy, -} from '../lib' -import { isAtomCallExpression } from './atom-rule' - -const match = new Set(['VariableDeclarator', 'PropertyDefinition', 'Property']) - -export const atomPostfixRule: Rule.RuleModule = { - meta: { - type: 'suggestion', - docs: { - description: 'Check postfix for every atom', - }, - fixable: 'code', - messages: { - incorrectVariableName: `atom "{{ atomName }}" should have postfix "{{ postfix }}"`, - }, - }, - create: function (context: Rule.RuleContext): Rule.RuleListener { - const importedFromReatom = new Map() - const postfix = context.settings.atomPostfix ?? 'Atom' - const badPostfix = (atomName: string) => !atomName.endsWith(postfix) - - return { - ImportDeclaration(node) { - extractImportDeclaration({ - from: node, - importsAlias: importedFromReatom, - packageName: '@reatom', - }) - }, - CallExpression: (node) => { - if (!isAtomCallExpression(node, importedFromReatom)) return - - const atomVariable = traverseBy('parent', { - match, - node, - }) - - const atomName = extractAssignedVariableName(atomVariable) - const atomIdentifier = extractAssignedVariable(atomVariable) - - if (!atomName || !atomIdentifier) { - return - } - - if (badPostfix(atomName)) { - reportIncorrectVariableName({ - context, - messageId: 'incorrectVariableName', - source: atomIdentifier, - incorrectName: atomName, - correctName: `${atomName}${postfix}`, - highlightNode: atomIdentifier, - postfix, - }) - } - }, - } - }, -} - -function reportIncorrectVariableName(config: { - messageId: 'incorrectVariableName' - context: Rule.RuleContext - highlightNode: Node - correctName: string - incorrectName: string - source: Node - postfix: string -}) { - const { - source, - incorrectName, - correctName, - highlightNode, - context, - postfix, - } = config - - context.report({ - messageId: config.messageId, - node: highlightNode, - data: { atomName: incorrectName, postfix }, - fix(fixer) { - return fixer.replaceText(source, correctName) - }, - }) -} diff --git a/packages/eslint-plugin/src/rules/atom-rule.ts b/packages/eslint-plugin/src/rules/atom-rule.ts deleted file mode 100644 index 76c3f14fb..000000000 --- a/packages/eslint-plugin/src/rules/atom-rule.ts +++ /dev/null @@ -1,126 +0,0 @@ -import type { Rule } from 'eslint' -import type { CallExpression, Identifier, Literal, Node, TemplateLiteral } from 'estree' -import { - extractAssignedVariableName, - extractImportDeclaration, - isLiteral, - isTemplateLiteral, - traverseBy, -} from '../lib' - -type AtomCallExpression = CallExpression & { - callee: Identifier - arguments: [Literal] | [Literal, Literal] -} - -export const atomRule: Rule.RuleModule = { - meta: { - type: 'suggestion', - docs: { - description: 'Add name for every atom call', - }, - fixable: 'code', - messages: { - missingName: `atom "{{ atomName }}" should has a name inside atom() call`, - unCorrectName: `atom "{{ atomName }}" should be named as it's variable name, rename it to "{{ atomName }}"` - }, - }, - create: function (context: Rule.RuleContext): Rule.RuleListener { - const importedFromReatom = new Map() - const missedName = (amountOfArguments: number) => amountOfArguments === 1 - - return { - ImportDeclaration(node) { - extractImportDeclaration({ - from: node, - importsAlias: importedFromReatom, - packageName: '@reatom', - }) - }, - CallExpression: (node) => { - if (!isAtomCallExpression(node, importedFromReatom)) return - - const matchBy = new Set([ - 'VariableDeclarator', - 'PropertyDefinition', - 'Property', - ]) - - const atomVariable = traverseBy('parent', { - match: matchBy, - node, - }) - - const atomName = extractAssignedVariableName(atomVariable) - if (!atomName) { - return - } - - if (missedName(node.arguments.length)) { - reportUnCorrectName({ - context, - messageId: 'missingName', - source: node.arguments[0], - changeType: 'insertBefore', - correctName: atomName, - highlightNode: node.callee, - }) - } else if (!validAtomVariable(node, atomName)) { - reportUnCorrectName({ - context, - messageId: 'unCorrectName', - source: node.arguments[1], - changeType: 'replace', - correctName: atomName, - highlightNode: node.arguments[1], - }) - } - }, - } - }, -} - -function validAtomVariable(node: CallExpression, correctName: string) { - if (isLiteral(node.arguments[1])) { - return validateLiteral(node.arguments[1], correctName); - } - - return true; -} - -function validateLiteral(node: Literal, correctName: string) { - return node.value === correctName; -} - -function reportUnCorrectName(config: { - messageId: 'unCorrectName' | 'missingName' - context: Rule.RuleContext - changeType: 'replace' | 'insertBefore' - highlightNode: Node - correctName: string - source: Node -}) { - const { source, correctName, highlightNode, changeType, context } = config - - context.report({ - messageId: config.messageId, - node: highlightNode, - data: { atomName: correctName }, - fix(fixer) { - return changeType === 'replace' - ? fixer.replaceText(source, `"${correctName}"`) - : fixer.insertTextAfter(source, `, "${correctName}"`) - }, - }) -} - -export function isAtomCallExpression( - node: CallExpression, - imported: Map, -): node is AtomCallExpression { - return ( - node?.type === 'CallExpression' && - node.callee?.type === 'Identifier' && - imported.get('atom') === node.callee.name - ) -} diff --git a/packages/eslint-plugin/src/rules/reatom-prefix-rule.ts b/packages/eslint-plugin/src/rules/reatom-prefix-rule.ts deleted file mode 100644 index b44624bf1..000000000 --- a/packages/eslint-plugin/src/rules/reatom-prefix-rule.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { Rule } from 'eslint' -import type { - ArrowFunctionExpression, - CallExpression, - Identifier, - Literal, - ObjectExpression, -} from 'estree' -import { - extractAssignedVariableName, - extractImportDeclaration, - traverseBy, -} from '../lib' - -type ReatomPrefixCallExpression = CallExpression & { - callee: Identifier - arguments: - | [ArrowFunctionExpression] - | [ArrowFunctionExpression, Literal] - | [ArrowFunctionExpression, ObjectExpression] -} - -export const reatomPrefixRule: Rule.RuleModule = { - meta: { - type: 'suggestion', - docs: { - description: 'Add name for every reatom* call', - }, - messages: { - noname: `variable assigned to {{ methodName }} should has a name "{{ assignedVariable }}" inside {{ methodName }} call`, - invalidName: `variable assigned to {{ methodName }} should be named as it's variable name, rename it to "{{ assignedVariable }}"`, - }, - fixable: 'code', - }, - create: function (context: Rule.RuleContext): Rule.RuleListener { - const importedFromReatom = new Map() - - return { - ImportDeclaration(node) { - extractImportDeclaration({ - from: node, - importsAlias: importedFromReatom, - packageName: '@reatom', - filter: (name) => name.startsWith('reatom'), - }) - }, - CallExpression(node) { - const methods = Array.from(importedFromReatom.values()) - const imported = - 'name' in node.callee && methods.includes(node.callee.name) - - if (!isReatomPrefixCallExpression(node) || !imported) { - return - } - - const matchBy = new Set([ - 'VariableDeclarator', - 'PropertyDefinition', - 'Property', - ]) - - const assignedVariable = traverseBy('parent', { - match: matchBy, - node, - }) - - const assignedVariableName = extractAssignedVariableName(assignedVariable) - - if (!assignedVariableName) { - return - } - - const initArguments = node.arguments - - const reportMessageConfig = { - methodName: node.callee.name, - assignedVariable: assignedVariableName, - } - - if (initArguments.length === 1) { - context.report({ - node, - messageId: 'noname', - data: reportMessageConfig, - fix(fixer) { - return fixer.insertTextAfter( - initArguments[0], - `, "${assignedVariableName}"`, - ) - }, - }) - } - - if (initArguments.length === 2) { - const last = initArguments[1] - - if (last?.type === 'Literal' && last.value !== assignedVariableName) { - context.report({ - node, - messageId: 'invalidName', - data: reportMessageConfig, - fix(fixer) { - return fixer.replaceText(last, `"${assignedVariableName}"`) - }, - }) - } - - if (initArguments[1]?.type === 'ObjectExpression') { - const methodConfig = initArguments[1] - - if ( - methodConfig.properties.every( - (value) => - value.type === 'Property' && - value.key.type === 'Identifier' && - value.key.name !== 'name', - ) && - 'properties' in methodConfig - ) { - const beforeInsert = methodConfig.properties.at(0) - - if (!beforeInsert) return - - context.report({ - node, - messageId: 'noname', - data: reportMessageConfig, - fix: (fixer) => - fixer.insertTextBefore( - beforeInsert, - `name: "${assignedVariableName}", `, - ), - }) - } - - const badProperty = initArguments[1].properties.find( - (value) => - value.type === 'Property' && - value.key.type === 'Identifier' && - value.key.name === 'name' && - value.value.type === 'Literal' && - value.value.value !== assignedVariableName, - ) - - if (badProperty) { - context.report({ - node, - messageId: 'invalidName', - data: reportMessageConfig, - fix(fixer) { - return fixer.replaceText( - badProperty.value, - `"${assignedVariableName}"`, - ) - }, - }) - } - } - } - }, - } - }, -} - -function isReatomPrefixCallExpression( - node?: CallExpression | null, -): node is ReatomPrefixCallExpression { - return ( - node?.type === 'CallExpression' && - node.callee?.type === 'Identifier' && - (node.arguments.length === 1 || - (node.arguments.length === 2 && node.arguments[1]?.type == 'Literal') || - (node.arguments.length === 2 && - node.arguments[1]?.type == 'ObjectExpression')) - ) -} diff --git a/packages/eslint-plugin/src/rules/unit-naming-rule.test.ts b/packages/eslint-plugin/src/rules/unit-naming-rule.test.ts new file mode 100644 index 000000000..f17193ca4 --- /dev/null +++ b/packages/eslint-plugin/src/rules/unit-naming-rule.test.ts @@ -0,0 +1,54 @@ +import { RuleTester } from 'eslint' +import { unitNamingRule } from './unit-naming-rule' + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, +}) + +const ImportAtom = 'import {atom} from "@reatom/framework"' + +tester.run('unit-naming-rule', unitNamingRule, { + valid: [ + `${ImportAtom}; const some = atom(0, 'some')`, + { + code: `${ImportAtom}; const $some = atom(0, '$some')`, + options: [{ atomPrefix: '$' }], + }, + { + code: `${ImportAtom}; const someAtom = atom(0, 'someAtom')`, + options: [{ atomPostfix: 'Atom' }], + }, + ], + invalid: [ + { + code: `${ImportAtom}; const some = atom(0)`, + errors: [{ messageId: 'nameMissing' }], + output: `${ImportAtom}; const some = atom(0, 'some')`, + }, + { + code: `${ImportAtom}; const some = atom(0, 'unrelated')`, + errors: [{ messageId: 'nameIncorrect' }], + output: `${ImportAtom}; const some = atom(0, 'some')`, + }, + { + code: `${ImportAtom}; const some = atom(0, 'some')`, + options: [{ atomPostfix: 'Atom' }], + errors: [{ messageId: 'postfixMissing' }], + output: `${ImportAtom}; const someAtom = atom(0, 'someAtom')`, + }, + { + code: `${ImportAtom}; const some = atom(0, 'SomeDomain._some')`, + options: [{ atomPostfix: 'Atom' }], + errors: [{ messageId: 'postfixMissing' }], + output: `${ImportAtom}; const someAtom = atom(0, 'SomeDomain._someAtom')`, + }, + { + code: `${ImportAtom}; const some = atom(0, 'SomeDomain._unrelated')`, + errors: [{ messageId: 'nameIncorrect' }], + output: `${ImportAtom}; const some = atom(0, 'SomeDomain._some')`, + }, + ], +}) diff --git a/packages/eslint-plugin/src/rules/unit-naming-rule.ts b/packages/eslint-plugin/src/rules/unit-naming-rule.ts new file mode 100644 index 000000000..188fdd550 --- /dev/null +++ b/packages/eslint-plugin/src/rules/unit-naming-rule.ts @@ -0,0 +1,176 @@ +import * as estree from 'estree' +import { Rule } from 'eslint' +import { createImportMap, isReatomFactoryName } from '../shared' + +class UnitName { + static fromString(string: string) { + const parts = string.split('.') + + let unit = parts.length === 1 ? string : parts[1]! + const domain = parts.length === 1 ? null : parts[0]! + const isPrivate = unit.startsWith('_') + if (isPrivate) unit = unit.slice(1) + + return new UnitName(unit, domain, isPrivate) + } + + constructor( + readonly unit: string, + readonly domain: string | null, + readonly isPrivate: boolean, + ) {} + + rename(unit: string) { + return new UnitName(unit, this.domain, this.isPrivate) + } + + toString() { + let result = this.unit + if (this.isPrivate) result = '_' + result + if (this.domain) result = this.domain + '.' + result + return result + } +} + +export const unitNamingRule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + recommended: true, + description: + 'Ensures that all Reatom entities specify the name parameter.', + }, + messages: { + nameMissing: 'Unit "{{unit}}" is missing a name.', + nameIncorrect: 'Unit "{{unit}}"" has incorrect name.', + prefixMissing: 'Unit "{{unit}}" name should start with "{{prefix}}".', + postfixMissing: 'Unit "{{unit}}" name should end with "{{postfix}}".', + }, + fixable: 'code', + }, + + create(context) { + const importMap = createImportMap('@reatom') + + const option = context.options[0] ?? {} + const atomPrefix = option.atomPrefix ?? '' + const atomPostfix = option.atomPostfix ?? '' + + return { + ImportDeclaration: importMap.onImportNode, + VariableDeclarator(node) { + if (node.id.type !== 'Identifier') return + if (!node.init) return + + const vary = node.id + const value = node.init + + checkNaming({ + imports: importMap.imported, + context, + atomPrefix, + atomPostfix, + vary, + value, + }) + }, + Property(node) { + if (node.key.type !== 'Identifier') return + + const vary = node.key + const value = node.value + + checkNaming({ + imports: importMap.imported, + context, + atomPrefix, + atomPostfix, + vary, + value, + }) + }, + } + }, +} + +const checkNaming = ({ + imports, + context, + atomPrefix, + atomPostfix, + vary, + value, +}: { + imports: ReadonlyMap + context: Rule.RuleContext + atomPrefix: string + atomPostfix: string + vary: estree.Identifier + value: estree.Node +}) => { + if (value.type !== 'CallExpression') return + if (value.callee.type !== 'Identifier') return + + const factoryName = imports.get(value.callee.name) + if (!factoryName) return + if (!isReatomFactoryName(factoryName)) return + + const nameArg = value.arguments[1] + if (nameArg?.type !== 'Literal' || typeof nameArg.value !== 'string') { + context.report({ + node: value, + messageId: 'nameMissing', + data: { unit: vary.name }, + fix: (fixer) => { + const arg = value.arguments[0] + if (!arg) return [] + return fixer.insertTextAfter(arg, `, '${vary.name}'`) + }, + }) + return + } + + const name = UnitName.fromString(nameArg.value) + + if (factoryName === 'atom' && !name.unit.startsWith(atomPrefix)) { + const correct = name.rename(atomPrefix + name.unit) + + context.report({ + node: nameArg, + messageId: 'prefixMissing', + data: { unit: vary.name, prefix: atomPrefix }, + fix: (fixer) => [ + fixer.replaceText(nameArg, `'${correct}'`), + fixer.replaceText(vary, correct.unit), + ], + }) + return + } + + if (factoryName === 'atom' && !name.unit.endsWith(atomPostfix)) { + const correct = name.rename(name.unit + atomPostfix) + + context.report({ + node: nameArg, + messageId: 'postfixMissing', + data: { unit: vary.name, postfix: atomPostfix }, + fix: (fixer) => [ + fixer.replaceText(nameArg, `'${correct}'`), + fixer.replaceText(vary, correct.unit), + ], + }) + return + } + + if (name.unit !== vary.name) { + const correct = name.rename(vary.name) + + context.report({ + node: nameArg, + messageId: 'nameIncorrect', + data: { unit: vary.name }, + fix: (fixer) => fixer.replaceText(nameArg, `'${correct}'`), + }) + return + } +} diff --git a/packages/eslint-plugin/src/shared.ts b/packages/eslint-plugin/src/shared.ts new file mode 100644 index 000000000..8829cd2c2 --- /dev/null +++ b/packages/eslint-plugin/src/shared.ts @@ -0,0 +1,52 @@ +import { Rule } from 'eslint' +import type * as estree from 'estree' + +const ReatomFactoryNames = ['atom', 'action'] +const ReatomFactoryPrefix = 'reatom' + +export const isReatomFactoryName = (name: string) => { + return ( + ReatomFactoryNames.includes(name) || // + name.startsWith(ReatomFactoryPrefix) + ) +} + +export const createImportMap = (packagePrefix: string) => { + const imported = new Map() + const local = new Map() + + const onImportNode = (node: estree.ImportDeclaration) => { + const source = node.source.value + if (typeof source !== 'string' || !source.startsWith(packagePrefix)) { + return + } + + for (const spec of node.specifiers) { + if (spec.type === 'ImportSpecifier') { + onImportSpec(spec) + } + } + } + + const onImportSpec = (spec: estree.ImportSpecifier) => { + imported.set(spec.imported.name, spec.local.name) + local.set(spec.local.name, spec.imported.name) + } + + return { + onImportNode, + imported: imported as ReadonlyMap, + local: local as ReadonlyMap, + } +} + +export const ascend = ( + node: Rule.Node, + ...types: ReadonlyArray +) => { + while (node && !types.includes(node.type as any)) { + node = node.parent + } + + return node as Extract | undefined +} diff --git a/packages/eslint-plugin/src/tests/atom-postfix-rule.test.ts b/packages/eslint-plugin/src/tests/atom-postfix-rule.test.ts deleted file mode 100644 index ad6bdcd06..000000000 --- a/packages/eslint-plugin/src/tests/atom-postfix-rule.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { RuleTester } from 'eslint' -import { atomPostfixRule } from '../rules/atom-postifx-rule' - -const tester = new RuleTester({ - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - }, - settings: { - atomPostfix: 'Atom' - } -}) - -const tester2 = new RuleTester({ - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - }, - settings: { - atomPostfix: '$' - } -}); - -tester2.run('reatom/atom-postfix-rule', atomPostfixRule, { - valid: [ - { - code: ` - import { atom } from '@reatom/framework' - const count$ = atom(0, "count$"); - `, - }, - { - code: `const count = atom(0);`, - }, - { - code: ` - import { atom } from "@reatom/framework" - const factory = ()=> { - const some$ = atom("", "some$") - const set = action(ctx => {}, "set") - return Object.assign(someAtom, { - set - }) - } - `, - }, - ], - invalid: [] -}) - -tester.run('reatom/atom-postfix-rule', atomPostfixRule, { - valid: [ - { - code: ` - import { atom } from '@reatom/framework' - const countAtom = atom(0, "countAtom"); - `, - }, - { - code: `const count = atom(0);`, - }, - { - code: ` - import { atom } from "@reatom/framework" - const factory = ()=> { - const someAtom = atom("", "someAtom") - const set = action(ctx => {}, "set") - return Object.assign(someAtom, { - set - }) - } - `, - }, - ], - invalid: [ - { - code: ` - import { atom } from '@reatom/framework' - const count = atom(0); - `, - errors: [ - { message: 'atom "count" should have postfix "Atom"' }, - ], - output: ` - import { atom } from '@reatom/framework' - const countAtom = atom(0); - `, - }, - { - code: ` - import { atom as createStore } from '@reatom/framework' - const store = createStore(0) - `, - errors: [ - { - message: `atom "store" should have postfix "Atom"`, - }, - ], - output: ` - import { atom as createStore } from '@reatom/framework' - const storeAtom = createStore(0) - `, - }, - { - code: ` - import { atom as createAtom } from '@reatom/framework' - const store = createAtom((ctx) => {}, '') - `, - errors: [ - { - message: `atom "store" should have postfix "Atom"`, - }, - ], - output: ` - import { atom as createAtom } from '@reatom/framework' - const storeAtom = createAtom((ctx) => {}, '') - `, - }, - { - code: ` - import { atom } from "@reatom/framework" - const handler = { - draggable: atom({}) - } - `, - errors: [ - { - message: `atom "draggable" should have postfix "Atom"`, - }, - ], - output: ` - import { atom } from "@reatom/framework" - const handler = { - draggableAtom: atom({}) - } - `, - }, - { - code: ` - import { atom } from "@reatom/core" - class SomeService { - some = atom({}, "") - } - `, - errors: [ - { - message: `atom "some" should have postfix "Atom"`, - }, - ], - output: ` - import { atom } from "@reatom/core" - class SomeService { - someAtom = atom({}, "") - } - `, - }, - ], -}) diff --git a/packages/eslint-plugin/src/tests/index.ts b/packages/eslint-plugin/src/tests/index.ts deleted file mode 100644 index 6bba4f181..000000000 --- a/packages/eslint-plugin/src/tests/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './atom-postfix-rule.test' -import './rule.test' diff --git a/packages/eslint-plugin/src/tests/rule.test.ts b/packages/eslint-plugin/src/tests/rule.test.ts deleted file mode 100644 index 4e2e9a478..000000000 --- a/packages/eslint-plugin/src/tests/rule.test.ts +++ /dev/null @@ -1,501 +0,0 @@ -import { RuleTester } from 'eslint' -import { actionRule } from '../rules/action-rule' -import { atomRule } from '../rules/atom-rule' -import { reatomPrefixRule } from '../rules/reatom-prefix-rule' - -const tester = new RuleTester({ - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - }, -}) - -tester.run('reatom/atom-rule', atomRule, { - valid: [ - { - code: ` - import { atom } from '@reatom/framework'; - const count = 'count'; - const countAtom = atom(0, \`\${count}Atom\`); - `, - }, - { - code: ` - import { atom } from '@reatom/framework'; - const domain = (name) => 'some.' + name; - const countAtom = atom(0, domain\`count\`); - `, - }, - { - code: ` - import { atom } from '@reatom/framework' - const countAtom = atom(0, "countAtom"); - `, - }, - { - code: `const countAtom = atom(0);`, - }, - { - code: 'const countAtom = atom(0, "count");', - }, - { - code: ` - import { atom } from "@reatom/framework" - const factory = ()=> { - const someAtom = atom("", "someAtom") - const set = action(ctx => {}, "set") - return Object.assign(someAtom, { - set - }) - } - `, - }, - ], - invalid: [ - { - code: ` - import { atom } from '@reatom/framework' - const countAtom = atom(0); - `, - errors: [ - { message: 'atom "countAtom" should has a name inside atom() call' }, - ], - output: ` - import { atom } from '@reatom/framework' - const countAtom = atom(0, "countAtom"); - `, - }, - { - code: ` - import { atom } from '@reatom/framework' - const countAtom = atom(0, "count"); - `, - errors: [ - { - message: `atom "countAtom" should be named as it's variable name, rename it to "countAtom"`, - line: 3, - column: 39, - endColumn: 46, - }, - ], - output: ` - import { atom } from '@reatom/framework' - const countAtom = atom(0, "countAtom"); - `, - }, - { - code: ` - import { atom as createStore } from '@reatom/framework' - const storeAtom = createStore(0) - `, - errors: [ - { - message: `atom "storeAtom" should has a name inside atom() call`, - }, - ], - output: ` - import { atom as createStore } from '@reatom/framework' - const storeAtom = createStore(0, "storeAtom") - `, - }, - { - code: ` - import { atom as createAtom } from '@reatom/framework' - const storeAtom = createAtom((ctx) => {}, '') - `, - errors: [ - { - message: `atom "storeAtom" should be named as it's variable name, rename it to "storeAtom"`, - line: 3, - column: 49, - endColumn: 51, - }, - ], - output: ` - import { atom as createAtom } from '@reatom/framework' - const storeAtom = createAtom((ctx) => {}, "storeAtom") - `, - }, - { - code: ` - import { atom } from "@reatom/framework" - const handler = { - draggableAtom: atom({}) - } - `, - errors: [ - { - message: `atom "draggableAtom" should has a name inside atom() call`, - }, - ], - output: ` - import { atom } from "@reatom/framework" - const handler = { - draggableAtom: atom({}, "draggableAtom") - } - `, - }, - { - code: ` - import { atom } from "@reatom/core" - class SomeService { - someAtom = atom({}, "") - } - `, - errors: [ - { - message: `atom "someAtom" should be named as it's variable name, rename it to "someAtom"`, - line: 4, - column: 29, - endColumn: 31, - }, - ], - output: ` - import { atom } from "@reatom/core" - class SomeService { - someAtom = atom({}, "someAtom") - } - `, - }, - ], -}) - -tester.run('reatom/action-rule', actionRule, { - valid: [ - { - code: ` - import { action } from '@reatom/framework' - const doSome = action("doSome"); - `, - }, - { - code: ` - import { action } from '@reatom/framework' - const doSome = action(() => {}, "doSome"); - `, - }, - { - code: ` - import { action } from '@reatom/framework' - const doSome = action(() => {}, \`\${domain}.doSome\`); - `, - }, - { - code: `const doSome = action();`, - }, - { - code: `const doSome = action("do");`, - }, - { - code: `const doSome = action(() => {});`, - }, - { - code: `const doSome = action(() => {}, "do");`, - }, - { - code: `const doSome = action(() => {}, \`\${domain}.doSome\`);`, - }, - ], - invalid: [ - { - code: ` - import { action } from '@reatom/framework' - const doSome = action(); - `, - errors: [ - { message: 'action "doSome" should has a name inside action() call' }, - ], - output: ` - import { action } from '@reatom/framework' - const doSome = action("doSome"); - `, - }, - { - code: ` - import { action } from '@reatom/framework' - const doSome = action("do"); - `, - errors: [ - { - message: `action "doSome" should be named as it's variable name, rename it to "doSome"`, - }, - ], - output: ` - import { action } from '@reatom/framework' - const doSome = action("doSome"); - `, - }, - { - code: ` - import { action } from '@reatom/framework' - const doSome = action(() => {}); - `, - errors: [ - { message: `action "doSome" should has a name inside action() call` }, - ], - output: ` - import { action } from '@reatom/framework' - const doSome = action(() => {}, "doSome"); - `, - }, - { - code: ` - import { action } from '@reatom/framework' - const doSome = action(() => {}, "do"); - `, - errors: [ - { - message: `action "doSome" should be named as it's variable name, rename it to "doSome"`, - }, - ], - output: ` - import { action } from '@reatom/framework' - const doSome = action(() => {}, "doSome"); - `, - }, - { - code: ` - import { action as createAction } from '@reatom/framework' - const inputChanged = createAction(() => {}) - `, - errors: [ - { - message: `action "inputChanged" should has a name inside action() call`, - }, - ], - output: ` - import { action as createAction } from '@reatom/framework' - const inputChanged = createAction(() => {}, "inputChanged") - `, - }, - { - code: ` - import { action } from "@reatom/framework" - const handler = { - draggable: action(ctx => {}) - } - `, - errors: [ - { - message: `action "draggable" should has a name inside action() call`, - }, - ], - output: ` - import { action } from "@reatom/framework" - const handler = { - draggable: action(ctx => {}, "draggable") - } - `, - }, - { - code: ` - import { action } from "@reatom/framework"; - const SomeModule = () => { - const factory = () => { - return action(ctx => {}, "") - } - return factory - } - `, - errors: [ - { - message: `action "factory" should be named as it's variable name, rename it to "factory"`, - }, - ], - output: ` - import { action } from "@reatom/framework"; - const SomeModule = () => { - const factory = () => { - return action(ctx => {}, "factory") - } - return factory - } - `, - }, - ], -}) - -const expectedReatomMessage = { - noname(assignedVariable: string, methodName: string) { - return `variable assigned to ${methodName} should has a name "${assignedVariable}" inside ${methodName} call` - }, - unCorrect(assignedVariable: string, methodName: string) { - return `variable assigned to ${methodName} should be named as it's variable name, rename it to "${assignedVariable}"` - }, -} - -tester.run('reatom/reatom-prefix-rule', reatomPrefixRule, { - valid: [ - { - code: ` - import { reatomAsync } from '@reatom/framework' - const fetchUser = reatomAsync(() => {}, "fetchUser"); - `, - }, - { - code: ` - import { reatomAsync } from '@reatom/framework' - const fetchUser = reatomAsync(() => {}, { - name: "fetchUser", - onRequest: () => {}, - }); - `, - }, - { - code: ` - import { reatomAsync } from '@reatom/framework' - const fetchUser = reatomAsync(() => {}, { name: "fetchUser" }); - `, - }, - { - code: ` - import { reatomRecord } from '@reatom/framework' - const user = reatomRecord({}, "user"); - `, - }, - ], - invalid: [ - { - code: ` - import { reatomAsync } from '@reatom/framework' - const fetchUser = reatomAsync(() => {}); - `, - errors: [ - { - message: expectedReatomMessage.noname('fetchUser', 'reatomAsync'), - }, - ], - output: ` - import { reatomAsync } from '@reatom/framework' - const fetchUser = reatomAsync(() => {}, "fetchUser"); - `, - }, - { - code: ` - import { reatomAsync } from '@reatom/framework' - const fetchUser = reatomAsync(() => {}, "fetch"); - `, - errors: [ - { - message: expectedReatomMessage.unCorrect('fetchUser', 'reatomAsync'), - }, - ], - output: ` - import { reatomAsync } from '@reatom/framework' - const fetchUser = reatomAsync(() => {}, "fetchUser"); - `, - }, - { - code: ` - import { reatomAsync } from '@reatom/framework' - const fetchUser = reatomAsync(() => {}, { onRequest: () => {} }); - `, - errors: [ - { - message: expectedReatomMessage.noname('fetchUser', 'reatomAsync'), - }, - ], - output: ` - import { reatomAsync } from '@reatom/framework' - const fetchUser = reatomAsync(() => {}, { name: "fetchUser", onRequest: () => {} }); - `, - }, - { - code: ` - import { reatomAsync } from '@reatom/framework' - const fetchUser = reatomAsync(() => {}, { name: "fetch" }); - `, - errors: [ - { - message: expectedReatomMessage.unCorrect('fetchUser', 'reatomAsync'), - }, - ], - output: ` - import { reatomAsync } from '@reatom/framework' - const fetchUser = reatomAsync(() => {}, { name: "fetchUser" }); - `, - }, - { - code: ` - import { reatomAsync } from '@reatom/framework' - const fetchUser = reatomAsync(() => {}); - `, - errors: [ - { - message: expectedReatomMessage.noname('fetchUser', 'reatomAsync'), - }, - ], - output: ` - import { reatomAsync } from '@reatom/framework' - const fetchUser = reatomAsync(() => {}, "fetchUser"); - `, - }, - { - code: ` - import { reatomRecord } from '@reatom/framework' - const user = reatomRecord({}, "u"); - `, - errors: [ - { - message: expectedReatomMessage.unCorrect('user', 'reatomRecord'), - }, - ], - output: ` - import { reatomRecord } from '@reatom/framework' - const user = reatomRecord({}, "user"); - `, - }, - { - code: ` - import { reatomAsync as asyncFn } from '@reatom/framework' - const fetchTodo = asyncFn(() => {}, '') - `, - errors: [ - { - message: expectedReatomMessage.unCorrect('fetchTodo', 'asyncFn'), - }, - ], - output: ` - import { reatomAsync as asyncFn } from '@reatom/framework' - const fetchTodo = asyncFn(() => {}, "fetchTodo") - `, - }, - { - code: ` - import { reatomBoolean as booleanState } from '@reatom/framework' - const openModalState = booleanState(() => {}, '') - `, - errors: [ - { - message: expectedReatomMessage.unCorrect( - 'openModalState', - 'booleanState', - ), - }, - ], - output: ` - import { reatomBoolean as booleanState } from '@reatom/framework' - const openModalState = booleanState(() => {}, "openModalState") - `, - }, - { - code: ` - import { reatomRecord as record } from "@reatom/framework" - class SomeService { - someRecord = record({}) - } - `, - errors: [ - { - message: expectedReatomMessage.noname('someRecord', 'record'), - }, - ], - output: ` - import { reatomRecord as record } from "@reatom/framework" - class SomeService { - someRecord = record({}, "someRecord") - } - `, - }, - ], -})