Custom Lints パッケージを使って自分で初めて Dart の静的解析のルールを自作したので、その共有。
sangria_lints | Flutter package
https://pub.dev
ある日開発している Flutter アプリと連携している Firebase Crashlytics でエラー監視していた。すると、Null check operator used on a null value
というメッセージでエラーが複数のウィジェットからあがっていた。すべてのエラーのスタックトレースを読むと共通して StatefulWidget
の setState
でエラーが吐かれていた。
調査してみると、どうやら StatefulWidget
の setState
を非同期処理で実行しているのが原因だった。ウィジェットがすでに破棄されているにもかかわらず、state を更新できずエラーになっていることがわかった。エラーが発生しないようにするには、setState
を実行する前に、mounted
の条件分岐でウィジェットの有無を確認することになる。
[Flutter]setStateメソッドの中で非同期処理をやってはいけない – みんプロ式 – 40代からの初心者向けスマホアプリ開発講座(Flutter)
https://minpro.net
ちなみに、デバッグモードで再現を試みると、setState() called after dispose()
とエラーが出ており、本文は下のものになる。Crashlytics に記載されているエラーメッセージは違うが、リリースモードだとメッセージが変更されると記載している issue を見つけた。
setState() called after dispose(): This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose dispose() method has been called).
mounted
の条件分岐を該当箇所に入れて、無事エラーを修正した。しかし、アプリ内で複数の非同期処理で setState
が呼ばれており、該当箇所全てを探すのが難しかった。また今度似た記述が追加される可能性を考えると、技術的に再発防止できないかと検討した。
静的解析のルールがないか検索したが、調べた限りだと、有料のものしかなかった。
use-setstate-synchronously | DCM - Code Quality Tool for Flutter Developers
https://dcm.dev
上のものをお見送りにするよりも、それなら自分も含めて誰もが無料使用できるルールを作りたく、実装にチャレンジした。個人的に今年の年始で AST の勉強をしたのもあり、自分の個人アプリでも使用する可能性があるので、技術的にレベルアップを図ろうと考えた。
作ったルールは上の既存のルールと同じように、set_setstate_synchronously
という名前にした。StatefulWidget
クラスを継承しているウィジェットの中で、非同期処理で mounted
でウィジェットの有無を確認しないでの setState
の使用に対して警告をあげるルールになる。
警告対象のコードに対しては下のような出力をしてくれる。
lib/use_setstate_synchronously_rule.dart:30:5 • Avoid calling setState across asynchronous gaps without seeing if the widget is mounted. • use_setstate_synchronously • WARNING
custom_lint | Dart package
https://pub.dev
自分で静的解析のルールを作成するには、custom_lint
パッケージを使用し、その README.md に倣って開発を進める。
README.md にある Usage のセクションを読んで、必要なパッケージたちをインストールする。
$ fvm dart add analyzer analyzer_plugin custom_lint_builder
この記事の内容がわかりやすくするために、先に自作のパッケージをディレクトリ構成を公開する。 lib ディレクトリにはルールそのものの実装を入れ、example ディレクトリの中でその静的解析ルールが期待通りに動作しているかを確認する。
➜ sangria git:(develop) tree
.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── analysis_options.yaml
├── example
│ ├── analysis_options.yaml
│ ├── lib
│ │ └── use_setstate_synchronously_rule.dart
│ ├── main.dart
│ ├── pubspec.lock
│ └── pubspec.yaml
├── lib
│ ├── sangria_lints.dart
│ └── src
│ └── rules
│ └── use_setstate_synchronously
│ ├── use_setstate_synchronously_rule.dart
│ └── use_setstate_synchronously_visitor.dart
├── pubspec.lock
└── pubspec.yaml
8 directories, 20 files
パッケージ内にエントリーポイントを用意し、getLintRules
に DartLintRule
を継承した独自の Lint ルールを定義したクラスを追加する。
custom_lint
は、デフォルトの挙動で利用側がパッケージから提供されるルールを analysis_options.yml
に記載することで制御できる。その挙動を明記するためにあえてリストをフィルタリングするコードを書いている。
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:sangria_lints/src/rules/use_setstate_synchronously/use_setstate_synchronously_rule.dart';
// Lint ルールのエントリポイント。
PluginBase createPlugin() => _SangriaLints();
class _SangriaLints extends PluginBase {
static final _lints = [UseSetStateSynchronouslyLintRule()];
@override
List<LintRule> getLintRules(CustomLintConfigs configs) {
return _lints.where((lint) => lint.isEnabled(configs)).toList();
}
}
ここからは、StatefulWidget の state を mounted で確認せずに、非同期で更新してるメソッドと関数 に対して警告をあげるルールを書いていく。そのノードを探すため、run
メソッドの中でその条件を満たしたコードを検知する実装を書く。
静的解析結果を監視する LintRuleNodeRegistry
を取得し、そこからさらに対象のノードを監視するメソッドにコールバックを登録してルールを作成していく。メソッドと関数を監視するのは addMethodDeclaration
と addFunctionExpression
になるので、それぞれに同じコールバックを登録する。各 node には、それぞれの AST が渡され、子ノードの中身を見て、警告をあげるルールにする。
class UseSetStateSynchronouslyLintRule extends DartLintRule {
UseSetStateSynchronouslyLintRule()
: super(
code: LintCode(
name: 'use_setstate_synchronously',
problemMessage:
'Avoid calling setState across asynchronous gaps without seeing if the widget is mounted.',
errorSeverity: ErrorSeverity.WARNING,
),
);
@override
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
// クラスメソッドやインスタンスメソッドの中身を解析・監視するコード
context.registry.addMethodDeclaration((node) {
// ...
});
// ウィジェットに渡すコールバックの中身を解析・監視するコード
context.registry.addFunctionExpression((node) {
//...
});
}
}
この addMethodDeclaration
や addFunctionExpression
については、custom_lint_visitor から対象のノードを監視するメソッドを選択できる。例えば、switch 文を解析したい場合は visitSwitchExpression
を、try 文を解析したい場合は visitTryStatement
にコールバックを登録する。
いきなり静的解析のルールを実装しようと上の LintRuleNodeRegistry
からメソッドを探すのは敷居が高いので。すでに公開されているルールの中身を読むのをおすすめする。pub.dev の中を検索すると、自分たちでルールを開発している企業たちが公開しているので、非常に参考になった。
Leobit / Internal Projects / Flutter Public / Leobit Lints · GitLab
https://gitlab.com
altive_lints | Dart package
https://pub.dev
例えば、今回の set_setstate_synchronously
についてだが、if 文の括弧内の処理を再起的にみていかないといけないため、visitor を用意する必要があった。なぜ再起的に見ていく必要があるかというと、if 文の中でさらにネストされた記述を監視するためになる。
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
class UseSetstateSynchronouslyVisitor extends RecursiveAstVisitor<void> {
UseSetstateSynchronouslyVisitor({
required this.reporter,
required this.lintCode,
});
final ErrorReporter reporter;
final LintCode lintCode;
@override
void visitIfStatement(node) {
final expressionStr = node.expression.toString();
if (expressionStr.contains('!mounted')) {
// ... !mounted で早期 return している場合警告をあげないなどのロジック
} else if (expressionStr.contains('mounted')) {
// ... mounted の条件でラップしている場合警告をあげないなどのロジック
} else {
super.visitIfStatement(node);
}
}
@override
void visitMethodInvocation(MethodInvocation node) {
// メソッドの名前が setState かつ、早期 return されず、mounted の条件分岐でラップされていない場合、警告をあげる。
// reporter.atNode(node, lintCode);
super.visitMethodInvocation(node);
}
}
一般公開する前に、作成した静的解析が実際のコードで無事に警告やエラーをあげてくれるかを確認する必要がある。custom_lint
には watch モードがあり、Hot-Reload でデバッグが可能となる。
$ fvm dart run custom_lint --watch
➜ example git:(develop) ✗ fvm dart run custom_lint --watch
The Dart VM service is listening on http://127.0.0.1:60785/-GnvsQVXzng=/
The Dart DevTools debugger and profiler is available at: http://127.0.0.1:60785/-GnvsQVXzng=/devtools/?uri=ws://127.0.0.1:60785/-GnvsQVXzng=/ws
Analyzing... 0.0s
No issues found!
Custom lint runner commands:
r: Force re-lint
q: Quit
example
ディレクトリに動作確認のファイルたちを入れていき、example
から lib
ディレクトリに書いたルールを参照するように書く。
dev_dependencies:
sangria_lints:
path: ../
custom_lint: 0.7.5
watch モードを ON にするため、analysis_option.yml
で debug: true
を書くのを忘れずに。
include: package:flutter_lints/flutter.yaml
analyzer:
plugins:
- custom_lint
custom_lint:
debug: true
そして、警告があがることを期待するコードの上にコメントで expect_lint: <rule_name>
を書き、動作確認する。上の custom_lint
コマンドを実行して、何も警告があがらなければ無事期待通り動作していることになる。VSCode のエディタで警告の波線が表示されるには、一度 built して初めて反映されるので、なかなか警告が表示されないのは注意。
import 'package:flutter/material.dart';
class UseSetstateSynchronouslyRuleWidget extends StatefulWidget {
const UseSetstateSynchronouslyRuleWidget({super.key});
@override
_UseSetstateSynchronouslyRuleWidgetState createState() =>
_UseSetstateSynchronouslyRuleWidgetState();
}
class _UseSetstateSynchronouslyRuleWidgetState
extends State<UseSetstateSynchronouslyRuleWidget> {
String displayText = '';
@override
void initState() {
super.initState();
}
Future<void> setStateAsynchronously() async {
// expect_lint: use_setstate_synchronously <- これ
setState(() {
displayText = 'random string';
});
if (!mounted) {
// expect_lint: use_setstate_synchronously
setState(() {
displayText = 'random string'
});
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
GestureDetector(
onTap: () async => await setStateAsynchronously(),
child: Text('addMethodDeclaration'),
),
GestureDetector(
onTap: () async {
// expect_lint: use_setstate_synchronously
setState(() {
displayText = 'random string'
});
if (!mounted) {
// expect_lint: use_setstate_synchronously
setState(() {
displayText = 'random string'
});
}
},
child: Text('addFunctionExpression'),
),
],
);
}
}
ちなみにルールのほうに print
でデバッグ文を書くと、下のように出力される。
[use_setstate_synchronously] run
[use_setstate_synchronously] run
1 つしかルールを含んでいないパッケージになってしまったが、これからまた開発して既存の analyzer になかったものがあれば、自分で開発して作っていく予定だ。ぜひインストールまでしてもらえると大変幸いに感じる。