見出し画像

ESLintのカスタムルールで治安維持活動をする - JSConf JP 2024

2024年11月23日(土)に開催されたJSConf JP 2024にて、ミイダスはプレミアムスポンサーとして参加しました。当日はスポンサーワークショップスペースで、ミイダスのフロントエンドにおける取り組みについて発表を行いました。本記事では、その発表内容を詳しくご紹介いたします。


ESLintのカスタムルールで治安維持活動をする

ミイダスでは、開発者がコーディング規約を意識せずに効率よく作業できる環境を目指して、ESLintのカスタムルールを開発しました。今回は、組織独自のコーディング規約を浸透させるために行った「治安維持活動」についてお話しします。
私はミイダスのフロントエンドエンジニアの溝渕と申します。現在は保守チームに所属し、いくつかのプロダクトの保守を担当しています。これまでの経歴としては、SIerを経てミイダスに入社しました。本発表では、ESLintのカスタムルールに関する取り組みとその背景についてお伝えします。

背景と課題

これまでミイダスのコーディング規約には、文書化されていない「暗黙の規約」が多く存在していました。その結果、開発者が規約を覚えておく必要があり、レビュー時にはレビュワーが規約違反をチェックする負担が大きい状況でした。また、規約違反のコードがレビューをすり抜けてしまうケースも発生していました。
さらに、TypeScript導入に伴い、新しい規約が増えることが予想されていました。規約の数に比例して開発者とレビュワーの負担が増えてしまうため、ESLintのカスタムルールを作成して、違反を自動検出できる規約を増やすことにしました。

既存の取り組み

プロジェクト開始前に、以下の取り組みをすでに実施していました。

  1. ESLintによる静的解析
    ESLintの提供するルールや、サードパーティプラグインのルールを使用。

  2. エディター拡張の利用、CIでのチェック
    VSCodeなどのエディターでESLintプラグインを活用し、開発中にリアルタイムで規約違反を検出。プルリクエスト作成時にESLintチェックを実施し、CIが通過しない限りマージを禁止。

新たに始めた取り組み

前述の課題を解決するために以下の取組を始めました。

  1. 規約の文書化
    規約を明文化することで、追加や変更が発生した場合にも対応しやすくしました。既存の規約については後々整理していく予定ですが、まずは新しい規約を文書に残す文化を作ることが重要だと考え新しい規約については必ず文書化するようルール化しました。

  2. ESLintカスタムルールの開発
    ESLintには多くのルールがあり、さらにサードパーティープラグインのルールも存在しますが、デフォルトのルールやサードパーティ製のルールでは対応できない独自規約をチェックするため、ESLintのカスタムルールを開発しました。

ESLintの使われ方

JavaScriptで開発をしていればほとんどの人がESLintを使っていると思います。ESLintの最も簡単な使い方は、ESLint公式、またはサードパーティが提供している設定をそのまま使う方法です。Airbnb社の設定が有名なので使っている人も多いのではないでしょうか。おそらく多くの人はそれでは足りずに、自分で特定のルールのon/offを切り替えたり、オプションを指定したりして、カスタマイズしていると思います。しかし、自分でESLintのルールを作っている人はあまりいないのではないでしょうか。
ESLintのルールを作ると、これまでレビューに頼るしかないと諦めていたような規約のチェックもできるようになります。

ESLintカスタムルールの開発手順

ここでは、簡単なカスタムルール作成の例として、「変数fooに文字列"bar"しか代入できない」というルールを作成する流れをご紹介します。このルールを通して、カスタムルール開発の基本構造を理解できます。

ディレクトリ構造の説明やセットアップの詳細については、公式チュートリアルを参照してください。
ESLint公式カスタムルールチュートリアル
今回のカスタムルールでは、「変数fooが宣言される際、その値が必ず"bar"であること」を確認するようにします。
例えば、変数fooに"bar"を代入している場合は問題ありません(OKなパターン)。しかし、fooに"baz"などの別の値が代入されている場合は、規約違反としてエラーを報告するルールを作成します。
具体的なアプローチ

  • まず、変数名がfooである宣言を特定します。

  • 次に、その変数が宣言時に値を持ち、その値が"bar"であるかどうかを確認します。

  • 条件に違反する場合(fooに"bar"以外の値が代入されている場合)、エラーを報告します。

このアプローチは比較的シンプルで、特定の変数や値に限定した規約を確実に適用することが可能です。複雑なルールではありませんが、基本的なルール構造を理解する良い例になります。ぜひ公式ドキュメントを参照しながら、同様のルールを試してみてください。

ルールの作成について

まず、右側にあるのが今回作成したサンプルです。これはチュートリアルから引用したもので、ESLintのルール作成構造に基づいています。Create関数内でオブジェクトを返し、その中にルールを定義する仕組みになっています。

ここで使用しているVariableDeclaratorは、AST Node Typeの一つです。AST Node Typeには様々な種類があり、FunctionExpressionやTypeScriptやJSX用の特定ノード(例: JSXOpeningElementやTSTypeLiteral)も含まれます。コードを細かく分解し、それぞれのパーツに名前を付けたものです。このVariableDeclaratorをオブジェクトのキーとして指定することで、変数宣言をすべてチェックできるようになります。
また、CSSライクなセレクターも併用可能で、例えば[attr="foo"]や:first-childのような指定もできます。さらに、ArrayExpression > Literal + SpreadElementのように組み合わせることも可能です。
しかし、「const foo = "bar"」のような変数宣言が、AST上でどのNode Typeに該当するのかが分からない場合、どうやってそれを特定すればよいのでしょうか。初めてカスタムルールを作るとき、このような疑問に直面することがあります。
Node Typeは、JavaScriptコードの構文要素を表すASTの一部分で、それぞれに名前が付けられています。たとえば、変数宣言はVariableDeclarator、配列リテラルはArrayExpression、文字列リテラルはLiteralというNode Typeになります。しかし、コードを直接見ただけでは、どの部分がどのNode Typeに該当するかを判断するのは困難です。
たとえば、次のようなケースを考えてみましょう。

  • コード:const foo = "bar";

  • このコードに含まれる構文要素(変数宣言、代入された値)はASTのどのNode Typeとして扱われているのか?

こうした情報を調べるためには、AST Explorerのようなツールを使うのが便利です。

AST Explorerを使ったNode Typeの特定


便利なツールとしてAST Explorerがあります。このツールを使うと、今回の対象ソースコードである「const foo = "bar"」を貼り付けるだけで、対応するAST構造が右側に表示されます。AST Node Typeを確認する際には非常に便利で、TypeScriptやJSXにも対応しています。
実際にこのコードを入力してみると、最初にProgramというNode Typeが表示されます。その中にVariableDeclarationが含まれ、さらにその下にVariableDeclaratorが現れます。このVariableDeclaratorノードを展開すると、Identifier(変数名)やその初期値である"bar"(リテラル)に関する情報が確認できます。このようにAST Explorerを使うと、ノードがどのNode Typeに対応しているのかを簡単に特定できます。
さらに、このツールでは左側のソースコード上でカーソルを移動させると、右側のASTビューで対応する部分が黄色くハイライトされます。これにより、どの部分がどのNode Typeに対応しているのかを直感的に理解できます。
AST Explorerは、TypeScriptやJSXのような特定の構文拡張にも対応しており、これらのコードに特有のAST Node Typeを明確に表示してくれるため、非常に使いやすく分かりやすいツールです。

さて、先ほどの話に戻ります。一つ前のスライドに示した完成したルールとAST Explorerを見比べながら進めていくと、ルールの動きや構造がより分かりやすくなると思います。
まず、ルールの概要ですが、VariableDeclaratorを対象に「すべての変数宣言をチェックする」というルールを記述しています。このルールでは、次のような条件分岐を用いて処理を進めます。

  1. Node.parent.kindがconstであるかを確認
    ここでnodeはVariableDeclaratorに該当する部分を指します。このノードのparent(親ノード)をたどり、そのkindがconstであるかをチェックします。この部分がconstではなくvarやletであれば、この分岐を通過しません。

  2. node.id.typeがIdentifierであることを確認
    次に、ノードのid(変数名部分)がIdentifierというタイプであることを確認します。このチェックにより、ノードが変数の識別子であることを確定します。

  3. node.id.nameがfooであるかを確認
    さらに、変数名がfooである場合のみ次の条件に進みます。

ここまでで、対象が「名前がfooである変数宣言」であることを特定できます。
次に進む条件は以下の通りです。

  1. node.init.typeがLiteralであることを確認
    変数の初期値(initプロパティ)がリテラルである場合にのみ進みます。

  2. リテラル値が"bar"以外である場合にエラーを報告
    ここでnode.init.valueが"bar"以外であれば、context.reportを呼び出してエラーとして報告します。この関数はESLintのルール違反を通知するために使用されます。

整理すると、このルールは「constで宣言された変数fooに、リテラル値として"bar"以外を代入するとエラーを報告する」というものです。
AST Explorerを使いながら確認すると、これらの処理がどの部分に対応しているかを視覚的に把握できるため、ルールの理解がさらに深まります。特に、ノードの親子関係やプロパティの値を簡単に確認できるため、ルール作成の際に非常に役立つツールです。

カスタムルールのテスト方法

ルールを作成した後に、「本当に自分が作ったルールが正しく動作しているのか」と不安に感じることがあるかもしれません。しかし、ESLintにはテストを書くための機能が標準で備わっています。そのため、追加のツールを導入することなく、ESLintだけでルールのテストを実施することが可能です。
具体例として、作成したルールをテストするコードが右側に示されています。このテストでは、以下のような構造になっています。

  1. ルールをインポート
    まず、先ほど作成したカスタムルールをインポートします。

  2. テストケースの定義
    ルールに従ったコード(OKなパターン)とルール違反となるコード(エラーが発生するパターン)を定義します。

テストケースには主に以下の2種類があります。

  • Valid(有効なコード)
    例:変数fooに"bar"を代入するコードはルール違反ではありません。このケースでは、エラーが報告されないことを確認します。

  • Invalid(無効なコード)
    例:変数fooに"baz"を代入するコードはルール違反となります。この場合、エラーが適切に報告されることを確認します。

  1. テストの実行と結果の確認
    テストを実行すると、Validケースでは「問題なし」、Invalidケースでは「ルール違反のエラー」として結果が返ってきます。これにより、ルールが期待通りに動作していることを確認できます。

このように、ESLintのテスト機能を活用することで、カスタムルールの品質を担保できます。テストを通じてルールの精度を高め、実際のコードベースに適用する際の信頼性を向上させましょう。

ESLintカスタムルールの実例

1つ目のルール: Next.jsのLinkコンポーネントの物理遷移チェック

まず1つ目のルールですが、ミイダスではNext.js.を使用しており、Next.jsのLinkコンポーネントというものがあります。これはHTMLでいうところのaタグに相当しますが、Linkタグを使うことで、物理遷移ではなく、仮想的なページ遷移が行えます。ただし、何でもかんでも仮想遷移にすれば良いというわけではなく、物理遷移が必要な場面も存在します。
例えば、ランディングページ(LP)からログインページに遷移する場合、LPにはアフィリエイトなどのサードパーティーのスクリプトが多く含まれているため、それをそのままログインページに持ち込んでしまうと、万が一サードパーティースクリプトの発行者が攻撃を受け、悪意あるスクリプトが埋め込まれてしまった場合、ユーザーネームやパスワードが外部サーバーに送信されるリスクがあります。そのため、重要な情報を入力するページは物理遷移にすべきだと判断し、特定のURLを物理遷移すべきかどうかチェックする機能を実装しました。
修正前のLinkコンポーネントを、修正後のaタグに変えることで、物理遷移が必要な場面では適切に処理できるようになりました。プロジェクトごとに物理遷移すべきURLは異なるため、その一覧をESLintルールのオプションとして渡せるようにしました。JSXのhref属性に、物理遷移すべきURLが含まれている場合、ESLintでエラーとして報告するようにしています。

2つ目のルール: レンダー関数の禁止

2つ目のルールは、Reactの関数コンポーネント内でレンダー関数を定義することを禁止するものです。ミイダスでは、クラスコンポーネント時代の名残で、レンダー関数が多用されていました。関数コンポーネント内にレンダー関数が存在すると、useCallbackを使用すべきかどうかの判断が必要になったり、別のコンポーネントに切り出すべきかどうかを考慮する必要が出てきます。そのため、安易にレンダー関数を作れないよう制限を設けました。このルールに従って修正すると、関数コンポーネントの外にレンダー関数を切り出すことになります。ESLintプラグインReactのユーティリティ関数を参考にして、関数コンポーネント内かどうかを判定し、関数名が「render」で始まる場合には規約違反として報告するようにしました。

3つ目のルール: Immutable.jsのプロパティ取得方法の統一

3つ目のルールは、Immutable Recordのプロパティの取得方法を制限するものです。Immutable.jsはFacebook社が提供しているOSSで、オブジェクトを不変にするためのライブラリです。RecordというのはImmutable.jsが提供するデータタイプで、このRecordのプロパティにアクセスする方法として、.get()と通常のドットアクセスがありますが、両方が混在しているとコードの可読性が低下します。そこで、ドットアクセスに統一することにしました。.get()を使用している箇所を規約違反として報告するようにしています。このアプローチには、TypeScriptのコンパイラAPIを使用し、.get()が呼び出されているオブジェクトのシンボル名がレコードだった場合に規約違反として報告する仕組みです。

大変だったポイント

ESLintのカスタムルールを作成する中で、いくつか苦労した点がありました。

AST Explorerを知らなかった初期の困難

最初の頃、AST Explorerの存在を知らず、コード中の特定の構文がどのNode Typeに該当するかを調べるのに苦労しました。ASTには非常に多くのNode Typeがあり、その中から該当するものを特定するのは手間がかかります。当初はnode.idやnode.parentなどのプロパティをconsole.debugで出力し、デバッグを繰り返しながら一つ一つ把握していました。そのため、作業効率が悪く、進行が遅れることもありました。
AST Explorerを活用するようになってからは、どのNode Typeが対象なのかを視覚的に確認できるようになり、作業スピードが大幅に向上しました。もし初めからこのツールの存在を知っていたら、もっとスムーズに進められたと思います。

ドキュメント不足の問題

Node Typeの仕様や、どのプロパティをチェックすべきかといった情報を網羅的に説明したドキュメントが見つからなかったことも課題でした。ASTやESLintの内部構造を深く理解する必要がある場合、公式ドキュメントやコミュニティのリソースだけでは十分ではなく、試行錯誤を繰り返さざるを得ない場面が多々ありました。

独自ルール作成のハードル

ESLintには、すでに多くの一般的なルールが用意されています。また、ReactやTypeScriptなどのコミュニティによるOSSプラグインも活発に開発されています。しかし、組織独自のコーディング規約を自動チェックするルールが必要な場合、それら既存のルールでは対応しきれないことがあります。このようなケースでは、自分たちでカスタムルールを作成する以外に方法がなく、その実装には労力が必要です。
多くの企業では、独自のルールが必要になった際も、コードレビューで手作業で対応することが多いようです。ESLintのカスタムルールを作ることは決して簡単ではありませんが、それをツール化することで、レビュワーの負担を軽減できるというメリットがあります。

結果と学び

今回の取り組みを通じて、以下のような成果と学びを得ることができました。

  1. 開発者とレビュワーの負担軽減
    作成したESLintのカスタムルールにより、規約違反を自動で検知できるようになりました。これにより、開発者やレビュワーが規約を意識する必要がなくなり、作業負担が軽減されました。

  2. 完璧を求めず、早期導入が重要
    社内で使用するルールであれば、偽陽性や偽陰性が多少発生する場合でも、よくあるケースをカバーできていれば十分です。すべてを完璧にすることに時間をかけるよりも、早期に導入して改善を繰り返すことが重要であると感じました。

  3. ASTの知識が不可欠
    ESLintルールの開発にはAST(Abstract Syntax Tree)の理解が欠かせません。特に、TypeScriptで開発する場合、ASTの構造を把握するのが容易になるため、TypeScriptを活用することが有効です。

  4. 参考となる既存ルールの活用
    作りたいルールに似た既存のルールを調査し、その実装を参考にすることで、新しいアプローチのヒントが得られることが分かりました。

  5. TypeScript Compiler APIの課題
    TypeScriptの型に関連するカスタムルールを作成する際には、TypeScript Compiler APIの知識が必要になります。ただし、ドキュメントがGitHub Wikiの1ページに限られているため、情報不足が課題となりました。

まとめ

カスタムルールの作成は難しい面もありますが、AST Explorerなどのツールを活用することで効率的に進めることができます。また、すべての規約を自動化するのは理想論かもしれませんが、可能な範囲でツール化することで開発プロセスがスムーズになり、生産性向上につながると実感しました。
今後も改善を重ねながら、より効果的な運用を目指していきたいと思います。
ありがとうございます。

ミイダス Techについて

ミイダスでは、定期的に技術イベントを開催しています。connpassやYouTubeチャンネルでミイダスのメンバーになった方には、最新の開催情報やアーカイブの公開情報が届きますのでぜひご登録をお願いいたします。

イベントページ:https://miidas-tech.connpass.com/
X(旧Twitter):https://twitter.com/miidas_tech


いいなと思ったら応援しよう!