Skip to content

Превью JS-плагинов Oxlint

Эта заметка анонсирует превью JS-плагинов Oxlint. JS-плагины уже вышли в альфу! См. анонс альфы JS-плагинов Oxlint про актуальные возможности и улучшения.


Ранее в этом году мы спросили сообщество, чтобы определить дизайн поддержки пользовательских JS-плагинов в Oxlint. Сегодня рады показать результат месяцев исследований, прототипирования и разработки:

Oxlint поддерживает плагины на JS!

Ключевые возможности

  • ESLint-совместимый API плагинов. Oxlint сможет запускать многие существующие плагины ESLint без изменений.
  • Альтернативный API — немного другой и даёт лучшую производительность.

Что это и чем не является

Это превью — только начало. Важно понимать:

  • В первом релизе реализована не вся ESLint plugin API.
  • Производительность уже хорошая, но станет значительно выше — впереди много оптимизаций.

Наиболее используемые API для правил проверки кода уже есть, поэтому многие правила ESLint уже работают. API для токенов отсутствуют — стилистические (форматирующие) правила пока не поддерживаются.

Попробуйте, пришлите фидбек и помогите расставить приоритеты следующей фазы.

В заметке

  1. Как пользоваться.
  2. Что дальше.
  3. Технические детали «иметь торт и съесть»: ESLint-совместимость и высокая производительность.

Quick Start

Установите Oxlint:

sh
pnpm add -D oxlint

Напишите простой JS-плагин:

js
// plugin.js

// The simplest rule of all - no debugger
const rule = {
  create(context) {
    return {
      DebuggerStatement(node) {
        context.report({
          message: "No debugger!",
          node,
        });
      },
    };
  },
};

const plugin = {
  meta: {
    name: "best-plugin-ever",
  },
  rules: {
    "no-debugger": rule,
  },
};

export default plugin;

Создайте конфиг с плагином:

json
// .oxlintrc.json
{
  "jsPlugins": ["./plugin.js"],
  "rules": {
    "best-plugin-ever/no-debugger": "error"
  }
}

Добавьте файл для линтинга:

js
// foo.js
debugger;

Запуск:

sh
pnpm oxlint

Ожидаемый вывод:

 x best-plugin-ever(no-debugger): No debugger!
  ,-[foo.js:1:1]
1 | debugger;
  : ^^^^^^^^^
  `----

Подробнее о написании плагинов — документация.

Alternative API

У Oxlint есть чуть другой API для лучшей производительности.

Этот альтернативный API даёт плагины, совместимые и с ESLint, и с Oxlint.

Пример правила: предупреждать, если в файле больше пяти объявлений классов:

Вариант ESLint

js
const rule = {
  create(context) {
    let classCount = 0;

    return {
      ClassDeclaration(node) {
        classCount++;
        if (classCount === 6) {
          context.report({ message: "Too many classes", node });
        }
      },
    };
  },
};

Альтернативный API

js
import { defineRule } from "oxlint";

const rule = defineRule({
  createOnce(context) {
    // Define counter variable
    let classCount;

    return {
      before() {
        // Reset counter before traversing AST of each file
        classCount = 0;
      },
      // Same as before
      ClassDeclaration(node) {
        classCount++;
        if (classCount === 6) {
          context.report({ message: "Too many classes", node });
        }
      },
    };
  },
});

Отличия

  1. Обёртка правила в defineRule(...).
diff
- const rule = {
+ const rule = defineRule({
  1. Вместо create используйте createOnce.
diff
-   create(context) {
+   createOnce(context) {
  1. Инициализацию «на файл» из тела create перенесите в хук before.
diff
-     let classCount = 0;
+     let classCount;

      return {
+       before() {
+         classCount = 0; // Reset counter
+       },
        ClassDeclaration(node) {
          classCount++;
          if (classCount === 6) {
            context.report({ message: "Too many classes", node });
          }
        },
      };
    },
  });

Это главное отличие: create у ESLint вызывается для каждого файла повторно, а createOnce — только один раз.

Остальные API ведут себя как в ESLint.

Зачем альтернативный API может сильно ускорять работу — в документации.

Performance

Как уже сказано, в этом первом превью мы не ставили производительность во главу. Цель — достаточно полный API для реальных проектов и фидбек ранних пользователей.

Сейчас скорость приличная, но не предел.

Важнее другое: прототип следующей версии показывает, что выбранная архитектура способна на исключительную производительность после серии оптимизаций (см. Under the hood).

В ближайшие месяцы будем их включать — ожидайте кратное ускорение относительно текущей версии.

Даже без этих оптимизаций Oxlint остаётся конкурентоспособным.

Oxlint против ESLint на среднем TS-проекте vuejs/core:

LinterTime
ESLint4,116 ms
ESLint multi-threaded3,710 ms
Oxlint48 ms
Oxlint with custom JS plugin236 ms
Details

INFO

sh
hyperfine -i --warmup 3 \
  './node_modules/.bin/oxlint --silent' \
  './node_modules/.bin/oxlint -c .oxlintrc-with-custom-plugin.json --silent' \
  'USE_CUSTOM_PLUGIN=true ./node_modules/.bin/eslint .' \
  'USE_CUSTOM_PLUGIN=true ./node_modules/.bin/eslint . --concurrency=auto'

Примечание: версия Oxlint в NPM на момент написания (1.23.0) содержала баг, из‑за которого этот бенчмарк сильно занижает стоимость JS-плагинов. Приведённые цифры получены на последнем main после исправления, коммит cd266b4c101c35c33e122457cdd0b514b44597a9. См. также ниже.

В этом примере простой JS-плагин заметно стоит дороже, но Oxlint всё равно примерно в 15 раз быстрее ESLint, даже с новым многопоточным раннером ESLint.

Очевидно, сложные или множественные JS-плагины увеличат затраты.

Features

Oxlint поддерживает большую часть ESLint API, обычно нужной правилам только на обходе AST. В том числе большинство правил с исправлениями кода.

Токенные API пока не поддерживаются — стилистические правила не работают.

Supported

  • AST traversal
  • AST exploration (node.parent, context.sourceCode.getAncestors)
  • Fixes
  • Selectors (ESLint docs)
  • SourceCode APIs (e.g. context.sourceCode.getText(node))

Not supported yet

  • Language server (IDE) support
  • Rule options
  • Suggestions
  • Scope analysis (implemented since v1.25.0)
  • SourceCode APIs related to tokens and comments (e.g. context.sourceCode.getTokens(node))
  • Control flow analysis

What's next

В ближайшие месяцы:

1. Расширение поверхности API плагинов

Цель — 100% ESLint plugin API, чтобы Oxlint мог запускать любой плагин ESLint без правок.

2. Производительность

Уже неплохо, но прототипы показали большой запас от дальнейших оптимизаций. Будем применять их и приближать JS-плагины к скорости Rust.

Under the hood

Дальше — необязательные «гиковские» детали реализации.

Главный вопрос: совместимость с ESLint или нет?

Мы задавали сообществу: нужен ли Oxlint ESLint-совместимый API плагинов.

Совместимость удобна для знакомства и миграции с ESLint.

Но Oxlint известен производительностью — терять её не хочется.

Задача прототипов — измерить компромисс между скоростью и совместимостью и найти решение «и торт, и поесть»: ESLint-совместимый API и приемлемая скорость («приемлемая» здесь — очень быстрая).

Считаем, что комбинацией подходов этого можно добиться.

Alternative API

Почему альтернативный API даёт потенциал к большей скорости — в документации.

Raw transfer

Инструменты вроде Oxc представляют код JS/TS как AST (abstract syntax tree). AST очень объёмные — намного больше исходного текста.

Обычно главный тормоз быстрого взаимодействия JS и нативного кода вроде Rust — сериализация и десериализация таких структур между «мирами».

Простейший путь передать AST из Rust в JS — сериализовать в JSON, передать строкой и «оживить» через JSON.parse. Это очень медленно; часто дороже, чем выигрыш от нативного кода. Другие форматы эффективнее JSON, но накладные расходы всё равно велики.

Мы сделали схему «raw transfer» без сериализации: используется нативный layout памяти Rust как формат данных (подробности).

«Raw transfer» — основа текущей реализации JS-плагинов.

Lazy deserialization

Вторая большая проблема производительности, особенно при JS в воркерах на нескольких ядрах, — сборщик мусора. Каждый объект нужно потом уничтожить; в JS это делает GC. Движки вроде V8 оптимизированы, но GC дорог и отнимает CPU у полезной работы.

У нас есть прототип AST-visitor с ленивой десериализацией — создаются только те части дерева, которые действительно нужны.

Например, для правила про классы visitor быстро проходит большую часть AST и создаёт JS-объекты только для узлов ClassDeclaration. Для остального (переменные, if, функции…) узлы можно не материализовать.

Эффект двойной:

  1. Raw transfer убирает стоимость сериализации; лень резко снижает и десериализацию.
  2. Сильно меньше давление на GC.

Похожий путь у Deno — отлично описан в посте Marvin Hagemeister; линтер Deno очень эффективен.

Мы видим, что комбинация ленивой десериализации и raw transfer даёт действительно высокую скорость: без этих двух накладных расходов JS-плагины работают намного быстрее.

Эта оптимизация ещё не в текущей версии JS-плагинов; появится в будущем релизе.

Try it out!

Попробуйте JS-плагины и поделитесь опытом — любой фидбек полезен.

Если не хватает API для ваших плагинов — напишите: будем закрывать пробелы в ближайшие месяца с приоритетом по спросу.

Удачного линтинга!


Edit: 18th Oct 2025

В первоначальной версии заметки от 9 октября были приведены результаты бенчмарка, сильно завышавшие производительность JS-плагинов Oxlint. Причиной был баг в Oxlint: при определённых конфигурациях с overrides JS-плагины пропускались на многих файлах. Из‑за этого мы переоценили скорость JS-плагинов в цитируемых бенчмарках.

Приносим искренние извинения и благодарим Herrington Darkholme за указание на ошибку.