【Flutter】custom_lintを導入して柔軟な開発ルールを運用する方法
株式会社Sally新人エンジニアの @wellPicker です。マダミスアプリ「UZU」の開発に携わっています。
日々の業務の中で学んだことを書き残すことで読者の皆様(と将来の自分)の役に立てればと思います。
前置き
Flutter開発において、lintツールを使って一貫性のあるコードスタイルやルールを適用することは、コードの可読性とメンテナンス性を向上させるために非常に重要です。しかし、通常のlintツールだけではプロジェクト固有のニーズに対応できない場合もあります。
本記事では、Flutterプロジェクトにcustom_lintを導入し、プロジェクト固有の開発ルールを設定・運用する方法について説明します。
custom_lintとは
custom_lintは、Flutterプロジェクトに独自のlintルールを定義・適用するためのパッケージです。
通常のlintツールと同様に、コードの静的解析によって実装の問題点などを検出・通知することが可能です。ただし、custom_lintでは具体的にどのような解析をするのかを自分で実装することで、独自の開発ルールを定義できます。
セットアップ
初めに、custom_lintを提供するための新しいパッケージを定義します。
flutter create --template=package your_package_name
のような形で新規パッケージを作成します。
その後、作成したパッケージのpubspec.yaml
に以下の3つの依存関係を追加します。
dependencies:
analyzer:
analyzer_plugin:
custom_lint_builder:
ルールを定義する
次に、実際にルールを定義します。
ルールを定義するためには、先ほど作成したパッケージのlib
配下にルールを定義したdartファイルを置く必要があります。
通常はルールを後からさらに拡張する場合が多いため、lib
配下を以下のような構造にするケースが多いと思います。
|__lib
|____rules
|______your_rule_1.dart
|______your_rule_2.dart
|____your_package_name.dart
上記の場合、your_rule_n.dart内で個別のルールを定義して、your_package_name.dart内でそれぞれのルールを使えるように定義します。
your_package_name.dartの中身は以下のようになります。
PluginBase createPlugin() => CustomLinter();
class CustomLinter extends PluginBase {
List<LintRule> getLintRules(CustomLintConfigs configs) => [
YourRule1(),
YourRule2(),
// ルールを追加する場合、ここに各ルールのクラスを追記する
...
];
}
その後、個別のルールを実際に定義していきます。
ルールの定義について、より詳しく知りたい人はAST/静的解析などのキーワードで検索してみると良いでしょう。
というよりも、自分もまだ勉強中なのでぜひ詳しい方は教えてください。
今回はとりあえず、「特定のディレクトリ配下の実装に特定の文字列が含まれている場合に注意する」ようなルールを定義します。
このルールはもともと、ファイルの依存関係が複雑にならないように特定のディレクトリ配下で特定のProviderの使用を禁止する目的で定義したものですが……なかなかニッチだと思いませんか?
custom_lintを導入することで、上記のような細かいルールの運用も可能になります。
具体的な実装は以下の通りです。
class RestrictedStringInTargetDirectory extends DartLintRule {
const RestrictedStringInTargetDirectory() : super(code: _code);
static const _code = LintCode(
name: 'restricted_string_in_target_directory',
problemMessage:
'restricted_stringはtarget_directory配下で使用しないでください。',
);
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
// ファイルのパスを取得
// target_directoryという文字列をパスに含むファイルのみを対象にする
final path = resolver.source.uri;
if (!path.toString().contains('target_directory')) return;
context.registry.addSimpleIdentifier((node) {
if (node.name == 'restricted_string') {
reporter.reportErrorForNode(_code, node, []);
}
});
}
}
ルールを運用する
カスタムルールを定義した後は、それを実際にプロジェクトで運用してみましょう。
依存関係の追加
まず、カスタムルールを導入したいプロジェクトのpubspec.yamlファイルにcustom_lintの依存関係を追加します。
dev_dependencies:
your_package_name:
path: (ここに、プロジェクトルートからの相対パスを書く)
これで準備はほぼ完了です。flutter pub get
を実行したら、ルールが適用されるようになります。
VSCodeなどのIDEを使用していれば通常のlintと同じように注意が表示されるほか、
dart run custom_lint
を実行することでも解析が可能です。
注:通常のlintとは異なり、flutter analyzeでは機能しないようなので注意してください。
設定ファイルから、個別にルールの有効化/無効化を切り替える
さて、これで最低限custom_lintを使用する準備は整ったのですが、今回はもう少し実用的な話まで掘り下げたいと思います。
UZUのプロジェクトはモノレポ構成を採用しているため、ルート直下に自作パッケージなども含めた必要な実装が全て集積しています。
しかし、例えば「このパッケージではこのルールを適用したいけど、あのパッケージではこのルールは適用したくない!」というふうに、状況に応じてルールの有効化/無効化を切り替える機能が欲しくなることが考えられます。
実は、custom_lintも通常のlinterと同様に設定ファイルから個別のルールの有効化/無効化を切り替えることが可能です。
方法は非常にシンプルで、有効化/無効化を切り替えたいディレクトリのanalysis_options.yaml
内で以下のように記述します。
custom_lint:
rules:
- restricted_string_in_target_directory: false
普通のlinterのルールとほぼ同じですね。
設定ファイルから、ルールに使われる変数を定義する
また、設定ファイル内でルールに使われる変数を定義することも可能です。
先ほど話した「特定のディレクトリ配下の実装に特定の文字列が含まれている場合に注意する」ルールは特にわかりやすい例ですが……このようなルールを定義する場合、使い場所に応じて「特定のディレクトリ」や「特定の文字列」の内容を変えたいと思いませんか?
実は、custom_lintはルールに使われる変数を定義することが可能です。
変数を具体的に設定するためには、先ほどと同様にanalysis_options.yaml
に次のような感じで記述します。
custom_lint:
rules:
- restricted_string_in_target_directory:
restricted_string: restricted_string
target_directory: target_directory
analysis_options.yaml
で定義した変数は、CustomLintConfigsインスタンスから取得することが可能です。
class _UzuCustomLinter extends PluginBase {
List<LintRule> getLintRules(CustomLintConfigs configs) => [
RestrictedStringInTargetDirectory(configs),
];
}
class RestrictedStringInTargetDirectory extends DartLintRule {
const RestrictedStringInTargetDirectory(
this.configs,
) : super(code: _code);
final CustomLintConfigs configs;
static const _code = LintCode(
name: 'restricted_string_in_target_directory',
problemMessage: 'ここのメッセージは表示されません',
);
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
// 設定ファイルで変数の定義が必須の場合、こんな感じで設定が抜けている時にエラーを返すようにすることできる
final ruleConfig = configs.rules['restricted_string_in_target_directory'];
if (ruleConfig == null) {
throw Exception(
'analysis_options.yamlにrestricted_string_in_target_directoryの設定がありません。',
);
}
final jsonConfig = ruleConfig.json;
final rs = jsonConfig['restricted_string'];
final td = jsonConfig['target_directory'];
if (rs == null || td == null) {
throw Exception(
'analysis_options.yamlのrestricted_string_in_target_directoryの設定に、restricted_stringとtarget_directoryを追加してください。',
);
}
final restrictedString = rs as String;
final targetDirectory = td as String;
// targetDirectoryを探す
final path = resolver.source.uri;
if (!path.toString().contains(targetDirectory)) return;
context.registry.addSimpleIdentifier((node) {
if (node.name == restrictedString) {
final problemMessage =
'$restrictedStringは$targetDirectory配下で使用しないでください。';
reporter.reportErrorForNode(
_code.copyWith(problemMessage: problemMessage),
node,
[],
);
}
});
}
}
extension on LintCode {
LintCode copyWith({required String problemMessage}) {
return LintCode(
name: name,
problemMessage: problemMessage,
);
}
}
まとめ
custom_lintを利用することで、プロジェクトに固有のFlutter開発ルールを定義することが可能です。
また、通常のlinterと同様に個別のルールの有効化/無効化を切り替えたり、変数を定義したりすることができるため、柔軟で拡張性の高いlintルールの運用ができることもcustom_lintの魅力です。
現在はまだ導入したばかりですが、今後さらに独自のルールを追加することでチームの開発体験を向上させていきたいと考えています。
読者の皆様もcustom_lintの導入を検討するきっかけになれば幸いです。
Discussion