goのast.Fileからファイル名を取得する方法
*ast.File
からファイル名を取得したい
goのファイルのimportなどの情報を取得するのに、直接 go/*
のpackageを使うよりも golang.org/x/tools/go/loader
を使うのが手軽。
pkgname := "golang.org/x/tools/refactor/rename" c := loader.Config{} c.Import(pkgname) prog, _ := c.Load() // これでrename packageに関する情報がいい感じに取得できる pkginfo := prog.Package(pkgname)
(defaultではフルでastがparseされ、型チェックも行われる)一方でこのloader packageで取得した結果はpackage単位の情報になってしまうので、個々のast.Fileのファイル名と対応付けるのに面倒さを感じていた。
以下のようなPackageInfoという型の値が返ってくる。
// PackageInfo holds the ASTs and facts derived by the type-checker // for a single package. // // Not mutated once exposed via the API. // type PackageInfo struct { Pkg *types.Package Importable bool // true if 'import "Pkg.Path()"' would resolve to this TransitivelyErrorFree bool // true if Pkg and all its dependencies are free of errors Files []*ast.File // syntax trees for the package's files Errors []error // non-nil if the package had errors types.Info // type-checker deductions. dir string // package directory checker *types.Checker // transient type-checker state errorFunc func(error) }
*ast.File
の値からそのASTの元のソースコードのファイル名を取得したい。ここで *ast.File
のNameはpackage名なことに注意。
*token.File
からファイル名は取得できる
*ast.File
自身がファイル名を持っていなくてもどこか別の場所にマッピングが保持されていれば取得できる。具体的には、*token.File
からファイル名を取得できる。 *token.File
はfsetから取れる(astなどをいじる時によく使う *token.FileSet
のこと)。
pkginfo := prog.Package(pkgname) fset := prog.Fset // 先頭のファイル名 fset.File(pkginfo.Files[0].Pos()).Name() // 末尾のファイル名 fset.File(pkginfo.Files[len(pkginfo.Files)-1].Pos()).Name()
動作する完全なgoのコードの例
動作する完全なgoのコードの例は以下。
package main import ( "fmt" "go/parser" "log" "os/user" "strings" "golang.org/x/tools/go/loader" ) func p(path string) { u, _ := user.Current() fmt.Println(strings.Replace(path, u.HomeDir, "~", 1)) } func main() { pkgname := "golang.org/x/tools/refactor/rename" c := loader.Config{ ParserMode: parser.PackageClauseOnly, } c.Import(pkgname) prog, err := c.Load() if err != nil { log.Fatal(err) } pkginfo := prog.Package(pkgname) fset := prog.Fset p(fset.File(pkginfo.Files[0].Pos()).Name()) p(fset.File(pkginfo.Files[len(pkginfo.Files)-1].Pos()).Name()) }
結果
~/go/src/golang.org/x/tools/refactor/rename/check.go ~/go/src/golang.org/x/tools/refactor/rename/util.go
goのコードの改行の情報はASTには含まれず、token.FileSetに格納されているという話
ASTを触る際にコメントの取扱いがだるいと一部で話題ですが。コメントの他にも取扱いが面倒なものがあります。それが改行の情報です。というよりもコードの字面上の情報を取り除いて取り扱いやすくした状態がASTなので装飾用の情報が入っていないというのは自然なことかもしれません。
ではどこに入っているのか?というとtoken.FileSetがもつtoken.Fileのlinesと言う場所にあります。
公開されていないフィールドなので取ってくるのがちょっと大変ですが。
// get *token.File from *ast.File tokenf := fset.File(f.Pos()) fmt.Println("lines=", reflect.ValueOf(tokenf).Elem().FieldByName("lines")) // lines= [0 13 14 23 30 43 57 69 76 82 93 94 126 128 129 149 150 164 192 263 283 311 328 345 348 349 370 403 404 442 472 548 549 578 612 669 670 706]
そんなわけで、期待した位置に改行を埋め込みたい場合にはこのlinesをアレコレする必要があります。実際この値を空にすると必要最低限の改行しかされなくなります。ASTをgo/printerなどで出力した場合に。
例
例えば以下の様な自分自身のコードのASTを取り出して出力するコードにlinesを空にする処理を入れてあげます。
package main import ( "fmt" "go/parser" "go/printer" "go/token" "log" "os" "reflect" "golang.org/x/tools/go/loader" ) // toplevel comment // main : func main() { fset := token.NewFileSet() config := loader.Config{Fset: fset, ParserMode: parser.ParseComments} config.Import(".") prog, err := config.Load() if err != nil { log.Fatal(err) } // get *ast.File f := prog.Package(".").Files[0] // get *token.File from *ast.File tokenf := fset.File(f.Pos()) fmt.Println("lines=", reflect.ValueOf(tokenf).Elem().FieldByName("lines")) // remove lines information fset.File(f.Pos()).SetLines(nil) fmt.Println("----------------------------------------") printer.Fprint(os.Stdout, fset, f) }
すると改行がない状態で出力されました。
lines= [0 13 14 23 30 43 57 69 76 82 93 94 126 128 129 149 150 160 174 202 273 293 321 338 355 358 359 377 410 411 446 476 552 553 582 616 673 674 710] ---------------------------------------- package main import ( "fmt" "go/parser" "go/printer" "go/token" "log" "os" "reflect" "golang.org/x/tools/go/loader" ) // toplevel comment // main : func main() { fset := token.NewFileSet() config := loader.Config{Fset: fset, ParserMode: parser.ParseComments} config.Import(".") prog, err := config.Load() if err != nil { log.Fatal(err) }// get *ast.File f := prog.Package(".").Files[0] // get *token.File from *ast.File tokenf := fset.File(f.Pos()) fmt.Println("lines=", reflect.ValueOf(tokenf).Elem().FieldByName("lines")) // remove lines information fset.File(f.Pos()).SetLines(nil) fmt.Println("----------------------------------------") printer.Fprint(os.Stdout, fset, f) }
逆に言うと、純粋なASTをparser経由ではなく手で直接作った場合にはこのあたりの情報も適宜与えてやる必要があるということです。大変。