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経由ではなく手で直接作った場合にはこのあたりの情報も適宜与えてやる必要があるということです。大変。