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

Абстрактные синтаксические деревья

Прежде чем мы поговорим о том, как написать правило ESLint, давайте быстро обсудим абстрактные синтаксические деревья или AST. AST — это представление исходного кода. Обычно они создаются путем токенизации файла, зная о синтаксисе достаточно, чтобы определить роль каждого токена, и связывая токены друг с другом как узлы в дереве.

Вот короткая программа на JavaScript и соответствующий AST, сгенерированный с помощью AST Explorer:

const strings = ["foo", "bar", "baz"];
strings.forEach((s, i) => console.log(`String ${i}:` + s));

AST обычно используются линтерами, потому что они позволяют структурированно понимать код без его компиляции или запуска. Это помогает линтерам работать быстро и с небольшим количеством зависимостей, поэтому их легко интегрировать в IDE.

Когда ESLint запускается, он создает AST для каждого оцениваемого файла, а затем проходит по узлам. Он перехватывает узлы, указанные в правилах, и запускает на них соответствующую логику правил. Если он определяет, что узел нарушает определенное правило, он сообщает о проблеме.

Написание пользовательского правила ESLint

Чтобы сделать это описание немного более конкретным, давайте рассмотрим простой (хотя и произвольный) пример. Допустим, мы хотим, чтобы строка «посвященный» была зарезервированным словом. У нас могут быть имена переменных, такие как isDevoted или devotedUserId, но мы не хотим, чтобы переменная называлась просто devoted.

Первый шаг — понять, как выглядит наш AST для определения переменной. Чтобы помочь нам на этом фронте, мы будем использовать AST Explorer. Это мощный инструмент, который позволит нам определить некоторый тестовый код, пройтись по AST нашего тестового кода, построить наше правило и увидеть результат выполнения этого правила в написанном нами тестовом коде.

После перехода к AST Explorer выберите JavaScript в качестве языка, а затем в разделе «Преобразование» выберите «ESLint». Теперь на экране должно быть 4 панели. Верхняя левая панель предназначена для вашего тестового кода, а верхняя правая панель — представление AST для вашего тестового кода. На нижней левой панели мы будем вводить наше правило, а на нижней правой панели будет вывод правила, запускаемого в тестовом коде.

Добавьте следующее в правую верхнюю панель проводника AST. Это послужит нашим тестовым кодом для правила, которое мы собираемся написать.

const devoted = 'rule violation';
const isDevoted = 'not a rule violation';
const get_devoted = () => {
    return 'also not a rule violation';
};

После ввода этого кода на панели тестового кода справа появится AST. Вы можете просмотреть AST на самой панели или, если щелкнуть разные части кода на панели тестового кода, панель AST автоматически перейдет к той части AST, которая представляет часть кода, по которой вы щелкнули ( эту функцию можно переключить, установив флажок «автофокус» на верхней правой панели).

Нажав на имена переменных, вы увидите, что они отображаются в свойстве «имя» типа узла «Идентификатор». Давайте используем это как отправную точку для нашего правила! В нижней правой панели добавьте следующее:

module.exports = {
  create(context) {
    return {
      Identifier(node) {
        console.log(node);
      }
    };
  },
}

Это создает контекст (объект с данными, относящимися к контексту, в котором выполнялось правило), который мы в конечном итоге будем использовать, чтобы сообщить инженеру об ошибке. Если вы посмотрите в консоль, вы увидите детали узла для каждого узла идентификатора в нашем тестовом коде. Данные те же, что и на панели AST, но с помощью консоли можно взглянуть на них по-другому.

Глядя на AST, мы знаем, что для нашего правила нам нужно ориентироваться на имя узла, поскольку это то, что мы хотим проверить на соответствие нашему «зарезервированному слову».

module.exports = {
  create(context) {
    return {
    Identifier(node) {
        if (node.name.toLowerCase() === 'devoted') {
          console.log(`OH NO, the variable name '${node.name}' breaks our rule`);
        } else {
          console.log(`'${node.name}' is a good variable name`);
        }
      }
    };
  },
}

Здесь мы видим в консоли, что devoted — это неудачный тестовый пример, а isDevoted и get_devoted пройдены. Отлично, мы придумали, как выявить закономерность, нарушающую наше правило! Следующим шагом является сообщение об этом нарушении.

Мы используем context.report(), чтобы сообщить о нарушении правила. Отчет будет либо предупреждением, либо ошибкой (в зависимости от того, как настроено правило в вашем .eslintrc.js). Объект, принятый context.report, имеет одно обязательное свойство сообщение, которое содержит сообщение, которое будет отправлено пользователю при нарушении правила. Приведенная ниже модификация нашего правила предупредит пользователя о нарушении правила сообщением:

module.exports = {
  create(context) {
    return {
    Identifier(node) {
        if (node.name.toLowerCase() === 'devoted') {
          return context.report({
            node,
            message: "'Devoted' is a reserved word"
          });
        }
        return null;
      }
    };
  },
}

Если вы сейчас посмотрите на нижнюю правую панель, вы увидите, что сообщение появляется для нашего не пройденного теста, а не для нашего пройденного теста. Ниже также находится раздел, который читает Fixed output follows:, а затем сообщает нам наш тестовый код как есть.

Объект, который мы передаем context.report, имеет необязательное свойство fix, которое позволяет нам определить функцию, которая будет автоматически применять исправление для устранения нарушения. Объект исправления, который мы используем для этого, имеет несколько функций, которые мы можем использовать для автоматического решения нашей проблемы. В этом случае, если мы хотим полностью заменить имя переменной другим именем переменной, мы можем использовать fixer.replaceText(node, string) для применения исправления.

module.exports = {
  create(context) {
    return {
    Identifier(node) {
        if (node.name.toLowerCase() === 'devoted') {
          return context.report({
            node,
            message: "Devoted is a reserved word",
            fix: function(fixer) {
              return fixer.replaceText(node, 'anotherVariableName');
            }
          });
        }
        return null;
      }
    };
  },
}

Введя это в нашу панель правил, мы увидим, что правило линтера не сработало, а вместо этого автоматически было применено наше исправление!

Настройка пользовательского правила ESLint

Теперь, когда у вас есть логика вашего правила, вам нужно добавить ее в свой собственный плагин ESLint и настроить правило в файле .eslintrc.js. Используя ту же настройку, которая показана в посте Выдача нескольких уровней ошибок с одним и тем же правилом ESlint, это так же просто, как:

  1. Создание файла, содержащего логику вашего правила:
touch eslint/rules/devoted-reserved-word.js

2. Добавление определения правила, которое вы написали в AST Explorer, в файл, созданный на шаге 1:

module.exports = {
  create(context) {
    return {
    Identifier(node) {
        if (node.name.toLowerCase() === 'devoted') {
          return context.report({
            node,
            message: "'Devoted' is a reserved word"
          });
        }
        return null;
      }
    };
  },
}

3. Экспорт вашего правила из модуля Node:

module.exports = {
  rules: {
    'devoted-reserved-word': require('./rules/devoted-reserved-word'),
    // Other custom rules in your ./rules directory
  },
}

4. Добавление конфигурации правила в ваш .eslintrc.js в корне вашего проекта:

module.exports = {
    // Other configuration
    "rules": {
        "custom-rules/devoted-reserved-word": "error",
        // Other rules
    }
};

Что дальше?

Надеюсь, это было полезное пошаговое руководство о том, как написать простое правило ESLint для вашего пользовательского плагина ESLint! Отсюда поиграйте в AST Explorer, потренируйтесь в написании некоторых правил и сделайте свою кодовую базу сильнее и чище с помощью большего количества линтинга!