18
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Fringe81Advent Calendar 2020

Day 13

elm-reviewを使ってモジュールの依存関係に制限を付ける

Last updated at Posted at 2020-12-13

Fringe81アドベントカレンダー2020の13日目の記事です(1日遅れ)


elm-reviewは一言で言うとElmのためのLintツールです。Elmのコードを解析して、Elmの文法では表現できないルールに沿って警告を出すことができます。

スクリーンショット 2020-12-14 1.06.16.png

  • 型アノテーションのない関数(画像で検知してるルール)
  • 未使用の関数
  • デバッグ出力(Debug.log)
  • 開発チームで非推奨な書き方

...などなど1、シンタックスの解析で検出できるものであれば実質何にでも使用することができます。今回はこのelm-reviewを使って、指定した依存関係に反したimportをしているモジュールを検出してみました。

Elmのモジュールおさらい

モジュール宣言

Elmでは1つのファイルに必ず1つのモジュールを宣言します。1つのファイルに複数のモジュールを宣言することはできません。つまり、1ファイル = 1モジュールになっています。

またそのモジュール名はディレクトリ構造によって決まります。例えば、Hoge/Fuga/Piyo.elmに宣言するモジュール名はHoge.Fuga.Piyoでなくてはなりません。

module Hoge.Fuga.Piyo exposing (publicFunction)

公開範囲

Elmではモジュール外に公開する型や関数の公開範囲を指定することはできません。そのため、一部のモジュールにだけ公開したいような関数が想定外のモジュールから使われていたとしても、そのルールを指定する方法がないのでコンパイルエラーにすることはできません。

module Pages.Home.Form exposing (form)
module CommonUI.DocumentEditor exposing (editor)

-- 汎用的に使われることを期待したモジュールが特定ページのモジュールをimportしている。
import Pages.Home.Form exposing (form)

このように、期待しない使い方がされていてもコンパイル時に気づくことはできません。もしかしたら開発が進んで実際に困った影響が出るまで気づかないかもしれませんね。
今回はelm-reviewを使ってこれを検知してみます。

ルールを考える

今回検査したいのは「CommonUI配下にあるモジュールがPages配下にあるモジュールをimportしたら警告する」というルールです。これを噛み砕いて文章にすると以下のようになります。

モジュール名がCommonUIから始まっている場合、モジュール名がPagesから始まるモジュールをimportしていたら警告を出す。

もう少し汎用的に使えるように、このようにしてみます。

A. 全てのモジュールは"レイヤー番号"を持ち、レイヤー番号が自モジュールよりも大きいモジュールをimportしていたら警告を出す。
B. Pages.*はレイヤー番号1、CommonUI.*はレイヤー番号0とする。

Bのレイヤー情報を入力として、Aのルールに違反するモジュールを検知する作戦で実装していきます。

ルールを実装する

Aの実装

elm-reviewにはmodule文とimport文を解析するためのAPIがあるので2、これを使ってAを実装します。

実装したコード(雰囲気だけ感じてください)

newModuleRuleSchema: モジュール単位でルールを適用する。
withModuleDefinitionVisitor: モジュール内のmodule文を解析する。
withImportVisitor: モジュール内のimport文を解析する。

処理の流れは以下のようになっています。

  1. withModuleDefinitionVisitorを使って検査するモジュールのレイヤー番号を計算する。
  2. withImportVisitorを使ってimport文を1つずつ見ていき、importしているモジュールのレイヤー番号を計算する。
  3. 2の結果が1の結果よりも大きければ警告を出す。

elm-reviewで解析する場合、Hoge.Fuga.Pomというモジュールは["Hoge", "Fuga", "Pom"]というList String型で表現されます。モジュール宣言の項で説明した通り、Elmのモジュールはディレクトリ構造で決まるので、「"Hoge"配下のモジュールかどうか」はList Stringについての関数として定義できます。

type alias ModuleName = List String

-- モジュール名を先頭から比較していき、ルールとなるモジュール名(rule)が全てターゲットになるモジュール名(target)と一致していればTrueを返す。
isMatchWith : ModuleName -> ModuleName -> Bool
isMatchWith rule target =
    case (rule, target) of
        ([], _) ->
            True

        (r :: rs, t :: ts) ->
            if r == t then
                isMatchWith rs ts

            else
                False

        (_ :: _, []) ->
            False
isMatchWith [ "Pages" ] [ "Pages", "Hoge" ] --> True

isMatchWith [ "Pages" ] [ "Waaa", "Pages" ] --> False

isMatchWith [ "Pages", "Hoge" ] [ "Pages" ] --> False

このisMatchWith関数を使って以下のようなシグネチャの関数を作ることでレイヤー番号を計算します。

type ModuleLayer
    = ModuleLayer (List ModuleName)
    | DefaultLayer -- どのレイヤーにも属さないモジュールのレイヤー


type ModuleLayerDependancy
    = ModuleLayerDependancy (List ModuleLayer)


layerNumber : ModuleLayerDependancy -> ModuleName -> Int

Bの実装

次に入力となるレイヤー情報のBを作成します。

実装したコード(この記事の例とは少し異なります)

moduleLayerRule : ModuleLayerDependency
moduleLayerRule =
    ModuleLayerDependency
        [ commonUILayer
        , DefaultLayer
        , pagesLayer
        ]


pagesLayer : ModuleLayer
pagesLayer =
    ModuleLayer
        [ [ "Pages" ]
        ]


commonUILayer : ModuleLayer
commonUILayer =
    ModuleLayer
        [ [ "CommonUI" ]
        ]

使ってみる

検査の対象にするディレクトリ構造はこちら。

スクリーンショット 2020-12-14 3.01.19.png

初めに、警告が出ないはずのPages --> CommonUIのimport文だけを書いてみます。

module Pages.Home.Form exposing (..)

import CommonUI.DocumentEditor

スクリーンショット 2020-12-14 3.04.34.png
問題なし:eyes:

次に、警告が出て欲しいPages <-- CommonUIのimport文を書いて試してみます。

module CommonUI.DocumentEditor exposing (..)

import Pages.Home.Form

スクリーンショット 2020-12-14 3.15.55.png
検知できました:thumbsup:
(DefaultLayerがあるのでPages.*のレイヤー番号は2になっています)

最後に

公開されているelm-reviewのパッケージにはコードの書き方を警告するものが多いですが自由度高くルールを自作できるので、冒頭でも書いた通りシンタックスの解析で検出できるものであれば実質何にでも使用することができます3

「Elmでああしたい、こうしたい。でもElmの文法だと厳しい...」という時の選択肢に入れておくと良いことがあるかもしれません。

  1. 公開されているルールはここから検索できます。

  2. 内部でelm-syntaxが使われています

  3. elm-reviewのREADME.mdには「いつelm-reviewを使うべきか」のガイドラインが載っています。何でもかんでもelm-reviewのルールにしないでね、というメッセージにもなっています。一度読んでみてください。

18
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?