Deno の組み込みリンター "deno_lint" の紹介 〜 ESLintの代替としても

9 min read読了の目安(約8600字

この記事は Deno Advent Calendar 2020 6日目の記事です。

5日目は -> Deno Standard Library Working Group について
7日目は -> (あとで埋める)

Deno とは

deno illust

こんにちは、@magurotuna です。

このアドベントカレンダーをご覧の方であれば、新進気鋭の JavaScript / TypeScript ランタイムである Deno のことはある程度ご存知の方も多いと思います。
しかし、あえて超ざっくりと説明すると、Node.js (以下 Node と書きます)を作った Ryan Dahl が、Node の反省点をいかして新しく作り直したものです。
"10 Things I Regret About Node.js" (Node.jsについての10の反省点)というタイトルでRyan自身が JSConf EU 2018 で発表しています。

@yosuke_furukawa さんの以下のまとめも参考になると思います。

Node.js における設計ミス By Ryan Dahl - from scratch

Deno は、モダンな言語エコシステムには必ず備わっているようなさまざまなツールを、「組み込み」で持っています。つまり、Deno インストールしたら、package.json を編集したり npm install したりすることなく、すぐさま以下のようなツールが使えるようになるということです:

  • テストランナー(Deno.test という組み込み関数を使って定義されたコードをテストする)
  • ベンチマーク測定
  • バンドラ(依存関係を解決して、単一の .js ファイルを出力する)
  • ドキュメンテーション(ファイルを解釈して、JSDoc を抽出して表示。この出力をベースに、サードパーティのライブラリであってもこのようなドキュメントサイトが生成される
  • REPL
  • フォーマッタ (prettier のようなもの)
  • リンターESLint のようなもの)

deno_lint とは

この記事では、最後の リンター を紹介したいと思います。

denoland/deno_lint - GitHub

Deno の標準ライブラリ (TypeScript で書かれている) やコア部分に関わるコード (JavaScript で書かれている) についても、この deno_lint によるリントが実行されています。

実行方法

deno_lint は上述の通り Deno に組み込まれているリンターで、Deno がインストールされていれば

# something.ts に対して実行
$ deno lint --unstable something.ts

# カレントディレクトリ以下の .js, .ts, .jsx, .tsx, .mjs に対して実行
$ deno lint --unstable

のようにして実行することができます。(Deno v1.5.4 時点では --unstable フラグが必要です)

ESLint, typescript-eslint の多くの "recommended" ルールを提供

現在(2020/12/05)、ESLinttypescript-eslint で "recommended" として扱われているルールの多くをサポートしています。

  • adjacent-overload-signatures
  • ban-ts-comment
  • ban-types
  • ban-untagged-ignore
  • camelcase
  • constructor-super
  • for-direction
  • getter-return
  • no-array-constructor
  • no-async-promise-executor
  • no-case-declarations
  • no-class-assign
  • no-compare-neg-zero
  • no-cond-assign
  • no-constant-condition
  • no-control-regex
  • no-debugger
  • no-delete-var
  • no-dupe-args
  • no-dupe-class-members
  • no-dupe-else-if
  • no-dupe-keys
  • no-duplicate-case
  • no-empty
  • no-empty-character-class
  • no-empty-interface
  • no-empty-pattern
  • no-ex-assign
  • no-explicit-any
  • no-extra-boolean-cast
  • no-extra-non-null-assertion
  • no-extra-semi
  • no-fallthrough
  • no-func-assign
  • no-global-assign
  • no-import-assign
  • no-inferrable-types
  • no-inner-declarations
  • no-invalid-regexp
  • no-irregular-whitespace
  • no-misused-new
  • no-mixed-spaces-and-tabs
  • no-namespace
  • no-new-symbol
  • no-obj-calls
  • no-octal
  • no-prototype-builtins
  • no-redeclare
  • no-regex-spaces
  • no-self-assign
  • no-setter-return
  • no-shadow-restricted-names
  • no-this-alias
  • no-this-before-super
  • no-undef
  • no-unreachable
  • no-unsafe-finally
  • no-unsafe-negation
  • no-unused-labels
  • no-with
  • prefer-as-const
  • prefer-const
  • prefer-namespace-keyword
  • require-await
  • require-yield
  • use-isnan
  • valid-typeof

それぞれのルールについての詳細は the deno_lint rule documentation で見ることができます。

速い!

README にも "blazing fast" という謳い文句がありますが、実際にどれくらいのスピードなのでしょうか。
かんたんなベンチマークをとってみましょう。今回は

  • TypeScript がメインのプロジェクト
  • コードベースの規模が大きい

の2つを条件に探してみた結果、

nestjs/nest - GitHub

を対象にしてみることにしました。

Nest.js を対象にベンチマークをとってみて、速さを比較

下準備

Nest.js は元から ESLint を使うようになっていて、リポジトリのルートディレクトリに .eslintrc.js がありますが、deno_lint と条件を揃えるために少々手を加えます。deno_lint が提供しているルールと同じルールだけを実行するよう、以下のように書き換えました。

{
	// ...前略
	"plugins": ["@typescript-eslint"],
	"rules": {
		"@typescript-eslint/adjacent-overload-signatures": "error",
		"@typescript-eslint/ban-ts-comment": "error",
		"@typescript-eslint/ban-types": "error",
		"camelcase": "error",
		"constructor-super": "error",
		// 以下、deno_lint に存在するルールを列挙
	}
}

実行するコマンドは以下のとおりです。今回は packages ディレクトリ以下のみを対象にします。

# ESLint
$ npx eslint ./packages

# deno_lint
$ deno lint --unstable ./packages

実行時間の測定に使ったスクリプトは付録としてこの記事の末尾に掲載します。

ベンチマーク結果

deno_lint, ESLint それぞれ30回実行し、平均をとったものが以下のグラフです。

benchmark

グラフのうまい描き方が分からずとんでもないグラフになってしまいましたが、deno_lint が 99ms、ESLint が 82127ms (= 1分22秒くらい) という結果になりました。あまりにも速すぎてなにか計測方法が間違っているのではないかと疑うレベルです。

30回それぞれの実行回数についても末尾に載せておきます。

Node.js プロジェクトでも利用できる

さて、ここまで Nest.js に対して deno_lint を走らせてきたのですが、Nest.js は当然 Deno のプロジェクトではなく、Node プロジェクトです。Node 向けに書かれたコードは、基本的にはそのままでは Deno では動きません。
しかし deno_lint については、Node 向けコードに対しても実行することができます。

以下のようなケースに当てはまる場合は、あなたが関わっている Node プロジェクトでも、deno_lint が有用かもしれません。

  • ESLint / typescript-eslint のベーシックなルールだけ使えれば十分
  • ESLint の実行時間が長くて普段の開発で困っている

おわり

以上、deno_lint の紹介でした。

まだまだ deno_lint は開発途上で、ユーザー独自のプラグイン機能や、設定ファイルによるルールの ON/OFF などが絶賛仕様策定・開発中という段階ですし、TypeScript に関するルールでは、型推論の結果を使ったルールを作ることが現時点では不可能、という制約もあったりします。

ドキュメントの整備や細かいリファクタリングなど、気軽に始められるコントリビュートチャンスもたくさんあるので、もし興味のある方がいらっしゃったらぜひご協力ください!
不明点があったら、Twitterdeno-ja-slack、または Deno の discord などでメンションいただければと思います!

License

冒頭に貼ったイラストには以下のライセンスが適用されます。

MIT License

Copyright (c) 2018-2020 the Deno authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

付録 ベンチマーク測定に利用したコード

以下のコード(benchmark.ts とします)を deno run --allow-run benchmark.ts で実行しました。

import {
  bench,
  BenchmarkTimer,
  runBenchmarks,
} from 'https://deno.land/std@0.79.0/testing/bench.ts';

const RUN_COUNT = 30;

bench({
  name: 'deno_lint',
  runs: RUN_COUNT,
  async func(b: BenchmarkTimer): Promise<void> {
    b.start();
    const proc = Deno.run({
      cmd: ['deno', 'lint', '--unstable', './packages'],
      stdout: 'null',
      stderr: 'null',
    });
    await proc.status();
    b.stop();
  },
});

bench({
  name: 'ESLint',
  runs: RUN_COUNT,
  async func(b: BenchmarkTimer): Promise<void> {
    b.start();
    const proc = Deno.run({
      cmd: ['npx', 'eslint', './packages'],
      stdout: 'null',
      stderr: 'null',
    });
    await proc.status();
    b.stop();
  },
});

const data = await runBenchmarks({ silent: true });
console.log(JSON.stringify(data.results));

出力は以下の通りです。

[
  {
    "name": "deno_lint",
    "totalMs": 2962,
    "runsCount": 30,
    "measuredRunsAvgMs": 98.73333333333333,
    "measuredRunsMs": [
      102,
      96,
      90,
      94,
      90,
      96,
      96,
      106,
      102,
      96,
      98,
      98,
      90,
      104,
      96,
      104,
      92,
      90,
      108,
      100,
      108,
      102,
      98,
      100,
      94,
      104,
      102,
      102,
      100,
      104
    ]
  },
  {
    "name": "ESLint",
    "totalMs": 2463816,
    "runsCount": 30,
    "measuredRunsAvgMs": 82127.2,
    "measuredRunsMs": [
      81738,
      81710,
      81386,
      83212,
      81446,
      82316,
      82332,
      80362,
      81888,
      82528,
      83166,
      82554,
      81818,
      82912,
      82422,
      83214,
      81958,
      82104,
      83006,
      82286,
      82384,
      82006,
      82248,
      81894,
      81898,
      81922,
      81968,
      80986,
      81742,
      82410
    ]
  }
]