👀

【Dart】コーディングルールをLintにしてみる【custom_lint】

2024/12/15に公開

はじめに

この記事はGMOメディア株式会社 Advent Calendar 2024の15日目の記事です。

私はFlutterのリードエンジニアとして、アーキテクチャの設計やFlutterのコード全般のレビューを担当しています。
コードレビューをしていると、「この指摘、前にもしたな」「ここの設計方針決めなかったっけ?」「この方針をAさんと決めたけどBさんには伝え忘れていました。」など、様々なコミュニケーション面の課題に直面します。こういった指摘をレビュー時に都度伝えるのは心苦しく、まためんどくさいものです。

「面倒なことは自動化しよう!」

前提

Dartには、デフォルトの公式Linterがあり、基本的なコーディング規約を強制することができます。
https://dart.dev/tools/linter-rules

しかし、以下のようなレビューでよく目にする指摘事項は、デフォルトのLinterでは対応できません。

  • コーディング規約
  • アーキテクチャの制約
  • Flutterでの一般的な実装上の注意点
  • Riverpodのread / watch の使い分け
  • その他、プロジェクト固有のルール

本題

デフォルトの公式Linterには無いルールを追加するために、custom_lintというパッケージを採用してみます。
このパッケージを導入することで、簡単に独自のLintルールを追加することができます。

導入するパッケージ:
https://pub.dev/packages/custom_lint

例として「applicationフォルダに*_service.dart以外のファイルを入れない」というルールをLintで強制してみます。

開発環境

Flutter: 3.24.5
Dart: 3.5.4
analyzer: 6.7.0
custom_lint: 0.7.0
custom_lint_builder: 0.7.0

最終的なファイル構造

develop_project
├── lib  // Lintを追加したいプロジェクトのコード群
├── packages  // ローカルパッケージをまとめているフォルダ
│   └── my_team_lint  // 今回作成するLintパッケージ
│       ├── lib
│       │   └── my_team_lint.dart  // Lintを追加するコード
│       └── pubspec.yaml  // Lintパッケージのpubspec.yaml
├── analysis_options.yaml  // Lintを追加したいプロジェクトのanalysis_options.yaml
└── pubspec.yaml  // Lintを追加したいプロジェクトのpubspec.yaml

コード

Lintパッケージのpubspec.yaml

name: my_team_lint
environment:
  sdk: "^3.5.0"
dependencies:
  analyzer: "^6.7.0"
  custom_lint_builder: "^0.7.0"

Lintを追加するコード

// my_team_lint.dart
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';

PluginBase createPlugin() => MyTeamLint();

class MyTeamLint extends PluginBase {
  
  List<LintRule> getLintRules(CustomLintConfigs configs) {
    return [
      const ApplicationLayerLintRule(),
    ];
  }
}

// 「`application`フォルダに`*_service.dart`以外のファイルを入れない」というルール
class ApplicationLayerLintRule extends DartLintRule {
  const ApplicationLayerLintRule() : super(code: _code);

  static const _code = LintCode(
    name: 'Application層ルール',
    problemMessage: 'Applicationフォルダ内のファイル名は*_service.dartの形式である必要があります。',
  );

  
  void run(
    CustomLintResolver resolver,
    ErrorReporter reporter,
    CustomLintContext context,
  ) {
    final filePath = resolver.source.uri.path;
    final pathSegments = filePath.split('/');

    final fullFileName = pathSegments.last;
    final fileName = fullFileName.split('.').first;

    final folderName = pathSegments[pathSegments.length - 2];
    if (folderName == 'application') {
      final validPattern = RegExp(r'^.+_service$');
      if (!validPattern.hasMatch(fileName)) {
        reporter.atOffset(
          offset: 0,
          length: 1,
          errorCode: code,
        );
      }
    }
  }

  
  List<Fix> getFixes() => []; // IDEでの自動修正候補を提供できる機能だが、今回はCI用なので使用しない
}

Lintを追加したいプロジェクトのpubspec.yaml

dev_dependencies:
  custom_lint: "^0.7.0"
  my_team_lint:
    path: ./packages/my_team_lint

Lintを追加したいプロジェクトのanalysis_options.yaml

analyzer:
  plugins:
    - custom_lint

結果

Lintを追加したいプロジェクトのルートで下記コマンドを実行すると、

$ dart run custom_lint

実行結果:

Building package executable... 
Built custom_lint:custom_lint.
Analyzing...                           0.0s

  lib/feature/sample/application/dummy_widget.dart:1:1 • Applicationフォルダ内のファイル名は*_service.dartの形式である必要があります。 • Application層ルール • INFO

1 issues found.

Lintの結果が表示されます。

完成!やったー!

あとはコマンドをGithubActions等CIツールで呼んであげるだけで、コーディングルールを強制することができます。

おわりに

今回はシンプルな一例を示しましたが、custom_lintで設定できるLintの幅はかなり大きく、設計をLintで表現することもできます。

例)

  • 特定のファイル名 x 特定のクラス名 をチェックする
  • 特定のフォルダ名 × 特定のファイル名 × import名 をチェックする
  • 設定値をanalysis_options.yamlに書いてLintパッケージに渡せる
    • ↑Lintパッケージをプロジェクトに依存させず、Lintパッケージ自体は汎用的なルールを設定できる。
    • ↑Lintパッケージは社内横断で共有し、プロジェクト独自のファイル名に合わせてLintをチューニングする。といった運用にも使える。
// Lintを追加したいプロジェクトのanalysis_options.yaml
custom_lint:
  rules:
    - import_rule:
      presentation_layer:
        target: "package:__/feature/*/presentation/*.dart"
        from: "package:__/feature/*/*.dart"
        message: "Presentationレイヤーでは許可された依存関係のみ使用できます。"

よく見る指摘事項をLint自動チェックすることで、

  • レビュー時間の短縮
  • コード品質の向上

が見込め、より本質的な課題の解決に時間を使えるようになります。

最高!

GMOメディアテックブログ

Discussion