Rust製の高速JavaScriptリンター「oxlint」を試してみた

Rust製の高速JavaScriptリンター「oxlint」を試してみた

2026.03.07

どうも!オペ部の西村祐二です!

ESLintの代替として注目されているoxlintを試してみました。
Rustで書かれた高速JavaScriptリンターということで、導入のしやすさや使い勝手、TypeScript関連ルールを実際に確かめてみます。

oxlintとは

oxlintは、Oxcプロジェクト(The JavaScript Oxidation Compiler)の一部として開発されているリンターです。

主な特徴(公式サイトより):

  • ESLintの50〜100倍高速 — Rustで書かれており、マルチスレッド対応
  • 650以上のルール(増加中) — ESLint、TypeScript、React、Jest、Unicorn等の主要プラグインをカバー
  • type-aware linting — TypeScriptの型情報を活用したルールに対応
  • マルチファイル解析import/no-cycleのようなクロスファイル分析もサポート

Oxcプロジェクトにはリンター以外にも、oxfmt(Prettier互換フォーマッター)、Parser、Transformer、Resolverなどのツールが含まれています。

やったこと

oxlintをゼロからセットアップし、TypeScript関連ルールを含む基本的な機能を試しました。

環境:

  • OS: macOS
  • oxlint: v1.51.0
  • oxlint-tsgolint: v0.16.0
  • TypeScript: v5.9.3
  • パッケージマネージャ: npm

試してみる

1. インストール

mkdir oxlint-trial && cd oxlint-trial
npm init -y
npm install -D oxlint typescript
mkdir src

TypeScriptの設定ファイルも用意します。

tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "noEmit": true
  },
  "include": ["src"]
}

2. 基本的なlint実行

package.jsonにスクリプトを追加します。

package.json
{
  "scripts": {
    "lint": "oxlint",
    "lint:fix": "oxlint --fix"
  }
}

試しに簡単なファイルを作って実行してみます。

src/index.ts
const greeting = "hello"
console.log(greeting)
npx oxlint src/

結果:

Found 0 warnings and 0 errors.
Finished in 16ms on 1 file with 93 rules using 12 threads.

エラーのないコードなので問題なしです。1ファイルに対して93ルールが適用され、16msで完了しました。設定ファイルなしでもデフォルトのcorrectnessカテゴリのルールが有効になるので、すぐに使い始められます。

3. マルチファイル解析: 循環importの検出

oxlintの特徴の一つに、複数ファイルにまたがるimport関係を解析する機能があります。import/no-cycleルールで循環importを検出できます。

わざと循環するモジュールを作ってみます。以下の2ファイルを作成します。

src/moduleA.ts
import { greet } from './moduleB'

export function hello() {
  return greet("world")
}
src/moduleB.ts
import { hello } from './moduleA'

export function greet(name: string) {
  return `${hello()} ${name}`
}

moduleA → moduleB → moduleA という循環が発生しています。

import/no-cycleはデフォルトでは有効になっていないため、プロジェクトルートに.oxlintrc.jsonを作成し、importプラグインとルールを有効化します。

.oxlintrc.json
{
  "plugins": ["import"],
  "rules": {
    "import/no-cycle": "error"
  }
}

対象ファイルを指定して実行します。

npx oxlint src/moduleA.ts src/moduleB.ts

結果:

x eslint-plugin-import(no-cycle): Dependency cycle detected
   ,-[src/moduleA.ts:1:23]
 1 | import { greet } from './moduleB'
   :                       ^^^^^^^^^^^
   `----
  help: Refactor to remove the cycle. Consider extracting shared code into
        a separate module that both files can import.
  note: These paths form a cycle:
           ╭──▶ ./moduleB (src/moduleB.ts)
           │         ⬇ imports
           │    ./moduleA (src/moduleA.ts)
           ╰─────────╯ imports the current file

循環の経路を図で示してくれます。どのファイルがどのファイルをimportして循環が発生しているかが一目瞭然です。2ファイル・58ルールで16msと高速でした。

次の手順に進む前に、手順2・3で作成したファイルと設定ファイルを削除しておきます。

rm src/index.ts src/moduleA.ts src/moduleB.ts .oxlintrc.json

4. type-aware linting: 型情報を使った解析

oxlintはtype-aware lintingにも対応しています。

通常のlintルールはソースコードの構文構造だけを見て判定しますが、type-aware lintingはTypeScriptの型情報も活用して解析します。これにより、構文だけでは検出できない問題を見つけられるようになります。

例えば:

  • no-floating-promisesawaitし忘れたPromiseを検出(変数の型がPromiseかどうかを見る)
  • no-unsafe-assignmentany型の値を安全でない形で代入していないかチェック
  • no-unnecessary-type-assertion — 不要な型アサーション(as)を検出

これらのルールを有効にするには、追加パッケージと--type-awareフラグが必要です。

npm install -D oxlint-tsgolint
npx oxlint --type-aware src/

CLIフラグの代わりに、設定ファイルでも指定できます(ここでは作成せず、参考として紹介します)。

.oxlintrc.json
{
  "options": {
    "typeAware": true
  }
}

no-floating-promisesを試す

実際にno-floating-promisesで試してみます。以下のように、awaitし忘れたPromiseがあるコードを用意します。

src/floating-promise.ts
async function fetchData(): Promise<string> {
  return "data"
}

function main() {
  // awaitもcatchもしていない
  fetchData()
}

main()

-D--deny)オプションは、指定したルールをエラーレベルで有効化するオプションです。

npx oxlint --type-aware -D typescript/no-floating-promises src/floating-promise.ts

結果:

 × typescript-eslint(no-floating-promises): Promises must be awaited, add void operator to ignore.
   ╭─[src/floating-promise.ts:7:3]
 6 │   // awaitもcatchもしていない
 7 │   fetchData()
   ·   ───────────
 8 │ }
   ╰────
  help: The promise must end with a call to .catch, or end with a call to .then with a rejection handler, or be explicitly marked as ignored with the `void` operator.

fetchData()の戻り値がPromise<string>であることを型情報から判定し、awaitの漏れを検出しています。これは構文解析だけではできない検出で、type-aware lintingならではの機能です。

最も一般的な修正方法はawaitを付けることです。main関数をasyncに変更し、fetchData()awaitを追加します。なお、main()自体もasyncになりPromiseを返すため、トップレベルの呼び出しには.catch()を付けてエラーを捕捉します。

src/floating-promise.ts
async function fetchData(): Promise<string> {
  return "data"
}

async function main() {
  await fetchData()
}

main().catch(console.error)

再度lintを実行すると、エラーが解消されていることを確認できます。

npx oxlint --type-aware -D typescript/no-floating-promises src/floating-promise.ts
Found 0 warnings and 0 errors.
Finished in 223ms on 1 file with 107 rules using 12 threads.

次の手順に進む前に、手順4で作成したファイルを削除しておきます。

rm src/floating-promise.ts

5. consistent-type-importsの判定精度を検証する

type-awareの動作を確認できたので、次は別の切り口でTypeScript関連ルールの精度を検証してみます。

typescript/consistent-type-importsは、型としてのみ使われているimportをimport typeに変更することを提案するルールです。

紛らわしいケースを試す

明らかに型だけで使っている場合(例: const data: User = ...)は判定が簡単です。しかし、typeofやenumは見た目が「値を参照している」ように見えるため、linterが誤判定しやすいケースです。こういった紛らわしいケースで正しく判定できるのか試してみました。

Case 1: typeofで参照しているimport

以下のファイルを作成します。

src/keys.ts
export const SoraModelKeys = {
  feature1: 'feature1',
  feature2: 'feature2',
} as const
src/case1-typeof.ts
// Case 1: typeof on a const object in type position
import { SoraModelKeys } from './keys'

type FeatureName =
  (typeof SoraModelKeys)[keyof typeof SoraModelKeys]

export type { FeatureName }

Case 2: enumをオブジェクト型のキーに使っているimport

src/types.ts
export enum ModelType {
  SD1 = 'SD1',
  SD2 = 'SD2',
  SD3 = 'SD3',
}

export interface Prices {
  price: number
}
src/case2-enum.ts
// Case 2: enum used as computed property key in type literal
import { ModelType, Prices } from './types'

type Config = {
  pricing?: {
    byModelType?: {
      [ModelType.SD1]?: Prices
      [ModelType.SD2]?: Prices
      [ModelType.SD3]?: Prices
    }
  }
}

export type { Config }

以下のコマンドで実行します。手順4で紹介した-Dオプションで対象ファイルを絞って実行します。

npx oxlint -D typescript/consistent-type-imports src/case1-typeof.ts src/case2-enum.ts

結果:

  × typescript-eslint(consistent-type-imports): All imports in the declaration are only used as types. Use `import type`.
   ╭─[src/case1-typeof.ts:2:1]
 1 │ // Case 1: typeof on a const object in type position
 2 │ import { SoraModelKeys } from './keys'
   · ──────────────────────────────────────
 3 │ 
   ╰────
  help: Replace the `import` declaration with `import type`. For example, change `import { Type } from 'module'` would become `import type { Type } from 'module'`.
  note: Using `import type` for type-only imports helps with tree-shaking, makes it clear that these imports don't affect runtime code, and can improve build performance by allowing bundlers to eliminate unused type imports.

  × typescript-eslint(consistent-type-imports): All imports in the declaration are only used as types. Use `import type`.
   ╭─[src/case2-enum.ts:2:1]
 1 │ // Case 2: enum used as computed property key in type literal
 2 │ import { ModelType, Prices } from './types'
   · ───────────────────────────────────────────
 3 │ 
   ╰────
  help: Replace the `import` declaration with `import type`. For example, change `import { Type } from 'module'` would become `import type { Type } from 'module'`.
  note: Using `import type` for type-only imports helps with tree-shaking, makes it clear that these imports don't affect runtime code, and can improve build performance by allowing bundlers to eliminate unused type imports.

Found 0 warnings and 2 errors.
Finished in 29ms on 2 files with 94 rules using 12 threads.

typeofで使っているimportもenumのcomputed property keyで使っているimportも、import typeへの変更を提案されました。

検証: oxlintの提案に従って大丈夫?

一見すると「typeofは値へのアクセスだからimport typeにしたらダメなのでは?」と思いましたが、実際にsrc/case1-typeof.tsimport typeに書き換えてTypeScriptのコンパイルを確認してみました。

src/case1-typeof.ts
import type { SoraModelKeys } from './keys'

type FeatureName =
  (typeof SoraModelKeys)[keyof typeof SoraModelKeys]

export type { FeatureName }
npx tsc --noEmit src/case1-typeof.ts

結果: コンパイル成功。 TypeScript 5.9.3ではimport typeに変更してもエラーになりません。

typeof SoraModelKeysは型の位置で使われており、TypeScriptは型情報だけあればこの型を解決できます。同様にenumのcomputed property keyも、型リテラル内ではTypeScriptが型レベルで解決するため、import typeで問題ありません。

気づいたこと

良かった点

  • セットアップが簡単npm install -D oxlint だけで即座に使い始められる。ESLintのような複雑な設定ファイルが不要
  • とにかく速い — 小規模プロジェクトでも体感できるほど高速。1ファイル93ルールが16ms、2ファイルの循環import検出も17msで完了
  • エラーメッセージが親切 — 「なぜこのルールが必要か」の説明(note)と「どう直せばいいか」の提案(help)が一緒に表示される
  • TypeScript関連ルールの精度が高いtypeofやenum computed keyのような一見紛らわしいケースも正しく判定した

意外だった点

  • typeofのimportもimport typeで良いtypeof Xが型位置にある場合、TypeScriptは型情報だけで解決可能。値のバインディングは不要だった。直感に反するが、oxlintの判定は正確
  • GitHub issueでも議論中 — 同じパターンについてissue #20055が報告されている。コントリビューターは「typescript-eslintでも同じ挙動」と回答しており、oxlint固有の問題ではない模様(issueはまだOpen)

注意すべき点

  • type-aware lintingには追加パッケージが必要oxlint-tsgolintを別途インストールする必要があります。
  • --fixは慎重に — 自動修正は意図しない変更を含む可能性があります。まず--fixなしで確認するのが安全かもしれないです。
  • ESLintとの完全互換ではない — ESLintの全ルールがカバーされているわけではないです。移行時はルール対応状況の確認が必要になります。

まとめ

oxlintを実際に触ってみて、セットアップの手軽さ、圧倒的な速度、そしてエラーメッセージのわかりやすさに驚きました。
TypeScript関連ルールについても、typeofやenumのcomputed property keyのような紛らわしいケースを正確に判定しており、実用的な精度だと感じました。

ESLintの完全な代替にはまだルールカバレッジの課題がありますが、速度を重視するプロジェクトでは検討する価値があるのかなと思いました。

誰かの参考になれば幸いです。


参考リンク:

この記事をシェアする

FacebookHatena blogX

関連記事