📏

Flutter での Lint のススメ

2024/05/23に公開

こんにちは、Sally社 CTO の @aitaro です。 普段はマーダーミステリーアプリ「ウズ」とマダミス制作ツール「ウズスタジオ」を開発しています。

https://uzu-app.com

はじめに

最近、社内でFlutterプロジェクトのLint設定をアップデートしたので、Lintルールに対する考え方と設定方法についての知見を共有するためにこの記事を書きました。Lintはコード品質を保つための重要なツールであり、特にチーム開発においてはその効果を最大限に引き出すことができます。本記事では、Dart Linter の力を最大限に活かして、優れたチーム開発体験を実現する方法を紹介します。

Lintツールの重要性

開発している言語に関わらず、Lintの適切な設定は以下の観点から重要です。特にチーム開発、大規模プロジェクトになるほど、その威力は大きくなります。

コードの一貫性

Lintツールにより、コードスタイルや規約を統一できます。これにより、チーム全員が同じスタイルでコーディングし、可読性が向上します。

バグの早期発見

Lintツールは、未使用の変数や未定義のメソッド呼び出しなど、コンパイル時に見逃されがちな問題を事前に警告し、潜在的なバグを早期に発見します。

コードレビューの効率化

Lint導入により、コードレビュー時のスタイルや規約に関する指摘が減り、より本質的なロジックや設計に集中できます。これはレビューの効率を上げ、高品質なコードを迅速にリリースできるようにします。

Flutter の Lint 適用の基本方針

ESLint との違い

ESLint は 混沌とする JavaScript に秩序をもたらすために必要とされてきました。 JavaScript はその歴史的経緯より言語仕様が複雑で、それゆえ ESLint として設定できるルールも公式非公式含め膨大な数があります。
それを管理するために、eslint-config-airbnb や eslint-config-standard などの preset rules を使うことが多いです 。

一方、Dart の Lint の数は限られています。また、Lint 自体も Dartチームによって管理されており、設定されている Lint もそれぞれ有用なものが多いです。Dart のこの量の Lint Rules であれば、チームで精査して適切に設定することが可能です。

Lint の一覧
https://dart.dev/tools/linter-rules

もちろん、Dart Linter の preset rules はいくつかあります。公式でメンテされているのは、core, recommended, flutter_lint の3つ存在し、他にも lint や 日本の方が作られている pedantic_mono などが挙げられます。
しかし、これらをチームやシステムの特性に合わせてlintをカスタマイズすることのコストや、非公式のlintの場合は、最新のルールへの追従やメンテナンスの観点から、特にチームでflutterアプリを開発するときは、自分たちで全てのルールを確認して、設定することをお勧めします。

全ての Lint を確認して、チームにあった Lint を設定する方法

analysis_options.yaml には include という他の analysis_options を include する設定項目と、linter.rules という個別の linter の on/off の設定をする項目とあります。
基本実装方針としては、 include で一旦全ての lint を include して linter.rules で個別の rule を off にしていく方針を取ります。これはチームで議論する量を最小限に抑えるためで、基本ONにした状態で、どうしても困ると言う合意形成ができたものだけ、個別にoffにしていくのが比較的スムーズに事が運びます。

全ての rules を include する方法

全ての rules を include する方法については、公式サイトから取得して手動で設定してもいいのですが、lint の rules は定期的に増えるので、定期的に更新しないと、新しいlintの恩恵が受けられないです。そこで lint の rules の更新の自動化を模索します。

一つ簡単な方法として all_lint_rules_community を利用するというものがあります。こちらは、1日1回 Github Actions を回して、lint rules に更新があれば、新しい package として publish されるというものです。しかし、そこまで活発にメンテされておらず、Github Actions の Scheduler が停止してしまっているので、これを使う方法は心許ないです。(GitHub Actions は 60日間活動がないリポジトリのSchedulerは停止してしまいます。 リンク)
そこで、自前で lint の rules の更新を行います。

以下がその GitHub Actions です。

name: 【Dart】 Update All Lint Rules
on:
  workflow_dispatch:
  schedule:
    - cron: "0 0 * * *"

jobs:
  update_all_lint_rules:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
      - run: curl -o all_lint_rules.yaml https://raw.githubusercontent.com/dart-lang/sdk/main/pkg/linter/example/all.yaml
      - id: cpr
        uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          commit-message: "chore: update all dart lint rules"
          committer: GitHub <noreply@github.com>
          author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
          branch: chore/sync-dart-lint-rules
          branch-suffix: timestamp
          delete-branch: true
          title: "chore: update all dart lint rules"
          body: "Update all dart lint rules"
      - name: Enable Pull Request Automerge
        if: steps.cpr.outputs.pull-request-operation == 'created'
        uses: peter-evans/enable-pull-request-automerge@v3
        with:
          pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
          merge-method: squash

https://raw.githubusercontent.com/dart-lang/sdk/main/pkg/linter/example/all.yaml は dart 公式のメンテナーがメンテしているパッケージに存在し、このファイルに全ての lint rules が設定された analysis_options が存在します。このワークフローのポイントとしては、そのファイルと local の all_lint_rules.yaml と同期させると言う点です。
その後変更があれば、PRを作ります。メインブランチに直commitしてもいいのですが、dart の lint が増えることで既存のコードでワーニングが発生する可能性もあるので、一旦PRを作ることでそちらでlintが通るかを確認します。また、そのPRで本当にそのlintが必要かを議論することもできます。

lint.rules で個別に lint を無効化していく

前節の方法で all_lint_rules.yaml に全てのlintを設定できた後は、lint.rules で不要なものを無効化していきます。
弊社では、以下のように設定しています。

include: ./all_lint_rules.yaml
linter:
  # rules については不要なものを列挙していく
  rules:
    # 好みの問題だが、"if (bool) return;" の方が読みやすいと感じる
    always_put_control_body_on_new_line: false

    # `prefer_relative_imports`と競合する
    always_use_package_imports: false

    # `omit_local_variable_types` などと競合する。
    # Dartのガイドラインに従い、不必要な型を避けてコードを読みやすくする。
    always_specify_types: false

    # `type_annotate_public_apis`と競合する
    avoid_annotating_with_dynamic: false

    # カスタムのenumライクなクラス(Flutterの"Colors"など)での誤検出
    # 運用した結果、そこまで有益とは思えなかった(dartにnamespaceとしての代替案が乏しい)
    avoid_classes_with_only_static_members: false

    # 好みの問題だが、cascade記法はそんなに一般的にではない
    cascade_invocations: false

    # debugメソッドは使っていない。
    diagnostic_describe_all_properties: false

    # 関数によってFutureが返される場合の誤検出
    discarded_futures: false

    # このプロジェクトはFlutterスタイルのtodosを使用していない
    flutter_style_todos: false

    # 意図を持ってアンダースコアをつける場合にそれを妨げる必要はない
    no_leading_underscores_for_local_identifiers: false

    # 現代的な IDE なら 80 文字制限は不要
    lines_longer_than_80_chars: false

    # `prefer_single_quotes`と競合する
    # シングルクォートは入力が簡単で、可読性に影響しない。
    prefer_double_quotes: false

    # 特に Widget の定義で `build`メソッドが単一のreturnを持つ場合、
    # 逆に読みづらくなる。
    prefer_expression_function_bodies: false

    # 記述が冗長になり、可読性が低下する。
    prefer_final_parameters: false

    # `prefer_final_locals`と競合する
    unnecessary_final: false

    # アプリケーションでは過剰
    public_member_api_docs: false

    # 偽陽性が多い
    # https://github.com/dart-lang/linter/issues/4346
    unnecessary_parenthesis: false

    # 日本語はwhitespaceをそんなに使わないので相性が悪い
    missing_whitespace_between_adjacent_strings: false

    # 例えばColumn内部の配列等について、3項演算子の方が見やすい可能性がある。
    # 各実装者に任せる
    prefer_if_elements_to_conditional_expressions: false

    # 明記した方が読みやすい場合があるので要議論
    # Provider とは相性がかなり悪い(なくなる予定ではあるが)
    # https://twitter.com/_mono/status/1220483766163951616
    avoid_types_on_closure_parameters: false

    # Flutter のスタイルガイドを違反する
    # https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants
    #
    # また、原因を深く追求していないが、 double を使うべきところで int を使ってしまうと、型変換でエラーが発生してしまうので、auto-fix も危険。
    # おそらく、 Tween 等 int と double で違う型に推論されるので良くない。
    # https://linear.app/xxxxxxxxx
    prefer_int_literals: false

    # borderが設定されている場合、ContainerとDecoratedBoxは挙動が異なる
    # 詳細は、https://github.com/flutter/flutter/issues/118777 や https://zenn.dev/aomi/articles/73475e127a4e3b を参照。
    use_decorated_box: false

もちろん、各lintを入れる入れないはチームで相談しながら、入れない lint だけを入れなかった理由とともに書いていくのが良いです。
これは期間をおいて再度 lint rules を検証するときや、新規メンバーがコーディングスタイルを確認するときに便利です。

まとめ

Flutterプロジェクトにおいては Lint は厳しく設定するほど、チーム開発での開発生産性が上がります。既存の rule セットを利用するのではなく、一旦全部有効化してからチーム内で要不要の議論を行うことが、Flutter での Lint 設定の恩恵を最大限に受けることができます。Lint は一度設定したら終わりではなく、チームで育てていくものです。メンテナビリティの高い Lint Rules を実現しましょう。

UZU テックブログ

Discussion