【Dart】コーディングルールをLintにしてみる【custom_lint】
はじめに
この記事はGMOメディア株式会社 Advent Calendar 2024の15日目の記事です。
私はFlutterのリードエンジニアとして、アーキテクチャの設計やFlutterのコード全般のレビューを担当しています。
コードレビューをしていると、「この指摘、前にもしたな」「ここの設計方針決めなかったっけ?」「この方針をAさんと決めたけどBさんには伝え忘れていました。」など、様々なコミュニケーション面の課題に直面します。こういった指摘をレビュー時に都度伝えるのは心苦しく、まためんどくさいものです。
「面倒なことは自動化しよう!」
前提
Dartには、デフォルトの公式Linterがあり、基本的なコーディング規約を強制することができます。
しかし、以下のようなレビューでよく目にする指摘事項は、デフォルトのLinterでは対応できません。
- コーディング規約
- アーキテクチャの制約
- Flutterでの一般的な実装上の注意点
- Riverpodのread / watch の使い分け
- その他、プロジェクト固有のルール
本題
デフォルトの公式Linterには無いルールを追加するために、custom_lintというパッケージを採用してみます。
このパッケージを導入することで、簡単に独自の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自動チェックすることで、
- レビュー時間の短縮
- コード品質の向上
が見込め、より本質的な課題の解決に時間を使えるようになります。
最高!
Discussion