Skip to content

Commit

Permalink
feat(eslint-plugin): complete fix if import already exsists (#845)
Browse files Browse the repository at this point in the history
  • Loading branch information
serikovlearning committed Oct 10, 2024
1 parent c4d044d commit 411989e
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 109 deletions.
4 changes: 2 additions & 2 deletions packages/eslint-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +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'
import { wrapScheduleInsteadOfCtxScheduleRule } from './rules/wrap-schedule-instead-of-ctx-schedule-rule'

const rules = {
'unit-naming-rule': unitNamingRule,
'async-rule': asyncRule,
'deprecate-ctx-schedule-rule': deprecateCtxScheduleRule,
'wrap-schedule-instead-of-ctx-schedule-rule': wrapScheduleInsteadOfCtxScheduleRule,
}

export default {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { deprecateCtxScheduleRule } from "./wrap-schedule-instead-of-ctx-schedule-rule";

const { RuleTester } = require("eslint");

import { wrapScheduleInsteadOfCtxScheduleRule } from './wrap-schedule-instead-of-ctx-schedule-rule'
import { RuleTester } from 'eslint'

const tester = new RuleTester({
parserOptions: {
Expand All @@ -10,46 +8,57 @@ const tester = new RuleTester({
},
})

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)'."}]
},
],
}
);
tester.run('wrap-schedule-instead-ctx-schedule', wrapScheduleInsteadOfCtxScheduleRule, {
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)'." }],
},
],
})
Original file line number Diff line number Diff line change
@@ -1,37 +1,29 @@
import * as estree from 'estree'
import { Rule } from 'eslint'
import { checkCallExpressionNode } from '../shared'
import { checkCallExpressionNodeValid } 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})`
type TImport = keyof typeof importsMap

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

export const deprecateCtxScheduleRule: Rule.RuleModule = {
export const wrapScheduleInsteadOfCtxScheduleRule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
Expand All @@ -44,39 +36,66 @@ export const deprecateCtxScheduleRule: Rule.RuleModule = {
create(context) {
let hasImport = false
let lastImport: estree.ImportDeclaration | null = null
let exsistsImportSpecifiers = new Set()

return {
ImportDeclaration(node: estree.ImportDeclaration) {
lastImport = node
if (isImportICareAbout(node)) {
hasImport = true
if (node.source.value === '@reatom/framework') {
lastImport = node

node.specifiers.forEach((specifier) => {
if (
specifier.type === 'ImportSpecifier' &&
specifier.imported &&
specifier.imported.type === 'Identifier' &&
importsMap[specifier.imported.name as TImport]
) {
hasImport = true
exsistsImportSpecifiers.add(specifier.imported.name)
}
})
}
},

CallExpression(node: estree.CallExpression) {
if (checkCallExpressionNode(node)) {
let cb = node.arguments[0]
let n = node.arguments[1]
if (checkCallExpressionNodeValid(node)) {
let callbackArgument = node.arguments[0]
let numberArgument = node.arguments[1]

context.report({
node,
message: getMessage(n),
message: getMessage(numberArgument),
fix(fixer) {
const fixes = []
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 callbackText = cb ? sourceCode.getText(cb) : '() => {}'
const nText = n ? sourceCode.getText(n) : ''
const neededImport = numberArgument ? 'schedule' : 'wrap'

fixes.push(fixer.replaceText(node, getTextToReplace(nText, callbackText)))
if (!exsistsImportSpecifiers.has(neededImport)) {
if (hasImport && lastImport) {
const exsistedSpecifier = lastImport.specifiers.find(
(specifier) =>
specifier.type == 'ImportSpecifier' &&
specifier.imported &&
specifier.imported.type === 'Identifier' &&
Object.keys(importsMap).includes(specifier.imported.name),
)

if (!hasImport) {
const newImport = importsMap[n ? 'schedule' : 'wrap']
fixes.push(
lastImport
? fixer.insertTextBefore(lastImport, newImport)
: fixer.insertTextAfterRange([0, 0], newImport),
)
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
Expand Down
50 changes: 25 additions & 25 deletions packages/eslint-plugin/src/shared.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
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): estree.Identifier[] => {
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.key.type === 'Identifier' ? property.key : []))
}
return []
}

export const checkCallExpressionNode = (node: estree.CallExpression) =>
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.property.type === 'Identifier'
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): estree.Identifier[] => {
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.key.type === 'Identifier' ? property.key : []))
}
return []
}

export const checkCallExpressionNodeValid = (node: estree.CallExpression) =>
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.property.type === 'Identifier'

0 comments on commit 411989e

Please sign in to comment.