Экспорт процессоров из плагина

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

Наиболее распространённые сценарии использования процессоров:

  • анализ JavaScript-кода внутри Markdown-документов;
  • проверка блоков кода в документации;
  • извлечение скриптов из HTML-файлов;
  • работа с собственными форматами файлов;
  • обработка шаблонов и смешанных форматов данных.

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


Структура экспорта процессоров

Современный ESLint ожидает, что плагин будет экспортировать объект processors. Каждый процессор представляет собой объект с набором методов, отвечающих за предварительную и последующую обработку содержимого файла.

Пример структуры плагина:

const plugin = {
  processors: {
    customProcessor: {
      preprocess(text, filename) {
        return [text];
      },

      postprocess(messages, filename) {
        return messages[0];
      },

      supportsAutofix: true
    }
  }
};

export default plugin;

В этом примере процессор регистрируется под именем customProcessor, после чего может использоваться в конфигурации ESLint.


Объект processors

Свойство processors является словарём, где:

  • ключ — имя процессора;
  • значение — объект процессора.

Пример:

export default {
  processors: {
    markdown: {
      preprocess() {},
      postprocess() {}
    },

    html: {
      preprocess() {},
      postprocess() {}
    }
  }
};

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

Если плагин называется eslint-plugin-docs, процессор markdown будет доступен как:

docs/markdown

Метод preprocess

Метод preprocess() вызывается первым. Он получает исходное содержимое файла и должен вернуть массив строк или виртуальных файлов для дальнейшего анализа.

Сигнатура метода:

preprocess(text, filename)

Параметры:

Параметр Описание
text Содержимое файла
filename Имя анализируемого файла

Простейшая реализация:

preprocess(text) {
  return [text];
}

В таком случае ESLint получает исходный код без изменений.


Разделение файла на несколько виртуальных документов

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

Пример:

preprocess(text) {
  const blocks = extractCodeBlocks(text);

  return blocks;
}

Если метод вернёт:

[
  "const a = 1;",
  "const b = 2;"
]

ESLint выполнит анализ каждого блока отдельно.

Это особенно полезно для:

  • Markdown-документов;
  • HTML-файлов;
  • шаблонов документации;
  • конфигурационных файлов со встроенным JavaScript.

Извлечение кода из Markdown

Классический пример работы процессора — извлечение JavaScript-блоков из Markdown.

Исходный документ:

# Документация

```js
const user = {};
```

```js
console.log(user);
```

Процессор может извлечь оба блока:

preprocess(text) {
  const matches = [];

  const regexp = /```js([\s\S]*?)```/g;

  let match;

  while ((match = regexp.exec(text))) {
    matches.push(match[1]);
  }

  return matches;
}

В результате ESLint будет анализировать только содержимое блоков кода.


Метод postprocess

После завершения проверки ESLint вызывает метод postprocess().

Сигнатура:

postprocess(messages, filename)

Параметры:

Параметр Описание
messages Массив результатов проверки
filename Имя файла

Если в preprocess() был возвращён массив из нескольких частей, то messages будет содержать результаты для каждой из них.

Пример:

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

Такое решение объединяет сообщения всех виртуальных файлов в единый список.


Структура сообщений об ошибках

Каждый элемент массива представляет собой объект диагностического сообщения ESLint.

Пример:

{
  ruleId: "no-unused-vars",
  severity: 2,
  message: "'x' is assigned a value but never used.",
  line: 3,
  column: 7
}

В postprocess() разрешается изменять эти сообщения перед возвратом в ESLint.


Корректировка координат ошибок

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

Рассмотрим Markdown:

Текст документа

```js
const a = ;
```

Фрагмент JavaScript начинается не с первой строки документа. Поэтому ошибка должна быть смещена относительно оригинального файла.

Пример корректировки:

postprocess(messages) {
  return messages[0].map(message => ({
    ...message,
    line: message.line + 2
  }));
}

После обработки пользователь увидит правильную позицию ошибки в исходном документе.


Поддержка автоматических исправлений

Процессор может сообщить ESLint о поддержке автоисправлений через свойство:

supportsAutofix: true

Пример:

export default {
  processors: {
    markdown: {
      preprocess(text) {
        return [text];
      },

      postprocess(messages) {
        return messages[0];
      },

      supportsAutofix: true
    }
  }
};

Если свойство отсутствует либо имеет значение false, механизм автоматического исправления для такого процессора считается неподдерживаемым.


Работа автоисправлений через процессор

Поддержка автоисправлений требует особой осторожности.

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

Например:

```js
var x = 1;
```

Правило может предложить замену:

let x = 1;

Но процессору необходимо корректно перенести изменение обратно в оригинальный Markdown-документ.

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


Именование процессоров

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

Удачные варианты:

processors: {
  markdown: {},
  html: {},
  templates: {}
}

Менее удачные:

processors: {
  proc1: {},
  parser2: {}
}

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


Экспорт нескольких процессоров

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

Пример:

export default {
  processors: {
    markdown: {
      preprocess() {},
      postprocess() {}
    },

    html: {
      preprocess() {},
      postprocess() {}
    },

    snippets: {
      preprocess() {},
      postprocess() {}
    }
  }
};

Такой подход позволяет объединять обработку различных форматов файлов в одном пакете.


Использование общих утилит

При наличии нескольких процессоров часто возникает необходимость в повторном использовании логики.

Пример:

function flattenMessages(messages) {
  return messages.flat();
}

export default {
  processors: {
    markdown: {
      preprocess() {},

      postprocess(messages) {
        return flattenMessages(messages);
      }
    },

    html: {
      preprocess() {},

      postprocess(messages) {
        return flattenMessages(messages);
      }
    }
  }
};

Это уменьшает дублирование кода и упрощает поддержку плагина.


Процессоры и парсеры

Процессор и парсер решают разные задачи.

Парсер отвечает за преобразование JavaScript-кода в AST.

Процессор отвечает за подготовку содержимого файла до передачи его парсеру.

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

Файл
  ↓
Процессор
  ↓
JavaScript-код
  ↓
Парсер
  ↓
AST
  ↓
Правила ESLint

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


Особенности работы с виртуальными файлами

Каждый элемент массива, возвращённого preprocess(), рассматривается ESLint как отдельный виртуальный файл.

Пример:

preprocess() {
  return [
    "const a = 1;",
    "const b = 2;",
    "const c = 3;"
  ];
}

ESLint выполнит три независимые проверки.

Результат в postprocess():

[
  messagesForFirstBlock,
  messagesForSecondBlock,
  messagesForThirdBlock
]

Необходимо учитывать это при объединении результатов.


Обработка отсутствующих блоков

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

Пример:

preprocess(text) {
  const blocks = extractBlocks(text);

  return blocks.length ? blocks : [""];
}

Либо:

preprocess(text) {
  const blocks = extractBlocks(text);

  return blocks;
}

Главное — обеспечить предсказуемое поведение для любых входных данных.


Организация кода процессора

Для крупных проектов удобно выносить процессоры в отдельные модули.

Структура:

plugin/
├── processors/
│   ├── markdown.js
│   ├── html.js
│   └── templates.js
└── index.js

Файл процессора:

export default {
  preprocess(text) {
    return [text];
  },

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

Главный файл плагина:

import markdown from "./processors/markdown.js";
import html from "./processors/html.js";

export default {
  processors: {
    markdown,
    html
  }
};

Подобная организация значительно упрощает развитие и сопровождение больших ESLint-плагинов.


Рекомендации по проектированию процессоров

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

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

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

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

Поддерживать обратную совместимость имён процессоров. После публикации процессор становится частью внешнего API плагина.

Тестировать вложенные структуры. Особенно это важно для Markdown, HTML и шаблонных языков, где код может находиться на различных уровнях вложенности.

Экспорт процессоров является одним из ключевых механизмов расширения ESLint, позволяющим применять правила линтера к данным, которые изначально не являются обычными JavaScript-файлами. Благодаря объекту processors, методам preprocess() и postprocess(), а также поддержке виртуальных документов, плагины могут интегрировать ESLint практически в любые текстовые форматы и специализированные языки разметки.