Skip to content

Commit

Permalink
feat(eslint-plugin): schedule-import-rule (#930)
Browse files Browse the repository at this point in the history
* feat(eslint-plugin): complete wrap-schedule-instead-of-ctx-schedule-rule (#845)

* feat(eslint-plugin): complete fix if import already exsists (#845)

* feat(eslint-plugin): used esquery selectors (#845)

* feat(eslint-plugin): renamed the rule (#845)
  • Loading branch information
serikovlearning authored Jan 25, 2025
1 parent 7a84493 commit 89d63d0
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 27 deletions.
4 changes: 4 additions & 0 deletions packages/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
{
"name": "pivaszbs",
"url": "https://github.com/pivaszbs"
},
{
"name": "serikovlearning",
"url": "https://github.com/serikovlearning"
}
],
"license": "MIT",
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
import './rules/unit-naming-rule.test'
import './rules/async-rule.test'
import './rules/schedule-import-rule.test'
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ESLint } from 'eslint'
import { asyncRule } from './rules/async-rule'
import { unitNamingRule } from './rules/unit-naming-rule'
import { scheduleImportRule } from './rules/schedule-import-rule.ts'

const rules = {
'unit-naming-rule': unitNamingRule,
'async-rule': asyncRule,
'schedule-import-rule': scheduleImportRule,
}

export default {
Expand Down
64 changes: 64 additions & 0 deletions packages/eslint-plugin/src/rules/schedule-import-rule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { scheduleImportRule } from './schedule-import-rule.ts'
import { RuleTester } from 'eslint'

const tester = new RuleTester({
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
})

tester.run('schedule-import-rule', scheduleImportRule, {
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)`,
},
{
code: `import { wrap, schedule } from "@reatom/framework";\nschedule(ctx, () => 'Dev', -1)`,
},
{
code: `import { schedule, wrap } from "@reatom/framework";\nwrap(ctx, () => 'Dev')`,
},
],
invalid: [
{
code: 'ctx.schedule()',
output: `import { wrap } from "@reatom/framework";\nwrap(ctx, () => {})`,
errors: [{ message: "Use 'wrap(ctx, cb)' instead of deprecated 'ctx.schedule(cb)'." }],
},
{
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)'." }],
},
{
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)'." }],
},
{
code: `import { wrap } from "@reatom/framework";\nctx.schedule(() => 'Dev', -1)`,
output: `import { wrap, 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')`,
output: `import { schedule, wrap } from "@reatom/framework";\nwrap(ctx, () => 'Dev')`,
errors: [{ message: "Use 'wrap(ctx, cb)' instead of deprecated 'ctx.schedule(cb)'." }],
},
],
})
97 changes: 97 additions & 0 deletions packages/eslint-plugin/src/rules/schedule-import-rule.ts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as estree from 'estree'
import { Rule } from 'eslint'

const importsMap = {
wrap: 'import { wrap } from "@reatom/framework";\n',
schedule: 'import { schedule } from "@reatom/framework";\n',
}
const importNames = Object.keys(importsMap)
type TImport = keyof typeof importsMap

const getTextToReplace = (numberArgumentText: string, callbackArgumentText: string) => {
if (Boolean(numberArgumentText)) {
return `schedule(ctx, ${callbackArgumentText}, ${numberArgumentText})`
}
return `wrap(ctx, ${callbackArgumentText})`
}
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)'."
}

export const scheduleImportRule: 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
let exsistsImportSpecifiers = new Set()

return {
ImportDeclaration(node) {
lastImport = node

if (node.source.value === '@reatom/framework') {
node.specifiers.forEach((specifier) => {
if (specifier.type === 'ImportSpecifier' && importNames.includes(specifier.imported.name)) {
hasImport = true
exsistsImportSpecifiers.add(specifier.imported.name)
}
})
}
},

'CallExpression[callee.type=MemberExpression][callee.object.type=Identifier][callee.property.type=Identifier]'(
node: estree.CallExpression,
) {
let callbackArgument = node.arguments[0]
let numberArgument = node.arguments[1]

context.report({
node,
message: getMessage(numberArgument),
fix(fixer) {
const fixes = [] as Rule.Fix[]
const sourceCode = context.sourceCode
const callbackArgumentText = callbackArgument ? sourceCode.getText(callbackArgument) : '() => {}'
const numberArgumentText = numberArgument ? sourceCode.getText(numberArgument) : ''

fixes.push(fixer.replaceText(node, getTextToReplace(numberArgumentText, callbackArgumentText)))

const neededImport = numberArgument ? 'schedule' : 'wrap'

if (!exsistsImportSpecifiers.has(neededImport)) {
if (hasImport && lastImport) {
const exsistedSpecifier = lastImport.specifiers.find(
(specifier) => specifier.type == 'ImportSpecifier' && importNames.includes(specifier.imported.name),
)

if (exsistedSpecifier) {
fixes.push(fixer.insertTextAfter(exsistedSpecifier, `, ${neededImport}`))
}
} else {
const importToAdd = importsMap[neededImport]
fixes.push(
lastImport
? fixer.insertTextBefore(lastImport, importToAdd)
: fixer.insertTextAfterRange([0, 0], importToAdd),
)
}
}

return fixes
},
})
},
}
},
}
54 changes: 27 additions & 27 deletions packages/eslint-plugin/src/shared.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import type * as estree from 'estree'

export const reatomFactoryList = ['atom', 'action', 'reaction'] as const
export const reatomFactoryPattern = new RegExp(`^(reatom\\w+|${reatomFactoryList.join('|')})$`)

export const patternNames = (pattern: estree.Pattern | null): estree.Identifier[] => {
if (!pattern) {
return []
}

if (pattern.type === 'AssignmentPattern') {
return patternNames(pattern.left)
}
if (pattern.type === 'Identifier') {
return [pattern]
}
if (pattern.type === 'ArrayPattern') {
return pattern.elements.flatMap(patternNames)
}

if (pattern.type === 'ObjectPattern') {
return pattern.properties.flatMap((property) =>
property.type === 'Property' && property.key.type === 'Identifier' ? property.key : [],
)
}
return []
}
import type * as estree from 'estree'

export const reatomFactoryList = ['atom', 'action', 'reaction'] as const
export const reatomFactoryPattern = new RegExp(`^(reatom\\w+|${reatomFactoryList.join('|')})$`)

export const patternNames = (pattern: estree.Pattern | null): estree.Identifier[] => {
if (!pattern) {
return []
}

if (pattern.type === 'AssignmentPattern') {
return patternNames(pattern.left)
}
if (pattern.type === 'Identifier') {
return [pattern]
}
if (pattern.type === 'ArrayPattern') {
return pattern.elements.flatMap(patternNames)
}

if (pattern.type === 'ObjectPattern') {
return pattern.properties.flatMap((property) =>
property.type === 'Property' && property.key.type === 'Identifier' ? property.key : [],
)
}
return []
}

0 comments on commit 89d63d0

Please sign in to comment.