Конфликт зон ответственности

Одной из наиболее распространённых проблем при использовании ESLint в современных JavaScript-проектах является пересечение обязанностей между различными инструментами разработки. По мере роста экосистемы JavaScript рядом с ESLint появились специализированные решения для форматирования кода, проверки типов, тестирования, анализа безопасности и контроля качества. В результате возникает вопрос: какой инструмент должен отвечать за конкретную задачу?

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


Понятие зоны ответственности инструмента

Зона ответственности — это набор задач, для решения которых инструмент был разработан.

Для ESLint основной областью ответственности являются:

  • анализ исходного кода;
  • выявление потенциальных ошибок;
  • контроль соблюдения соглашений по написанию кода;
  • обеспечение единообразия стиля разработки;
  • обнаружение подозрительных конструкций.

Примеры задач ESLint:

if (value = 10) {
    console.log(value);
}

Правило:

{
    "rules": {
        "no-cond-assign": "error"
    }
}

ESLint обнаружит ошибочное присваивание внутри условия.

Другой пример:

const user = {
    name: "John"
};

console.log(user.age.toUpperCase());

С использованием дополнительных анализаторов и правил ESLint способен выявить подозрительные места, указывающие на потенциальную ошибку.

Однако не каждая проблема должна решаться средствами ESLint.


ESLint и форматирование кода

Наиболее известный конфликт возникает между ESLint и Prettier.

Исторически ESLint содержал множество правил форматирования:

{
    "rules": {
        "indent": ["error", 4],
        "quotes": ["error", "single"],
        "semi": ["error", "always"]
    }
}

Такие правила отвечали за:

  • отступы;
  • точки с запятой;
  • кавычки;
  • переносы строк;
  • пробелы;
  • расположение скобок.

Со временем появились специализированные форматтеры, главным из которых стал Prettier.

Пример.

Исходный код:

const user={name:"John",age:25}

После обработки Prettier:

const user = { name: "John", age: 25 };

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

  • ESLint требует один стиль;
  • Prettier автоматически применяет другой.

Например:

{
    "rules": {
        "quotes": ["error", "single"]
    }
}

и одновременно:

const name = "John";

Prettier может сохранить двойные кавычки, а ESLint будет требовать одинарные.

Результат:

  1. Форматтер изменяет код.
  2. Линтер сообщает об ошибке.
  3. Разработчик снова исправляет код.
  4. Возникает бесконечный цикл исправлений.

Поэтому в современных проектах принято разделять обязанности:

Инструмент Ответственность
ESLint Анализ качества кода
Prettier Форматирование

Для устранения конфликтов используется пакет:

eslint-config-prettier

Он отключает правила ESLint, пересекающиеся с Prettier.

Пример:

import eslintConfigPrettier from "eslint-config-prettier";

export default [
    eslintConfigPrettier
];

Такой подход считается стандартом современной разработки.


ESLint и TypeScript

Другой важный источник конфликтов связан с TypeScript.

TypeScript выполняет:

  • статическую проверку типов;
  • анализ совместимости типов;
  • обнаружение ошибок типизации;
  • контроль интерфейсов и дженериков.

Например:

const age: number = "25";

Ошибка будет обнаружена компилятором TypeScript ещё до запуска ESLint.

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

Неверный подход:

ESLint → проверка типов
TypeScript → проверка типов

Правильный подход:

TypeScript → типизация
ESLint → качество кода

Рассмотрим пример.

function getUser(id: number) {
    return fetch(`/users/${id}`);
}

TypeScript контролирует тип параметра:

getUser("123");

Ошибка:

Argument of type 'string'
is not assignable to parameter of type 'number'

ESLint в данном случае не нужен.

Зато ESLint способен обнаружить проблемы другого рода:

async function loadUser(id: number) {
    fetch(`/users/${id}`);
}

Здесь промис создаётся, но результат не используется.

Специальные правила могут указать на потенциальную ошибку:

{
    "rules": {
        "@typescript-eslint/no-floating-promises": "error"
    }
}

Так достигается разделение обязанностей между инструментами.


Конфликт между ESLint и компилятором

Компиляторы также выполняют собственные проверки.

Например, TypeScript может обнаруживать неиспользуемые переменные:

const name = "John";

Если переменная нигде не используется:

{
    "compilerOptions": {
        "noUnusedLocals": true
    }
}

Компилятор выдаст предупреждение.

Одновременно ESLint может содержать правило:

{
    "rules": {
        "no-unused-vars": "error"
    }
}

Возникает двойное сообщение об одной и той же проблеме.

Разработчик получает:

TS6133: 'name' is declared but never read.

и

'Name' is assigned a value but never used.

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

Обычно выбирается один источник диагностики.

Часто используется схема:

TypeScript → ошибки типов
ESLint → качество кода

При этом часть проверок компилятора отключается либо заменяется специализированными правилами ESLint.


ESLint и средства анализа безопасности

Для поиска уязвимостей существуют отдельные инструменты:

  • npm audit;
  • Snyk;
  • SonarQube;
  • CodeQL;
  • Dependabot.

Иногда возникает соблазн использовать ESLint как полноценный сканер безопасности.

Например:

eval(userInput);

Линтер способен обнаружить опасную конструкцию:

{
    "rules": {
        "no-eval": "error"
    }
}

Однако сложные угрозы выходят за пределы возможностей ESLint.

Пример:

import vulnerableLibrary from "old-package";

ESLint не определяет наличие известной CVE-уязвимости в библиотеке.

Этим занимаются специализированные решения.

Разделение обязанностей выглядит следующим образом:

Инструмент Назначение
ESLint Подозрительные конструкции
npm audit Уязвимые зависимости
Snyk Анализ безопасности
CodeQL Глубокий анализ кода

ESLint и тестирование

Иногда правила линтера пытаются использовать для контроля качества тестов.

Пример теста:

test("sum", () => {
    expect(sum(2, 3)).toBe(5);
});

Существуют плагины:

eslint-plugin-jest

Они проверяют:

  • корректность структуры тестов;
  • наличие ошибочных паттернов;
  • правильное использование API Jest.

Однако ESLint не выполняет сами тесты.

Он не способен определить:

expect(sum(2, 3)).toBe(10);

как логическую ошибку.

Для этого предназначен тестовый фреймворк.

Разделение обязанностей:

Инструмент Функция
ESLint Анализ тестового кода
Jest Выполнение тестов
Vitest Выполнение тестов
Mocha Выполнение тестов

Конфликт между правилами различных плагинов

По мере роста проекта количество плагинов увеличивается.

Например:

eslint-plugin-import
eslint-plugin-react
eslint-plugin-unicorn
eslint-plugin-sonarjs

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

Иногда они противоречат друг другу.

Пример.

Одно правило требует:

export default User;

Другое правило рекомендует:

export { User };

Линтер начинает выдавать взаимоисключающие рекомендации.

В подобных случаях необходимо определить главный источник архитектурных соглашений и отключить конфликтующие правила.

Пример:

{
    "rules": {
        "import/prefer-default-export": "off"
    }
}

или

{
    "rules": {
        "import/no-default-export": "off"
    }
}

Выбор зависит от стандартов конкретного проекта.


Архитектурный конфликт правил

Иногда ESLint используется для навязывания архитектурных решений.

Например:

src/
├── components/
├── services/
├── utils/
└── pages/

Появляется требование:

pages → services
pages → components
services → utils

и запрет:

utils → pages

Стандартные правила ESLint не предназначены для подобных задач.

Для этого используются специализированные плагины:

eslint-plugin-boundaries

или

eslint-plugin-import

с дополнительными ограничениями.

Пример:

{
    "rules": {
        "import/no-cycle": "error"
    }
}

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

Поэтому архитектурные проверки должны ограничиваться действительно важными правилами.


Признаки конфликта зон ответственности

О наличии проблемы обычно свидетельствуют следующие признаки:

Дублирующиеся сообщения

Одну ошибку одновременно сообщают:

  • ESLint;
  • TypeScript;
  • IDE.

Противоречивые исправления

После автоматического исправления:

eslint --fix

форматтер снова меняет код:

prettier --write

или наоборот.

Слишком большой конфигурационный файл

Например:

export default [
    ...,
    ...,
    ...,
    ...,
    ...,
    ...
];

Конфигурация начинает содержать десятки плагинов и сотни правил.

Увеличение времени проверки

Линтер выполняется заметно дольше:

eslint .

Причиной часто становится выполнение задач, не относящихся напрямую к анализу качества кода.

Трудность поддержки

Изменение одного правила вызывает цепочку изменений в нескольких инструментах одновременно.


Принципы разделения ответственности

При проектировании конфигурации ESLint обычно придерживаются следующих принципов.

Форматирование должно выполняться форматтером.

Prettier → форматирование

Типизация должна выполняться системой типов.

TypeScript → типы

Тестирование должно выполняться тестовыми фреймворками.

Jest/Vitest → тесты

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

npm audit
Snyk
CodeQL

ESLint должен заниматься качеством исходного кода.

Примеры задач ESLint:

  • неиспользуемые переменные;
  • подозрительные конструкции;
  • ошибки асинхронности;
  • проблемы импортов;
  • нарушение соглашений проекта;
  • потенциальные дефекты выполнения.

Чёткое разделение зон ответственности делает конфигурацию проще, уменьшает количество конфликтов между инструментами и позволяет каждому компоненту процесса разработки выполнять именно те задачи, для которых он был создан.