From c4d044df73dfc9a32c894b198035e2aa18fd5983 Mon Sep 17 00:00:00 2001 From: Egor Serikov Date: Sat, 5 Oct 2024 00:13:21 +0600 Subject: [PATCH] feat(eslint-plugin): complete wrap-schedule-instead-of-ctx-schedule-rule (#845) --- packages/eslint-plugin/package.json | 4 + packages/eslint-plugin/src/index.test.ts | 1 + packages/eslint-plugin/src/index.ts | 2 + ...edule-instead-of-ctx-schedule-rule.test.ts | 55 ++++++++++++ ...p-schedule-instead-of-ctx-schedule-rule.ts | 89 +++++++++++++++++++ packages/eslint-plugin/src/shared.ts | 5 ++ 6 files changed, 156 insertions(+) create mode 100644 packages/eslint-plugin/src/rules/wrap-schedule-instead-of-ctx-schedule-rule.test.ts create mode 100644 packages/eslint-plugin/src/rules/wrap-schedule-instead-of-ctx-schedule-rule.ts diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 97143680d..4b5d2220c 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -35,6 +35,10 @@ { "name": "pivaszbs", "url": "https://github.com/pivaszbs" + }, + { + "name": "serikovlearning", + "url": "https://github.com/serikovlearning" } ], "license": "MIT", diff --git a/packages/eslint-plugin/src/index.test.ts b/packages/eslint-plugin/src/index.test.ts index 70e61f3da..48f728e13 100644 --- a/packages/eslint-plugin/src/index.test.ts +++ b/packages/eslint-plugin/src/index.test.ts @@ -1,2 +1,3 @@ import './rules/unit-naming-rule.test' import './rules/async-rule.test' +import './rules/wrap-schedule-instead-of-ctx-schedule-rule.test' diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts index 8646f0509..3ceb63b46 100644 --- a/packages/eslint-plugin/src/index.ts +++ b/packages/eslint-plugin/src/index.ts @@ -1,10 +1,12 @@ import { ESLint } from 'eslint' import { asyncRule } from './rules/async-rule' import { unitNamingRule } from './rules/unit-naming-rule' +import { deprecateCtxScheduleRule } from './rules/wrap-schedule-instead-of-ctx-schedule-rule' const rules = { 'unit-naming-rule': unitNamingRule, 'async-rule': asyncRule, + 'deprecate-ctx-schedule-rule': deprecateCtxScheduleRule, } export default { diff --git a/packages/eslint-plugin/src/rules/wrap-schedule-instead-of-ctx-schedule-rule.test.ts b/packages/eslint-plugin/src/rules/wrap-schedule-instead-of-ctx-schedule-rule.test.ts new file mode 100644 index 000000000..c2f600992 --- /dev/null +++ b/packages/eslint-plugin/src/rules/wrap-schedule-instead-of-ctx-schedule-rule.test.ts @@ -0,0 +1,55 @@ +import { deprecateCtxScheduleRule } from "./wrap-schedule-instead-of-ctx-schedule-rule"; + +const { RuleTester } = require("eslint"); + + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, +}) + +tester.run( + "wrap-schedule-instead-ctx-schedule", + deprecateCtxScheduleRule, + { + valid: [ + { + code: `import { wrap } from "@reatom/framework";\nwrap(ctx, () => {})`, + }, + { + code: `import { wrap } from "@reatom/framework";\nwrap(ctx, () => 'Dev')`, + }, + { + code: `import { schedule } from "@reatom/framework";\nschedule(ctx, () => 'Dev', -1)`, + }, + { + code: `import { schedule } from "@reatom/framework";\nschedule(ctx, () => 'Dev', -1)`, + }, + + ], + invalid: [ + { + code: "ctx.schedule()", + output: `import { wrap } from "@reatom/framework";\nwrap(ctx, () => {})`, + errors: [{message: "Use 'wrap(ctx, cb)' instead of deprecated 'ctx.schedule(cb, n)'."}] + }, + { + code: "ctx.schedule(() => 'Dev')", + output: `import { wrap } from "@reatom/framework";\nwrap(ctx, () => 'Dev')`, + errors: [{message: "Use 'wrap(ctx, cb)' instead of deprecated 'ctx.schedule(cb, n)'."}] + }, + { + code: "ctx.schedule(() => 'Dev', -1)", + output: `import { schedule } from "@reatom/framework";\nschedule(ctx, () => 'Dev', -1)`, + errors: [{message: "Use 'schedule(ctx, cb, n)' instead of deprecated 'ctx.schedule(cb, n)'."}] + }, + { + code: `import { schedule } from "@reatom/framework";\nctx.schedule(() => 'Dev', -1)`, + output: `import { schedule } from "@reatom/framework";\nschedule(ctx, () => 'Dev', -1)`, + errors: [{message: "Use 'schedule(ctx, cb, n)' instead of deprecated 'ctx.schedule(cb, n)'."}] + }, + ], + } +); diff --git a/packages/eslint-plugin/src/rules/wrap-schedule-instead-of-ctx-schedule-rule.ts b/packages/eslint-plugin/src/rules/wrap-schedule-instead-of-ctx-schedule-rule.ts new file mode 100644 index 000000000..18a54f81e --- /dev/null +++ b/packages/eslint-plugin/src/rules/wrap-schedule-instead-of-ctx-schedule-rule.ts @@ -0,0 +1,89 @@ +import * as estree from 'estree' +import { Rule } from 'eslint' +import { checkCallExpressionNode } from '../shared' + +const importsMap = { + wrap: 'import { wrap } from "@reatom/framework";\n', + schedule: 'import { schedule } from "@reatom/framework";\n', +} + +const getTextToReplace = (nText: string, callbackText: string) => { + if (Boolean(nText)) { + return `schedule(ctx, ${callbackText}, ${nText})` + } + return `wrap(ctx, ${callbackText})` +} + +const getMessage = (n?: estree.Expression | estree.SpreadElement) => { + if (Boolean(n)) { + return "Use 'schedule(ctx, cb, n)' instead of deprecated 'ctx.schedule(cb, n)'." + } + + return "Use 'wrap(ctx, cb)' instead of deprecated 'ctx.schedule(cb, n)'." +} + +const isImportICareAbout = (node: estree.ImportDeclaration) => { + return node.specifiers.some( + (specifier) => + specifier.type === 'ImportSpecifier' && + (specifier.imported.name === 'wrap' || specifier.imported.name === 'schedule') && + node.source.value === '@reatom/framework', + ) +} + +export const deprecateCtxScheduleRule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + description: "Method 'ctx.schedule' is deprecated in v4", + }, + fixable: 'code', + hasSuggestions: true, + schema: [], + }, + create(context) { + let hasImport = false + let lastImport: estree.ImportDeclaration | null = null + + return { + ImportDeclaration(node: estree.ImportDeclaration) { + lastImport = node + if (isImportICareAbout(node)) { + hasImport = true + } + }, + + CallExpression(node: estree.CallExpression) { + if (checkCallExpressionNode(node)) { + let cb = node.arguments[0] + let n = node.arguments[1] + + context.report({ + node, + message: getMessage(n), + fix(fixer) { + const fixes = [] + const sourceCode = context.sourceCode + + const callbackText = cb ? sourceCode.getText(cb) : '() => {}' + const nText = n ? sourceCode.getText(n) : '' + + fixes.push(fixer.replaceText(node, getTextToReplace(nText, callbackText))) + + if (!hasImport) { + const newImport = importsMap[n ? 'schedule' : 'wrap'] + fixes.push( + lastImport + ? fixer.insertTextBefore(lastImport, newImport) + : fixer.insertTextAfterRange([0, 0], newImport), + ) + } + + return fixes + }, + }) + } + }, + } + }, +} diff --git a/packages/eslint-plugin/src/shared.ts b/packages/eslint-plugin/src/shared.ts index 06f721e27..42dca0aaf 100644 --- a/packages/eslint-plugin/src/shared.ts +++ b/packages/eslint-plugin/src/shared.ts @@ -18,3 +18,8 @@ export const patternNames = (pattern: estree.Pattern): estree.Identifier[] => { } return [] } + +export const checkCallExpressionNode = (node: estree.CallExpression) => + node.callee.type === 'MemberExpression' && + node.callee.object.type === 'Identifier' && + node.callee.property.type === 'Identifier'