Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DartASTとその活用

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for そた そた
November 12, 2025

 DartASTとその活用

Avatar for そた

そた

November 12, 2025
Tweet

More Decks by そた

Other Decks in Programming

Transcript

  1. ASTとは 構文構造を木構造で表現したもの • Abstract = 抽象的な(詳細を省略) • Syntax = 構文

    • Tree = 木構造 ポイント: ASTはコードの構造を表現するが、フォーマット情報は保持しない コードの持つ抽象的な意味だけを構造化 ASTに対し、改行や空白、かっこなどコードの詳細を持つ木構造を CST: 具象構文技(Concrete Syntax Tree)と呼ぶ
  2. ソースコードを文字列として扱ってみる 例えばソースコードの中から print()の関数を呼び出している箇所を探したい // Dartでprinterを制御してprintする void main() async { final

    printText = 'print'; final printer = Printer(); await printer.print(printText); print('print(Done!)'); } 変数名、コメント、文字列、メソッドなど区別がつかない!!!
  3. Parserのイベントについて パーサー: 「変数宣言が始まった!」 : listener.beginVariablesDeclaration(...) パーサー: 「型 'int' を見つけた!」 :

    listener.handleIdentifier('int', ...) パーサー: 「識別子 'x' を見つけた!」 : listener.handleIdentifier('x', ...) パーサー: 「変数初期化が始まった!」 : listener.beginVariableInitializer(...) パーサー: 「整数型の値2が見つかった!」: listener.handleLiteralInt(2) パーサー: 「変数初期化が終了した!」 : listener.endVariableInitializer(...) パーサー: 「変数宣言が終わった!」 : listener.endVariablesDeclaration(...) int x = 2;
  4. SyntacticEntity /// 構文エンティティ(トークンまたは ASTノード)を表すインターフェース。 /// ソースファイル内の位置と範囲を持つ。 abstract class SyntacticEntity {

    /// 構文エンティティの最後の文字の次の文字までの、 /// ファイルの先頭からのオフセットを返す。 int get end; /// 構文エンティティのソース範囲内の文字数を返す。 int get length; /// 構文エンティティの最初の文字までの、ファイルの先頭からのオフセットを返す。 int get offset; }
  5. Token abstract class Token implements SyntacticEntity { TokenType get type;

    // トークンの種類 String get lexeme; // ソースコード内の実際の文字列 Token? get previous; // 前のトークン Token? get next; // 次のトークン bool get isEof; // EOFトークンかどうか bool get isIdentifier; // 識別子かどうか bool get isKeyword; // キーワードかどうか bool get isOperator; // 演算子かどうか bool get isSynthetic; // 合成トークンかどうか … // 他にもis~というプロパティが複数 }
  6. AstNode abstract final class AstNode implements SyntacticEntity { Token get

    beginToken; Token get endToken; Iterable<SyntacticEntity> get childEntities; AstNode? get parent; AstNode get root; }
  7. beginToken/endToken トークン情報を保持 ノードがソースコード内のどの位置、範囲のものかを正確に特定 Token get beginToken; // 最初のトークン Token get

    endToken; // 最後のトークン // 例 a + b BinaryExpression ← 二項式を表すAstNode ├─ beginToken: Token('a') ← 開始トークン └─ endToken: Token('b') ← 終了トークン
  8. childEntities 子要素へのアクセスが可能 そのノードの内容を構成する全てのエンティティ Iterable<SyntacticEntity> get childEntities; // 例 a +

    b を表すBinaryExpression childEntities = [ SimpleIdentifier('a'), // ASTノード Token('+'), // トークン SimpleIdentifier('b') // ASTノード ]
  9. AstNodeの構造 1. beginToken : 開始トークン 2. endToken : 終了トークン 3.

    childEntities : 子要素 4. parent : 親ノード 5. root : ルートノード の5つの主要なプロパティでコードの特定と木構造を実現 実装するクラスによりその構文の意味を表現
  10. AstNodeの種類 主要なAstNodeタイプ Elementタイプ 説明 例 MethodInvocation 関数の実行 print(‘hoge’) SimpleIdentifier 識別子

    print(‘hoge’) ClassDeclaration クラス定義 class MyClass {...} ArgumentList 引数のリスト print(text) NamedType 型 Future<void> main {... CompilationUnit コンパイル単位 ファイル全体
  11. Visitorを動かしてみる print関数の呼び出し箇所を探索するRecursiveAstVisitor class PrintCallVisitor extends RecursiveAstVisitor<void> { final List<MethodInvocation> methodCalls

    = []; @override void visitMethodInvocation(MethodInvocation node) { // 'print'関数の呼び出しかチェック if (node.methodName.name == 'print' && node.target == null) { methodCalls.add(node); } super.visitMethodInvocation(node); } }
  12. Visitorの集めた情報をもとにrenameする compilationUnit.accept(visitor); final methodCalls = visitor.methodCalls ..sort((a, b) => a.methodName.offset.compareTo(b.methodName.offset));

    // 直前にbufferに追加したコードのオフセットを保持する変数 int lastOffset = 0; final buffer = StringBuffer(); for (var call in methodCalls) { //元のソースコードのprint以外のソースコードを bufferに追加 buffer.write(sourceCode.substring(lastOffset, call.methodName.offset)); // 'print'の代わりに'log.info'をbufferに追加 buffer.write('log.info'); // 次のループでprintの次からをbufferに追加できるようにoffsetをずらす lastOffset = call.methodName.length + call.methodName.offset; } // 最後のprintより後のコードをbufferに追加 buffer.write(sourceCode.substring(lastOffset));
  13. このような場合は? void main() async { final printText = 'print'; final

    inkJetPrinter = InkJetPrinter(); final laserPrinter = LaserPrinter(); await inkJetPrinter.print(printText); await laserPrinter.print(printText); }
  14. このような場合は? class Printer { Future<void> print(String message) async { await

    Future.delayed(Duration(seconds: 1)); log('Generic printing: $message'); } Future<void> printMultiple(List<String> messages) async { for (final message in messages) { await print(message); } } }
  15. Resolved ASTの持つ型情報とは 全ての式AstNode(Expression)に型情報が付与されていること Unresolved ASTでの状態 Resolved ASTでの状態 // Resolved AST

    BinaryExpression('a + b') ├─ leftOperand: SimpleIdentifier('a') │ └─ staticType: int ├─ operator: Token('+') ├─ rightOperand: SimpleIdentifier('b') │ └─ staticType: int └─ staticType: int // intの足し算なのでint // ソースコード int a = 10; int b = 20; int c = a + b; // 'a + b'の型は? // Unresolved AST BinaryExpression('a + b') └─ staticType: null // 型情報なし
  16. Resolved ASTの持つ識別子の意味とは 識別子が何を参照しているかが解決していること Unresolved ASTでの状態 Resolved ASTでの状態 // Resolved AST

    SimpleIdentifier('x') └─ element: VariableElement // 変数xを参照 └─ staticType: int // 型はint // ソースコード: int y = x; SimpleIdentifier('x') // 文字列"x"としてのみ認識
  17. Resolved ASTの持つElementとは コード内で宣言された要素(クラス、関数、変数など)の詳細情報 前提: AST(構文構造)と Element(意味構造)は別モデル 例: • 構文構造: コードの書き方、構造(「int

    x = 42;」という書き方) • 意味構造: コードの意味、内容(「xという名前のint型の変数」という意味) int x = 42; AST: 構文的な構造(書き方) └─ VariableDeclaration('int x = 42;') Element: 意味的な構造(意味) └─ VariableElement(name: 'x', type: int)
  18. Elementの種類 主要なElementタイプ Elementタイプ 説明 例 VariableElement 変数 int x =

    42; MethodElement メソッド void print() {...} ClassElement クラス class MyClass {...} ConstructorElement コンストラクタ const MyClass() PropertyAccessorElement ゲッター/セッター int get value => … LibraryElement ライブラリ ファイル全体
  19. Elementの構造 ElementもASTのような階層構造を持つ LibraryElement └─ CompilationUnitElement ├─ ClassElement('Printer') │ ├─ MethodElement('print')

    │ │ └─ ParameterElement('msg') │ └─ FieldElement('count') └─ FunctionElement('main') └─ LocalVariableElement('args')
  20. Elementが持つ情報 MethodElementの例 // ソースコード class Printer { void print(String msg)

    { ... } } // MethodElementが持つ情報 MethodElement { name: 'print', // メソッド名 returnType: void, // 戻り値の型 parameters: [ // パラメータリスト ParameterElement('msg', type: String) ], enclosingElement: ClassElement('Printer'), // 定義クラス // ... その他多くの情報 }
  21. Elementが持つ情報 MethodElementの例 // ソースコード class Printer { void print(String msg)

    { ... } } // MethodElementが持つ情報 MethodElement { name: 'print', // メソッド名 returnType: void, // 戻り値の型 parameters: [ // パラメータリスト ParameterElement('msg', type: String) ], enclosingElement: ClassElement('Printer'), // 定義クラス // ... その他多くの情報 } どのクラスに定義されているかの情報!
  22. 特定のクラスのメソッドのみを収集したい final List<SyntacticEntity> methodNames = []; @override void visitMethodInvocation(MethodInvocation node)

    { final element = node.methodName.element; if (element is MethodElement && node.methodName.name == 'print') { final enclosingElement = element.enclosingElement; // 指定されたクラスのメソッドかチェック if (enclosingElement is ClassElement && enclosingElement.name == 'InkJetPrinter') { methodNames.add(node.methodName); } } super.visitMethodInvocation(node); }
  23. Resolved ASTはどう作るの? Unresolved ASTと比べると結構複雑 final targetPath = File('lib/target_source.dart').absolute.path; final collection

    = AnalysisContextCollection( includedPaths: [Directory(targetPath).parent.path], ); final context = collection.contextFor(targetPath); final result = await context.currentSession.getResolvedUnit(targetPath) as ResolvedUnitResult; final unit = result.unit;