Goのソースコードを処理するツール(コードチェックやデータ生成など)を作ってみたいと思ったので、Goの構文解析に入門してみました。
まえがき
ソースコードを何らかのツールにかけて処理したいことはよくあるかと思います。
例えばコードのルールチェックやフォーマットを行うとか、特定のフォーマットで書かれたコード(やコメント)から何らかのデータ(APIドキュメントなど)を自動生成するなどです。
昨今ではこのような処理を行うためのツールが多数提供されており、多くのプロジェクトで利用されています。
しかし、プロジェクトに固有なルールであるとか、独自のデータファイルを生成したいとなったとき、それら既存のツールでは対応できないこともあります。
そのような場合は「自分たちでツールを作る」ということが考えられますが、その際にはどのようにしてソースコードを処理すればよいのでしょうか?
簡単なツールであればちょっとしたスクリプトと正規表現などの文字列処理で実現することも可能かと思いますが、そういった単純な実装では文法を考慮した厳密な処理が難しかったり、謎の正規表現だらけでツール自身の保守性が悪くなることも少なくありません。
幸いにも私が携わっている案件にはGoを利用しているものも多く、Goには標準で構文解析を行うライブラリが用意されているため、これを利用してツールを作成できるようになりたいと思ったのが、本記事を書くに至った理由です。
どんなツールを作れるようになるか
構文解析ができるようになると、次のような様々なツールが作れそうです。
- ソースコードの静的解析ツール
- コーディング規約のチェックや構文エラー、潜在的バグの検出など
- コードの品質評価ツールなど
- 依存関係の視覚化ツールなど
- リファクタリングツール
- 冗長な部分やより良い記法への自動修正など
- コード変換ツール
- コードを一定のルールで整形するフォーマッター
- 新旧バージョンの記法への変換や、別の言語への変換など
- コード生成ツール1
- 新規プロジェクトやファイルのスケルトンを自動生成するなど
- テストコードの自動生成など
- データ生成ツール
- ソースコードから関数やパラメータなどのドキュメントの自動生成
- SwaggerのAPIドキュメントの生成など
- IDEの機能拡張
- シンタックスハイライトや定義位置の表示、リファクタリングなどの機能
本記事の動作環境
本記事は次の環境で動作確認しながら書きました。
- macOS Ventura
- Go v1.22
OSについては特に環境依存はないかと思います。
Goのバージョンについては、大きく離れていなければ(ここ1、2年ぐらいのバージョンであれば)、それほど差異はないかと思います。
(ジェネリクスの追加など大きな変更がある場合は、その限りではないです)
今回やってみること
今回扱う内容は次の3つです。
- AST(抽象構文木)の取得
- 関数一覧の抽出
- コードの書き換え(関数名の変更)
私自身、Goの構文解析は初めてなので、まずはソースコードを読み込んで構造を把握したり、特定の部分を選択する方法を学びます。
そして最後にちょっとしたコードの書き換えにチャレンジしてみます。
今回はまだ実用的なツールの実装までは行いません(今後、実用的なツールの開発にも手を出していきたいですね)。
それぞれの実践パート(上記3項目)では、最初にサンプルコードと実行結果を提示し、その後にコードの解説を載せる形としています。
お手元で実行してみなくても動作が見て取れるようになっていますが、是非ともご自身の手で実践してみてください。
Goの構文解析用パッケージの紹介
Go言語には静的解析用のパッケージがgo/*
として標準で用意されています。
ここでは、今回直接使用する4つのパッケージを紹介します。
-
go/token
: 字句(トークン)についてのパッケージ -
go/parser
: 構文解析を行うパッケージ -
go/ast
: 抽象構文木(AST)についてのパッケージ -
go/format
: ソースコードのフォーマットを行うパッケージ
他にも型についてのパッケージなど色々ありますが割愛します。
興味のある方は公式サイトのパッケージ一覧を覗いてみてください。
go/ directory - go - Go Packages
https://pkg.go.dev/go
余談:Goコンパイラはどうやってできている?
GoのコンパイラはGo自身で書かれています(初期の頃はCで書かれていました)。
よって、私はてっきりGoのコンパイラもこれらのパッケージを使っているのかと思っていたのですが、どうやら違うようです。
Goのコンパイラはgo/src/cmd/compile
以下にソースコードがあるのですが、go/parser
などはほぼ利用せず、 cmd/compile/internal
以下に専用のパッケージとしてまとめられているようです。
一方、gofmt
などの周辺ツールはgo/*
を利用して作られています。
今回対象とするコード
さて、今回の構文解析で対象とするコードとして、次のような簡単なコードを用意してみました。
もっと複雑なコードで試してみたいという方は、是非とも書き換えてチャレンジしてみてください。
package main
import "fmt"
func main() {
// 名前
name := "Alice"
hello(name)
}
// helloは挨拶を表示する関数です。
func hello(name string) {
fmt.Printf("Hello, %s!\n", name)
}
コメントの扱いも見ていきたいので、簡単なコメントも付けてみました。
なお、対象とするソースコードは構文解析ツールで処理するいわばデータであり、解析ツール自身のビルド対象外としたいので、_target/
以下に置くこととしました。
(.
や_
から始まるファイルやディレクトリはGoのビルドなどの対象外となります)
以下のサンプルコードでは対象コードをハードコードしていますが、余裕があれば引数で受け取るようなアレンジをしてもよいでしょう。
AST(抽象構文木)を取得してみる
AST(Abstract Syntax Tree: 抽象構文木)は、プログラムの文法構造を木構造で表したものです。
まずはこれを取得して表示するコードを書いてみます。
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)
func main() {
target := "./_target/hello.go"
if err := printAst(target); err != nil {
panic(err)
}
}
func printAst(filename string) error {
fmt.Println("-- printAst -------------------")
fset := token.NewFileSet() // (1)
f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) // (2)
if err != nil {
return err
}
ast.Print(fset, f) // (3)
return nil
}
これを実行すると、次のような出力が得られます。
-- printAst -------------------
0 *ast.File {
1 . Package: ./_target/hello.go:1:1
2 . Name: *ast.Ident {
3 . . NamePos: ./_target/hello.go:1:9
4 . . Name: "main"
5 . }
6 . Decls: []ast.Decl (len = 3) {
7 . . 0: *ast.GenDecl {
8 . . . TokPos: ./_target/hello.go:3:1
9 . . . Tok: import
10 . . . Lparen: -
11 . . . Specs: []ast.Spec (len = 1) {
12 . . . . 0: *ast.ImportSpec {
13 . . . . . Path: *ast.BasicLit {
14 . . . . . . ValuePos: ./_target/hello.go:3:8
15 . . . . . . Kind: STRING
16 . . . . . . Value: "\"fmt\""
17 . . . . . }
18 . . . . . EndPos: -
19 . . . . }
20 . . . }
21 . . . Rparen: -
22 . . }
23 . . 1: *ast.FuncDecl {
24 . . . Name: *ast.Ident {
25 . . . . NamePos: ./_target/hello.go:5:6
26 . . . . Name: "main"
:
(以下略)
完全な出力(200行弱)
-- printAst -------------------
0 *ast.File {
1 . Package: ./_target/hello.go:1:1
2 . Name: *ast.Ident {
3 . . NamePos: ./_target/hello.go:1:9
4 . . Name: "main"
5 . }
6 . Decls: []ast.Decl (len = 3) {
7 . . 0: *ast.GenDecl {
8 . . . TokPos: ./_target/hello.go:3:1
9 . . . Tok: import
10 . . . Lparen: -
11 . . . Specs: []ast.Spec (len = 1) {
12 . . . . 0: *ast.ImportSpec {
13 . . . . . Path: *ast.BasicLit {
14 . . . . . . ValuePos: ./_target/hello.go:3:8
15 . . . . . . Kind: STRING
16 . . . . . . Value: "\"fmt\""
17 . . . . . }
18 . . . . . EndPos: -
19 . . . . }
20 . . . }
21 . . . Rparen: -
22 . . }
23 . . 1: *ast.FuncDecl {
24 . . . Name: *ast.Ident {
25 . . . . NamePos: ./_target/hello.go:5:6
26 . . . . Name: "main"
27 . . . . Obj: *ast.Object {
28 . . . . . Kind: func
29 . . . . . Name: "main"
30 . . . . . Decl: *(obj @ 23)
31 . . . . }
32 . . . }
33 . . . Type: *ast.FuncType {
34 . . . . Func: ./_target/hello.go:5:1
35 . . . . Params: *ast.FieldList {
36 . . . . . Opening: ./_target/hello.go:5:10
37 . . . . . Closing: ./_target/hello.go:5:11
38 . . . . }
39 . . . }
40 . . . Body: *ast.BlockStmt {
41 . . . . Lbrace: ./_target/hello.go:5:13
42 . . . . List: []ast.Stmt (len = 2) {
43 . . . . . 0: *ast.AssignStmt {
44 . . . . . . Lhs: []ast.Expr (len = 1) {
45 . . . . . . . 0: *ast.Ident {
46 . . . . . . . . NamePos: ./_target/hello.go:7:2
47 . . . . . . . . Name: "name"
48 . . . . . . . . Obj: *ast.Object {
49 . . . . . . . . . Kind: var
50 . . . . . . . . . Name: "name"
51 . . . . . . . . . Decl: *(obj @ 43)
52 . . . . . . . . }
53 . . . . . . . }
54 . . . . . . }
55 . . . . . . TokPos: ./_target/hello.go:7:7
56 . . . . . . Tok: :=
57 . . . . . . Rhs: []ast.Expr (len = 1) {
58 . . . . . . . 0: *ast.BasicLit {
59 . . . . . . . . ValuePos: ./_target/hello.go:7:10
60 . . . . . . . . Kind: STRING
61 . . . . . . . . Value: "\"Alice\""
62 . . . . . . . }
63 . . . . . . }
64 . . . . . }
65 . . . . . 1: *ast.ExprStmt {
66 . . . . . . X: *ast.CallExpr {
67 . . . . . . . Fun: *ast.Ident {
68 . . . . . . . . NamePos: ./_target/hello.go:9:2
69 . . . . . . . . Name: "hello"
70 . . . . . . . . Obj: *ast.Object {
71 . . . . . . . . . Kind: func
72 . . . . . . . . . Name: "hello"
73 . . . . . . . . . Decl: *ast.FuncDecl {
74 . . . . . . . . . . Doc: *ast.CommentGroup {
75 . . . . . . . . . . . List: []*ast.Comment (len = 1) {
76 . . . . . . . . . . . . 0: *ast.Comment {
77 . . . . . . . . . . . . . Slash: ./_target/hello.go:12:1
78 . . . . . . . . . . . . . Text: "// helloは挨拶を表示する関数です。"
79 . . . . . . . . . . . . }
80 . . . . . . . . . . . }
81 . . . . . . . . . . }
82 . . . . . . . . . . Name: *ast.Ident {
83 . . . . . . . . . . . NamePos: ./_target/hello.go:13:6
84 . . . . . . . . . . . Name: "hello"
85 . . . . . . . . . . . Obj: *(obj @ 70)
86 . . . . . . . . . . }
87 . . . . . . . . . . Type: *ast.FuncType {
88 . . . . . . . . . . . Func: ./_target/hello.go:13:1
89 . . . . . . . . . . . Params: *ast.FieldList {
90 . . . . . . . . . . . . Opening: ./_target/hello.go:13:11
91 . . . . . . . . . . . . List: []*ast.Field (len = 1) {
92 . . . . . . . . . . . . . 0: *ast.Field {
93 . . . . . . . . . . . . . . Names: []*ast.Ident (len = 1) {
94 . . . . . . . . . . . . . . . 0: *ast.Ident {
95 . . . . . . . . . . . . . . . . NamePos: ./_target/hello.go:13:12
96 . . . . . . . . . . . . . . . . Name: "name"
97 . . . . . . . . . . . . . . . . Obj: *ast.Object {
98 . . . . . . . . . . . . . . . . . Kind: var
99 . . . . . . . . . . . . . . . . . Name: "name"
100 . . . . . . . . . . . . . . . . . Decl: *(obj @ 92)
101 . . . . . . . . . . . . . . . . }
102 . . . . . . . . . . . . . . . }
103 . . . . . . . . . . . . . . }
104 . . . . . . . . . . . . . . Type: *ast.Ident {
105 . . . . . . . . . . . . . . . NamePos: ./_target/hello.go:13:17
106 . . . . . . . . . . . . . . . Name: "string"
107 . . . . . . . . . . . . . . }
108 . . . . . . . . . . . . . }
109 . . . . . . . . . . . . }
110 . . . . . . . . . . . . Closing: ./_target/hello.go:13:23
111 . . . . . . . . . . . }
112 . . . . . . . . . . }
113 . . . . . . . . . . Body: *ast.BlockStmt {
114 . . . . . . . . . . . Lbrace: ./_target/hello.go:13:25
115 . . . . . . . . . . . List: []ast.Stmt (len = 1) {
116 . . . . . . . . . . . . 0: *ast.ExprStmt {
117 . . . . . . . . . . . . . X: *ast.CallExpr {
118 . . . . . . . . . . . . . . Fun: *ast.SelectorExpr {
119 . . . . . . . . . . . . . . . X: *ast.Ident {
120 . . . . . . . . . . . . . . . . NamePos: ./_target/hello.go:14:2
121 . . . . . . . . . . . . . . . . Name: "fmt"
122 . . . . . . . . . . . . . . . }
123 . . . . . . . . . . . . . . . Sel: *ast.Ident {
124 . . . . . . . . . . . . . . . . NamePos: ./_target/hello.go:14:6
125 . . . . . . . . . . . . . . . . Name: "Printf"
126 . . . . . . . . . . . . . . . }
127 . . . . . . . . . . . . . . }
128 . . . . . . . . . . . . . . Lparen: ./_target/hello.go:14:12
129 . . . . . . . . . . . . . . Args: []ast.Expr (len = 2) {
130 . . . . . . . . . . . . . . . 0: *ast.BasicLit {
131 . . . . . . . . . . . . . . . . ValuePos: ./_target/hello.go:14:13
132 . . . . . . . . . . . . . . . . Kind: STRING
133 . . . . . . . . . . . . . . . . Value: "\"Hello, %s!\\n\""
134 . . . . . . . . . . . . . . . }
135 . . . . . . . . . . . . . . . 1: *ast.Ident {
136 . . . . . . . . . . . . . . . . NamePos: ./_target/hello.go:14:29
137 . . . . . . . . . . . . . . . . Name: "name"
138 . . . . . . . . . . . . . . . . Obj: *(obj @ 97)
139 . . . . . . . . . . . . . . . }
140 . . . . . . . . . . . . . . }
141 . . . . . . . . . . . . . . Ellipsis: -
142 . . . . . . . . . . . . . . Rparen: ./_target/hello.go:14:33
143 . . . . . . . . . . . . . }
144 . . . . . . . . . . . . }
145 . . . . . . . . . . . }
146 . . . . . . . . . . . Rbrace: ./_target/hello.go:15:1
147 . . . . . . . . . . }
148 . . . . . . . . . }
149 . . . . . . . . }
150 . . . . . . . }
151 . . . . . . . Lparen: ./_target/hello.go:9:7
152 . . . . . . . Args: []ast.Expr (len = 1) {
153 . . . . . . . . 0: *ast.Ident {
154 . . . . . . . . . NamePos: ./_target/hello.go:9:8
155 . . . . . . . . . Name: "name"
156 . . . . . . . . . Obj: *(obj @ 48)
157 . . . . . . . . }
158 . . . . . . . }
159 . . . . . . . Ellipsis: -
160 . . . . . . . Rparen: ./_target/hello.go:9:12
161 . . . . . . }
162 . . . . . }
163 . . . . }
164 . . . . Rbrace: ./_target/hello.go:10:1
165 . . . }
166 . . }
167 . . 2: *(obj @ 73)
168 . }
169 . FileStart: ./_target/hello.go:1:1
170 . FileEnd: ./_target/hello.go:15:3
171 . Scope: *ast.Scope {
172 . . Objects: map[string]*ast.Object (len = 2) {
173 . . . "main": *(obj @ 27)
174 . . . "hello": *(obj @ 70)
175 . . }
176 . }
177 . Imports: []*ast.ImportSpec (len = 1) {
178 . . 0: *(obj @ 12)
179 . }
180 . Unresolved: []*ast.Ident (len = 2) {
181 . . 0: *(obj @ 104)
182 . . 1: *(obj @ 119)
183 . }
184 . Comments: []*ast.CommentGroup (len = 2) {
185 . . 0: *ast.CommentGroup {
186 . . . List: []*ast.Comment (len = 1) {
187 . . . . 0: *ast.Comment {
188 . . . . . Slash: ./_target/hello.go:6:2
189 . . . . . Text: "// 名前"
190 . . . . }
191 . . . }
192 . . }
193 . . 1: *(obj @ 74)
194 . }
195 . GoVersion: ""
196 }
*ast.File
をルートとして、なんとなくパッケージ宣言(Package
とName
の部分)や、import
宣言(Decls
の0番目)、main
関数(Decls
の1番目)らしきものが並んでいるのが見て取れるかと思います。
興味がある方は自分で実行したり「完全な出力」を眺めて、hello
関数や変数なども探ってみてください。
コードの説明
(1) token.FileSet
の作成
fset := token.NewFileSet() // (1)
まず、(1)のtoken.NewFileSet()
でtoken.FileSet
を作成します。
これは解析したファイルの行などの情報を記録するための入れ物です。
ファイルを解析するときに併せて指定し、ファイルの情報を蓄積させていく仕組みになっています(複数のファイル情報を格納できます)。
このあとの処理で得られるASTの各ノードは、ファイル上の位置情報を直接は持っておらず、このtoken.FileSet
内のインデックスのような整数で持っています。
そのため、「この関数は何というファイルの何行目、何桁目か」のような位置を得るには、token.FileSet
を使って変換する必要があります。
(2) ファイルの解析
f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) // (2)
if err != nil {
return err
}
続いて、(2)ではparser.ParseFile()
を使ってファイルを解析しています。
引数は次のようになっています。
- 第1引数: ファイルの情報を格納するための
token.FileSet
を指定します- (1)で作成したものを渡します
- 第2引数: 解析対象のファイル名を指定します
- 第3引数: ソースコードを指定しますが、いくつかの指定方法があります
-
nil
を指定すると第2引数で指定したファイルを読み込んで処理してくれます - その他に
string
や[]byte
、io.Reader
を指定することができ、その場合、第2引数のファイル名は位置情報の記録にだけ使用されます
-
- 第4引数: 解析のモードを指定します
- 例えばコメントも解析対象にするには
parser.ParseComments
を指定します - 他にはこのようなモードが定義されています(詳細は割愛します)
const ( PackageClauseOnly Mode = 1 << iota // stop parsing after package clause ImportsOnly // stop parsing after import declarations ParseComments // parse comments and add them to AST Trace // print a trace of parsed productions DeclarationErrors // report declaration errors SpuriousErrors // same as AllErrors, for backward-compatibility SkipObjectResolution // skip deprecated identifier resolution; see ParseFile AllErrors = SpuriousErrors // report all errors (not just the first 10 on different lines) )
- 例えばコメントも解析対象にするには
戻り値は*ast.File
というファイル情報のノードになります。
解析エラーの場合はもちろんerror
が返ってくるのですが、*ast.File
がnil
にならずに返ってくることがあります。
例えば途中に構文エラーがあるような場合は、error
と共に該当箇所だけast.BadStmt
などになったツリーが返ってきます。
(3) ASTの表示
ast.Print(fset, f) // (3)
これは単にASTをダンプ表示しているだけです。
- 第1引数:
token.FileSet
を指定します-
nil
も渡せますが、その場合は位置情報がインデックスの整数で表示されます
-
- 第2引数: 表示したいノードを指定します(ルートでなくても構いません)
関数一覧を抽出してみる
続いて、特定の種類のノードを処理する方法を見てみたいと思います。
ここでは関数宣言のノードを対象に処理する、という想定で関数一覧を表示してみます。
// :
// main関数は割愛します
// :
func printFunctions(filename string) error {
fmt.Println("-- printFunctions -------------------")
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
return err
}
ast.Inspect(f, func(n ast.Node) bool { // (1)
if v, ok := n.(*ast.FuncDecl); ok { // (2)
pos := fset.Position(v.Pos()) // (3)
fmt.Printf("%s (%s)\n", v.Name, pos)
}
return true
})
return nil
}
これを実行すると、次のような出力が得られます。
-- printFunctions -------------------
main (./_target/hello.go:5:1)
hello (./_target/hello.go:13:1)
先のASTのダンプ表示にも含まれる情報なので何の面白みもありませんが、関数宣言の関数名(main
とhello
)だけ列挙されているのが見て取れます。
コードの説明
(1) ast.Inspect
でノードのトラバース
ast.Inspect(f, func(n ast.Node) bool { // (1)
:
return true
})
ast.Inspect()
を使うと、ノードを順に辿って任意の処理を行うことができます。
引数は次のようになっています。
- 第1引数: ノードを指定します
- 第2引数: ノードを処理する関数を指定します
- ノードを順番に辿りながらこの関数が呼び出されます
- 子ノードの処理が終わったあとなど、
nil
が渡されて呼ばれることもあるので注意してください
- 子ノードの処理が終わったあとなど、
-
true
を返すと、今のノードの子ノードに進みます -
false
を返すと、子ノードには行かずに次の兄弟ノードに進みます
- ノードを順番に辿りながらこの関数が呼び出されます
今回のように、ASTの中から特定のノードに対して処理を行いたいときに利用できます。
(2) ノードの選択
if v, ok := n.(*ast.FuncDecl); ok { // (2)
:
}
ast.Inspect()
に渡した関数は、様々なノードを引数として呼び出されるので、ここで対象としたいノード型に型アサーションして処理するというのがよくあるパターンになるかと思います。
ノードの種類によって処理を分けたい場合はswitch
などでもよいでしょう。
ここでは関数宣言だけを対象としたいので*ast.FuncDecl
としています。
他にどのような型があるか知りたい方は、go/ast
パッケージのドキュメントを御覧ください。
(3) ノードの処理
(2)で*ast.FuncDecl
を得たので、ここから関数名と位置を取得して表示しています。
pos := fset.Position(v.Pos()) // (3)
fmt.Printf("%s (%s)\n", v.Name, pos)
関数名はast.FuncDecl
のName
フィールドで得られました。
ファイル名や位置(行番号など)については、最初のコード解説で出てきたtoken.FileSet
を使って変換します。
v.Pos()
には整数しか入っていないので、それをtoke.FileSet
のPosition()
関数に渡して変換します。
Position()
の戻り値はtoken.Position
型で、次のような定義になっています。
type Position struct {
Filename string // filename, if any
Offset int // offset, starting at 0
Line int // line number, starting at 1
Column int // column number, starting at 1 (byte count)
}
これでファイル名や行番号、桁などの位置情報が得られました。
ソースコードを書き換えてみる
さて、ASTを眺めているだけではつまらないので、今度はコードの書き換えに挑戦してみたいと思います。
ここではhello
という関数名をgreet
に変更してみます。
対象は関数宣言、関数呼び出し、関数宣言についているコメント中の関数名です。
// :
// main関数は割愛します
// :
func renameFunctions(filename string) error {
fmt.Println("-- renameFunctions -------------------")
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
return err
}
ast.Inspect(f, func(n ast.Node) bool {
// 関数宣言に対する処理
if v, ok := n.(*ast.FuncDecl); ok {
if v.Name != nil && v.Name.Name == "hello" { // (1)
v.Name.Name = "greet"
}
if v.Doc != nil { // (2)
for _, doc := range v.Doc.List {
doc.Text = strings.ReplaceAll(doc.Text, "hello", "greet")
}
}
}
// 関数呼び出しに対する処理
if v, ok := n.(*ast.CallExpr); ok { // (3)
if ident, ok := v.Fun.(*ast.Ident); ok {
if ident.Name == "hello" {
ident.Name = "greet"
}
}
}
return true
})
return format.Node(os.Stdout, fset, f) // (4)
}
これを実行すると、次のような出力が得られます。
-- renameFunctions -------------------
package main
import "fmt"
func main() {
// 名前
name := "Alice"
greet(name)
}
// greetは挨拶を表示する関数です。
func greet(name string) {
fmt.Printf("Hello, %s!\n", name)
}
hello
関数がgreet
関数になっていますね。
コードの説明
ast.Inspect()
までの流れは「関数一覧を取得してみる」と同じです。
違いは、ast.Inspect()
内での処理内容です。
(1) 関数宣言の関数名を変更
if v, ok := n.(*ast.FuncDecl); ok {
if v.Name != nil && v.Name.Name == "hello" { // (1)
v.Name.Name = "greet"
}
:
}
関数宣言(*ast.FuncDecl
)のName
がhello
であれば、greet
に更新しています。
v.Name.Name
としているのは、v.Name
がただのstring
ではなく、*ast.Ident
型だからです。
type Ident struct {
NamePos token.Pos // identifier position
Name string // identifier name
Obj *Object // denoted object, or nil. Deprecated: see Object.
}
(2) 関数宣言についているコメントを置換
続いて、同関数宣言に付随しているDoc
フィールドにより、関数宣言についているコメントを処理します。
解析時のモードでparser.ParseComments
を付けていないと得られないので注意してください。
if v, ok := n.(*ast.FuncDecl); ok { // (1)
:
if v.Doc != nil { // (2)
for _, doc := range v.Doc.List {
doc.Text = strings.ReplaceAll(doc.Text, "hello", "greet")
}
}
}
Doc
フィールドは*ast.CommentGroup
という型で、コメント(ast.Comment
)がリストで格納されています。
//
で始まる一行コメントの場合は、複数行並べてもひと塊ではなく一行コメントが複数という扱いになります。
/* 〜 */
形式のコメントの場合は、これ一つで1コメントとなります。
type CommentGroup struct {
List []*Comment // len(List) > 0
}
type Comment struct {
Slash token.Pos // position of "/" starting the comment
Text string // comment text (excluding '\n' for //-style comments)
}
これで関数宣言のコメントにアクセスできたので、今回は雑に文字列置換でhello
をgreet
に置換してみました。
(3) 呼び出し関数名の変更
関数名を変えたら呼び出し側も変えないといけないですね。
というわけで、関数呼び出しを表すast.CallExpr
を捕捉して、呼び出し関数名を変更します。
if v, ok := n.(*ast.CallExpr); ok { // (3)
if ident, ok := v.Fun.(*ast.Ident); ok {
if ident.Name == "hello" {
ident.Name = "greet"
}
}
}
ast.CallExpr
は次のような型です。
関数を指し示すものの他に引数や括弧の位置などが含まれています。
type CallExpr struct {
Fun Expr // function expression
Lparen token.Pos // position of "("
Args []Expr // function arguments; or nil
Ellipsis token.Pos // position of "..." (token.NoPos if there is no "...")
Rparen token.Pos // position of ")"
}
このFun
を*ast.Ident
に型アサーションして、関数宣言のときと同じようにName
を書き換えています。
今回は例ということで*ast.Ident
として処理しましたが、実際のFun
には他にも何種類かの型が入っている可能性があります。
例えばパッケージや構造体の関数をfoo.Bar()
のように呼び出した場合はast.SelectorExpr
という型だったり、即時関数(func() { ... }()
)はast.FuncLit
という型が入っていたりします。
また、実際のリファクタリングツールのような処理をするには、別ファイルでの呼び出しや変数に代入した関数など、もっと色々と考慮することがあるでしょう。
(4) 変更したASTを書き戻す
return format.Node(os.Stdout, fset, f) // (4)
最後に変更した内容(書き換えたAST)をフォーマットして書き出します。
これにはgo/format
パッケージのNode()
関数を使います。
引数は次のようになっています。
- 第1引数: 出力先となる
io.Writer
- 第2引数: 解析時の
token.FileSet
- 第3引数: 書き出すノード
- コードスニペットのように特定の式だけ書き出すということもできます
今回は標準出力に書き出しましたが、ここを元のファイルのio.Writer
にしてあげれば、変更した内容をファイルに書き戻すことができますね。
おわりに
Goプログラミングはそこそこ長い間やってきたのですが、Goのソースコード自体を構文解析してみるというのは初めての体験でした。
今回は本当に入門レベルの内容ということもありますが、思いの外簡単に処理できそうだということが分かって良かったです。
今後はもっと踏み込んで実践的なツールを作ってみたいと思っています。
ちなみに、より実践的なツールを作る際はgolang.org/x/tools/go/ast/astutil
という便利パッケージを利用することが多いそうです。
こちらについても探っていきたいですね。
-
構文解析というよりはASTからコードを生成するほうが近いかも ↩