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

Процессор в ESLint представляет собой механизм предварительной обработки исходного кода перед этапом линтинга и последующей агрегации результатов анализа. Он используется в ситуациях, когда файл не может быть напрямую интерпретирован как стандартный JavaScript, либо содержит несколько логических блоков кода, требующих раздельной проверки. На практике процессоры применяются для Markdown, HTML, Vue/SFC-подобных структур, кастомных DSL и любых форматов, где JavaScript встроен внутрь другого синтаксиса.

ESLint ожидает, что входные данные для анализа — это текст JavaScript-модуля. Процессор разрывает это ограничение, преобразуя исходный файл в набор виртуальных фрагментов, каждый из которых анализируется как отдельный источник кода.

Работа процессора состоит из двух фаз:

  • preprocess — разбиение исходного текста на массив «виртуальных файлов»
  • postprocess — объединение результатов линтинга обратно в контекст исходного файла

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

Контракт процессора

Процессор описывается как объект с определённой структурой:

module.exports = {
  processors: {
    ".ext": {
      preprocess(text, filename) {
        return [];
      },
      postprocess(messages, filename) {
        return [];
      }
    }
  }
};

Ключ .ext связывает процессор с типом файлов. Например, .md или .html.

preprocess

Функция preprocess получает:

  • text — исходное содержимое файла
  • filename — путь к файлу

Возвращаемое значение — массив строк, где каждая строка рассматривается ESLint как отдельный виртуальный файл.

Простейший пример:

preprocess(text, filename) {
  const scripts = [];

  const matches = text.match(/```js([\s\S]*?)```/g) || [];

  for (const block of matches) {
    scripts.push(block.replace(/```js|```/g, ""));
  }

  return scripts;
}

В этом случае Markdown-файл разбивается на отдельные JavaScript-блоки.

Каждый элемент массива становится отдельным модулем, который ESLint анализирует независимо.

Виртуальные файлы и их идентификация

После выполнения preprocess ESLint создаёт виртуальные источники с индексированными именами:

example.md0
example.md1
example.md2

Индекс добавляется автоматически, что позволяет сопоставлять сообщения диагностики с конкретным блоком исходного файла.

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

postprocess

После завершения анализа всех виртуальных модулей ESLint передаёт результаты обратно в процессор:

postprocess(messages, filename) {
  return messages.flat();
}

Аргумент messages — массив массивов диагностик, где каждый вложенный массив соответствует одному виртуальному файлу.

Структура сообщения ESLint:

{
  ruleId: "no-undef",
  message: "x is not defined",
  line: 1,
  column: 5,
  severity: 2
}

Задача postprocess — объединить результаты и при необходимости скорректировать координаты или фильтровать сообщения.

Пример процессора для Markdown

Типичный кейс — извлечение JavaScript из Markdown:

module.exports = {
  processors: {
    markdown: {
      preprocess(text) {
        const blocks = [];
        const regex = /```(?:js|javascript)\n([\s\S]*?)```/g;

        let match;

        while ((match = regex.exec(text)) !== null) {
          blocks.push(match[1]);
        }

        return blocks;
      },

      postprocess(messages) {
        return messages.flat();
      }
    }
  }
};

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

Связь с конфигурацией ESLint

Для активации процессора он должен быть подключён в конфигурации:

module.exports = {
  plugins: ["custom"],
  overrides: [
    {
      files: ["*.md"],
      processor: "custom/markdown"
    }
  ]
};

Здесь custom — имя плагина, в котором экспортирован процессор.

Ограничения модели процессоров

Процессоры работают на уровне текстового преобразования и не имеют доступа к полноценному AST исходного файла до разбивки. Это накладывает ряд ограничений:

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

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

Обработка координат и source mapping

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

postprocess(messages) {
  return messages.map((blockMessages, index) => {
    return blockMessages.map(msg => {
      msg.line += index * 1000;
      return msg;
    });
  }).flat();
}

В реальных реализациях вместо условных смещений применяется точное отображение позиций через таблицы соответствия.

Использование processors для HTML и embedded scripts

HTML-файлы часто содержат встроенные скрипты:

<script>
  var a = 1;
</script>

Процессор может извлечь содержимое <script> тегов:

preprocess(text) {
  const scripts = [];
  const regex = /<script[^>]*>([\s\S]*?)<\/script>/g;

  let match;

  while ((match = regex.exec(text))) {
    scripts.push(match[1]);
  }

  return scripts;
}

Такой подход позволяет применять JavaScript-правила ESLint к коду внутри HTML без изменения основного пайплайна.

Архитектурная роль процессора

Процессор занимает промежуточный слой между источником данных и ядром анализа:

Файл → preprocess → виртуальные модули → ESLint rules → postprocess → результат

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

Отличие процессоров от парсеров

Важно различать процессоры и парсеры:

  • Парсер формирует AST из JavaScript-кода
  • Процессор подготавливает текст до этапа парсинга

Процессор не заменяет parser, а лишь подаёт ему корректные входные данные. Внутри ESLint они работают последовательно, но решают разные задачи трансформации кода.