Skip to content

Технический превью типозависимого линтинга в Oxlint

Эта заметка анонсирует технический превью типозависимого линтинга. Актуальная альфа с улучшенной стабильностью, настройкой и покрытием правил — в анонсе альфы типозависимого линтинга.


Мы рады объявить о типозависимом линтинге в oxlint!

Долгожданные no-floating-promises и связанные правила уже здесь.

Этот превью-релиз призван вовлечь сообщество в обсуждение за счёт документирования решений и технических деталей.

Быстрый старт

Если oxlint уже настроен, установите oxlint-tsgolint и запускайте с флагом --type-aware:

bash
pnpm add -D oxlint-tsgolint@latest
pnpm dlx oxlint --type-aware

Если oxlint не настроен, но нужно увидеть no-floating-promises в действии:

bash
pnpm add -D oxlint-tsgolint@latest
pnpm dlx oxlint@latest --type-aware -A all -D typescript/no-floating-promises

Ожидается вывод вида:

js
 × typescript-eslint(no-floating-promises): Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.
   ╭─[packages/rolldown/src/api/watch/watcher.ts:30:7]
29await this.close();
30originClose();
   ·       ──────────────
31 │     };
   ╰────

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

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

По нашим замерам репозитории, где раньше typescript-eslint работал около минуты, теперь завершаются менее чем за 10 секунд.

Это достигается за счёт typescript-go, «TypeScript примерно в 10 раз быстрее» на Go.

Проекты из oxc-ecosystem-ci:

ПроектФайлыВремя
napi-rs1441.0s
preact2452.7s
rolldown3141.5s
bluesky11527.0s

Типозависимый линтинг

См. материал Rust-Based JavaScript Linters: Fast, But No Typed Linting Right Now о текущем состоянии типозависимого линтинга в экосистеме.

Технические детали

Ядро новой функциональности — oxc-project/tsgolint.

Проект tsgolint изначально прототипировался как typescript-eslint/tsgolint. Команда typescript-eslint решила не вкладываться в этот прототип, планируя развивать типизированный линтинг в ESLInt через typescript-eslint.

@boshen обратился к @auvred за форком с ограниченным охватом под oxlint — только типозависимые правила без полного разрешения конфигурации линтера.

@auvred любезно продолжил разработку в организации Oxc.

Архитектура

oxlint (Rust) и tsgolint (Go) собираются в отдельные бинарники.

oxlint — «фронтенд» для tsgolint: CLI, обход путей, ignore и вывод диагностик.

tsgolint — бэкенд: принимает пути и конфигурацию, возвращает структурированные диагностики.

Простой конвейер:

oxlint CLI (передаёт пути + правила + конфигурацию)
  -> tsgolint (возвращает диагностики)
  -> oxlint CLI (печатает диагностики)

tsgolint

tsgolint не общается с typescript-go через публичные API.

Вместо этого компилируется typescript-go с shim внутренних API для их открытия.

Все типозависимые правила пишутся напрямую к этим shim API.

Это не рекомендуемый способ доступа к internals, но он работает!

Процесс решений

Написать свой типчекер

Ранее заброшенные попытки включали:

Есть ещё работа над Biome 2.0 со своим выводом типов.

Мы решили, что собственный инференсер или типчекер нецелесообразен из‑за необходимости поспевать за быстро меняющимся TypeScript.

Общение с компилятором TypeScript

До typescript-go проекты добавляли плагины к публичному API TS, маппя AST в estree или обходя AST TS. Примеры:

Мы также рассматривали IPC с oxlint, но отказались.

С typescript-go команда TypeScript склоняется к кодированию AST TS и декодированию на стороне JS через IPC.

Эти подходы работают, но всё равно дают:

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

Замечания

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

Нужна другая версия TypeScript

Планируем выпускать снапшоты версий typescript-go и синхронизировать номера с TypeScript. Тогда можно будет ставить oxlint-typescript с подходящей версией TS.

Минус: может потребоваться обновлять TypeScript, если oxlint-tsgolint требует изменений.

Стоимость сопровождения tsgolint

Shim внутренних API TS несёт риск. Но AST и visitor в TS довольно стабильны. Мы принимаем риск и чиним поломки при обновлении typescript-go.

Наш форк typescript-go синхронизируется ежедневно.

Проблемы производительности

tsgolint пока плохо масштабируется на огромные монорепы с сотнями проектов или множеством project references.

Возможны зависания, deadlock или OOM при ошибках.

Мы активно это устраняем, профилируем и шлём улучшения в typescript-go на благо всех пользователей.

Член команды @camc314 уже прислал множество PR, заметно ускоривших отдельные пути.

Релиз v1.0 для tsgolint

Для tsgolint v1.0 запланировано:

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

Благодарности

Благодарим:

  • команду TypeScript за typescript-go;
  • команду typescript-eslint за тёплую поддержку;
  • @auvred за создание tsgolint;
  • @camchenry за интеграцию oxlint + tsgolint;
  • @camc314 за работу над производительностью.

Сообщество

Нам интересна обратная связь по oxlint и типозависимому линтингу.

Контакты:

Дальнейшие шаги

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

bash
pnpm add -D oxlint@latest oxlint-tsgolint@latest
pnpm dlx oxlint --init # создать .oxlintrc.json

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

Используйте флаг CLI --type-aware.

bash
pnpm dlx oxlint --type-aware

Экспериментируйте с типозависимыми правилами в .oxlintrc.json:

json
{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "rules": {
    "typescript/await-thenable": "error",
    "typescript/no-array-delete": "error",
    "typescript/no-base-to-string": "error",
    "typescript/no-confusing-void-expression": "error",
    "typescript/no-duplicate-type-constituents": "error",
    "typescript/no-floating-promises": "error",
    "typescript/no-for-in-array": "error",
    "typescript/no-implied-eval": "error",
    "typescript/no-meaningless-void-operator": "error",
    "typescript/no-misused-promises": "error",
    "typescript/no-misused-spread": "error",
    "typescript/no-mixed-enums": "error",
    "typescript/no-redundant-type-constituents": "error",
    "typescript/no-unnecessary-boolean-literal-compare": "error",
    "typescript/no-unnecessary-template-expression": "error",
    "typescript/no-unnecessary-type-arguments": "error",
    "typescript/no-unnecessary-type-assertion": "error",
    "typescript/no-unsafe-argument": "error",
    "typescript/no-unsafe-assignment": "error",
    "typescript/no-unsafe-call": "error",
    "typescript/no-unsafe-enum-comparison": "error",
    "typescript/no-unsafe-member-access": "error",
    "typescript/no-unsafe-return": "error",
    "typescript/no-unsafe-type-assertion": "error",
    "typescript/no-unsafe-unary-minus": "error",
    "typescript/non-nullable-type-assertion-style": "error",
    "typescript/only-throw-error": "error",
    "typescript/prefer-promise-reject-errors": "error",
    "typescript/prefer-reduce-type-parameter": "error",
    "typescript/prefer-return-this-type": "error",
    "typescript/promise-function-async": "error",
    "typescript/related-getter-setter-pairs": "error",
    "typescript/require-array-sort-compare": "error",
    "typescript/require-await": "error",
    "typescript/restrict-plus-operands": "error",
    "typescript/restrict-template-expressions": "error",
    "typescript/return-await": "error",
    "typescript/switch-exhaustiveness-check": "error",
    "typescript/unbound-method": "error",
    "typescript/use-unknown-in-catch-callback-variable": "error"
  }
}