ムキムキマッチョマン

心も体もムキムキマッチョマン

ESLint Pluginを書くのに必要な知識、あるいは強力な制約で得られるものについて

こんにちは。ムキムキマッチョマンになりたい人です。心も体もムキムキマッチョマン。 フロントエンド領域でのムキムキマッチョマン目指して頑張っています。 この記事は技術記事の皮を被った、IT知識トレーニングの記録です。

はじめに

フロントエンド開発において、コーディング規約やいろいろな制約を持たせたいときに重宝されるのがLinter、静的解析ツールです。 静的解析ツールではソースコードを実行などせず解析し、文法上の間違いや表記の統一の問題を見つけ、場合によってはそれの解決まで行ってくれます。

2024年現在様々なLinterがある中で、ESLintはプラグインを中心としたエコシステムを形成し色々ありながらもまだシェアを保っています。 今回はそのESLintプラグインを使う話です。

制約と誓約

リスクはバネ!! 制約と覚悟が大きい程念は強く働く!!

ー クラピカ / HUNTER x HUNTER

制約と制約は少年ジャンプ連載の漫画「HUNTER x HUNTER」に出てくる概念で強い縛りを設けそれの対価として自身の強化など大きな利益を得るという仕組みの話です。

詳しい話はすでに自分よりも詳細にかつわかりやすく書いていただいてる方がいますのでそちらを紹介させていただきます。

speakerdeck.com

近いものでいえば同じく少年ジャンプ連載の漫画「呪術廻戦」にて「縛り」という概念が出てきます。

信じる信じないの話ではない これは〝縛り〟

誓約だ

守らねば罰を受けるのは俺 身に余る私益をむさぼれば報いを受ける

ー 呪術廻戦

前提

  • 業務開発やチームでの開発を想定しています
  • 制約や縛りがない環境を理想とした上での話をします。
  • 自由闊達にプログラムを書き合い、お互いで議論し合いながら高めあう。1つの理想の形だと思います。

制約をより強くすることで得られるものも大きくしたい

より具体的な話をしていきます。

制約と誓約は「縛りを設けるとそれに応じて強化など利益が得られること」でした。縛りも同様です。 これの原理自体は正直自分も詳しくないのすが...

ここからは制約を強制させる仕組みを用いて「制約と誓約の効能」を最大化したいという話をしていきます。

制約を強制させる仕組み

フロントエンド開発の場合、一般的に配列の中身を走査した上で操作を加え新たな配列を作る時にはmapを使う方が良いとされています。 仮にこれをルールとした時にそれをコードレビューの際に都度指摘したり、ペアプロやモブプロの時に教えていてはキリがないです。

一般的にそうと言われているから知識として持っておくべきという話があるかもしれませんが、それは個人に期待しすぎな気もします。

これらを解決するために静的解析ツールでそもそもエラーとして扱ってしまい、書く側に正しい形で強制させようというのが自分の提案です。 静的解析ツールは事前に文法的な間違いや書式の統一(末尾カンマやインデントなど軽微なもの)をさせるためのもので、本来用途からはややズレた使い方ではありますが。

詰まるところ目指したいのは「ソースコードの様式・書き方を強力にLinterで強制(あるいは矯正)すること統一され、コードの品質が優らずとも劣らない状態を作ること」です

コードで表現できない制約は制約たり得ない

配列の中身を走査した上で操作を加え新たな配列を作る時にはmapを使う方が良い

これは一見成立しているように見えて、制約として機能していません。 例えば以下のような場合、これらは制約を無視するしかなくせっかく設けたとしてもプログラムを書く側の裁量で判断できてしまいます。

  • for of を使いたい場合はエラーを無視するのが良いのか?
  • 逆にmapを使うべきでないケースは?

強制力を働かせたい制約なのに、その強制力が弱まるような定義の仕方はうまく機能するはずがありません。

曖昧な表現を避けて制約がなすべき条件を明確化して整理しそれらを強制する=制約をコードとして表現するのが良いでしょう。

コードとして制約を強制できる仕組みESLint Plugin

ESLint Pluginではコードを静的解析した際にその解析結果を使って独自のカスタムルールにて強力な制約が設定できます。

本題:ESLint Pluginを書くのに必要な知識、あるいはASTNodeの話

ではESLint Pluginを書こうとなった時に実は関連するドキュメントが充実してないことに気づきます。 その中でどう情報を集め、どうルールを書いていくかを残り書いてきます。

公式ドキュメント

ESLint 公式のcreate pluginではjsを利用して、プラグインそのものを記述する方法が紹介されています。

eslint.org

Get staredくらいにはちょうどいいんですが、実際にプラグインを開発していく際にどうコードを読み解き分析するかの部分についての説明はほとんどありません。

eslintのrule実装を見にいく

一番わかりやすい参考資料はeslint本体のルール実装です。 例えば識別子にundefinedを使うことを禁止するno-undefinedのルール実装を見てみましょう。 (※undefinedというグローバル変数に別の値を入れて等価演算子をバグらせることなどができます)

eslint/lib/rules/no-undefined.js at main · eslint/eslint · GitHub

/**
 * @fileoverview Rule to flag references to the undefined variable.
 * @author Michael Ficarra
 */
"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('../shared/types').Rule} */
module.exports = {
    meta: {
        type: "suggestion",

        docs: {
            description: "Disallow the use of `undefined` as an identifier",
            recommended: false,
            frozen: true,
            url: "https://eslint.org/docs/latest/rules/no-undefined"
        },

        schema: [],

        messages: {
            unexpectedUndefined: "Unexpected use of undefined."
        }
    },

    create(context) {

        const sourceCode = context.sourceCode;

        /**
         * Report an invalid "undefined" identifier node.
         * @param {ASTNode} node The node to report.
         * @returns {void}
         */
        function report(node) {
            context.report({
                node,
                messageId: "unexpectedUndefined"
            });
        }

        /**
         * Checks the given scope for references to `undefined` and reports
         * all references found.
         * @param {eslint-scope.Scope} scope The scope to check.
         * @returns {void}
         */
        function checkScope(scope) {
            const undefinedVar = scope.set.get("undefined");

            if (!undefinedVar) {
                return;
            }

            const references = undefinedVar.references;

            const defs = undefinedVar.defs;

            // Report non-initializing references (those are covered in defs below)
            references
                .filter(ref => !ref.init)
                .forEach(ref => report(ref.identifier));

            defs.forEach(def => report(def.name));
        }

        return {
            "Program:exit"(node) {
                const globalScope = sourceCode.getScope(node);

                const stack = [globalScope];

                while (stack.length) {
                    const scope = stack.pop();

                    stack.push(...scope.childScopes);
                    checkScope(scope);
                }
            }
        };

    }
};

ASTへの理解

これらコードを見ていく上で出てくるのが「Statement」や「Declaration」という単語です。 これらを理解していく上で重要になるのが AST(抽象構文木)です。 ASTはプログラミング言語のソースコードを木構造で表現し、式や文、宣言などを構造化しJSONで表現するものです。 一般的なASTであれば式をexpression、文をstatementと表現します。

詳細はこちらの記事をご覧ください。

efcl.info

実際のコードで具体的にどんなASTが出てくるのかは大まかに以下のツールで試すこともできます。

astexplorer.net

estreeパーサー

AST自体は標準仕様がなく、パーサーによって最終的に出力される構文木や対応している構文に差があります。 ESLintではestreeと呼ばれるパーサーを利用しています。 ちなみにtypescript-eslintの場合は一度TypeScriptのASTを出力した後それをestree互換にしており2段階でパースしていたりします。

ESLintプラグインを0から作るなら参考にしたいテンプレート

@kotarella さんという方が作ったこのGitHubリポジトリでは、ESLintプラグインの開発環境のガイドラインとしてTypeScriptを使ったものやテストのサンプルなどが含まれています。 3年前が最終更新ということもあり、実際使う場合はやや調整の必要がありますがかなり参考になるでしょう。

github.com

最後に

ESLintプラグインを使ってみたい人向けなのか、ESLintプラグインはいいぞの記事なのかがわかりませんが、こんなとこで。