🚀

Rust製のLinter「Oxlint」が速すぎる

2023/12/31に公開

Rust製のLinterである「Oxlint」が2023/12/12に利用可能となったので、ドキュメントをまとめてみました

https://oxc-project.github.io/docs/guide/usage/parser.html

特徴

  • ESLintの50倍から100倍速く、CPUコア数に応じてスケールする
  • Node.jsが不要で、コマンドだけで実行可能
  • デフォルトで他のESLintプラグインのルールを統合
  • .eslintignore をサポート
  • ESLintコメントの無効化をサポート

インストール

oxlintはNode.jsを必須としていないので、ダウンロードせずコマンドだけでも利用可能です

インストールする時

npm i -D oxlint

コマンドの時

npx oxlint@latest

Lintルール

Oxlintはまだプラグインシステムを提供していませんが、TypeScript、React、Jest、Unicorn、JSX-a11y、Importなどの一般的なプラグインからのルールを積極的に統合しています

eslinttypescripteslint-plugin-reacteslint-plugin-jesteslint-plugin-unicorn, eslint-plugin-jsx-a11yから、217個のルール(執筆時点)が統合されています

統合されているルールは誤った、冗長な、または紛らわしいコード—を識別するもので、多くの人に受け入れられやすい構成だと思います

ルールの一覧は以下のコマンドで確認できます

npx oxlint@latest --rules
ルール一覧(執筆時点)
• deepscan: bad-array-method-on-arguments
• deepscan: bad-char-at-comparison
• deepscan: bad-comparison-sequence
• deepscan: bad-min-max-func
• deepscan: bad-replace-all-arg
• deepscan: missing-throw
• deepscan: number-arg-out-of-range
• deepscan: uninvoked-array-callback
• eslint: for-direction
• eslint: no-async-promise-executor
• eslint: no-caller
• eslint: no-class-assign
• eslint: no-compare-neg-zero
• eslint: no-cond-assign
• eslint: no-const-assign
• eslint: no-constant-binary-expression
• eslint: no-constant-condition
• eslint: no-control-regex
• eslint: no-debugger
• eslint: no-delete-var
• eslint: no-dupe-class-members
• eslint: no-dupe-else-if
• eslint: no-dupe-keys
• eslint: no-duplicate-case
• eslint: no-empty-character-class
• eslint: no-empty-pattern
• eslint: no-ex-assign
• eslint: no-extra-boolean-cast
• eslint: no-func-assign
• eslint: no-global-assign
• eslint: no-inner-declarations
• eslint: no-loss-of-precision
• eslint: no-new-symbol
• eslint: no-obj-calls
• eslint: no-self-assign
• eslint: no-setter-return
• eslint: no-shadow-restricted-names
• eslint: no-sparse-arrays
• eslint: no-unsafe-finally
• eslint: no-unsafe-negation
• eslint: no-unused-labels
• eslint: no-useless-catch
• eslint: no-useless-escape
• eslint: require-yield
• eslint: use-isnan
• eslint: valid-typeof
• typescript: no-extra-non-null-assertion
• typescript: no-misused-new
• typescript: no-namespace
• typescript: no-non-null-asserted-optional-chain
• typescript: no-this-alias
• typescript: no-unsafe-declaration-merging
• typescript: prefer-as-const
• jest: expect-expect
• jest: no-conditional-expect
• jest: no-disabled-tests
• jest: no-export
• jest: no-focused-tests
• jest: no-standalone-expect
• jest: valid-describe-callback
• jest: valid-expect
• jest: valid-title
• unicorn: no-document-cookie
• unicorn: no-empty-file
• unicorn: no-invalid-remove-event-listener
• unicorn: no-thenable
• unicorn: no-unnecessary-await
• unicorn: no-useless-fallback-in-spread
• unicorn: no-useless-length-check
• unicorn: no-useless-spread
• unicorn: prefer-set-size
• unicorn: prefer-string-starts-ends-with
• react: jsx-key
• react: jsx-no-duplicate-props
• react: jsx-no-useless-fragment
• react: no-children-prop
• react: no-find-dom-node
• react: no-render-return-value
• react: no-string-refs
• react: no-is-mounted
• jsx_a11y: alt-text
• jsx_a11y: anchor-has-content
• jsx_a11y: anchor-is-valid
• jsx_a11y: aria-props
• jsx_a11y: heading-has-content
• jsx_a11y: html-has-lang
• jsx_a11y: iframe-has-title
• jsx_a11y: img-redundant-alt
• jsx_a11y: no-access-key
• jsx_a11y: no-aria-hidden-on-focusable
• jsx_a11y: no-autofocus
• jsx_a11y: scope
• jsx_a11y: tab-index-no-positive
• jsx_a11y: no-distracting-elements
• oxc: const-comparisons
• oxc: double-comparisons
Restriction (15):
• deepscan: bad-bitwise-operator
• eslint: no-bitwise
• eslint: no-console
• eslint: no-empty
• eslint: no-eval
• eslint: no-regex-spaces
• eslint: no-unsafe-optional-chaining
• typescript: no-explicit-any
• typescript: no-var-requires
• unicorn: no-abusive-eslint-disable
• unicorn: no-array-reduce
• unicorn: no-array-for-each
• unicorn: no-nested-ternary
• unicorn: prefer-number-properties
• react: no-dangerously-set-inner-html
Pedantic (44):
• eslint: array-callback-return
• eslint: eqeqeq
• eslint: no-array-constructor
• eslint: no-case-declarations
• eslint: no-mixed-operators
• eslint: no-prototype-builtins
• eslint: no-return-await
• eslint: no-self-compare
• typescript: ban-types
• typescript: no-duplicate-enum-values
• unicorn: escape-case
• unicorn: explicit-length-check
• unicorn: new-for-builtins
• unicorn: no-hex-escape
• unicorn: no-instanceof-array
• unicorn: no-lonely-if
• unicorn: no-negated-condition
• unicorn: no-new-buffer
• unicorn: no-object-as-default-parameter
• unicorn: no-static-only-class
• unicorn: no-this-assignment
• unicorn: no-typeof-undefined
• unicorn: no-unreadable-iife
• unicorn: no-useless-promise-resolve-reject
• unicorn: no-useless-switch-case
• unicorn: prefer-array-flat
• unicorn: prefer-array-some
• unicorn: prefer-blob-reading-methods
• unicorn: prefer-code-point
• unicorn: prefer-date-now
• unicorn: prefer-dom-node-append
• unicorn: prefer-dom-node-dataset
• unicorn: prefer-dom-node-remove
• unicorn: prefer-event-target
• unicorn: prefer-math-trunc
• unicorn: prefer-native-coercion-functions
• unicorn: prefer-prototype-methods
• unicorn: prefer-query-selector
• unicorn: prefer-regexp-test
• unicorn: prefer-string-replace-all
• unicorn: prefer-string-slice
• unicorn: prefer-type-error
• unicorn: require-number-to-fixed-digits-argument
• react: no-unescaped-entities
Style (40):
• eslint: default-case-last
• typescript: adjacent-overload-signatures
• typescript: no-empty-interface
• jest: max-expects
• jest: no-alias-methods
• jest: no-confusing-set-timeout
• jest: no-deprecated-functions
• jest: no-done-callback
• jest: no-hooks
• jest: no-identical-title
• jest: no-interpolation-in-snapshots
• jest: no-jasmine-globals
• jest: no-mocks-import
• jest: no-test-prefixes
• jest: prefer-todo
• unicorn: catch-error-name
• unicorn: prefer-node-protocol
• unicorn: empty-brace-spaces
• unicorn: error-message
• unicorn: filename-case
• unicorn: no-await-expression-member
• unicorn: no-console-spaces
• unicorn: no-null
• unicorn: no-unreadable-array-destructuring
• unicorn: no-zero-fractions
• unicorn: number-literal-case
• unicorn: numeric-separators-style
• unicorn: prefer-array-flat-map
• unicorn: prefer-dom-node-text-content
• unicorn: prefer-includes
• unicorn: prefer-logical-operator-over-ternary
• unicorn: prefer-modern-dom-apis
• unicorn: prefer-optional-catch-binding
• unicorn: prefer-reflect-apply
• unicorn: prefer-spread
• unicorn: prefer-string-trim-start-end
• unicorn: require-array-join-separator
• unicorn: switch-case-braces
• unicorn: text-encoding-identifier-case
• unicorn: throw-new-error
Suspicious (7):
• eslint: no-empty-static-block
• typescript: no-unnecessary-type-constraint
• jest: no-commented-out-tests
• unicorn: no-new-array
• unicorn: prefer-add-event-listener
• react: jsx-no-comment-text-nodes
• react: react-in-jsx-scope
Nursery (14):
• eslint: constructor-super
• eslint: getter-return
• eslint: no-fallthrough
• eslint: no-import-assign
• eslint: no-redeclare
• eslint: no-undef
• typescript: ban-ts-comment
• typescript: consistent-type-exports
• import: default
• import: named
• import: no-cycle
• import: no-self-import
• import: no-amd
• import: export
Perf (1):
• oxc: no-accumulating-spread
Total: 217

実行

実行すると以下のような結果が表示されます
様々なESLintのpluginのルールを統合しているので、どのルールに引っかかったかも併せて表示されます

65ファイルが19msということで、かなり高速に実行されていることが分かります(高スペPCだと5msでした)

ESLintの時

Oxlintの時

ベンチマークもOxlintが用意しているみたいです
https://github.com/oxc-project/bench-javascript-linter

その他

vscode拡張機能追加

vscodeの拡張機能も追加します
https://marketplace.visualstudio.com/items?itemName=oxc.oxc-vscode

他のメンバーにも拡張機能をレコメンドするために、.vscode/extensions.jsonを作成します

.vscode/extensions.json
{
  "recommendations": [
    "esbenp.prettier-vscode",
    "dbaeumer.vscode-eslint",
    "streetsidesoftware.code-spell-checker",
    "voorjaar.windicss-intellisense",
    "oxc.oxc-vscode"
  ]
}

husky, lintstagedに組み込む

husky, lintstagedの導入は以下を参考にしてください
https://zenn.dev/risu729/articles/latest-husky-lint-staged

husky, lintstagedの導入と、pre-commit時にlintstagedが出来ている状態で以下を追加します

package.json
{
  "scripts": {
    "lint": "npx oxlint@latest",
    "lint:fix": "npx oxlint@latest --fix",
  }
  ...
}
.lintstaged
{
  "*.{ts,tsx,js,jsx}": "npm run lint:fix"
}

このようにコミット、プッシュ時にoxlintが発火して、エラーが出るとGitコマンドを失敗させることが出来ます

コミット

プッシュ

GitHub Actionsに組み込む

push時にOxlintのチェックをさせる、GitHub Actionsを追加します

.github/workflows/lint.yml
name: lint
on: push

jobs:
  lint:
    name: Lint JS
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npx --yes oxlint@0.0.0 # change to the latest release

最後に

現在、ESLintのconfigファイルを読み取って実行できるよう進行中!!

とのことで、既存の .eslintrcなどの設定ファイルを読み込んで、Oxlintが使えるとなる未来が来そうですね!
huskyや、CIなどでLintするタイミングは何度もあるので、Lint時間が短縮されるのはとても助かると思います!
これからの開発に期待大ですね!

Discussion