Elmのlintツールelm-reviewのProjectRuleSchemaを使ってボイラープレート生成する

2023/07/22に公開

Elmのlintツールelm-reviewのProjectRuleSchemaを使ってボイラープレート生成する

By ymtszw
@Elm-jp Online #3 (2023/07/22) [1]


jfmengels/elm-review


コード生成ツールとして


elm-reviewの構造

  • Rule型のデータでlintルールを表現する
    • かいつまんで言うと「プロジェクトまたはモジュールを評価する関数
  • Ruleを実行した結果、Errorが1つ以上あればlint警告となる
  • ErrorFixを提供しても良い

ProjectRuleSchemaModuleRuleSchema

  • ほぼ読んで字のごとく、
    • プロジェクト全体を評価するルールか、
    • モジュール(Elmの場合=ファイル)を単体で評価するルールか

ボイラープレート生成 (1)

  • Elmの文脈でよく言われる「ボイラープレート」
    • Client-side routingのあるアプリで、個別のページモジュールをMainモジュールに結線する
    • Model, Msg, init, update, subscription, (view)からなる、いわゆる「コンポーネント」的モジュール同士を結線する
  • といった「モジュール間の結線」を指すことが多い(体感)

→つまり複数のモジュールの情報が必要となる。したがって、


ボイラープレート生成 (2)

  • ProjectRuleSchemaを使ってプロジェクト全体から情報を集め、
  • 未結線のモジュールが見つかったらErrorとし、
  • Fixを提供して結線する実装を生成しよう!

…となる。 [2]


実際のコード例

以下、Siiibo証券で実際に使っているコードをもとに紹介していきます。 [3]
(elm-review 2.13.1現在)

rule : Rule
rule =
    Rule.newProjectRuleSchema "GenerateBoilerplate" initialContext
        |> Rule.withModuleVisitor moduleVisitor
        |> Rule.withModuleContextUsingContextCreator contextCreator
        |> Rule.withFinalProjectEvaluation finalEvaluation
        |> Rule.providesFixesForProjectRule
        |> Rule.fromProjectRuleSchema

ProjectRuleSchemaの構造 (1)

    Rule.newProjectRuleSchema "GenerateBoilerplate" initialContext
  • ProjectRuleSchemaを使うよ、という宣言とRuleの命名。
  • initialContextでは、プロジェクト全体から必要な情報を集めて格納するデータ置き場(ProjectContext)を初期化する
type State
    = Visiting
    | Visited { key : ModuleKey, implementedPages : PageFileDict, endOfDeclarationsRow : Int }

initialContext =
    { state = Visiting, currentPages = Dict.empty }

ProjectRuleSchemaの構造 (2)

        |> Rule.withModuleVisitor moduleVisitor
  • プロジェクト内の個別のモジュール(ファイル)を評価するのがmoduleVisitor
moduleVisitor =
    Rule.withModuleDefinitionVisitor accumulateCurrentPageFile
        >> Rule.withImportVisitor accumulatePageFileFromPreviousImport
        >> Rule.withDeclarationListVisitor annotatePageFilesFromPreviousImplementation
  • 内部的にはModuleRuleSchemaが使われており、withXXXXXXVisitor系関数はそこに評価器を加えていく(Builderパターン)

ProjectRuleSchemaの構造 (3)

        |> Rule.withModuleContextUsingContextCreator contextCreator
  • moduleVisitorが利用するModuleContextの初期化と、moduleVisitorが評価されたあとにProjectContextに結果を反映する処理を定義
contextCreator =
    { fromProjectToModule = Rule.initContextCreator fromProjectToModule |> Rule.withModuleKey |> Rule.withModuleName
    , fromModuleToProject = Rule.initContextCreator fromModuleToProject
    , foldProjectContexts = foldProjectContexts
    }

ハマりどころ

  • このあたりがProjectRuleSchemaの難しいところ。全体構造がデカい
  • ドキュメントにProjectRuleSchemaどのようにプロジェクトが走査されていくか説明があるので、困ったらそこに立ち返ろう
  • 逆に言うと、走査の仕組みを初めに大まかに把握しておかないと、デカい型定義リストを目の前にしてハマることになる

moduleVisitorの中身

  • elm-syntaxで各モジュールのソースコードがASTとして評価可能な形で関数に渡ってくる
  • 関数記述自体はそれに対する愚直なパターンマッチで実装します
  • 詳細は割愛(発表時には投影するかも)

fromModuleToProjectfoldProjectContexts

  • 2つの関数で、moduleVisitorが各モジュールを評価した結果をProjectContextに蓄積する
  • (恐らく)並列処理を可能にするため、プロジェクト内のファイルは順不同でmoduleVisitorに評価されるという前提がある
  • 評価結果を順不同にProjectContextに合流させて支障なきよう、
    • foldProjectContextsは順番に依存しないよう実装
    • ProjectContext自体、DictSetなどの挿入順序に依存しないデータ構造を活用

ProjectRuleSchemaの構造 (4)

        |> Rule.withFinalProjectEvaluation finalEvaluation
  • すべてのプロジェクト内モジュールを評価し終わり、最終評価に必要な情報をProjectContextに蓄積し終わったあと、finalEvaluationErrorの有無を判定する
  • ここでやりたいボイラープレート生成なら、
    • ProjectContextcurrentPagesに現在のページモジュールの一覧が蓄積されているので、
    • それを元に必要なコードをelm-syntaxのAPIを使って書き出すことになる

Tips & Tricks


漸進的なFix

w:800 以前の実装


  • 1年くらいこの実装だったが、自動生成をdevサーバで継続的に実行したくなった
  • すると、ファイルを一旦空にして全体を書き換えるのは一瞬コンパイルが通らないコードになって不都合
  • ということで必要な箇所だけ書き換える・書き換える必要がなければno-op、という漸進的なFixにしたい

直前の実装から情報を集める

  • ProjectContextにあったstateがここで意味を持つ
  • state = Visited { ... }状態になった場合、ペイロードのimplementedPages評価直前のファイルで実装済みだった関数情報が入っている仕組み

finalEvaluationで"diffをとる"

  • するとcurrentPagesimplementedPagesで差分があるか、"diffをとる"実装が可能になる
  • Diffが何らか存在したら(=ボイラープレートに更新が必要になったら)Errorとしてfixを提供、そうでなければ何もしない
  • (時間あったら投影でコード紹介)

フォーマットされたコードの生成

  • finalEvaluationでfixを提供する際、elm-syntax-dslのElm.Prettyが活用できる
  • その名の通りprettyな(=elm-format済みの)コードが生成できる

elm-reviewによる差分検知の仕組み

  • 実はフォーマット済みのコードをfixで生成するのには意味がある
  • elm-review CLI自身も内部的にelm-formatを呼び出すようになっており、fixが提供したコードはelm-reviewが最終的に整形する
  • 2.13.1現在、「Ruleのfixが提供したコード」と「それをelm-reviewが最終的に整形したコード」に差分があると、elm-review CLIは「fixあり」と報告し続けてしまう
  • Fix時点で整形済みのコードを吐くようにすればきれいにfixが完了する、という理屈

elm-review 2.13.1でできないこと


前回実行との差分検知

  • 直前の実装から情報を集めるでやったようなことは、多くのコンパイラやツールでは「前回実行時のコンテキスト情報を記録ファイルに書き出しておき、次回実行時に読み出す」という形で実現されることが多い
    • いわゆるmanifest(目録)ファイル
  • この機構が今のelm-reviewにはないので、実際のソースコードから情報を再構成した、という事情
    • とはいえmanifestファイルも万能ではなくて、例えば別のメンバーがコード生成をしたときのmanifestが手元マシンになければ、結局実装からの再構成は必要

任意ファイルの読み出し

  • manifestファイル機構を自前で実装できないのはこれができないからでもある
  • 今はREADME.mdファイル、elm.jsonファイル、Elmファイルにしかvisitorを定義できない
  • (無理やりやるなら、README.mdに何らかデータを仕込めば悪用できそうではあるが)

ファイルの新規作成

  • そもそもelm-reviewはあくまでlinterが出自
  • なので既存ファイルを評価して警告・fixすることはできても、全く新しいファイルを作り出すことはできない
  • プロジェクトルールでコード生成する場面ではほしいこともあるのだが…
  • それもあってか、純粋な「Elmで書けるElmのコード生成ツール」としては最近mdgriffith/elm-codegenも出てきてます

あとがき

  • Siiibo証券のフロントエンド開発で日常的に使っているelm-reviewのProjectRuleSchemaによるコード生成を紹介した
  • 表現力が高いので複雑ではあるものの、一度分かればいろいろ応用できます✌
  • 著者紹介(tokyo.ex#20のスライド)

脚注
  1. この発表資料はZennで公開しつつ、Marpスライドとして作成しているので水平線がいっぱい入っています ↩︎

  2. elm-spaなどの「フレームワーク」は、そういった結線機構をdevサーバが提供している ↩︎

  3. 一部省略しているので動かすためにはある程度手を入れる必要あり ↩︎

Siiiboテックブログ

Discussion