Dartの静的解析を理解する:Analyzer Diagnosticsで解読するASTとElement Modelの仕組み【Dart Analyzer入門①】
はじめに | Dart Analyzerとは何か?
こんにちは、株式会社メンバーズ Cross Applicationカンパニーの田原です。
FlutterやDart開発の品質を支える生命線、それが静的解析(Static Analysis)です。コードを書き間違えた瞬間にエディタに現れる赤色の波線や、修正候補を提示してくれるQuick Fix。これら「開発中のリアルタイムな指摘」を司っているのが、Dart Analyzerという強力な静的解析エンジンです。
// A value of type 'int' can't be assigned to a variable of type 'String'.
String calcResult = 1 + 2 * 3; Dart Analyzerは、単にエラーを見つけるだけのツールではありません。ソースコードをAST(抽象構文木)とElement Modelという高度なデータ構造に変換し、プログラムの「形」と「意味」を深く理解しています。
本記事では、専用ツール「Dart Analyzer Diagnostics」を用いて、この静的解析の裏側を可視化します。複雑なコードが内部でどのように構造化され、型安全性が担保されているのか、その仕組みを徹底解説します。
実行のための「変換」から、開発のための「解析」へ
通常、プログラミング言語のコンパイラの役割は、ソースコードを機械語や別の中間コードに変換することです。しかし、私たちが日々エディタに向かう中で求めているのは、出力結果だけではありません。
リアルタイムのフィードバック:タイピングの直後に、論理の破綻を知りたい。
構造的な安全性の担保:型の不整合やnullの混入を、実行する前に未然に防ぎたい。
リファクタリング:たった一箇所の変更を、ソースコード全体へ整合性を保ったまま波及させたい。
Dart Analyzerは、実行ファイルを生成するツールではなく、コンパイラが内部で行う「解析」というプロセスを切り出し、開発者支援に特化させた静的解析インフラです。
私たちがコードを一文字書き換えるたびに、背後でコードの「構造」と「意味」を即座に再計算する。このバックグラウンドでの高度な処理こそが、Flutter/Dartにおける開発体験の本質なのです。
Dart Analyzer Diagnosticsの起動と使い方
Analyzerがコードをどう解釈しているかを実際に確認するために、シンプルな変数定義が実装された次のファイルを用意します。
int_variable.dart
int calcResult = 1 + 2 * 3;ファイルを作成したら、Analyzerの内部を覗くための専用ツールを起動します。
コマンドを実行:VS Codeのコマンドパレット(Cmd+Shift+P / Ctrl+Shift+P)を開き、「Dart: Open Analyzer Diagnostics / Insights」を選択します。
ブラウザを確認:ブラウザが立ち上がり、localhost:11011(ポートは環境により異なります)が開きます。
解析メニューへ:左サイドメニューから「Contexts」を選択します。現在アクティブなプロジェクトのコンテキスト(解析単位)が表示されるので、その中にある「Context files」を辿り、作成したファイルを探します。
内部情報を開く:目的のファイルの横に並んでいる [ast] や [element] というリンクをクリックしてください。
これらをクリックすることで、ソースコードという「文字列」が、Analyzerの内部でどのような「データ構造」として保持されているのかが明らかになります。
Dart静的解析を支える2つの柱 | ASTとElement Model
Dart Analyzerによる静的解析は、「構文」と「意味」という相補的な二つのデータモデルによって支えられています。
AST (Abstract Syntax Tree / 抽象構文木
役割: コードの「形」をツリー状に表現したもの。
内容: 1 + 2 * 3 がどのような優先順位で並んでいるか、どの単語がどの位置にあるかという「文法構造」を保持します。
Element Model (意味論的モデル)
役割: コードの「意味(セマンティクス)」を表現したもの。
内容: 変数 calcResult はどこで定義されたのか、どんな属性(型、getter/setter)を持つのかといった、情報の「実体」を管理します。
なぜこの二重構造が必要なのか
たとえば、コードの中に + という記号があるだけでは、それが「整数の加算」か「文字列の結合」かは判然としません。Analyzerは、記号の左右にあるElement(意味)を照らし合わせることで、初めてその記号が果たすべき役割を確定させます。この「形」に「意味」を繋ぎ合わせるステップこそが、静的解析の核心である「名前解決」と呼ばれるプロセスです。
AST(抽象構文木)を解読する | コードの『形』はどう表現されるか
int calcResult = 1 + 2 * 3; という一行を、Analyzerがどう解読しているかを紐解きます。
AST: int_variable.dart
CompilationUnitImpl [0..27]
┊ declared fragment = /Users/stf05514/ast_test/lib/samples/int_variable.dart
┊ TopLevelVariableDeclarationImpl [0..26]
┊ ┊ name = calcResult
┊ ┊ VariableDeclarationListImpl [0..25]
┊ ┊ ┊ NamedTypeImpl [0..2]
┊ ┊ ┊ ┊ name = int
┊ ┊ ┊ ┊ type = int
┊ ┊ ┊ VariableDeclarationImpl [4..25]
┊ ┊ ┊ ┊ declared fragment = int calcResult
┊ ┊ ┊ ┊ name = calcResult
┊ ┊ ┊ ┊ BinaryExpressionImpl [17..25]
┊ ┊ ┊ ┊ ┊ element = num +(num other)
┊ ┊ ┊ ┊ ┊ static type = int
┊ ┊ ┊ ┊ ┊ IntegerLiteralImpl [17..17]
┊ ┊ ┊ ┊ ┊ ┊ static type = int
┊ ┊ ┊ ┊ ┊ BinaryExpressionImpl [21..25]
┊ ┊ ┊ ┊ ┊ ┊ element = num *(num other)
┊ ┊ ┊ ┊ ┊ ┊ static type = int
┊ ┊ ┊ ┊ ┊ ┊ IntegerLiteralImpl [21..21]
┊ ┊ ┊ ┊ ┊ ┊ ┊ static type = int
┊ ┊ ┊ ┊ ┊ ┊ IntegerLiteralImpl [25..25]
┊ ┊ ┊ ┊ ┊ ┊ ┊ static type = intAnalyzer Diagnosticsで確認できるASTログは、右にインデントが進むほど、情報の階層が深いことを示しています。Analyzerは、この一行を以下のような入れ子構造として捉えています。
CompilationUnitImpl
: ファイル全体の「器」です。importなどの「ディレクティブ」と、コードの本体である「宣言」を格納します。
TopLevelVariableDeclarationImpl
: クラスの外などで定義された「トップレベル変数の宣言」という単位です。
VariableDeclarationListImpl
: 変数の「型」と「個別の変数定義」を束ねるリストです。Dartでは int a, b; のように一括宣言ができるため、この階層が必要になります。
重要ポイントの解説
① 型の定義(NamedTypeImpl
)
Analyzerはまず左辺を解析し、この変数がどのような性質を持つべきかという「期待値(期待される型)」をセットします。
┊ ┊ ┊ NamedTypeImpl [0..2]
┊ ┊ ┊ ┊ name = int
┊ ┊ ┊ ┊ type = int <-- 【解決された型 = 右辺への期待値】ここで注目すべきは、name と type の使い分けです。
name: ソースコード上に記述された生の文字列(ラベル)です。
type: 解析によって確定した「型」の真実です。
たとえば、typedef MyInt = int; と定義し、コード上で MyInt calcResult = ... と記述したとします。このとき、AST上の name は MyInt に変わりますが、type は変わらずに int を指し続けます。 Analyzerがエイリアスに惑わされず、迷いなく「これは数値である」と確信できるのは、この階層化された定義のおかげです。
② 計算の構造(BinaryExpressionImpl
)
次に、右辺の計算式を分解します。Analyzerは単に左から順に処理するのではなく、演算子の優先順位を考慮して構造化します。
┊ ┊ ┊ ┊ BinaryExpressionImpl [10..18] <-- 「1 + (2 * 3)」全体
┊ ┊ ┊ ┊ ┊ element = num +(num other)
┊ ┊ ┊ ┊ ┊ static type = int
┊ ┊ ┊ ┊ ┊ IntegerLiteralImpl [10..10] <-- 「1」
┊ ┊ ┊ ┊ ┊ ┊ static type = int
┊ ┊ ┊ ┊ ┊ BinaryExpressionImpl [14..18] <-- 「2 * 3」部分ここで登場する BinaryExpressionImpl は「二項演算」、つまり 1 + 2 や 2 * 3 のように2つの値を用いた計算を指します。一方の IntegerLiteralImpl は、ソースコードに直接書かれた「整数値」そのものです。 ログの階層に注目すると、2 * 3 が一つの塊(BinaryExpression)として深くネストされており、その計算結果が 1 と足し合わされる構造になっています。つまり、Analyzerは計算を実行する前から、すでに「算数の優先順位」に基づいた論理的なツリーを組み上げていることがわかります。
③ 名前解決と型推論(事実の確定)
解析のクライマックスは、記号に意味を与える「名前解決」と、それによって導き出される「型の確定」です。
┊ ┊ ┊ ┊ ┊ BinaryExpressionImpl [21..25]
┊ ┊ ┊ ┊ ┊ element = num +(num other) <-- 【名前解決】
┊ ┊ ┊ ┊ ┊ static type = int <-- 【事実の確定】ソースコード上の単なる + という記号を、Analyzerは「これは num クラス(int の親クラス)で定義されている + メソッドを呼び出しているのだ」と特定します。
この紐付けが完了して初めて、計算の結果として得られる型が int であるという「確定した事実(static type)」が導き出されます。
ここで最も重要なのが、「左辺の期待値」と「右辺の事実」の照合です。
左辺の期待: 手順①で読み取った NamedTypeImpl(この変数は int であるべき)。
右辺の事実: 手順③で確定した static type(この計算結果は int である)。
Analyzerはこの両者を突き合わせ、「期待される型」と「実際の型」が一致することを確認して初めて、この一行に矛盾がないと判断します。もしここが String calcResult = 1 + 2 * 3; であれば、「期待(String)に対し、事実は(int)である」という不一致を検出し、私たちのエディタにあの赤い波線を引くのです。
Element Modelを解読する | 変数やGetter/Setterの「意味」
ASTが「書き方の形」を表現するのに対し、Element Modelは「そのコードがシステム内で持つ役割や実体」を定義します。
Element model: int_variable.dart
library package:ast_test/samples/int_variable.dart (LibraryElementImpl)
┊ annotations = []
┊ isDartAsync = false
┊ isDartCore = false
┊ isInSdk = false
┊ fragments[0]: null (CompilationUnitElementImpl)
┊ ┊ imports = {}
┊ ┊ source = Source (uri="package:ast_test/samples/int_variable.dart", path="/Users/stf05514/ast_test/lib/samples/int_variable.dart")
┊ int get calcResult (GetterElementImpl)
┊ ┊ annotations = []
┊ ┊ hasImplicitReturnType = false
┊ ┊ isAbstract = false
┊ ┊ isExternal = false
┊ ┊ isStatic = true
┊ ┊ returnType = int
┊ ┊ type = int Function()
┊ ┊ typeParameters = []
┊ ┊ fragments[0]: calcResult (PropertyAccessorElementImpl_ImplicitGetter)
┊ ┊ ┊ isAsynchronous = false
┊ ┊ ┊ isGenerator = false
┊ ┊ ┊ isSynchronous = true
┊ set calcResult(int _calcResult) (SetterElementImpl)
┊ ┊ annotations = []
┊ ┊ hasImplicitReturnType = false
┊ ┊ isAbstract = false
┊ ┊ isExternal = false
┊ ┊ isStatic = true
┊ ┊ returnType = void
┊ ┊ type = void Function(int)
┊ ┊ typeParameters = []
┊ ┊ fragments[0]: calcResult (PropertyAccessorElementImpl_ImplicitSetter)
┊ ┊ ┊ isAsynchronous = false
┊ ┊ ┊ isGenerator = false
┊ ┊ ┊ isSynchronous = true
┊ ┊ int _calcResult (FormalParameterElementImpl)
┊ ┊ ┊ annotations = []
┊ ┊ ┊ hasImplicitType = false
┊ ┊ ┊ isConst = false
┊ ┊ ┊ isFinal = false
┊ ┊ ┊ isInitializingFormal = false
┊ ┊ ┊ isStatic = false
┊ ┊ ┊ parameterKind = required-positional
┊ ┊ ┊ type = int
┊ ┊ ┊ fragments[0]: _calcResult (ParameterElementImpl_ofImplicitSetter)
┊ int calcResult (TopLevelVariableElementImpl2)
┊ ┊ annotations = []
┊ ┊ hasImplicitType = false
┊ ┊ isConst = false
┊ ┊ isFinal = false
┊ ┊ isStatic = true
┊ ┊ type = int
┊ ┊ fragments[0]: calcResult (TopLevelVariableElementImpl)
┊ ┊ ┊ nameOffset = 4注目すべき「3つの実体」の正体
Dartでは、トップレベル変数を1つ宣言すると、Analyzerの内部では「Getter(取得)」「Setter(設定)」「Variable(変数実体)」という3つのElementが生成されます。一見過剰に思えるこの構造こそが、Dartの柔軟な言語設計を支える基盤です。
① Getter (GetterElementImpl)
┊ int get calcResult (GetterElementImpl)
┊ ┊ annotations = []
┊ ┊ hasImplicitReturnType = false
┊ ┊ isAbstract = false
┊ ┊ isExternal = false
┊ ┊ isStatic = true
┊ ┊ returnType = int
┊ ┊ type = int Function()
┊ ┊ typeParameters = []
┊ ┊ fragments[0]: calcResult (PropertyAccessorElementImpl_ImplicitGetter)
┊ ┊ ┊ isAsynchronous = false
┊ ┊ ┊ isGenerator = false
┊ ┊ ┊ isSynchronous = trueプログラムがこの変数を使おうとした時(値を読み取る時)、AnalyzerはこのGetterの定義を参照します。
int get calcResult (GetterElementImpl)
calcResult という識別子に対するGetter要素の定義であることを示しています。annotations = []
アノテーションは「メタデータ」です。もし @deprecated が付いていれば、AnalyzerはこのGetterを参照するすべてのコードに対して警告を生成します。ここが空であることは、制約なしに参照可能な状態であることを意味します。hasImplicitReturnType = false
型推論(var)に依存せず、明示的に int と型指定がなされたことを示します。Analyzerはこのフラグにより、開発者の明示的な型定義の意図を把握します。isAbstract = false
abstract class 内のGetter のように「名前はあるが中身がない」状態ではなく、この変数のためのメモリ領域と、値を取り出すための具体的なコードが生成されていることを保証しています。isExternal = false
Dart SDK の内部コードや Web 開発(JS 連携)では、実体が別言語にある場合に external を使います。これが false ということは、Dart の実行環境(VM やコンパイル結果)の中で完結する、ピュアな Dart 定義であることを指します。isStatic = true
トップレベル変数は、内部的にはライブラリ全体の「静的メンバ」として扱われます。インスタンス化を経由せず、直接この識別子を参照できる権限を定義しています。returnType = int
このGetterを参照した結果として得られるデータの型です。後続の処理(演算など)の妥当性を判断するための、主要な情報となります。type = int Function()
ここがDartの設計の美しさです。内部的には「引数なしでintを返す関数」として定義されています。つまり、変数へのアクセスは、実は「値を返すメソッドの呼び出し」にすり替えられています。これが「変数も関数も等しくオブジェクトとして扱う」というDartの一貫性を支えています。typeParameters = []
もしこれが List<T> クラスの中にあるGetterなら、ここには型情報が入ります。空であるということは、このGetterが「誰が使っても常に int を返す」固定的なルールであることを示しています。fragments[0]: calcResult (PropertyAccessorElementImpl_ImplicitGetter)
実装の断片(fragment)の情報です。一つのElementの定義が複数のファイルに分散する可能性があるため、このようにリスト形式で管理されています。ImplicitGetter(暗黙のGetter)という名前が重要で、ソースコード上に get と書いていないのに、Dartが自動で作成したGetterであることを示しています。isAsynchronous = false
非同期(async)ではないことを示します。await で待つ必要はありません。もし isAsynchronous が true なら、値を取り出すたびに Future が返り、プログラムの実行を一時中断して待つ(await)必要が出てきます。isGenerator = false
Stream や Iterable(yield を使うような処理)ではないことを示しています。これが false ということは、この変数が「次々と値を流し続ける特殊なもの」ではなく、「呼べばその時の値を一つ返してくれる、ごく普通の変数」として定義されていることを表しています。isSynchronous = true
同期処理であることを示します。呼び出したその瞬間に、即座に値を返します。
② Setter (SetterElementImpl)
┊ set calcResult(int _calcResult) (SetterElementImpl)
┊ ┊ annotations = []
┊ ┊ hasImplicitReturnType = false
┊ ┊ isAbstract = false
┊ ┊ isExternal = false
┊ ┊ isStatic = true
┊ ┊ returnType = void
┊ ┊ type = void Function(int)
┊ ┊ typeParameters = []
┊ ┊ fragments[0]: calcResult (PropertyAccessorElementImpl_ImplicitSetter)
┊ ┊ ┊ isAsynchronous = false
┊ ┊ ┊ isGenerator = false
┊ ┊ ┊ isSynchronous = true
┊ ┊ int _calcResult (FormalParameterElementImpl)
┊ ┊ ┊ annotations = []
┊ ┊ ┊ hasImplicitType = false
┊ ┊ ┊ isConst = false
┊ ┊ ┊ isFinal = false
┊ ┊ ┊ isInitializingFormal = false
┊ ┊ ┊ isStatic = false
┊ ┊ ┊ parameterKind = required-positional
┊ ┊ ┊ type = int
┊ ┊ ┊ fragments[0]: _calcResult (ParameterElementImpl_ofImplicitSetter)Setterは、変数の値を更新(代入)する際の書き込み用定義です。Setterは、単なる代入に見えて、実は「外部からデータを受け取り、整合性をチェックして、内部へ流し込む」という、一つの小さな関数の役割を果たしています。
set calcResult(int _calcResult) (SetterElementImpl)
_calcResult という名前は、ソースコード上には存在しません。Analyzerが代入をメソッドと同じようにロジックとして扱うために、内部的に捏造した「引数名」です。annotations = []
特別な注釈(@protected など)は何もついていません。このSetterが、特別な制約なく公開されていることを意味します。hasImplicitReturnType = false
Setterの戻り値は常に void ですが、これが「暗黙」ではなく明確に定義されていることを示します。isAbstract = false
Getter同様、この変数のためのメモリ領域と、値を取り出すための具体的なコードが生成されていることを保証しています。isExternal = false
Getter同様、ピュアな Dart 定義であることを指します。isStatic = true
トップレベル変数のSetterは、特定のインスタンスに依存しない「ライブラリレベルの静的メソッド」として扱われます。returnType = void
Setterは数学的な「代入」ではなく、一方的な「命令」です。値を返すと、代入式 a = b = c のような連鎖(チェイン)の挙動が複雑になるため、Dart ではSetterの戻り値は厳格に void(何も返さない)と決められています。type = void Function(int)
Setterの正体です。 内部的には「intを一つ受け取って何も返さない関数」として定義されています。代入という行為は、実はこの「関数への値渡し」として処理されているのです。ここにも、Dartの設計思想の一貫性が現れています。typeParameters = []
このSetterが扱う型に、<T> のような不確定な要素(ジェネリクス)が含まれていないことを示しています。これにより、Analyzerは「このSetterは常に int 型の代入のみを許容すればよい」と確信を持ち、迷いのない高速な解析が可能になりますfragments[0]: calcResult (PropertyAccessorElementImpl_ImplicitSetter)
「暗黙(Implicit)」という言葉が示す通り、これは開発者が set 構文を記述したのではなく、変数宣言に伴ってDartが自動的に用意したSetterであることを意味します。isAsynchronous = false / isSynchronous = true
もしSetterが非同期(async)になれるとしたら、「代入したのに、実際に値が変わるのは 1 秒後」という恐ろしいことが起きてしまいます。Dart では代入の整合性を保つため、Setterは必ずその場で完了する同期処理(Synchronous)でなければならないという強い制約があります。isGenerator = false
このSetterが yield などを用いたジェネレータ関数ではないことを示します。通常の代入操作は単一の値を確定させるものであり、複数の値を次々と生成する性質(IterableやStream)は持たないため、このフラグは常に false となります。
int _calcResult (FormalParameterElementImpl)
これは、calcResult = 10 と書いたときの 10 を、Analyzerが内部的に「関数の引数」として扱っていることを示しています。この検査をパスしない限り、変数の実体までデータは届きません。hasImplicitType = false
受け取る値の型が「なんでもいい(暗黙的)」ではなく、はっきりと int であると指定されていることを示します。これにより、型違いの値を入り口で確実にシャットアウトできます。isConst = false / isFinal = false
これらは「渡された値そのものが、このSetterの中で変更可能かどうか」という内部ルールです。通常のSetterでは、渡された値をそのまま変数に流し込むため、ここが true になることはありません。isInitializingFormal = false
これはコンストラクタの this.x のような「初期化のための引数」かどうかを指します。通常のSetterでは false ですが、これが false であることは、このSetterが「すでに存在している変数」を書き換えるために動いていることを示しています。parameterKind = required-positional
Setterにおいて「値を渡さない(代入しない)」という選択肢はあり得ません。必ず 1 つの値を、決まった場所(代入式の右辺)に置かなければならないという構文上の強制力を表しています。type = int
Setterが受け入れるべき型として定義された、厳格な制約です。 AST解析によって算出された「右辺の型(static type)」が、この「セッターが定義する型(int)」に代入可能かどうかを最終的に検証します。この定義が存在することで、Analyzerは型不整合を論理的に検出し、型安全性を担保しています。
③ 変数の実体 (TopLevelVariableElementImpl2)
┊ int calcResult (TopLevelVariableElementImpl2)
┊ ┊ annotations = []
┊ ┊ hasImplicitType = false
┊ ┊ isConst = false
┊ ┊ isFinal = false
┊ ┊ isStatic = true
┊ ┊ type = int
┊ ┊ fragments[0]: calcResult (TopLevelVariableElementImpl)
┊ ┊ ┊ nameOffset = 4GetterやSetterという「振る舞い(アクセサ)」の情報に対し、こちらは変数そのものの定義属性を保持する要素です。Analyzerにおけるデータの「宣言実体」に相当します。
int calcResult (TopLevelVariableElementImpl2)
ここから変数そのものの定義が始まります。末尾の 2 は、Analyzerパッケージの内部リファクタリングに伴う新しい実装モデルであることを示しています。(build changelog | Dart package -Pub.dev
)
annotations = []
@late や @deprecated などの注釈が何もついていない標準的な定義であることを表しています。hasImplicitType = false
「Implicit(暗黙)」ではない、つまり var と書かず、しっかり int と書いたことがここに記録されます。Analyzerはこれを見て、「型は推論結果ではなく、ユーザーの明確な意思である」と判断します。isConst = false / isFinal = false
コンパイル時定数(const)でも、変更不可(final)でもないことを示しています。「この変数は後から何度でも書き換えが可能である」という性質が確定しているからこそ、Analyzerはセッターの存在を正当なものとして許可しています。isStatic = true
ファイルのトップレベルにあるため、特定のオブジェクトに縛られない「静的な存在」として、どこからでもアクセスできる固定のメモリ領域が割り当てられていることを意味します。type = int
この変数が保持するデータの正体は int 型であるという最終的な宣言です。
fragments[0]: calcResult (TopLevelVariableElementImpl)
この変数の「定義の実体」がどこにあるかを示しています。nameOffset = 4
ソースコードの先頭から「4文字目」に変数の名前があるという、物理的な位置情報です。Analyzerがこのコード上の番地を正確に把握しているおかげで、IDEで変数名をクリックした際に、寸分違わず正しい定義場所へジャンプ(Go to Definition)できるのです。
まとめ | 静的解析を理解すると世界が変わる
今回見てきたのは、単なるエラーチェックの裏側ではなく、「ソースコードを論理的なデータ構造として扱う仕組み」そのものです。これらを知ることで、日々の開発に以下のような視点が加わります。
エラーの根拠が明確になる:
「なぜか波線が出ている」という状態から、「左辺で定義したNamedType(型)と、右辺の解析結果であるstatic typeが矛盾している」といった、Analyzer側の論理に沿ってエラーを捉えられるようになります。コード生成パッケージへの理解が深まる:
freezed や riverpod のコード生成(riverpod_generator)など、Dartの強力なツールの多くは、内部で今回覗いた Element モデル をスキャンして情報を読み取っています。内部構造を知ることで、これらのツールがどのようにコードを理解しているのか、その仕組みが見えてきます。言語仕様の意図を意識できる:
constやfinalといったキーワードが、単なる書き方のルールではなく、Analyzerに対して「不変性」を保証し、最適化を助けるための重要なメタデータとして機能していることを実感できます。
もし、新しい構文や複雑な関数定義に出会って「Dartの仕様がいまいち掴めない」と感じたら、ぜひAnalyzer Diagnosticsでそのコードを覗いてみてください。
関数が内部でどうExecutableElementとして扱われているか、非同期処理(async)がフラグとしてどう管理されているか。文字としての仕様書を読む以上に、Analyzerという「Dartを最も理解している存在」の視点を直接体験することで、言語への理解は飛躍的に向上します。
最後までお読みいただき、ありがとうございました!
この記事を書いた人
What is BEMA!?
Be Engineer, More Agile


