Одной из наиболее распространённых проблем при использовании 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 и 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 };
В подобных случаях возникает дублирование обязанностей:
Например:
{
"rules": {
"quotes": ["error", "single"]
}
}
и одновременно:
const name = "John";
Prettier может сохранить двойные кавычки, а ESLint будет требовать одинарные.
Результат:
Поэтому в современных проектах принято разделять обязанности:
| Инструмент | Ответственность |
|---|---|
| ESLint | Анализ качества кода |
| Prettier | Форматирование |
Для устранения конфликтов используется пакет:
eslint-config-prettier
Он отключает правила ESLint, пересекающиеся с Prettier.
Пример:
import eslintConfigPrettier from "eslint-config-prettier";
export default [
eslintConfigPrettier
];
Такой подход считается стандартом современной разработки.
Другой важный источник конфликтов связан с 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"
}
}
Так достигается разделение обязанностей между инструментами.
Компиляторы также выполняют собственные проверки.
Например, 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 как полноценный сканер безопасности.
Например:
eval(userInput);
Линтер способен обнаружить опасную конструкцию:
{
"rules": {
"no-eval": "error"
}
}
Однако сложные угрозы выходят за пределы возможностей ESLint.
Пример:
import vulnerableLibrary from "old-package";
ESLint не определяет наличие известной CVE-уязвимости в библиотеке.
Этим занимаются специализированные решения.
Разделение обязанностей выглядит следующим образом:
| Инструмент | Назначение |
|---|---|
| ESLint | Подозрительные конструкции |
| npm audit | Уязвимые зависимости |
| Snyk | Анализ безопасности |
| CodeQL | Глубокий анализ кода |
Иногда правила линтера пытаются использовать для контроля качества тестов.
Пример теста:
test("sum", () => {
expect(sum(2, 3)).toBe(5);
});
Существуют плагины:
eslint-plugin-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 --fix
форматтер снова меняет код:
prettier --write
или наоборот.
Например:
export default [
...,
...,
...,
...,
...,
...
];
Конфигурация начинает содержать десятки плагинов и сотни правил.
Линтер выполняется заметно дольше:
eslint .
Причиной часто становится выполнение задач, не относящихся напрямую к анализу качества кода.
Изменение одного правила вызывает цепочку изменений в нескольких инструментах одновременно.
При проектировании конфигурации ESLint обычно придерживаются следующих принципов.
Форматирование должно выполняться форматтером.
Prettier → форматирование
Типизация должна выполняться системой типов.
TypeScript → типы
Тестирование должно выполняться тестовыми фреймворками.
Jest/Vitest → тесты
Поиск уязвимостей должен выполняться средствами безопасности.
npm audit
Snyk
CodeQL
ESLint должен заниматься качеством исходного кода.
Примеры задач ESLint:
Чёткое разделение зон ответственности делает конфигурацию проще, уменьшает количество конфликтов между инструментами и позволяет каждому компоненту процесса разработки выполнять именно те задачи, для которых он был создан.