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

DartASTとその活用

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;