Fixable-правила и их особенности

В экосистеме ESLint существует особая категория правил, способных не только обнаруживать нарушения, но и автоматически исправлять их. Такие правила называются fixable-правилами (исправляемыми правилами). Их основная задача заключается в автоматизации рутинных изменений исходного кода без участия разработчика.

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

Пример:

const message = "Hello"

При активном правиле semi ESLint обнаружит отсутствие точки с запятой:

const message = "Hello";

Вместо вывода только предупреждения инструмент автоматически внесёт изменение в исходный файл.


Как работает механизм исправлений

Каждое правило ESLint может сообщать о найденных проблемах через метод context.report().

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

context.report({
    node,
    message: "Ошибка оформления"
});

Чтобы правило стало исправляемым, в объект отчёта добавляется функция fix.

context.report({
    node,
    message: "Лишний пробел",
    fix(fixer) {
        return fixer.remove(node);
    }
});

При запуске с флагом:

eslint . --fix

ESLint выполнит следующие действия:

  1. Найдёт нарушение.
  2. Получит исправление через функцию fix.
  3. Сформирует набор изменений.
  4. Проверит отсутствие конфликтов.
  5. Применит изменения к файлу.

Метаданные fixable-правил

Каждое исправляемое правило обязано явно сообщать ESLint о возможности автоматического исправления.

Для этого используется свойство fixable внутри объекта meta.

Пример:

module.exports = {
    meta: {
        fixable: "code"
    },

    create(context) {
        return {};
    }
};

Возможны два значения:

Значение Назначение
code Изменение исходного кода
whitespace Изменение только пробельных символов

Пример правила форматирования:

meta: {
    fixable: "whitespace"
}

Пример правила изменения синтаксиса:

meta: {
    fixable: "code"
}

Если функция fix присутствует, но свойство meta.fixable отсутствует, ESLint выдаст ошибку при загрузке правила.


Объект fixer

Для внесения изменений используется специальный объект fixer, который передаётся в функцию исправления.

fix(fixer) {
    // исправление
}

Объект предоставляет набор методов для модификации текста.


insertTextAfter

Вставка текста после узла.

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

const value = 10

Исправление:

fix(fixer) {
    return fixer.insertTextAfter(node, ";");
}

Результат:

const value = 10;

insertTextBefore

Вставка текста перед узлом.

Пример:

fix(fixer) {
    return fixer.insertTextBefore(node, "/* generated */\n");
}

Результат:

/* generated */
const value = 10;

replaceText

Замена текста узла.

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

var count = 5;

Исправление:

fix(fixer) {
    return fixer.replaceText(node, "let count = 5");
}

Результат:

let count = 5;

replaceTextRange

Замена диапазона символов.

Пример:

fix(fixer) {
    return fixer.replaceTextRange(
        [10, 15],
        "const"
    );
}

Изменяется только указанный диапазон текста.


remove

Удаление узла целиком.

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

debugger;

Исправление:

fix(fixer) {
    return fixer.remove(node);
}

Результат:


removeRange

Удаление произвольного диапазона.

fix(fixer) {
    return fixer.removeRange([20, 30]);
}

Метод применяется при необходимости удалить часть конструкции, а не весь AST-узел.


Пример собственного fixable-правила

Рассмотрим правило, запрещающее использование var.

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

var user = "Alex";

Реализация:

module.exports = {
    meta: {
        fixable: "code"
    },

    create(context) {
        return {
            VariableDeclaration(node) {
                if (node.kind !== "var") {
                    return;
                }

                context.report({
                    node,
                    message: "Используйте let вместо var",

                    fix(fixer) {
                        return fixer.replaceTextRange(
                            [node.range[0], node.range[0] + 3],
                            "let"
                        );
                    }
                });
            }
        };
    }
};

После запуска:

eslint src --fix

Получается:

let user = "Alex";

Возврат нескольких исправлений

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

В этом случае функция возвращает массив исправлений.

Пример:

fix(fixer) {
    return [
        fixer.insertTextBefore(node, "("),
        fixer.insertTextAfter(node, ")")
    ];
}

ESLint рассматривает их как единое исправление.


Генераторы исправлений

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

Пример:

fix(fixer) {
    return [
        fixer.remove(tokenA),
        fixer.remove(tokenB),
        fixer.insertTextAfter(tokenC, ";")
    ];
}

Либо:

fix(fixer) {
    const fixes = [];

    fixes.push(
        fixer.remove(tokenA)
    );

    fixes.push(
        fixer.remove(tokenB)
    );

    return fixes;
}

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


Конфликты исправлений

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

Например:

Правило №1:

const a=1;

const a = 1;

Правило №2:

const a=1;

const number = 1;

Если диапазоны изменений пересекаются, ESLint не сможет применить оба исправления одновременно.

Для предотвращения повреждения кода используется механизм разрешения конфликтов.

В результате:

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

Многопроходное исправление

После внесения исправлений ESLint запускает повторный анализ файла.

Схема работы:

Проверка
    ↓
Исправление
    ↓
Повторная проверка
    ↓
Новые исправления
    ↓
Повторная проверка

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

Пример:

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

var message="Hello"

Первый проход:

let message="Hello"

Второй проход:

let message = "Hello"

Третий проход:

let message = "Hello";

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


Требования к безопасным исправлениям

Исправление должно сохранять поведение программы.

Хорошее исправление:

const value=10;

const value = 10;

Семантика программы не меняется.

Опасное исправление:

foo == bar

foo === bar

Хотя изменение часто считается желательным, оно способно изменить результат вычислений.

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


Принцип минимальности изменений

Качественное fixable-правило изменяет только необходимый участок кода.

Нежелательный вариант:

fix(fixer) {
    return fixer.replaceText(
        parentNode,
        generateEntireFile()
    );
}

Предпочтительный вариант:

fix(fixer) {
    return fixer.replaceText(
        offendingToken,
        replacement
    );
}

Минимальные изменения:

  • легче анализируются;
  • реже конфликтуют;
  • проще объединяются с исправлениями других правил;
  • уменьшают риск ошибок.

Работа с SourceCode

Для точных исправлений часто используется объект SourceCode.

Получение объекта:

const sourceCode =
    context.getSourceCode();

Получение исходного текста узла:

const text =
    sourceCode.getText(node);

Использование при исправлении:

fix(fixer) {
    const text = sourceCode.getText(node);

    return fixer.replaceText(
        node,
        text.trim()
    );
}

Это позволяет строить исправления на основе фактического содержимого файла.


Ограничения fixable-правил

Автоматические исправления не предназначены для сложного рефакторинга.

Нежелательные сценарии:

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

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

  • codemods;
  • Babel Transform;
  • jscodeshift;
  • TypeScript Compiler API.

Fixable-правила ориентированы на локальные, безопасные и предсказуемые изменения внутри одного файла.


Типичные категории fixable-правил

Наиболее распространённые автоматически исправляемые правила относятся к следующим группам:

Форматирование

quotes
semi
comma-spacing
indent

Исправляют:

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

Стилистические соглашения

object-curly-spacing
array-bracket-spacing
arrow-parens

Приводят код к единому стилю оформления.

Современный синтаксис

prefer-const
object-shorthand

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

Удаление лишних элементов

no-extra-semi
no-trailing-spaces

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


Особенности проектирования собственных fixable-правил

При разработке собственных правил обычно соблюдаются следующие принципы:

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

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