📄

Oxlintがv1になっていたので触ってみた

に公開

はじめに

6月頃にBiome v2がリリースされましたが、同時期にOxlint v1も安定版がリリースされていました。最近、Biomeを導入するプロジェクトに関わったついでに、Oxlintも少し気になったので触ってみることにしました。
Biomeはオールインワンのツールであり厳密な比較にはなりませんが、Biomeユーザーとしての目線でOxlintを見ていきます。

Oxlintの特徴

Oxlintは、高速かつESLint互換のルール(+独自ルール)を提供するRust製のリンターです。
初期設定ほぼなく気軽に使い始められ、ESLintを使用したプロジェクトの移行にも優しく、併用も可能といったところがポイントです。

  • ESLintの約50~100倍高速
  • Biomeの約2.5倍高速
  • js,jsx,ts,tsx等のJavaScript系拡張子に対応
  • vue,astro,svelteの <script> に対応
  • VSCode、WebStorm等のエディタ拡張に対応

ベンチマーク情報

OxlintとBiomeの違い

BiomeもRust製で、Linter部分はOxlintにはやや劣りますが十分に高速です。JavaScript系ファイル以外にも対応している部分があり、フォーマットにも対応しているため、汎用的で一貫したツールとしての魅力があります。指摘されるエラーについても、具体的なエラー表示となるように気を配られています。

  • linter以外にもformatterとしての機能も持つ
  • JavaScript系ファイル以外に、CSS,JSON,GraphQLなども対応
  • Oxlintにlint速度劣るが、十分な処理速度
  • エラー表示が丁寧

使ってみる

ひとまず、導入からコマンド実行の辺りで使用感を確認してみたいと思います。

導入

npxで即時実行

npx oxlint

または、npmでdevDependencyへ追加を行い、package.jsonにコマンドを追加します。

npm add -D oxlint
{
  "scripts": {
    "oxlint": "oxlint src/"
  }
}

デフォルトconfigファイルの生成

npx oxlint --init

たくさんのオプションが書き出されていますが、あくまでデフォルト値なので、好みで変更したりする判断材料になるかもしれません。

.oxlintrc.json
{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "plugins": [
    "unicorn",
    "typescript",
    "oxc"
  ],
  "categories": {},
  "rules": {
    "for-direction": "warn",
    "no-async-promise-executor": "warn",
    "no-caller": "warn",
    "no-class-assign": "warn",
    "no-compare-neg-zero": "warn",
    "no-cond-assign": "warn",
    "no-const-assign": "warn",
    "no-constant-binary-expression": "warn",
    "no-constant-condition": "warn",
    "no-control-regex": "warn",
    "no-debugger": "warn",
    "no-delete-var": "warn",
    "no-dupe-class-members": "warn",
    "no-dupe-else-if": "warn",
    "no-dupe-keys": "warn",
    "no-duplicate-case": "warn",
    "no-empty-character-class": "warn",
    "no-empty-pattern": "warn",
    "no-empty-static-block": "warn",
    "no-eval": "warn",
    "no-ex-assign": "warn",
    "no-extra-boolean-cast": "warn",
    "no-func-assign": "warn",
    "no-global-assign": "warn",
    "no-import-assign": "warn",
    "no-invalid-regexp": "warn",
    "no-irregular-whitespace": "warn",
    "no-loss-of-precision": "warn",
    "no-new-native-nonconstructor": "warn",
    "no-nonoctal-decimal-escape": "warn",
    "no-obj-calls": "warn",
    "no-self-assign": "warn",
    "no-setter-return": "warn",
    "no-shadow-restricted-names": "warn",
    "no-sparse-arrays": "warn",
    "no-this-before-super": "warn",
    "no-unassigned-vars": "warn",
    "no-unsafe-finally": "warn",
    "no-unsafe-negation": "warn",
    "no-unsafe-optional-chaining": "warn",
    "no-unused-labels": "warn",
    "no-unused-private-class-members": "warn",
    "no-unused-vars": "warn",
    "no-useless-backreference": "warn",
    "no-useless-catch": "warn",
    "no-useless-escape": "warn",
    "no-useless-rename": "warn",
    "no-with": "warn",
    "require-yield": "warn",
    "use-isnan": "warn",
    "valid-typeof": "warn",
    "oxc/bad-array-method-on-arguments": "warn",
    "oxc/bad-char-at-comparison": "warn",
    "oxc/bad-comparison-sequence": "warn",
    "oxc/bad-min-max-func": "warn",
    "oxc/bad-object-literal-comparison": "warn",
    "oxc/bad-replace-all-arg": "warn",
    "oxc/const-comparisons": "warn",
    "oxc/double-comparisons": "warn",
    "oxc/erasing-op": "warn",
    "oxc/missing-throw": "warn",
    "oxc/number-arg-out-of-range": "warn",
    "oxc/only-used-in-recursion": "warn",
    "oxc/uninvoked-array-callback": "warn",
    "typescript/no-duplicate-enum-values": "warn",
    "typescript/no-extra-non-null-assertion": "warn",
    "typescript/no-misused-new": "warn",
    "typescript/no-non-null-asserted-optional-chain": "warn",
    "typescript/no-this-alias": "warn",
    "typescript/no-unnecessary-parameter-property-assignment": "warn",
    "typescript/no-unsafe-declaration-merging": "warn",
    "typescript/no-useless-empty-export": "warn",
    "typescript/no-wrapper-object-types": "warn",
    "typescript/prefer-as-const": "warn",
    "typescript/triple-slash-reference": "warn",
    "unicorn/no-await-in-promise-methods": "warn",
    "unicorn/no-empty-file": "warn",
    "unicorn/no-invalid-fetch-options": "warn",
    "unicorn/no-invalid-remove-event-listener": "warn",
    "unicorn/no-new-array": "warn",
    "unicorn/no-single-promise-in-promise-methods": "warn",
    "unicorn/no-thenable": "warn",
    "unicorn/no-unnecessary-await": "warn",
    "unicorn/no-useless-fallback-in-spread": "warn",
    "unicorn/no-useless-length-check": "warn",
    "unicorn/no-useless-spread": "warn",
    "unicorn/prefer-set-size": "warn",
    "unicorn/prefer-string-starts-ends-with": "warn"
  },
  "settings": {
    "jsx-a11y": {
      "polymorphicPropName": null,
      "components": {},
      "attributes": {}
    },
    "next": {
      "rootDir": []
    },
    "react": {
      "formComponents": [],
      "linkComponents": []
    },
    "jsdoc": {
      "ignorePrivate": false,
      "ignoreInternal": false,
      "ignoreReplacesDocs": true,
      "overrideReplacesDocs": true,
      "augmentsExtendsReplacesDocs": false,
      "implementsReplacesDocs": false,
      "exemptDestructuredRootsFromChecks": false,
      "tagNamePreference": {}
    }
  },
  "env": {
    "builtin": true
  },
  "globals": {},
  "ignorePatterns": []
}

参考:Biomeのinitコマンド実行時の生成ファイル内容

biome.json
{
	"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
	"vcs": {
		"enabled": false,
		"clientKind": "git",
		"useIgnoreFile": false
	},
	"files": {
		"ignoreUnknown": false
	},
	"formatter": {
		"enabled": true,
		"indentStyle": "tab"
	},
	"linter": {
		"enabled": true,
		"rules": {
			"recommended": true
		}
	},
	"javascript": {
		"formatter": {
			"quoteStyle": "double"
		}
	},
	"assist": {
		"enabled": true,
		"actions": {
			"source": {
				"organizeImports": "on"
			}
		}
	}
}

実行結果の比較

実行してみると、oxlintは視覚的に少しリッチに出力されていてわかりやすいように感じました。
Biomeはシンプルながら代替手段の提案もしており、こちらも良い印象です。

oxlintの実行結果

  ⚠ eslint(no-unused-vars): Variable 'unused' is declared but never used. Unused variables should start with a '_'.
   ╭─[src/lib/utilities.ts:7:9]
 6 │ 
 7 │   const unused = 'test';
   ·         ───┬──
   ·            ╰── 'unused' is declared here
 8 │ 
   ╰────
  help: Consider removing this declaration.

Biomeの実行結果

src/lib/utilities.ts:7:9 lint/correctness/noUnusedVariables  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ⚠ This variable unused is unused.
  
    5 │   }
    6 │ 
  > 7 │   const unused = 'test';
      │         ^^^^^^
    8 │ 
    9 │   return a + b;
  
  ℹ Unused variables are often the result of an incomplete refactoring, typos, or other sources of bugs.
  
  ℹ Unsafe fix: If this is intentional, prepend unused with an underscore.
  
     5  5 │     }
     6  6 │   
     7    │ - ··const·unused·=·'test';
        7 │ + ··const·_unused·=·'test';
     8  8 │   
     9  9 │     return a + b;

ただ、特に設定していない段階では別途記述していた == を使用している記述について指摘はしてくれませんでした。

Biomeの指摘

src/lib/utilities.ts:3:9 lint/suspicious/noDoubleEquals  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Using == may be unsafe if you are relying on type coercion.
  
    1 │ export function add(a: number, b: number) {
    2 │ 
  > 3 │   if (a == b) {
      │         ^^
    4 │     console.log('equal');
    5 │   }
  
  ℹ == is only allowed when comparing against null.
  
  ℹ Unsafe fix: Use === instead.
  
    3 │ ··if·(a·===·b)·{
      │           +

これについてはeslintの eqeqeq を有効化する必要があります。

有効化後の指摘

  × eslint(eqeqeq): Expected === and instead saw ==
   ╭─[src/lib/utilities.ts:3:9]
 2 │ 
 3 │   if (a == b) {
   ·         ──
 4 │     console.log('equal');
   ╰────
  help: Prefer === operator

新規導入で選択するにはチーム内でのルール設定に関する議論が多く発生しそうな気がするので、この点はBiomeの方が最初から厳し目にルール設定されていて、些細な議論を無くす思想の強みが現れていそうですね。

ESLintからの移行

お試しでnext.jsのボイラープレートにeslintを入れた状態で、少しオプションを設定した状態で oxlint-migrate を実行してみます。

……が、うまく動作しませんでした。

next/core-web-vitals 等の影響でNext.jsのプロジェクトではそのまま移行という感じにはいかないようです。

[eslint-patch] Failed to patch ESLint when using next/core-web-vitals configuration #5049

これはoxlintやbiomeなど広範囲の問題かと思いますので、別途クリアする必要がありそうです。

一旦コメントアウトして簡易的にルールを追加して実行してみました。

eslint.config.js
const eslintConfig = [
  // 今回は一旦コメントアウト
  // ...compat.extends("next/core-web-vitals", "next/typescript"),
  {
    rules: {
      eqeqeq: "error",
      "no-unused-vars": "error",
      "no-console": "warn",
      curly: "error",
      semi: ["error", "always"],
    },
  },
];

migrateコマンドの実行

npx @oxlint/migrate --output-file .oxlintrc.json

生成されたのは以下です。

.oxlintrc.json
{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "plugins": [],
  "categories": {
    "correctness": "off"
  },
  "env": {
    "builtin": true
  },
  "rules": {
    "eqeqeq": "error",
    "no-unused-vars": "error",
    "no-console": "warn",
    "curly": "error"
  }
}

こちらはうまく動作し、linter向けルールが移行されています。一方でsemiに関するルールは移行されていません。これは、そもそもoxlintがlinterに過ぎないからで、スタイル系のルールは未対応です。Prettierの部分を担うプロジェクトも動いていはいるようですが、まだWIP段階です。(2025/08現在)

The formatter is currently work in progress.

加えて、prettierのplugin-oxcプラグインへの案内が記載されています。

移行コマンドはありますが、現在Oxlintを使用している人はeslintのプラグイン(eslint-plugin-oxlint)を使用し、oxlintを実行後にeslintを実行するという併用パターンで運用しているようです。

感想

速度が早いのは正義ですが、やはりBiomeがしばらく導入・移行先の有力な候補になりそうです。

現段階ではeslintやPrettierとの組み合わせ部分が多いので、導入やセットアップに少し煩わしさを感じる部分があります。なるべくインストールするもの、設定する項目、チームとしての方針など、少なくシンプルに決まるほうが個人的には良いなと思う部分も多く、Biomeの良さを改めて実感しました。

とはいえ、oxcのプロジェクトは今後も要チェックです。oxcのformatterなどが出揃ってきたらまた話が変わってくるかもしれませんね。

chot Inc. tech blog

Discussion