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