 
                        
                    Как создать свой плагин ESLint в Typescript с помощью шаблона, тестов и публикации
20 марта 2023 г.Содержание
- Инициализировать репозиторий с помощью шаблона
- Исходная структура шаблона
- Добавление правил с помощью скриптов из шаблона
- Написать тесты для подключаемого модуля Eslint
- Записать правило Эслинта
- Небольшое объяснение AST
- Окончательный вариант
- Обновление документов с помощью скриптов
- Публикация плагинов
- Подключите его к своему приложению.
Фон
Я постараюсь написать туториал на основе моего PR в репозитории Reatom с пошаговым объяснением: https ://github.com/artalar/Reatom/pull/488
Если вы хотите узнать больше, вы можете прочитать выпуск https://github.com/artalar/reatom/issues/487.
Чтобы добавить немного контекста, Reatom — это библиотека управления состоянием. Атомы — это концепция в Reatom, библиотеке управления состоянием для React.
Что такое плагины и правила ESLint?
Подключаемые модули ESLint — это расширения, которые работают с основным пакетом ESLint для обеспечения соблюдения определенных стандартов кодирования. Плагины содержат папку rules, в которой определяются отдельные правила для обеспечения соблюдения этих стандартов.
Каждый модуль rule имеет свойство meta, описывающее правило, и свойство create, определяющее  поведение.
Функция create принимает аргумент context, который используется для взаимодействия с проверяемым кодом, и вы можете использовать его для определения логики вашего правила, например, для требования строгого именования. соглашения для вашей библиотеки.
Давайте углубимся в код
Инициализировать репозиторий
Создание нового проекта TypeScript eslint
npx degit https://github.com/pivaszbs/typescript-template-eslint-plugin reatom-eslint-plugin 
Затем перейдите в новый каталог проекта и установите зависимости с помощью:
cd reatom-eslint-plugin && npm i
Я хочу быть хорошим мальчиком, поэтому запускаю git.
git init && git add . && git commit -m "init"
Затем откройте файл package.json и найдите поле name. Это поле необходимо, потому что оно будет основной точкой входа для вашего плагина, когда он будет использоваться. Вы можете изменить его на следующее:
"name": "eslint-plugin-reatom"
В качестве альтернативы вы можете использовать соглашение об именовании пакетов с ограниченной областью действия:
"name": "@reatom/eslint-plugin"
Исходная структура
- scripts // some automation to concentrate on writing rules
- docs
  - rules // here will be generated by npm run add-rule files
- src
  - configs
      recommended.ts // generated config 
  - rules
      // all your rules
index.ts // Connection point to your plugin, autogenerated by scripts/lib/update-lib-index.ts
В общем индексе файлы будут генерироваться скриптами, так что вам не нужно об этом беспокоиться :ухмыляясь:
/* DON'T EDIT THIS FILE. This is generated by 'scripts/lib/update-lib-index.ts' */
import { recommended } from './configs/recommended';
import exampleRule from './rules/example-rule'
export const configs = {
    recommended
};
export const rules = {
    'example-rule': exampleRule
};
Добавление правил и обновление документов
В этом репозитории вы найдете несколько удобных скриптов для добавления правил и обновления документов. Чтобы добавить новое правило, вы можете использовать следующую команду:
npm run add-rule atom-rule suggestion
Это создаст три раздела для нового правила: документация, тесты и фактический код. Мы можем пока пропустить раздел документации и сосредоточиться на последних двух.
Написание тестов
Как энтузиаст TDD (разработка через тестирование), мы начнем с создания нескольких простых тестов в файле tests/atom-rule.ts:
// tests/atom-rule.ts
tester.run('atom-rule', atomRule, {
    valid: [
        {
            code: 'const countAtom = atom(0, "countAtom");'
        },
    ],
    invalid: [
        {
            code: `const countAtom = atom(0);`,
            errors: [{ message: 'atom name is not defined' }]
        },
        {
            code: 'const countAtom = atom(0, "count");',
            errors: [{ message: 'atom name is defined bad'}]
        },
    ]
});
Если вы запустите тесты сейчас, они потерпят неудачу, потому что мы еще не внедрили atomRule.
Написание правила
atomRule определяет поведение правила. Вот простая реализация:
import { Rule } from "eslint";
const rule: Rule.RuleModule = {
  meta: {
    docs: {
      description: "Add name for every atom call", // simply describe your rule
      recommended: true, // if it's recommended, then npm run update will add it to recommmended config
    },
    type: "suggestion"
  },
  create: function (context: Rule.RuleContext): Rule.RuleListener {
    return {
        VariableDeclaration: node => { // listener for declaration, here we can specifiy more specific selector
            node.declarations.forEach(d => {
                if (d.init?.type !== 'CallExpression') return;
                if (d.init.callee.type !== 'Identifier') return;
                if (d.init.callee.name !== 'atom') return;
                if (d.id.type !== 'Identifier') return;
                // just guard everything that we don't need
                if (d.init.arguments.length <= 1) {
                    // show error in user code
                    context.report({ 
                        message: `atom name is not defined`,
                        // here we can pass what will be underlined by red/yellow line
                        node,
                    })
                }
                if (d.init.arguments[1]?.type !== 'Literal') return;
                // just another guard
                if (d.init.arguments[1].value !== d.id.name) {
                    context.report({ message: `atom name is defined bad`, node })
                }
            })
        }
    };
},
};
export default rule;
Это простой вариант, но здесь мы можем легко понять, что происходит.
Чтобы лучше понять структуру AST вашего кода, вы можете использовать https://astexplorer.net/ или просто проанализированные узлы console.log.
Небольшое пояснение для лучшего понимания типов AST
Вот небольшое описание каждого идентификатора в небольшом примере:
const kek = atom('kek')
- 
Идентификатор: интерфейс TypeScript, представляющий узел идентификатора в AST.
- 
const kek = atom('kek'), kek, и atom являются узлами идентификаторов.< /p> 2. .Literal: интерфейс TypeScript, который представляет узел буквального значения (строка, число, логическое значение и т. д.) в AST. const kek = atom(‘kek’), ‘kek’ — это литерал
3. CallExpression: интерфейс TypeScript, представляющий узел выражения вызова функции в абстрактном синтаксическом дереве (AST).
- 
В нашем примере atom(‘kek’) — это CallExpression, который состоит из atom — идентификатора и kek — литерала. 4. .VariableDeclarator: интерфейс TypeScript, представляющий узел декларатора переменных в AST
- 
В нашем примере все выражение, кроме const, представляет собой VariableDeclarator kek = atom(‘kek’) 5. Узел: интерфейс TypeScript, представляющий общий узел AST.
Или просто с помощью astexplorer
Окончательный вариант
Последние тесты
tester.run('atom-rule', rule, {
  valid: [
      {
          code: `
          import { atom } from '@reatom/framework'
          const countAtom = atom(0, "countAtom");
          `
      },
      {
          code: `const countAtom = atom(0);`,
      },
      {
          code: 'const countAtom = atom(0, "count");',
      },
  ],
  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"` }],
          output: `
          import { atom } from '@reatom/framework'
          const countAtom = atom(0, "countAtom");
          `,
      },
  ]
});
Из тестов мы понимаем, что нам нужно как-то изменить исходный код, используя наше правило.
Как исправить правило?
Добавить простую строку в контекстный отчет.
fix: fixer => fixer.replaceText(node, replaceString)
узел — может быть фактическим узлом или диапазоном символов, которые вы хотите заменить.
replaceString — какой код вы ожидаете увидеть.
Не забудьте добавить fixable: 'code' или fixable: 'whitespace' для метатегов вашего правила.
Если вы не знаете, как это исправить с помощью eslint, просто попробуйте свой существующий проект.
eslint --fix ./src
Сам код
import { Rule } from "eslint";
import { CallExpression, Identifier, Literal, VariableDeclarator, Node } from 'estree';
import { isIdentifier, isLiteral } from "../lib";
type AtomCallExpression = CallExpression & { callee: Identifier, arguments: [Literal] | [Literal, Literal] }
type AtomVariableDeclarator = VariableDeclarator & { id: Identifier, init: AtomCallExpression }
const noname = (atomName: string) => `atom "${atomName}" should has a name inside atom() call`;
const invalidName = (atomName: string) => `atom "${atomName}" should be named as it's variable name, rename it to "${atomName}"`;
export const atomRule: Rule.RuleModule = {
    meta: {
        type: 'suggestion',
        docs: {
            recommended: true,
            description: "Add name for every atom call"
        },
        fixable: 'code'
    },
    create: function (context: Rule.RuleContext): Rule.RuleListener {
        let importedFromReatom = false;
        return {
            ImportSpecifier(node) {
                const imported = node.imported.name;
                // @ts-ignore
                const from = node.parent.source.value;
                if (from.startsWith('@reatom') && imported === 'atom') {
                    importedFromReatom = true;
                }
            },
            VariableDeclarator: d => {
                if (!isAtomVariableDeclarator(d) || !importedFromReatom) return;
                if (d.init.arguments.length === 1) {
                    reportUndefinedAtomName(context, d);
                } else if (isLiteral(d.init.arguments[1]) && d.init.arguments[1].value !== d.id.name) {
                    reportBadAtomName(context, d);
                }
            }
        };
    }
}
function isAtomCallExpression(node?: Node | null): node is AtomCallExpression {
    return node?.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === 'atom';
}
function isAtomVariableDeclarator(node: VariableDeclarator): node is AtomVariableDeclarator {
    return isAtomCallExpression(node.init) && isIdentifier(node.id);
}
function reportUndefinedAtomName(context: Rule.RuleContext, d: AtomVariableDeclarator) {
    context.report({
        message: noname(d.id.name),
        node: d,
        fix: fixer => fixer.insertTextAfter(d.init.arguments[0], `, "${d.id.name}"`)
    });
}
function reportBadAtomName(context: Rule.RuleContext, d: AtomVariableDeclarator) {
    context.report({
        message: invalidName(d.id.name),
        node: d,
        fix: fixer => fixer.replaceText(d.init.arguments[1], `"${d.id.name}"`)
    });
}
Как видите, в нем просто больше ошибок, защита типов и проверка импорта. И, конечно же, я делаю правило поправимым.
Обновление документов
Чтобы обновить документы, вы можете использовать следующую команду:
npm run update
Эта команда обновит README.md и обновит документы для каждого правила (но вам нужно немного написать о каждом правиле в файле docs/{rule}).
Кроме того, как я уже сказал, вам не нужно беспокоиться об индексном файле.
Этап публикации
Убедитесь, что версия указана в файле package.json.
  "version": "1.0.0"
Пишите термином, если это не 1.0.0.
npm version 1.0.0
Тогда просто пишите в корень.
npm publish
Все будет создано и опубликовано с указанным вами именем пакета.
Подключите его к своему приложению
Я называю свой пакет.
@reatom/eslint-plugin
Итак, мне нужно установить его.
npm i @reatom/eslint-plugin
И добавить в мою конфигурацию .eslintrc.
module.exports = {
    plugins: [
        "@reatom"
    ],
// use all rules
     extends: [
        "plugin:@reatom/recommended"
     ],
    // or pick some
    rules: {
        '@reatom/atom-rule': 'error',
        // aditional rules, you can see it in PR
        '@reatom/action-rule': 'error',
        '@reatom/reatom-prefix-rule': 'error'
    }
}
И все просто работает (просто для reatom-eslint-plugin нужно везде писать “reatom” вместо “@reatom”).
Заключение
В этом руководстве мы рассмотрели процесс создания подключаемого модуля ESLint для библиотеки управления состоянием Reatom. Мы покрываем:
- Как написать плагин eslint на Typescript.
- Как покрыть это тестами.
- Как заставить его работать с параметром --fix.
- Как использовать мой шаблон.
- Как опубликовать подключаемый модуль eslint.
- Как добавить его в существующий репозиторий с помощью eslint
Ресурсы для дальнейшего изучения и изучения
- https://github.com/pivaszbs/typescript-template-eslint-plugin ли>
- https://astexplorer.net/
- https://github.com/artalar/reatom/pull/488/files ли>
- https://eslint.org/docs/latest/extend/plugins
- https://www.reatom.dev/
- https://github.com/artalar/reatom
- https://docs.npmjs.com/about-semantic-versioning
Удачи :)
Оригинал
