Написание suggest-фиксов

Механизм “suggest”-фиксов в ESLint предназначен для случаев, когда автоматическое исправление возможно, но не должно применяться без явного подтверждения. Это промежуточный уровень между жёстким autofix и простым сообщением о проблеме. Он особенно важен в правилах, где существует несколько допустимых вариантов исправления или где изменение кода может повлиять на поведение программы.


Система исправлений в ESLint строится вокруг двух независимых механизмов:

  • fix (автоисправление) — применяется автоматически при запуске eslint --fix
  • suggest (предложения) — выдаются как варианты исправления, но требуют явного выбора

Каждое правило может комбинировать оба подхода, но поведение должно быть строго разделено: autofix обязан быть безопасным и однозначным, тогда как suggest допускает неоднозначность.


Метаданные правила и включение поддержки исправлений

Чтобы ESLint начал учитывать исправления, правило должно явно объявлять возможности в meta:

meta: {
  fixable: "code",
  hasSuggestions: true,
}

Поля имеют строгую семантику:

  • fixable: "code" — правило поддерживает автоматическое исправление через fixer API
  • hasSuggestions: true — правило возвращает список альтернативных исправлений

Если hasSuggestions не указано, массив suggest будет игнорироваться ESLint-движком.


Базовая структура suggest-фикса

Suggest-исправления формируются внутри context.report:

context.report({
  node,
  message: "Использование устаревшего метода",
  suggest: [
    {
      desc: "Заменить на newMethod()",
      fix(fixer) {
        return fixer.replaceText(node, "newMethod()");
      }
    }
  ]
});

Ключевые элементы:

  • desc — описание действия, отображается в IDE
  • fix — функция, возвращающая набор операций fixer API

Различие между fix и suggest

Обе конструкции используют fixer, но различаются по семантике применения:

Автофикс

fix(fixer) {
  return fixer.replaceText(node, "const");
}
  • применяется без участия пользователя
  • должен быть детерминированным
  • не должен изменять поведение программы

Suggest

suggest: [
  {
    desc: "Заменить на const",
    fix(fixer) {
      return fixer.replaceText(node, "const");
    }
  }
]
  • отображается как выбор в IDE
  • может содержать несколько альтернатив
  • допускает изменение поведения, если это осознанный выбор

Несколько вариантов исправления

Suggest-API позволяет возвращать несколько альтернативных стратегий:

suggest: [
  {
    desc: "Удалить выражение",
    fix(fixer) {
      return fixer.remove(node);
    }
  },
  {
    desc: "Заменить на null",
    fix(fixer) {
      return fixer.replaceText(node, "null");
    }
  }
]

Такая модель используется в случаях:

  • неоднозначного семантического исправления
  • миграций API
  • оптимизаций, зависящих от контекста

API fixer и операции над кодом

Fixer предоставляет минимальный набор операций:

  • replaceText(node, text)
  • replaceTextRange(range, text)
  • insertTextBefore(node, text)
  • insertTextAfter(node, text)
  • remove(node)

Внутри suggest-фиксов используются те же операции, но с более осторожной логикой.


Работа с AST и sourceCode

Suggest-фиксы почти всегда зависят от AST-структуры:

const sourceCode = context.getSourceCode();
const text = sourceCode.getText(node);

Часто используются:

  • node.range — точные позиции в коде
  • sourceCode.getText(node) — получение исходного текста
  • sourceCode.getCommentsBefore(node) — работа с комментариями

Важно избегать ручных вычислений строк, если доступен AST-узел.


Условия корректного suggest-фикса

Suggest-исправления должны соблюдать несколько технических требований:

1. Изолированность

Каждое исправление должно быть применимо независимо:

suggest: [
  {
    desc: "Удалить переменную",
    fix(fixer) {
      return fixer.remove(node);
    }
  }
]

Нельзя строить suggest, зависящий от других suggestions.


2. Без конфликтов диапазонов

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

Пример проблемного случая:

fixer.replaceText(node.left, "a");
fixer.replaceText(node, "b");

Такие операции должны быть объединены или разделены по альтернативам.


3. Стабильность результата

Fix должен всегда давать одинаковый результат для одного AST:

  • недопустим рандом
  • недопустимы условия, зависящие от внешнего состояния

Комбинирование fix и suggest

Правило может одновременно иметь автофикс и suggestions:

meta: {
  fixable: "code",
  hasSuggestions: true
}

context.report({
  node,
  message: "Устаревший синтаксис",
  fix(fixer) {
    return fixer.replaceText(node, "newSyntax()");
  },
  suggest: [
    {
      desc: "Использовать альтернативный вариант",
      fix(fixer) {
        return fixer.replaceText(node, "alternativeSyntax()");
      }
    }
  ]
});

Поведение:

  • fix применяется при --fix
  • suggest показывается в редакторе
  • пользователь выбирает альтернативы вручную

Использование messageId вместо message

В сложных правилах применяется messageId:

meta: {
  messages: {
    replaceWithConst: "Используйте const",
    replaceWithLet: "Используйте let"
  }
}
context.report({
  node,
  messageId: "replaceWithConst",
  suggest: [
    {
      desc: "Заменить на const",
      fix(fixer) {
        return fixer.replaceText(node, "const");
      }
    }
  ]
});

Преимущества:

  • локализация сообщений
  • единая точка управления текстами
  • удобство тестирования

Типичные паттерны suggest-фиксов

Миграция API

suggest: [
  {
    desc: "Заменить на новый API",
    fix(fixer) {
      return fixer.replaceText(node, "newApi()");
    }
  }
]

Используется при изменении библиотек и депрекейте функций.


Удаление потенциально опасного кода

suggest: [
  {
    desc: "Удалить небезопасный вызов",
    fix(fixer) {
      return fixer.remove(node);
    }
  }
]

Оборачивание выражений

suggest: [
  {
    desc: "Обернуть в try/catch",
    fix(fixer) {
      return fixer.insertTextBefore(node, "try { ");
    }
  }
]

(в реальных правилах обычно комбинируется несколько операций)


Производительность suggest-логики

Хотя suggest-фиксы не выполняются автоматически, их создание влияет на производительность анализа:

  • вычисление AST обхода остаётся тем же
  • генерация большого числа suggestions увеличивает память
  • сложные вычисления внутри fix замедляют linting

Рекомендуется:

  • минимизировать вычисления внутри fix
  • избегать повторных getText для одного узла
  • кешировать результаты внутри visitor

Интеграция с IDE

Suggest-фиксы активно используются редакторами:

  • VS Code ESLint extension
  • WebStorm inspections
  • Neovim LSP интеграции

IDE отображает:

  • список desc
  • предпросмотр diff
  • кнопку применения конкретного фикса

Тестирование suggest-фиксов

При тестировании через RuleTester необходимо учитывать, что suggestions проверяются отдельно:

{
  code: "var a = 1",
  errors: [
    {
      messageId: "replaceWithConst",
      suggestions: [
        {
          messageId: "replaceWithConst",
          output: "const a = 1"
        }
      ]
    }
  ]
}

Важно:

  • проверяется именно output
  • порядок suggestions должен быть детерминирован

Частые ошибки при реализации

  • возврат fix вместо массива объектов в suggest
  • использование non-AST строк вместо node.range
  • смешивание auto-fix и suggest без логического разделения
  • генерация слишком большого числа вариантов
  • зависимость fix от внешнего состояния

Архитектурные принципы проектирования suggest-фиксов

  • каждый suggestion представляет отдельную стратегию изменения кода
  • autofix должен быть наиболее безопасным вариантом
  • suggestions допускают компромисс между корректностью и семантикой
  • логика генерации fix должна быть максимально предсказуемой

Практика построения правил с несколькими уровнями исправлений

Типичная структура зрелого правила:

  • error — фиксируется как нарушение
  • fix — безопасное автоматическое исправление
  • suggest — альтернативные трансформации

Такая модель позволяет разделить:

  • механическую трансформацию кода
  • семантические решения разработчика
  • миграционные сценарии с несколькими исходами