slonoed

Пользовательские правила eslint

May 19, 2019 • ☕️ 7 min read

Я уже писал про процесс код ревью. Теперь хочу осветить одну важную связанную с этим тему. Если быть точнее, инструментарий, а именно пользовательские правила eslint.

Eslint — мощный инструмент, предоставляющий огромное число проверок кода и даже автоматическое исправление ошибок. А экосистема плагинов позволяет расширить набор до практически любых требований.

Однако бывает необходимость добавить проверки, которых нет в готовых плагинах. Часто это стилевые договоренности в команде или особенности внутреннего кода.

В этом руководстве я покажу:

  1. как настроить eslint, чтобы добавление правил было таким же простым, как и написание другого кода в проекте
  2. как создать пользовательское правило

Я исхожу из того, что в проекте уже есть eslint и не буду рассказывать, как его настроить.

Создание пользовательского правила

Давайте разберем создание нового правила на примере запрета использования ключевого слова await в сложных выражениях. Такое правило не будет показывать ошибку в следующих случаях:

// Декларирование переменной
const a = await b
// Присваивание переменной
a = await b
// Возврат значения из функции
return await b
// Выражение
await b

Все остальные случаи использования будут генерировать ошибку:

1 + await b
a = foo(await b)
a = {
  x: foo(),
  z: await b
}

Я использую это правило в своих проектах, потому как вижу пользу от явного обозначения асинхронной операции. К тому же код, который компилируется для старых браузеров проще отлаживать. Однако, я вижу, что не всем это правило кажется обоснованным. Я не хочу аргументировать полезность использования этого правила в работе, а просто использую его для примера.

Немного теории

Большинство компиляторов при компиляции языка используют построение абстрактного синтаксического дерева (abstract syntax tree, AST). AST представляет собой иерархический набор данных, описывающий программу. Для визуального представления AST очень удобно использовать сервис astexplorer.net. Он предоставляет широкий выбор языков и парсеров. Интерфейс выглядит довольно просто. В верхней части можно выбрать язык и парсер, слева вводить исходный код, а справа будет показываться AST.

astexplorer window

В этом тексте я буду использовать JavaScript и Babel, но полученный опыт можно легко перенести на любой другой язык.

Давайте разберём простейший пример объявления функции.

function foo(arg) {
  return arg
}

astexplorer File tree

У каждого элемента дерева есть поле type, показывающее тип узла, а также дополнительные поля: свойства элемента или его потомки. Например, у объявления функции (FunctionDeclaration) есть свойство async: false, указывающее что это не асинхронная функция.

Если представить поддерево функции в виде схемы, то получится такая картина:

tree

Зачем вообще нужно это дерево? Дело в том, что программно намного проще манипулировать логическими объектами, а не обрабатывать строки. Сам процесс построения дерева может быть сложным и требующим хорошего понимания спецификации языка. К счастью, eslint предоставляет API работающий на уровне уже готового дерева. И в следующей секции я покажу, как это API использовать.

Настройка eslint

Я предполагаю, что в вашем проекте уже настроен eslint, если нет, то используйте официальную документацию.

Дополнительно нам нужно установить плагин для работы с локальными правилами.

npm install eslint-plugin-local-rules --save-dev

После чего нужно добавить в конфигурацию eslint подключение этого плагина, а также включить наше кастомное правило.

module.exports = {
  // ... другие настройки
  plugins: ['eslint-plugin-local-rules', /* другие плагины */],
  rules: {
    // ... другие правила
    'local-rules/no-bad-await': 'error' // Наше будущее правило
  }
};

В примере используется JS файл конфигурации, но легко адаптировать под JSON или YAML.

Создание правила

Для начала создайте файл eslint-local-rules.js в корне проекта. И добавьте в него следующий код

module.exports = {
  "no-bad-await": {
    create: context => {
      const allowedAncestors = [
        "ReturnStatement",
        "VariableDeclarator",
        "ExpressionStatement",
        "AssignmentExpression"
      ];

      return {
        AwaitExpression: node => {
          const ancestors = context.getAncestors(node);
          const last = ancestors[ancestors.length - 1];

          if (!allowedAncestors.includes(last.type)) {
            context.report({
              node,
              message: "Не используйте await в выражениях."
            });
          }
        }
      };
    }
  }
};

Данный файл должен экспортировать объект. Ключ в таком объекте является именем правила (в нашем случае no-bad-await), а значение - самим правилом. Простейшее правило можно описать объектом с одним методом: create. Этот метод получает первым аргументом объект context, содержащий различную информацию о настройках и полезные утилиты для работы с AST. Вернуть он должен объект, реализующий паттерн “посетитель” (visitor).

{
  AwaitExpression: node => {
    const ancestors = context.getAncestors(node);
    const last = ancestors[ancestors.length - 1];

    if (!allowedAncestors.includes(last.type)) {
      context.report({
        node,
        message: "Не используйте await в выражениях."
      });
    }
  }
};

Ключ в таком объекте является типом узла дерева, а значение — функцией, которая будет вызвана при посещении узла. При запуске eslint строит дерево, идёт по узлам, начиная с корня, и вызывает соответствующие функции, передавая в них текущий узел.

В нашем случае функция будет вызываться на каждое найденое выражение await.

Давайте разберем код функции-посетителя детально. Для начала нам нужно получить родительский узел, чтобы понять, является ли он запрещенным. Для этого мы используем API контекста getAncestors, возвращающий массив всех предков и берем последний из массива.

const ancestors = context.getAncestors(node);
const last = ancestors[ancestors.length - 1];

После чего мы проверяем, что родитель узла имеет один из разрешенных типов.

if (!allowedAncestors.includes(last.type)) {
  context.report({
    node,
    message: "Не используйте await в выражениях."
  });
}

Например, в коде

async function t(b) {
  await b
}

родителем у AwaitExpression является ExpressionStatement

ExpressionStatement as a parent

А в коде

async function t(b) {
  1 + await b
}

Родителем является BinaryExpression

BinaryExpression as a parent

И если родитель имеет тип, отличный от разрешенных, то выводим ошибку, вызвав метод context.report.

context.report({
  node,
  message: "Не используйте await в выражениях."
});

Заметьте, что одним из параметром является node. Это параметр нужен, чтобы указать eslint (и редакторам его использующим), какой участок показать как ошибочный. Это не обязательно должен быть узел, переданный в “посетителя”, может быть его родитель или любой другой.

Естественно, внутренний API для правил намного богаче и позволяет эффективно делать любые проверки кода. Ознакомиться детальнее можно в документации. Но самое важное, что небольшие правила можно написать быстро и легко.

Организационные вопросы

Когда должны выполняться проверки?

Проверки обязательно должны выполняться автоматически при создании Pull Request, до того как код попадет на ревью к человеку.

Существует практика делать проверки, используя прекоммит хук. Я считаю её неправильной в виду того, что она сильно раздражает, задерживая коммит. При этом не освобождает от необходимости запуска проверок на сервере.

Когда вносить правило?

После того, как на ревью появилась дискуссия с разными мнениями и решение было найдено. Даже если все текущие участники договорились про стиль, придет новый человек и обязательно повторит тоже самое.

Как вносить правило, если в коде уже есть проблемы?

Если проблем немного и их легко поправить разом, то лучше так и сделать. Если их много и они требуют времени, то хорошим вариантом будет использовать режим предупреждения (warning, показать пример настройки). Тогда люди, которые пишут новый код, будут видеть предупреждения. Старый код будет уходить со временем и планомерным рефакторингом. Можно использовать аргумент --max-warnings, который позволяет зафиксировать максимальное количество предупреждений.


Если у вас есть вопросы или вы хотите обсудить текст, то напишите мне в twitter.