ESLint против ошибок: битва за идеальный код!

Однажды на daily meeting техлид сообщил радостную новость: к нам в команду придёт новый automation QA, который покроет e2e-тестами наш проект. QA пришёл, посмотрел наш проект, и он ему не понравился. Необходимо было добавить на кнопки, поля атрибут data-testid, но не просто так один раз прописать, а чтобы вся команда получала предупреждения, если нужный атрибут не был добавлен. Не то чтобы этого атрибута совсем не было, но для выполнения задач e2e-тестирования его было катастрофически мало. И вот так началось моё знакомство с инструментом линтинга ESLint.

ESLint против ошибок: битва за идеальный код!

Что такое линтинг?

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

  • Синтаксические ошибки: линтеры помогают обнаруживать синтаксические ошибки в коде, такие как неправильное использование ключевых слов, отсутствие скобок, неправильное использование операторов и т. д.
  • Стилевые несоответствия: линтеры могут проверять стиль кода в соответствии с определенными стандартами или рекомендациями по оформлению. Это может включать правила о форматировании кода, отступах, расстановке скобок, именовании переменных и других стилевых аспектах.
  • Потенциальные ошибки: линтеры могут обнаруживать потенциальные ошибки в коде, которые могут привести к неожиданному поведению при выполнении. Например, использование неинициализированных переменных, потенциальные утечки памяти, неиспользуемый код и т. д.

Но самая основная цель линтинга — это сократить время на code-review. Т. е. команда принимает соглашение, например прописывает атрибут data-testid в нужных компонентах, а программа линтер проверяет, следует ли конкретный программист этому соглашению или нет. Т. е. будем следовать принципу Винни-Пуха: «Нужно делать так, как нужно, а как не нужно — делать не нужно».
Самый популярный инструмент для линтинга JavaScript-кода — это Eslint. Он позволяет анализировать JavaScript-файлы на предмет синтаксических ошибок, стилевых несоответствий и других проблем согласно заданным правилам.
ESLint также имеет поддержку для множества плагинов, что позволяет расширить его функциональность для обнаружения специфических проблем или интеграции с другими инструментами разработки. ESLint легко расширяем, и написать свой плагин больших проблем не возникнет.

Что имеем на текущем проекте?

ESLint против ошибок: битва за идеальный код!

Я решил разобраться, а как здесь и сейчас настроен линтинг на текущем проекте?
Конфиг линтера не блистал функционалом

   {
    "ignorePatterns": [
      "node_modules",
      "dist",
      "public"
    ],
    "extends": [
      "react-app",
      "react-app/jest"
    ],
    "rules": {
      "react/prop-types": "off",
      "no-console": "warn"
    },

Вероятно, как создали проект через create-react-app, сделали базовые настройки и больше его не касались.
Самое ключевое в этом конфиге — это плагин "react-app", который реализует уйму правил для проверки. А вот какие конкретно, можно увидеть при помощи команды

npx eslint --print-config .\package.json > result.json

Я просмотрел этот файл, по умолчанию в нашем проекте используется 119 правил! А я и не знал. Ряд из них очень занимательные. Например, запрещено сравнение с отрицательным нулём (-0) no-compare-neg-zero. У меня даже как в голову не приходило, что нуль может быть положительный и отрицательный, но по синтаксису JS возможно всё. И линтер стоит на страже, ну нельзя сравнивать с отрицательным нулём. 🙂
А вообще полный список правил, которые идут из коробки, можно почитать здесь rules.
Возник следующий вопрос, а сколько времени уходит на линтинг всего проекта?
Помогла вот эта команда

Measure-Command { npx eslint "./**/*.{js,jsx,ts,tsx}" }

Оказалось, около 7,5 секунд TotalSeconds : 7,4977735.
Но правил так много, а на какие правила уходит больше всего времени линтинга?
Написал такую команду в package.json

"lint-js": "set TIMING=1 && npx eslint \"./**/*.{js,jsx,ts,tsx}\"",

И у меня появилась десятка лидеров.

ESLint против ошибок: битва за идеальный код!

Ряд правил вызывает ряд вопросов. @typescript-eslint/no-redeclare про переобъявление переменной через var. Но 7 секунд на линтинг не так и много, так что решил это правило оставить.
После всех манипуляций у меня на проекте обнаружилось около 280 ошибок и предупреждений, которые нужно исправить.
Но не все их необходимо исправлять вручную. Есть волшебный ключ --fix. Этот ключ исправляет ошибки, которые eslint может исправить сам. Проставит там, где нужно, пробелы, точки с запятой и т. д.
"eslint:fix": "npx eslint \"./**/*.{js,jsx,ts,tsx}\" --fix",
Оказалось, очень много ошибок на файлы в неверной кодировке, т. е. во всём проекте UTF-8, а часть файлов — UTF-8 BOM. На исправление существующих ошибок ушло где-то 2 часа.

Внедрение плагина для проверки селекторов.

ESLint против ошибок: битва за идеальный код!

И вот настал волнующий момент, я нашёл плагин для eslint eslint-plugin-test-selectors, который проверяет, есть у интерактивных элементов атрибут data-test-id или нет.
Но радость была недолгой. Проблем было две:
Нужно проверять наличие атрибута data-testid, а не data-test-id. Небольшая чёрточка, но хотелось, чтобы всё было в едином стиле.
При отсутствии необходимого атрибута плагин орал, что всё пропало, и возникла страшная ошибка, которую немедленно сию же секунду исправить.
Но поведение плагинов можно легко поменять в конфиге, иногда даже на противоположное, главное, чтобы настройки позволяли. Вот так сейчас выглядит часть конфига.

"test-selectors/onClick": [
  "warn",
  "always",
  {
    "testAttribute": "data-testid"
  }
],

Т. е. если есть компонент, у которого присутствует событие onClick, то надо вывести предупреждение, если у этого компонента нет атрибута data-testid.
Во время работы возник маленький нюанс, я написал настройки правила, а WebStorm эту настройку не видит. Оказалось, надо выбрать в меню «Apply ESLint Code Style Rules», чтобы применились новые правила линтера, но это фишка именно WebStorm.

ESLint против ошибок: битва за идеальный код!

Оказалось, что в нашем проекте вагон и маленькая тележка компонентов, у которых не прописан атрибут data-testid. Я убил день и пофиксил все ошибки и предупреждения, и себе присвоил титул «паровозик, который смог».
А вообще, есть страница Awesome ESLint, на которой собраны популярные плагины и конфиги для eslint.
Руки так и тянутся пользу причинять да ласкам подвергать через максимальное количество плагинов, но я настоятельно рекомендую внедрять плагины только для тех библиотек, которые используются в проекте.
Например, у нас на проекте используется милый русскому сердцу store Effector, поэтому был установлен плагин eslint-plugin-effector, а также TypeScript, для него плагин @typescript-eslint.
После подключения плагинов и запуска линтера первое желание возникает сразу отключить все правила с ошибками. Но так делать не нужно, лучше зреть в корень. Почему авторы плагина посчитали, что это ошибка, какие есть варианты её исправления?
Например, у нашего хранилища линтер нашёл очень много ошибок, связанных с getState. Переходим на страницу правила effector/no-getState и читаем, что может возникнуть «состояние гонки» и лучше использовать ещё один объект, который хранит значение состояния. Писать код легко, но писать хороший код — сложно.
Отдельная боль — это использование any в TypeScript, я не нашёл однозначного ответа для решения этой проблемы, только создал таску в Jira. Программист — это человек, который не сдаётся никогда.

Написание собственного плагина

ESLint против ошибок: битва за идеальный код!

Коллеге senior, который проводил code review, понравилась исправление существующих ошибок на проекте и проверка наличия атрибута data-testid. И он предложил, а было бы хорошо, даже замечательно, проводить проверку, если есть атрибут data-testid, записан ли он в kebab-case. Kebab-case, т. е. все буквы маленькие, слова разделяются дефисом. Например, node-package-manager.
При наименовании data-testid атрибутов я пришёл, как ни странно, к схеме БЭМ. Т. е. сначала имя родительского компонента (Блок), затем конкретного элемента (Элемент), а затем нюансы (Модификатор) и делал это в kebab-case.
Как только я решил написать свой плагин, я подумал, наверное, это какая-то очень сложная вещь и тут нужны суперзнания в computer science и не только. Оказалось, всё достаточно просто.
Как работает линтинг?
Линтеры относятся к инструментам статического анализа кода, то есть без его реального выполнения. В их задачи входит:

  • анализ, парсинг строк исходного кода и построение на его основе AST (абстрактного синтаксического дерева);
  • запуск и выполнение правил над полученным AST;
  • отображение результатов анализа.

Чтобы посмотреть, как выглядит AST-дерево, проще зайти на сайт https://astexplorer.net/, вставить кусок кода и станет всё понятно. Это просто JSON-объект.

ESLint против ошибок: битва за идеальный код!

В целом, ESLint строит AST и затем, используя правила конфигурации, определяет, соответствует узел AST правилу или не соответствует, проверяются и различные аспекты кода. Это названия переменных, неправильное использование ключевых слов, стилевые ошибки и так далее.
А зачем писать плагин, если можно не писать? Например, в ESLint есть правило no-restricted-syntax. В которое можно как запрос css-селектора в формате AST передать нужное выражение.
Например, для setTimeout, чтобы у него было не более 2-х аргументов, нужное правило будет выглядеть так

"selector": "CallExpression[callee.name='setTimeout'][arguments.length!=2]",

Но если нужен именно плагин, который реализует набор правил. То анатомия правила следующая

module.exports = {
    meta: {
        // TODO: add metadata
    },
    create(context) {
        return {
            "IfStatement > BlockStatement": function(blockStatementNode) {...},
            "FunctionDeclaration[params.length>3]": function(functionDeclarationNode) {...}
        };
    }
};

Т. е. сначала описывает метаинформация, название правила, его тип и т. д. И самое важное — функция create.
Она должна вернуть объект с обратными вызовами. Каждый обратный вызов, соответствующий определённому селектору, принимает в качестве аргумента текущий обрабатываемый узел AST, который подходит под этот селектор.
Т. е. пришёл узел, подходящий под селектор, вызвали функцию. Функция провела проверку и сказала, всё хорошо или всё плохо.
Я уже начал писать плагин, но возникла проблема чисто бюрократическая. Все npm-пакеты у нас находятся во внутреннем репозитории Nexus, и просто так без многочисленных согласований и обоснований что-то добавить туда сильно трудоёмко. Поэтому решили kebab-case проверять на code review.

Заключение

ESLint против ошибок: битва за идеальный код!

ESLint — мощный инструмент для анализа вашего кода и сокращения времени code review. Я постарался рассказать:

  • что такое линтинг;
  • как узнать правила, используемые на проекте;
  • как определить, какие правила отнимают больше всего времени;
  • какие существуют плагины для ESLint;
  • как написать свой плагин;

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

P. S. После введения новых правил линтер разработчики в команде начали их отключать через @ts-ignore, но это уже другая история — это заболевание, передаётся через коммиты и не проблема не ESLint.