Closed44

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

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

公式ドキュメントのTools & techniquesのチャプターにStatic analysisのセクションがある。セクションには下記の項目が含まれている。

  • Customizing static analysis:静的解析をカスタマイズする
  • Fixing common type problems:よくある型の問題を修正する
  • Diagnostic messages:診断的メッセージ
  • Linter rules:リンターのルール

上記を一読することに加えてVSCodeに組み込む方法について調べることを目標とする。

https://dart.dev/guides/language/analysis-options

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida
void increment() {
  if (1 < 10) ;
}

VSCodeで上記のようなコードを書いて;にカーソルを合わせると下記のようなメッセージがちゃんと表示される。

Avoid empty statements

ちなみにCommand + .を押すとQuick Fixが表示される。

Command + Shift + Mを押すと問題を一覧表示できる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida
dart analyze

上記のコマンドを実行しても下記のように問題を報告してくれる。

Analyzing hello_lint...                1.2s

   info • lib/hello_lint.dart:2:13 • Avoid empty statements. •
          empty_statements

1 issue found.
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida
var controller = StreamController<String>();

上記のコードを書いても下記のメッセージが表示されない。

info - Close instances of `dart.core.Sink`. - close_sinks

このままドキュメントを読み進めれば原因はいずれわかるだろう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

設定ファイルの主なルートレベルのエントリは下記の3点。

  • include:ベースとなる設定ファイルののURL、複数は指定できない
  • analyzer:静的解析の設定(タイプチェックの厳しさ、除外するファイル、指定ルールの無視など)
  • linter:リンターのルール
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida
analyzer:
  language:
    strict-casts: true
    strict-inference: true
    strict-raw-types: true

厳しい型チェックを有効にする。一つ一つ見ていく。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

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

これは必須でも良いかも。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

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>{};
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

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."

下記にとても詳しく書いてある。

https://github.com/dart-lang/language/blob/master/resources/type-system/strict-raw-types.md

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

続いてincludelinterのエントリーについて学ぶ。

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つは削除する。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

続いて個々のルールを設定する方法、下記のように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
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ファイル単位の場合はanalysis_options.yamlに下記のように設定する。

analyzer:
  exclude:
    - lib/client.dart
    - lib/server/*.g.dart
    - test/_data/**
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

行+ルール単位の場合は対象とする行の直前の行か同じ行の直後にコメントを追加する。

// ignore: invalid_assignment
int x = '';
int x = ''; // ignore: invalid_assignment
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

リンタールールにはinfo, warning, errorの3つの重大度(severity)がある。

それぞれデフォルトの重大度があるが変更可能。

特定のリンタールールを無視するには下記のように設定する。

analyzer:
  errors:
    todo: ignore

デフォルトの重大度を変更するには下記のように設定する。

analyzer:
  errors:
    invalid_assignment: warning
    missing_return: error
    dead_code: info
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

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

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

まずは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
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

次によくあるエラーの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

エラーメッセージが表示される理由はIterableaddメソッドを持たないことに起因する。

修正するには型引数を明示する。

void main() {
  var c = C<List>([]).collection;
  c.add(2);
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

よくあるエラーの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;
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

よくあるエラーの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
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

よくあるエラーの4つ目「Unexpected collection element type」

原因についてはリストやマップなどを使う時に推論された型が意図していた型と異なること。

エラーメッセージを表示させるコード例

void main() {
  var map = {'a': 1, 'b': 2, 'c': 3};
  map['d'] = 1.5;
}

mapMap<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;
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

よくあるエラーの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()を最後に呼び出す必要があるのか不思議だ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

よくあるエラーの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");
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

よくあるエラーの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を指定したのでTObject?であると推論されたが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);
}

初見だとエラーメッセージから原因を読み取れないので今のうちに知れて良かった。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ここまで、よくある静的エラー。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)
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

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;
}

できた!親クラスに置くのがベストと書いてある。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

"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;
  }
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

続いて"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);
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

続いて"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なる機能があることを知らなかった。

https://dart.dev/guides/language/language-tour#adding-features-to-a-class-mixins

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

続いて"Override inference"

直訳するとオーバーライド推論

親クラスのメソッドをオーバーライドする時に引数の型が推論されること

class A {
  int m(String s) => 0;
}

class B extends A {
  
  m(s) => 1;
}

上記の例ではBクラスのmメソッドのsStringと推論できるので明示する必要がない。

それでは下記のような場合はどうなるだろう。

class A {
  int m(num n) => 0;
}

class B {
  num m(int i) => 0;
}

class C implements A, B {
  
  m(n) => 1;
}

この場合Cm(n)int m(num n) => 1と推論される。

理由はint Function(num)num Function(int)の親となる型と推論されるため。

引数は前者が後者の親となる型、戻り値は前者が後者の子となる型。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

続いて5点目"Part file"

定義はpart ofキーワードが含まれるソースコード

part ofはDartのライブラリに関連する機能らしいがよくわからない

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

続いて6点目"Potentially non-nullable"

直訳は潜在的に非Null

ある型が明らかに非Nullまたは型引数なら、その型はPotentially non-nullableと呼ばれる。

非Nullとは例えばString?のようにクエスチョンマークがついていないこと。

ただクエスチョンマークが付いてなくてもNull型やdynamicはNullになり得るので一概に非Nullとは言えない。

型引数をTとして例えばintのような非Nullの型ならTも非Nullとなる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

最後に7点目"Public library"

定義はパッケージのlibディレクトリに含まれ、lib/srcディレクトリに含まれないライブラリのこと。

ライブラリって何なのだろう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

長かったけど遂に最後の"Linter rules"ページ

このページもとても長いので前ページの"Diagnostic messages"と同様に最初の方だけを読んでいこう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

まず1つ目のセクション"Predefined rule sets"

lintsとflutter_lintsパッケージに事前に定義されたルールセットがある

これらをベースラインとして、あとは個々に設定を必要に応じて加えるのが良さそう

lintsにはcoreとrecommendedの2つルールセットがあり、flutter_lintsにはflutterの1つのルールセットがある。

coreはrecommendedに含まれ、recommendedはflutterに含まれるという関係性がある。

ルールセットを利用するにはanalysis_options.yamlファイルでincludeを使う。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

続いて2つ目のセクション"Rule types"

個々のルールは下記3つのいずれかのルールに分類される。

  • Errors: エラーやミスの可能性があるもの
  • Styles: コードの書き方に関するもの、Dartのコーディング規約に反するもの
  • Pub: パッケージのセットアップに関するもの

Pubが良くわからないので調べた。

例えばdepend_on_referenced_packagesというルール

importしているパッケージがpubspec.yamlのdependenciesに含まれないことを検出する。

コード例は下記の通り

import 'package:a/a.dart';
dependencies:
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

続いて3つ目のセクション"Maturity levels"

直訳すると成熟度の水準

個々のルールは下記3つのいずれかのmaturity levelに分類される。

  • Stable: 安全に使用することができる、最新版のDartで動作検証がされている
  • Experimental: 発達段階なので注意深く使う必要がある
  • Deperecated: 非推奨、将来的に削除される可能性がある
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

最後に4つ目のセクション"Quick fixes"

一部のルールではエディタのクイックフィックス機能を使って自動的に修正することができる。

エディタの代わりにdart fixコマンドを実行しても良い。

VSCodeならCommand + .でクイックフィックス機能を呼び出せる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

そういえば最初の方でclose_sinksがよくわからないみたいな投稿をした。

https://zenn.dev/link/comments/1f2b178c389e84

公式ドキュメントの"Linter rules"ページ、"close_sinks"セクションに記載がある。

https://dart.dev/tools/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
このスクラップは2023/01/10にクローズされました