Dartの静的解析について調べる

公式ドキュメントのTools & techniquesのチャプターにStatic analysisのセクションがある。セクションには下記の項目が含まれている。
- Customizing static analysis:静的解析をカスタマイズする
- Fixing common type problems:よくある型の問題を修正する
- Diagnostic messages:診断的メッセージ
- Linter rules:リンターのルール
上記を一読することに加えてVSCodeに組み込む方法について調べることを目標とする。

flutter create
するのもアレなのでdart create
とか実行すればDartのプロジェクトを作れるのかと思ったらドンピシャだった。
ターミナルで下記のコマンドを実行してはじめてのDartプロジェクトを作成する。
dart create hello_lint

void increment() {
if (1 < 10) ;
}
VSCodeで上記のようなコードを書いて;
にカーソルを合わせると下記のようなメッセージがちゃんと表示される。
Avoid empty statements
ちなみにCommand + .を押すとQuick Fixが表示される。
Command + Shift + Mを押すと問題を一覧表示できる。

dart analyze
上記のコマンドを実行しても下記のように問題を報告してくれる。
Analyzing hello_lint... 1.2s
info • lib/hello_lint.dart:2:13 • Avoid empty statements. •
empty_statements
1 issue found.

var controller = StreamController<String>();
上記のコードを書いても下記のメッセージが表示されない。
info - Close instances of `dart.core.Sink`. - close_sinks
このままドキュメントを読み進めれば原因はいずれわかるだろう。

静的解析の設定ファイルの名前は analysis_options.yaml
で位置はパッケージのルート。

設定ファイルの主なルートレベルのエントリは下記の3点。
- include:ベースとなる設定ファイルののURL、複数は指定できない
- analyzer:静的解析の設定(タイプチェックの厳しさ、除外するファイル、指定ルールの無視など)
- linter:リンターのルール

analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
厳しい型チェックを有効にする。一つ一つ見ていく。

strict-casts
はdynamicからの暗黙的な型変換を禁止する。
dynamic a = "1";
String b = a;
有効にすると下記のメッセージが表示される。
error • lib/hello_lint.dart:12:14 • A value of type 'dynamic' can't be
assigned to a variable of type 'String'. Try changing the type of
the variable, or casting the right-hand type to 'String'. •
invalid_assignment
これは必須でも良いかも。

strict-inference
は型推論でdynamicが選ばれなくなる。
final lines = {};
有効にすると下記のメッセージが表示される。
info • lib/hello_lint.dart:11:17 • The type argument(s) of 'Map' can't be
inferred. Use explicit type argument(s) for 'Map'. •
inference_failure_on_collection_literal
下記のように型を明示することでエラーが消える。
final lines = <String, String>{};

strict-raw-types
は型推論で型引数にdynamicが選ばれなくなる。
List numbers = [1, 2, 3];
有効にすると下記のエラーメッセージが表示される。
info • lib/hello_lint.dart:11:3 • The generic type 'List<dynamic>' should
have explicit type arguments but doesn't. Use explicit type
arguments for 'List<dynamic>'. • strict_raw_type
strict-inference
と似ているがraw typeを使わせないようにすることが主な目的。
raw typeとは型引数が省略された型と定義されるようだ。
Under this feature, a type with omitted type argument(s) is defined as a "raw type."
下記にとても詳しく書いてある。

続いてinclude
やlinter
のエントリーについて学ぶ。
Dartチームは2つのリンターのルールーセットを提供している。
- Core rules:致命的な問題を特定するのを助ける
- Recommended rules:core rulesの拡張版、ルールが追加されている
Flutterの場合はflutter_lints
を使おうと書いてる、flutter_lintsはrecommended rulesのさらに拡張版のようだ。
それぞれの使い方は下記の通り。
include: package:lints/core.yaml
include: package:lints/recommended.yaml
include: package:flutter_lints/flutter.yaml
上記のうちのいずれか1つを残して残りの2つは削除する。

続いて個々のルールを設定する方法、下記のようにlinter.rulesにリストとしてエントリーを追加する。
linter:
rules:
- always_declare_return_types
- cancel_subscriptions
- close_sinks
- comment_references
- one_member_abstracts
- only_throw_errors
- package_api_docs
- prefer_final_in_for_each
- prefer_single_quotes
無効にしたい場合はリストではなくマップを使う。
linter:
rules:
avoid_shadowing_type_parameters: false
await_only_futures: true

静的解析の対象から外すには下記の3つの方法を選べる。
- ファイル単位
- ファイル+ルール単位
- 行+ルール単位

ファイル単位の場合はanalysis_options.yamlに下記のように設定する。
analyzer:
exclude:
- lib/client.dart
- lib/server/*.g.dart
- test/_data/**

ファイル+ルール単位の場合はファイルに下記のコメントを追加する。
// ignore_for_file: unused_local_variable

行+ルール単位の場合は対象とする行の直前の行か同じ行の直後にコメントを追加する。
// ignore: invalid_assignment
int x = '';
int x = ''; // ignore: invalid_assignment

リンタールールにはinfo
, warning
, error
の3つの重大度(severity)がある。
それぞれデフォルトの重大度があるが変更可能。
特定のリンタールールを無視するには下記のように設定する。
analyzer:
errors:
todo: ignore
デフォルトの重大度を変更するには下記のように設定する。
analyzer:
errors:
invalid_assignment: warning
missing_return: error
dead_code: info

ここまで「Customizing static analysis:静的解析をカスタマイズする」
ここから「Fixing common type problems:よくある型の問題を修正する」

まずはVSCodeなどのエディタでちゃんと静的解析のメッセージが表示されるかどうかを確認する方法。
ソースコードの適当な位置に下記のコードを追加する。
bool b = [0][0];
下記のようなエラーメッセージが表示されたら静的解析がしっかり動いている。
error • lib/hello_lint.dart:13:12 • A value of type 'int' can't be assigned to a variable of type 'bool'. Try
changing the type of the variable, or casting the right-hand type to 'bool'. • invalid_assignment

次によくあるエラーの1つ目「Undefined member」
原因としては下記の2つが考えられる。
- 関数の戻り値などが親クラスだがソースコードでは子クラスを仮定している場合
- ジェネリックなクラスのインスタンスを作成する時に型引数が省略されている場合
前者のエラーメッセージを表示させるコード例
class A {
void method1() {}
}
class B extends A {
void method2() {}
}
A makeA() {
return B();
}
void main() {
final b = makeA();
b.method2();
}
表示されるエラーメッセージ
error • lib/hello_lint.dart:19:5 • The method 'method2' isn't defined for the type 'A'. Try correcting the name to
the name of an existing method, or defining a method named 'method2'. • undefined_method
修正するにはas
キーワードを使用する。
void main() {
final b = makeA() as B;
b.method2();
}
後者のエラーメッセージを表示させるコード例
class C<T extends Iterable> {
final T collection;
C(this.collection);
}
void main() {
var c = C(Iterable.empty()).collection;
c.add(2);
}
表示されるエラーメッセージ
error • lib/hello_lint.dart:8:5 • The method 'add' isn't defined for the type 'Iterable'. Try correcting the name
to the name of an existing method, or defining a method named 'add'. • undefined_method
エラーメッセージが表示される理由はIterable
がadd
メソッドを持たないことに起因する。
修正するには型引数を明示する。
void main() {
var c = C<List>([]).collection;
c.add(2);
}

よくあるエラーの2つ目「Invalid method override」
原因としては子クラスが親クラスのメソッドをオーバーライドする時に引数の型を限定してしまっていること。
エラーメッセージを表示させるコード例
abstract class NumberAdder {
num add(num a, num b);
}
class MyAdder extends NumberAdder {
num add(int a, int b) => a + b;
}
表示されるエラーメッセージ
error • lib/hello_lint.dart:7:7 • 'MyAdder.add' ('num Function(int, int)') isn't a valid override of 'NumberAdder.add'
('num Function(num, num)'). • invalid_override
- The member being overridden at lib/hello_lint.dart:2:7.
修正するにはオーバーライドするメソッドの引数の型を変更しないようにする。
class MyAdder extends NumberAdder {
num add(num a, num b) => a + b;
}

よくあるエラーの3つ目「Missing type arguments」
原因としては子クラスが親クラスを継承する時に型引数を忘れてしまっていること。
エラーメッセージを表示させるコード例
class Superclass<T> {
void method(T param) { ... }
}
class Subclass extends Superclass {
void method(int param) { ... }
}
型引数が省略されるとTがdynamic
と推論される。
表示されるエラーメッセージ
error • lib/hello_lint.dart:7:8 • 'Subclass.method' ('void Function(int)') isn't a valid override of
'Superclass.method' ('void Function(dynamic)'). • invalid_override
- The member being overridden at lib/hello_lint.dart:2:8.
修正するには親クラスの型引数を指定する。
class Superclass<T> {
void method(T param) { ... }
}
class Subclass extends Superclass<int> {
void method(int param) { ... }
}
strict-raw-types
の静的解析を有効にしてSuperclass
を型引数なしで使えないようにした方が良い。
analyzer:
language:
strict-raw-types: true

よくあるエラーの4つ目「Unexpected collection element type」
原因についてはリストやマップなどを使う時に推論された型が意図していた型と異なること。
エラーメッセージを表示させるコード例
void main() {
var map = {'a': 1, 'b': 2, 'c': 3};
map['d'] = 1.5;
}
map
はMap<String, int>と推論されるが1.5は
double`なのでエラーとなる。
表示されるエラーメッセージ
error • lib/hello_lint.dart:3:14 • A value of type 'double' can't be assigned to a variable of type 'int'.
Try changing the type of the variable, or casting the right-hand type to 'int'. •
invalid_assignment
修正するにはMap
の型引数を明示的に指定する。
void main() {
var map = <String, num>{'a': 1, 'b': 2, 'c': 3};
map['d'] = 1.5;
}

よくあるエラーの5つ目「Constructor initialization list super() call」
原因についてはコンストラクタの初期化リストの最後でsuper()
を呼び出していないこと
エラーメッセージを表示させるコード例
class A {
String name;
A(this.name);
}
class B extends A {
int _age;
B(String firstname, String familyname, int age)
: super("$firstname $familyname"),
_age = age;
}
表示されるエラーメッセージ
error • lib/hello_lint.dart:10:9 • The superconstructor call must be last in an initializer list: 'A'. •
super_invocation_not_last
修正するにはsuper()
を最後に呼び出す。
class A {
String name;
A(this.name);
}
class B extends A {
final int _age;
B(String firstname, String familyname, int age)
: _age = age,
super("$firstname $familyname");
}
なんでsuper()
を最後に呼び出す必要があるのか不思議だ。

よくあるエラーの6つ目「The argument type … can’t be assigned to the parameter type …」
タイトルが長い!
原因についてはdynamic
を想定しているのにdynamic
以外の型が指定されていること。
エラーメッセージを表示させるコード例
void filterValues(bool Function(dynamic) filter) {}
void main() {
filterValues((String x) => x.contains('Hello'));
}
表示されるエラーメッセージ
error • lib/hello_lint.dart:4:16 • The argument type 'bool Function(String)' can't be assigned to the parameter
type 'bool Function(dynamic)'. • argument_type_not_assignable
修正するには型引数かキャストを使用する。
void filterValues<T>(bool Function(T) filter) {}
void main() {
filterValues((String x) => x.contains('Hello'));
}
void filterValues<T>(bool Function(dynamic) filter) {}
void main() {
filterValues((x) => (x as String).contains('Hello'));
}
ちなみに下記のような場合は特にエラーにはならない。
void filterValues<T>(dynamic something) {}
void main() {
filterValues("string");
}

よくあるエラーの7つ目「Incorrect type inference」
原因については省略された型引数をうまく推論できないこと。
エラーメッセージを表示させるコード例
void main() {
List<int> ints = [1, 2, 3];
var maximumOrNull = ints.fold(null, (a, b) => a == null || a < b ? b : a);
}
表示されるエラーメッセージ
error • lib/hello_lint.dart:3:49 • The return type 'Object' isn't a 'Null', as required by the closure's
context. • return_of_invalid_type_from_closure
詳しくはよくわからないがints.fold
の第1引数にnull
を指定したのでT
がObject?
であると推論されたがa == null || a < b ? b : a
の型がObject
なので一致しないみたいな意味なのだろうか?
修正するには型引数を明示的に指定する。
void main() {
List<int> ints = [1, 2, 3];
var maximumOrNull = ints.fold<int?>(null, (a, b) => a == null || a < b ? b : a);
}
初見だとエラーメッセージから原因を読み取れないので今のうちに知れて良かった。

ここまで、よくある静的エラー。7件と多かった...
ここから、よくあるランタイムエラー。といっても1件だけ。
タイトルは「Invalid casts」
例えばint
からString
などキャストできない型へ変換すると実行時の例外として発生する。
通常は静的解析でわかるがdynamic
を使用しているとコンパイルが通ってしまう。
例外を発生させるコード例
void assumeStrings(dynamic objects) {
List<String> strings = objects; // Runtime downcast check
String string = strings[0]; // Expect a String value
}
void main() {
assumeStrings([1, 2, 3]);
}
表示されるエラーメッセージ
Unhandled exception:
type 'List<int>' is not a subtype of type 'List<String>'
#0 assumeStrings
bin/hello_lint.dart:2
#1 main
bin/hello_lint.dart:7
#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)
Exited (255)
修正するにはcast()
を使用すると書いてあるんだけど修正できない... なんで...
void assumeStrings(dynamic objects) {
List<String> strings = objects; // Runtime downcast check
String string = strings[0]; // Expect a String value
}
void main() {
assumeStrings([1, 2, 3].cast<String>());
}
エラーメッセージはちょっと変わった。
Unhandled exception:
type 'int' is not a subtype of type 'String' in type cast
#0 _CastListBase.[] (dart:_internal/cast.dart:99:46)
#1 assumeStrings
bin/hello_lint.dart:3
#2 main
bin/hello_lint.dart:7
#3 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#4 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)
Exited (255)

Fixing common type problemsページの最後のセクション「Appendix: The covariant keyword」
covariant
キーワードを使うとよくある静的エラーの2つ目「Invalid method override」の静的解析を無効にできる。
エラーメッセージを表示させるコード例
abstract class NumberAdder {
num add(num a, num b);
}
class MyAdder extends NumberAdder {
num add(covariant int a, covariant int b) => a + b;
}
静的解析のエラーメッセージが消えた!
covariant
キーワードは親クラスにも置けるらしい、やってみよう。
abstract class NumberAdder {
num add(covariant num a, covariant num b);
}
class MyAdder extends NumberAdder {
num add(int a, int b) => a + b;
}
できた!親クラスに置くのがベストと書いてある。

ここまで"Fixing common type problems"ページ、ここから"Diagnostic messages"ページ。

"Diagnostic messages"ページはとても長いのでGlossaryだけを対象にしよう。
まずは"Constant context"
定義は自動的に定数として扱われるのでconst
キーワードを含める必要がない範囲。
下記5点がConstant contextとなる。
- constなリストやマップの中
- constなコンストラクターの引数
- constな変数の右辺
- アノテーション
- switch文のcase
例は下記の通り
var l = const [/*constant context*/];
var p = const Point(/*constant context*/);
const v = /*constant context*/;
void f(int e) {
switch (e) {
case /*constant context*/:
break;
}
}

続いて"Definite assignment"
直訳すると「確実に代入していること」
Definite assignment分析では下記の任意のローカル変数が下記の3つにいずれかに分類される。
- Definitely assigned: 確実に代入されている
- Definitely unasigned: 確実に代入されていない
- Might or might not be assigned: 代入されているかも知れないし、されていないかも知れない
例は下記の通り
// Definitely assigned
void f(String name) {
String s = 'Hello $name!';
print(s);
}
// Definitely unassigned
void g() {
String s;
print(s);
}
// Might or might not be assigned
void g(String name, bool casual) {
String s;
if (casual) {
s = 'Hi $name!';
}
print(s);
}

続いて"Mixin application"
定義はmixinが適用されたクラス
class A {}
mixin M {}
class B extends A with M {}
この場合、BはMのmixin applicationとなる。
ちなみに下記のようにすることでMixin+親クラスをまとめることができる。
class A {}
mixin M {}
class A_M = A with M;
class B extends A_M {}
Dartにmixinなる機能があることを知らなかった。

続いて"Override inference"
直訳するとオーバーライド推論
親クラスのメソッドをオーバーライドする時に引数の型が推論されること
class A {
int m(String s) => 0;
}
class B extends A {
m(s) => 1;
}
上記の例ではBクラスのm
メソッドのs
はString
と推論できるので明示する必要がない。
それでは下記のような場合はどうなるだろう。
class A {
int m(num n) => 0;
}
class B {
num m(int i) => 0;
}
class C implements A, B {
m(n) => 1;
}
この場合C
のm(n)
はint m(num n) => 1
と推論される。
理由はint Function(num)
がnum Function(int)
の親となる型と推論されるため。
引数は前者が後者の親となる型、戻り値は前者が後者の子となる型。

続いて5点目"Part file"
定義はpart of
キーワードが含まれるソースコード
part of
はDartのライブラリに関連する機能らしいがよくわからない

続いて6点目"Potentially non-nullable"
直訳は潜在的に非Null
ある型が明らかに非Nullまたは型引数なら、その型はPotentially non-nullableと呼ばれる。
非Nullとは例えばString?
のようにクエスチョンマークがついていないこと。
ただクエスチョンマークが付いてなくてもNull
型やdynamic
はNullになり得るので一概に非Nullとは言えない。
型引数をT
として例えばint
のような非Nullの型ならT
も非Nullとなる。

最後に7点目"Public library"
定義はパッケージのlibディレクトリに含まれ、lib/srcディレクトリに含まれないライブラリのこと。
ライブラリって何なのだろう。

長かったけど遂に最後の"Linter rules"ページ
このページもとても長いので前ページの"Diagnostic messages"と同様に最初の方だけを読んでいこう。

まず1つ目のセクション"Predefined rule sets"
lintsとflutter_lintsパッケージに事前に定義されたルールセットがある
これらをベースラインとして、あとは個々に設定を必要に応じて加えるのが良さそう
lintsにはcoreとrecommendedの2つルールセットがあり、flutter_lintsにはflutterの1つのルールセットがある。
coreはrecommendedに含まれ、recommendedはflutterに含まれるという関係性がある。
ルールセットを利用するにはanalysis_options.yamlファイルでinclude
を使う。

続いて2つ目のセクション"Rule types"
個々のルールは下記3つのいずれかのルールに分類される。
- Errors: エラーやミスの可能性があるもの
- Styles: コードの書き方に関するもの、Dartのコーディング規約に反するもの
- Pub: パッケージのセットアップに関するもの
Pubが良くわからないので調べた。
例えばdepend_on_referenced_packages
というルール
import
しているパッケージがpubspec.yamlのdependencies
に含まれないことを検出する。
コード例は下記の通り
import 'package:a/a.dart';
dependencies:

続いて3つ目のセクション"Maturity levels"
直訳すると成熟度の水準
個々のルールは下記3つのいずれかのmaturity levelに分類される。
- Stable: 安全に使用することができる、最新版のDartで動作検証がされている
- Experimental: 発達段階なので注意深く使う必要がある
- Deperecated: 非推奨、将来的に削除される可能性がある

最後に4つ目のセクション"Quick fixes"
一部のルールではエディタのクイックフィックス機能を使って自動的に修正することができる。
エディタの代わりにdart fix
コマンドを実行しても良い。
VSCodeならCommand + .でクイックフィックス機能を呼び出せる。

そういえば最初の方でclose_sinksがよくわからないみたいな投稿をした。
公式ドキュメントの"Linter rules"ページ、"close_sinks"セクションに記載がある。
例えば次のようなソースコードを考える。
import 'dart:io';
void someFunction(filename) {
IOSink sink = File(filename).openWrite();
sink.flush();
}
analysis_options.yamlが下記の内容と仮定する。
linter:
rules:
- close_sinks
dart analyze
を実行すると下記のメッセージが表示される。
info • lib/hello_lint.dart:4:10 • Close instances of `dart.core.Sink`. • close_sinks

以上をもって一旦終わり!また必要があれば追記しよう。