Введение

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

Одно из реализованных нами решений - использование автоматических инструментов, таких как линтер. Они помогают нам придерживаться определенных условностей.
В этой статье рассматривается практический пример: убедитесь, что все имена перечислений являются PascalCase.

Внутренне мы не были согласованы в способах записи имен перечислений в нашем внешнем коде. У нас были имена PascalCase и CAPITAL_LETTERS.
Целью было выбрать одно соглашение и обеспечить его соблюдение во всей базе кода. Мы решили выбрать PascalCase, потому что мы предпочитаем его.
Нам нужно было не только пройти через всю кодовую базу, чтобы переименовать перечисления, нам также нужен был способ обеспечить, чтобы новые были PascalCase.
Это было достигнуто. с помощью линтера и написав правило линтера.

Но что такое линтеры и правила линта?
Линтеры - это инструменты, которые выполняют статический анализ кода. Это означает, что они анализируют код, не выполняя его. Они проверяют выполнение некоторых правил, правил lint.
Если какие-то правила не выполняются, то разработчик будет уведомлен линтером и должен будет исправить код.

В нашем конкретном случае правило:
Все имена перечислений - это PascalCase

Мы увидим, как его закодировать, чтобы линтер понимал и применял.

Написание правила lint

Наш интерфейс написан на TypeScript. Таким образом, мы используем ESLint вместе с typescript-eslint в качестве линтера.

Правило ESLint - это объект, содержащий ряд свойств.
Полную документацию вы можете найти на официальном сайте. Однако некоторые свойства мы рассмотрим вместе.

Сначала мы увидим метаданные правила. Затем мы увидим его тело, содержащее логику для анализа кода.

Метаданные

Метаданные документируют правило, его тип и возможности.

module.exports = {
  meta: {
    type: "layout",
    docs: {
      description: "enforce pascal case enum names",
      category: "Stylistic Issues",
    },
    fixable: "code",
    messages: {
      pascalCaseEnumNamesMessage: "Enum name should be pascal case, was: {{name}}, expected: {{pascalCaseName}}",
    },
  },
}

В атрибуте meta мы указываем следующие свойства:

  • type установлен в layout, потому что правило касается внешнего вида кода.
  • Свойство docs документирует правило для читателей. category выбран среди перечисленных на официальной странице документации.
  • fixable указывает, может ли правило исправить код, чтобы правило выполнялось.
  • messages определяет сообщения, используемые правилом. Сообщения здесь указывать не обязательно, но потом будет проще написать тесты. В этом примере предоставленное сообщение содержит 2 заполнителя: name и pascalCaseName.

Теперь, когда мы прошли через метаданные правила, нам нужно написать логику правила.

Логика

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

Визуализация абстрактного синтаксического дерева

Линтер анализирует наш код, чтобы построить из него абстрактное синтаксическое дерево.
AST - это древовидная структура данных, которая используется для представления кода.

Пример:

const a = b + c;

Соответствующий AST этого выражения:

После того, как AST построен, линтер проходит его и проверяет выполнение правил lint на узлах AST. Правило lint - это просто функция, которая принимает узел AST в качестве аргумента и использует его для проверки некоторых проверок.

В нашем конкретном случае мы хотим убедиться, что перечисления относятся к PascalCase.

Линтер посетит AST и остановится, когда найдет узел, соответствующий объявлению перечисления. Для визуализации этого узла мы используем astexplorer. Этот инструмент создает AST из фрагмента кода.
В нашем случае, когда мы пишем код TypeScript, нам нужно выбрать JavaScript в качестве языка и @typescript-eslint/parser в качестве парсера.
Затем мы можем написать фрагмент кода, содержащий объявление перечисления, чтобы увидеть, как выглядит соответствующий AST.

enum Animal {
  Cat,
  Dog,
}

Что соответствует следующему AST:

Наша цель - написать правило, гарантирующее, что Animal написано Animal, а не animal или ANIMAL. Это означает, что нам нужно проверить имя в узле Identifier узла TSEnumDeclaration.

Но как отфильтровать узлы, к которым применить правило?

Выбор узла

Чтобы выбрать узлы для проверки, мы используем селекторы. Как всегда, в официальной документации есть отличная страница о селекторах.
Из официальной документации:

Селектор - это строка, которая может использоваться для сопоставления узлов в абстрактном синтаксическом дереве (AST).

Мы можем видеть аналогию с селекторами CSS, которые используются для выбора элементов HTML.
Мы будем использовать селектор для выбора узла идентификатора, который является дочерним по отношению к узлу TSEnumDeclaration.

Селектор для выбора дочернего элемента узла использует следующий синтаксис:
"ParentNode > ChildrenNode"

Таким образом, в нашем случае нам понадобится следующий селектор:
"TSEnumDeclaration > Identifier"

Вот и все!

Сообщение о проблеме

А теперь давайте продолжим писать наше правило:

module.exports = {
  meta: {
    type: "layout",
    docs: {
      description: "enforce pascal case enum names",
      category: "Stylistic Issues",
    },
    fixable: "code",
    messages: {
      pascalCaseEnumNamesMessage: "Enum name should be pascal case, was: {{name}}, expected: {{pascalCaseName}}",
    },
  },
  create: function (context) {
    return {
      “TSEnumDeclaration > Identifier”: (node) => {
        // body to be filled
      }
    }
  }
}

Свойство create сопоставляется с функцией.
Оно принимает в качестве аргумента объект context, который содержит: информацию о контексте правила, нескольких служебных методах и особенно функцию report, которая используется для сообщения о проверке. не удовлетворены.

Свойство create должно возвращать объект. Он содержит селекторы с функцией проверки узла, выбранного соответствующим селектором. В нашем случае функция, сопоставленная с селектором "TSEnumDeclaration > Identifier", получит в качестве аргумента узел идентификатора. Тело этой функции должно проверять, что name, содержащийся в узле Identifier, является PascalCase:

module.exports = {
  meta: {
    type: "layout",
    docs: {
      description: "enforce pascal case enum names",
      category: "Stylistic Issues",
    },
    fixable: "code",
    messages: {
      pascalCaseEnumNamesMessage: "Enum name should be pascal case, was: {{name}}, expected: {{pascalCaseName}}",
    },
  },
  create: function (context) {
    return {
      “TSEnumDeclaration > Identifier”: (node) => {
        const name = node.name;
        const pascalCaseName = toPascalCase(name);
        if (name !== pascalCaseName) {
          context.report({
            node,
            messageId: "pascalCaseEnumNamesMessage",
            data: {
              name,
              pascalCaseName,
            },
          });
        }
      }
    }
  }
}

Если имя в узле Identifier не PascalCase, он сообщает об ошибке. Кроме того, мы передаем сообщение в функцию отчета, которое будет отображено инженеру. В нашем случае сообщение приходит из раздела meta правила.
Свойство data определяет значения заполнителей в сообщении.

Предлагаем исправление

В разделе meta правила мы указали, что правило может предлагать исправление через свойство fixable. Для этого мы передаем функцию в качестве аргумента report, которая описывает, как исправить код.

module.exports = {
  meta: {
    type: "layout",
    docs: {
      description: "enforce pascal case enum names",
      category: "Stylistic Issues",
    },
    fixable: "code",
    messages: {
      pascalCaseEnumNamesMessage: "Enum name should be pascal case, was: {{name}}, expected: {{pascalCaseName}}",
    },
  },
  create: function (context) {
    return {
      “TSEnumDeclaration > Identifier”: (node) => {
        const name = node.name;
        const pascalCaseName = toPascalCase(name);
        if (name !== pascalCaseName) {
          context.report({
            node,
            messageId: "pascalCaseEnumNamesMessage",
            data: {
              name,
              pascalCaseName,
            },
            fix: function (fixer) {
              // body to be filled
            }
          });
        }
      }
    }
  }
}

Функция fix принимает в качестве аргумента объект fixer. Объект fixer содержит несколько утилит, удобных при исправлении кода. В нашем случае, если name не является PascalCase, нам нужно заменить его версией PascalCase. Мы можем использовать утилиту replaceText на fixer. Он берет узел и новый текст и заменяет текст в данном узле новым текстом.

module.exports = {
  meta: {
    type: "layout",
    docs: {
      description: "enforce pascal case enum names",
      category: "Stylistic Issues",
    },
    fixable: "code",
    messages: {
      pascalCaseEnumNamesMessage:
        "Enum name should be pascal case, was: {{name}}, expected: {{pascalCaseName}}",
    },
  },
  create: function (context) {
    return {
      “TSEnumDeclaration > Identifier”: (node) => {
        const name = node.name;
        const pascalCaseName = toPascalCase(name);
        if (name !== pascalCaseName) {
          context.report({
            node,
            messageId: "pascalCaseEnumNamesMessage",
            data: {
              name,
              pascalCaseName,
            },
            fix: function (fixer) {
              return fixer.replaceText(node, pascalCaseName);
            }
          });
        }
      }
    }
  }
}

И это все! У нас есть правило, согласно которому имена перечислений должны быть PascalCase.

Теперь давайте посмотрим, как мы можем проверить это правило.

Проверка правила

ESLint позволяет писать модульные тесты для правила через объект RuleTester (официальная документация).

При создании экземпляра RuleTester нам необходимо указать синтаксический анализатор @typescript-eslint/parser.

Объект RuleTester принимает в качестве аргументов имя теста, объект правила и объект, в котором реализованы тесты.

const RuleTester = require("eslint").RuleTester;
const pascalCaseEnumNamesRule = require("eslint-plugin-alan/lib/rules/naming/pascal-case-enum-names/pascal-case-enum-names");
const ruleTester = new RuleTester({
  parser: require.resolve("@typescript-eslint/parser"),
});
ruleTester.run("pascal-case-enum-names", pascalCaseEnumNamesRule, {
  // Tests goes here
});

Пишем тесты в третий объект. В свойстве valid перечислены все тесты, которые должны пройти, а в свойстве invalid перечислены тесты, которые должны пройти.

const RuleTester = require("eslint").RuleTester;
const pascalCaseEnumNamesRule = require("eslint-plugin-alan/lib/rules/naming/pascal-case-enum-names/pascal-case-enum-names");
const ruleTester = new RuleTester({
  parser: require.resolve("@typescript-eslint/parser"),
});
ruleTester.run("pascal-case-enum-names", pascalCaseEnumNamesRule, {
  valid: [
    {
      code: "enum ContractStatusType {}",
    },
  ],
  invalid: [
    {
      code: "enum contract_status_type {}",
      errors: [
        {
          messageId: "pascalCaseEnumNamesMessage",
          data: {
            name: "contract_status_type",
            pascalCaseName: "ContractStatusType",
            type: "Identifier",
          },
        },
      ],
      output: "enum ContractStatusType {}",
    },
    {
      code: "enum contractStatusType {}",
      errors: [
        {
          messageId: "pascalCaseEnumNamesMessage",
          data: {
            name: "contractStatusType",
            pascalCaseName: "ContractStatusType",
            type: "Identifier",
          },
        },
      ],
      output: "enum ContractStatusType {}",
    },
  ],
});

Для всех тестов мы пишем код, который будет проверяться правилом.
Для invalid тестов мы добавляем свойство errors, в котором перечислены ожидаемые ошибки. Свойство errors содержит сообщение, которое должно сообщаться правилом в случае, если оно не выполнено. В дополнение к errors, тесты invalid могут указывать код, зафиксированный правилом в свойстве output.

Заключение

Благодаря этому правилу мы теперь выровнены по именам перечислений во всей нашей интерфейсной кодовой базе.
Это простое правило, но служит примером для написания более сложных правил. Фактически, мы регулярно добавляем новые, чтобы не рекомендовать старые коды и предлагать инженерам лучшие альтернативы.

В заключение, мы настаиваем на все большей и большей согласованности в нашем коде в Alan. Линтерс будет большим подспорьем в этом путешествии!