golangの現在のファイルのimport関係に即したTypeStringの表記を作る

TypeString?

go/typesにQuqlifierという型がある。これと一緒にTypeStringという関数がある(いずれも、GOROOT/src/go/types/typestring.goに定義されている)。これらは型情報付きの表記をするために使われる。

type Qualifier func(*Package) string

// TypeString returns the string representation of typ.
// The Qualifier controls the printing of
// package-level objects, and may be nil.
func TypeString(typ Type, qf Qualifier) string {
    var buf bytes.Buffer
    WriteType(&buf, typ, qf)
    return buf.String()
}

Qualifierの例としては、go/typesのRelativeTo()という関数がわかりやすい。

// RelativeTo(pkg) returns a Qualifier that fully qualifies members of
// all packages other than pkg.
func RelativeTo(pkg *Package) Qualifier {
    if pkg == nil {
        return nil
    }
    return func(other *Package) string {
        if pkg == other {
            return "" // same package; unqualified
        }
        return other.Path()
    }
}

実装を見ればわかるとおり元のpackageと共通なら空文字でそうでなければpackageのpath名を付記した表現になる。

例えば golang.org/x/tools/go/loaderConfig を同一package上で使ったらどうなるか。あるいは異なるpackage(e.g. main)で使ったらどうなるかということを表せる

package main

import (
    "fmt"
    "go/types"

    xloader "golang.org/x/tools/go/loader"
)

func main() {
    conf := xloader.Config{}
    conf.Import(".")
    conf.Import("io")
    info, _ := conf.Load()

    loaderPkg := info.Package("golang.org/x/tools/go/loader").Pkg
    loaderConfig := loaderPkg.Scope().Lookup("Config")

    mainPkg := info.Package(".").Pkg

    {
        fmt.Println("in main package")
        qf := types.RelativeTo(mainPkg)

        fmt.Println("    ", types.TypeString(loaderConfig.Type(), qf))
      // main packageの中では golang.org/x/tools/go/loader.Config
    }
    {
        fmt.Println("in loader package")
        qf := types.RelativeTo(loaderPkg)
        fmt.Println("    ", types.TypeString(loaderConfig.Type(), qf))
      // golang.org/x/tools/go/loaderの中では Config
    }
}

面白いのはPointer型などでも良い感じに表記が得られる点。

// pointer *golang.org/x/tools/go/loader.Config
fmt.Println("    ", types.TypeString(types.NewPointer(loaderConfig.Type()), qf))

// slices []golang.org/x/tools/go/loader.Config
fmt.Println("    ", types.TypeString(types.NewSlice(loaderConfig.Type()), qf))

// map map[string]*golang.org/x/tools/go/loader.Config
fmt.Println("    ", types.TypeString(
    types.NewMap(
        types.Universe.Lookup("string").Type(),
        types.NewPointer(loaderConfig.Type()),
    ),
    qf,
))

似たような仕組みを使って良い感じの表記を作りたい

例えば通常importを使ってpackageから取り出したsymbolを使う場合にはimport nameに.を付けて使う。

import "golang.org/x/tools/go/loader"

// 使うときには
loader.Config

また名前付きでimportしたときにはそれが使われる。

import xloader "golang.org/x/tools/go/loader"

// 使うときには
xloader.Config

このimport nameの部分を意識せず使えるようになったら便利なような気がしていた。それでやってみようというのが今回のテーマ。

import nameなどの取得

importされたpackageに関してはファイル単位で異なるので、ast.Fileから見る必要がある。 各ast.FileのImportsのなかにimportされた用のImportSpecが入っているのでそれを取り出す。named importされた場合にはImportSpec.Nameがnil以外になる。

/*
これが元

import (
   "fmt"
   "go/types"

   xloader "golang.org/x/tools/go/loader"
)
*/

f := info.Package(".").Files[0]
for _, is := range f.Imports {
    fmt.Printf("name=%s path=%s\n", is.Name, is.Path.Value)
}
fmt.Println(f.Name)

// name=<nil> path="fmt"
// name=<nil> path="go/types"
// name=xloader path="golang.org/x/tools/go/loader"

2つを組み合わせてimport関係に即したTypeStringの表記を作る

上の2つを組み合わせて、あるファイルにおけるあるpackageに属するsymbolの表記を計算させることができる。 RelativeToと同様に同じパッケージの場合には空文字列。自身のast.Fileのimportsを見てprefixを決める(ここはキャッシュしても良いかもしれない)

すると例えばgolang.org/x/tools/go/loader をxloaderというimport nameでimportした場合のxloader.Configなどが取得できるようになる。

// .上ではxloaderという名前でimportされている
qf := NameTo(info.Package(".").Pkg, info.Package(".").Files[0])
// xloader.Config
fmt.Println(types.TypeString(loaderConfig.Type(), qf))

これが何かコード生成をする時に使えるかもしれない。これで任意のファイル上でのコード生成みたいな処理が書きやすくなる気がする。

NameToというのは以下の様な実装。

// NameTo :
func NameTo(pkg *types.Package, f *ast.File) types.Qualifier {
    return func(other *types.Package) string {
        if pkg == other {
            return "" // same package; unqualified
        }
        // todo: cache
        for _, is := range f.Imports {
            if is.Path.Value[1:len(is.Path.Value)-1] == other.Path() {
                if is.Name != nil {
                    return is.Name.String()
                }
                return other.Name()
            }
        }
        return other.Name() // todo: add import
    }
}

(ただもう少し利用方法など考えたほうが良い)

code

このコードを実行すると、このコードのソースコードをparseして、このコードがimportした文脈で golang.org/x/tools/go/loader の Configの表記を出力する。

package main

import (
    "fmt"
    "go/ast"
    "go/types"

    xloader "golang.org/x/tools/go/loader"
)

func main() {
    conf := xloader.Config{}
    conf.Import(".")
    info, _ := conf.Load()

    loaderPkg := info.Package("golang.org/x/tools/go/loader").Pkg
    loaderConfig := loaderPkg.Scope().Lookup("Config")

    qf := NameTo(info.Package(".").Pkg, info.Package(".").Files[0])
    fmt.Println(types.TypeString(loaderConfig.Type(), qf))
}

// NameTo :
func NameTo(pkg *types.Package, f *ast.File) types.Qualifier {
    return func(other *types.Package) string {
        if pkg == other {
            return "" // same package; unqualified
        }
        // todo: cache
        for _, is := range f.Imports {
            if is.Path.Value[1:len(is.Path.Value)-1] == other.Path() {
                if is.Name != nil {
                    return is.Name.String()
                }
                return other.Name()
            }
        }
        return other.Name() // todo: add import
    }
}

実行結果

xloader.Config