Go_Abstract_Syntax_Tree_AST

date
Aug 17, 2023
slug
abstractSyntaxTree
status
Published
tags
Golang
AST
summary
ASTパッケージを活用することで、goコードを効果的に解析
type
Post

Go_抽象構文木Abstract Syntax Tree

Go言語は、抽象構文木(Abstract Syntax Tree, AST)を操作する強力なツールセットを提供しています。ASTパッケージを活用することで、goコードを効果的に解析し、プログラムの構造を理解することができます。この記事では、Go言語のASTパッケージを使用して、型とそのメソッドに焦点を当てて解析する方法をサンプルで表示します。

1. ASTパッケージ例

Go言語のASTパッケージは、プログラムのソースコードを抽象構文木(AST)として表現し、その構造を操作するための機能を提供します。ASTパッケージは、コード解析、変換、リファクタリング、コード生成などのタスクに使用されます。
1.ASTノードを操作する基本的な例です:
package main import ( "fmt" "go/parser" "go/token" "go/ast" ) func main() { // ソースコード sourceCode := ` package main import "fmt" func main() { fmt.Println("Hello, world!") } ` // 構文解析器の設定 fset := token.NewFileSet() node, err := parser.ParseFile(fset, "example.go", sourceCode, parser.ParseComments) if err != nil { fmt.Println("Error parsing source code:", err) return } // ASTノードを操作 ast.Inspect(node, func(n ast.Node) bool { if ident, ok := n.(*ast.Ident); ok { fmt.Println("Ident found:", ident.Name) } return true }) }
Output:
Ident found: main Ident found: main Ident found: fmt Ident found: Println
機能と概念:
  1. Node(ノード): AST内の各要素はノードとして表現されます。ノードはプログラムのさまざまな構成要素(変数、関数、定数、式など)を表します。各ノードタイプは、ast.Node インターフェースを満たす具体的な型として表現されます。
  1. Visitorパターン: Go言語のASTを操作する一般的な方法は、Visitorパターンを使用することです。Visitorは、ノードを訪問して特定の操作を実行するためのメソッドの集合です。ast.Inspect ファンクションを使用して、ノードツリーを再帰的に探索し、Visitor関数を適用します。
  1. TokenとFileSet: ASTノードは、ソースコード内のトークンと関連付けられます。ASTノードとトークン間の関連性を保つために、token.Tokentoken.Position を使用します。また、token.FileSet は、ファイルの位置情報を管理するためのもので、構文解析時にトークンの位置情報を保持します。
  1. ソースコード解析: parser.ParseFile 関数を使用して、ソースコードを解析しASTノードを生成します。この関数には、token.FileSet、ファイル名、ソースコード、解析オプションなどが渡されます。
  1. ノードの種類: ASTノードは多くの種類があります。例えば、ast.Ident は識別子(変数名、関数名など)、ast.FuncDecl は関数の宣言、ast.IfStmt は条件文、などです。それぞれのノードタイプには、特定のフィールドとメソッドが含まれています。
  1. ノードの操作: ノードのフィールドやメソッドを使用して、ノード内の情報を取得したり変更したりすることができます。例えば、関数内のすべての識別子を収集する、条件文を特定の条件で変更する、などの操作が可能です

2. サンプルコード

  1. ソースコード内の関数宣言を検出
    1. このサンプルコードでは、指定されたソースコードを解析し、関数宣言を検出してその関数名を表示しています。注意すべき点は、関数宣言は *ast.FuncDecl タイプのノードとして表現されることです。 node.Decls はソースコード内のすべての宣言を含むスライスです。parser.ParseFile を使用してソースコードを解析し、ASTノードを取得し、for ループでノードを走査して関数宣言を検出します。
      package main import ( "fmt" "go/parser" "go/token" "go/ast" ) func main() { // ソースコード sourceCode := ` package main import "fmt" func main() { fmt.Println("Hello, world!") } func myFunction() { fmt.Println("This is my function.") } ` // 構文解析器の設定 fset := token.NewFileSet() node, err := parser.ParseFile(fset, "example.go", sourceCode, parser.ParseComments) if err != nil { fmt.Println("Error parsing source code:", err) return } // 関数宣言を検出して表示 for _, decl := range node.Decls { if funcDecl, isFunc := decl.(*ast.FuncDecl); isFunc { fmt.Printf("Function Name: %s\n", funcDecl.Name.Name) } } }
      Output:
      Function Name: main Function Name: myFunction
  1. 関数の引数を検出し、引数名と型を表示
    1. ソースコード内の関数宣言から引数を取得し、引数名と型情報を表示しています。funcDecl.Type.Params.List は、関数の引数リストを表すASTノードのスライスです。引数の名前は field.Names から取得し、引数の型は field.Type から取得します。
      package main import ( "fmt" "go/parser" "go/token" "go/ast" ) func main() { // ソースコード sourceCode := ` package main import "fmt" func myFunction(arg1 int, arg2 string) { fmt.Println(arg1, arg2) } func main() { myFunction(42, "Hello") } ` // 構文解析器の設定 fset := token.NewFileSet() node, err := parser.ParseFile(fset, "example.go", sourceCode, parser.ParseComments) if err != nil { fmt.Println("Error parsing source code:", err) return } // 関数の引数を検出して表示 for _, decl := range node.Decls { if funcDecl, isFunc := decl.(*ast.FuncDecl); isFunc { if funcDecl.Body != nil { fmt.Printf("Function Name: %s\n", funcDecl.Name.Name) for _, field := range funcDecl.Type.Params.List { for _, name := range field.Names { fmt.Printf("Parameter Name: %s, Type: %s\n", name.Name, field.Type) } } } } } }
      Output:
      Function Name: myFunction Parameter Name: arg1, Type: int Parameter Name: arg2, Type: string Function Name: main
  1. 関数の引数とレシーバー(メソッドの場合)を検出し、名前と型を表示
    1. ソースコード内の関数とメソッドの引数とレシーバーを解析し、それらの名前と型を表示します。ast.FuncDeclRecv フィールドは、メソッドの場合にレシーバー情報を持つASTノードを表します。
      package main import ( "fmt" "go/parser" "go/token" "go/ast" ) func main() { // ソースコード sourceCode := ` package main import "fmt" type MyStruct struct { Field1 int Field2 string } func (m *MyStruct) Method(arg1 int, arg2 string) { fmt.Println(arg1, arg2) } func main() { myStruct := MyStruct{} myStruct.Method(42, "Hello") } ` // 構文解析器の設定 fset := token.NewFileSet() node, err := parser.ParseFile(fset, "example.go", sourceCode, parser.ParseComments) if err != nil { fmt.Println("Error parsing source code:", err) return } // 関数やメソッドの引数とレシーバーを検出して表示 for _, decl := range node.Decls { switch d := decl.(type) { case *ast.FuncDecl: if d.Recv != nil { for _, field := range d.Recv.List { for _, name := range field.Names { fmt.Printf("Receiver Name: %s, Type: %s\n", name.Name, field.Type) } } } if d.Body != nil { fmt.Printf("Function Name: %s\n", d.Name.Name) for _, field := range d.Type.Params.List { for _, name := range field.Names { fmt.Printf("Parameter Name: %s, Type: %s\n", name.Name, field.Type) } } } } } }
      Output:
      Receiver Name: m, Type: &{%!s(token.Pos=98) MyStruct} Function Name: Method Parameter Name: arg1, Type: int Parameter Name: arg2, Type: string Function Name: main
  1. ユーザ定義型の解析
    1. ソースコード内でユーザが定義した構造体型(MyStruct)を解析し、その型名とフィールド情報を表示します。ast.GenDecl ノードは、一般的な宣言を表すASTノードであり、ここでは type 宣言が対象です。ユーザ定義型は ast.TypeSpec の配列として表現されます。ユーザ定義の型の内部は ast.StructType に含まれています。
      package main import ( "fmt" "go/parser" "go/token" "go/ast" ) func main() { // ソースコード sourceCode := ` package main import "fmt" type MyStruct struct { Field1 int Field2 string } func main() { myStruct := MyStruct{} fmt.Println(myStruct.Field1, myStruct.Field2) } ` // 構文解析器の設定 fset := token.NewFileSet() node, err := parser.ParseFile(fset, "example.go", sourceCode, parser.ParseComments) if err != nil { fmt.Println("Error parsing source code:", err) return } // ユーザ定義型の情報を検出して表示 for _, decl := range node.Decls { if genDecl, isGenDecl := decl.(*ast.GenDecl); isGenDecl { for _, spec := range genDecl.Specs { if typeSpec, isTypeSpec := spec.(*ast.TypeSpec); isTypeSpec { if structType, isStructType := typeSpec.Type.(*ast.StructType); isStructType { fmt.Printf("Type Name: %s\n", typeSpec.Name.Name) for _, field := range structType.Fields.List { fmt.Printf("Field Name: %s, Type: %s\n", field.Names[0].Name, field.Type) } } } } } } }
      Output:
      Type Name: MyStruct Field Name: Field1, Type: int Field Name: Field2, Type: string
  1. ユーザが定義した型に関連するメソッドの情報を抽出
    1. ソースコード内でユーザが定義した型に関連するメソッドの情報を解析し、メソッド名とレシーバーの型、および引数名と型情報を表示します。ast.FuncDeclRecv フィールドは、メソッドの場合にレシーバー情報を持つASTノードを表します。
      package main import ( "fmt" "go/parser" "go/token" "go/ast" ) func main() { // ソースコード sourceCode := ` package main import "fmt" type MyStruct struct { Field1 int Field2 string } func (m *MyStruct) MethodWithArgs(arg1 int, arg2 string) { fmt.Println(arg1, arg2) } func (m MyStruct) AnotherMethod(arg1 float64) { fmt.Println(arg1) } func main() { myStruct := MyStruct{} myStruct.MethodWithArgs(42, "Hello") myStruct.AnotherMethod(3.14) } ` // 構文解析器の設定 fset := token.NewFileSet() node, err := parser.ParseFile(fset, "example.go", sourceCode, parser.ParseComments) if err != nil { fmt.Println("Error parsing source code:", err) return } // ユーザ定義型のメソッド引数情報を検出して表示 for _, decl := range node.Decls { if funcDecl, isFunc := decl.(*ast.FuncDecl); isFunc { if funcDecl.Recv != nil { for _, field := range funcDecl.Recv.List { if _, isStarExpr := field.Type.(*ast.StarExpr); isStarExpr { // ポインタレシーバー fmt.Printf("Method Name: %s, Receiver Type: *%s\n", funcDecl.Name.Name, field.Type.(*ast.StarExpr).X) } else { // 値レシーバー fmt.Printf("Method Name: %s, Receiver Type: %s\n", funcDecl.Name.Name, field.Type) } } for _, field := range funcDecl.Type.Params.List { for _, name := range field.Names { fmt.Printf("Parameter Name: %s, Type: %s\n", name.Name, field.Type) } } } } } }
      Output:
      Method Name: MethodWithArgs, Receiver Type: *MyStruct Parameter Name: arg1, Type: int Parameter Name: arg2, Type: string Method Name: AnotherMethod, Receiver Type: MyStruct Parameter Name: arg1, Type: float64

3. まとめ

ASTパッケージを使用して解析の実用例を触りました。自動コード生成、リファクタリングツール、スタティック解析ツールなど、開発プロセスでの活用できます。
 
記事に関する疑問があればお気軽にご連絡ください。