Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14493] ファイルの概要

このコミットは、Go言語の標準ライブラリである go/printer パッケージに対する機能拡張と改善を目的としています。具体的には、go/printerast.Decl (宣言) および ast.Stmt (ステートメント) のリストを直接入力として受け入れ、整形できるようになりました。また、printer.ConfigIndent フィールドが追加され、出力されるコード全体のベースとなるインデント量を設定できるようになっています。これにより、特定のコードブロックや部分的なコードスニペットをより柔軟に整形することが可能になります。

コミット

commit 2a982e8e25f35735268711d21c77aaaee75f8366
Author: Robert Griesemer <gri@golang.org>
Date:   Mon Nov 26 13:14:04 2012 -0800

    go/printer: Permit declaration and statement lists as input.
    
    Also: Can set base indentation in printer.Config: all code
    is going to be indented by at least that amount (except for
    raw string literals spanning multiple lines, since their
    values must not change).
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6847086

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/2a982e8e25f35735268711d21c77aaaee75f8366

元コミット内容

go/printer: Permit declaration and statement lists as input.

Also: Can set base indentation in printer.Config: all code
is going to be indented by at least that amount (except for
raw string literals spanning multiple lines, since their
values must not change).

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6847086

変更の背景

Go言語の go/printer パッケージは、Goのソースコードを整形(pretty-print)するためのツールです。これまでの go/printer は、主に完全なGoのソースファイル(*ast.File)を整形することを想定していました。しかし、GoのツールチェインやIDE、あるいはその他のコード生成ツールなどでは、ファイル全体ではなく、特定の宣言のリスト([]ast.Decl)やステートメントのリスト([]ast.Stmt)といった、より小さなASTノードの集合を整形したいというニーズがありました。

このコミットの背景には、以下のような課題と要望があったと考えられます。

  1. 部分的なコード整形への対応: Goのソースファイル全体ではなく、関数本体のステートメントリストや、トップレベルの宣言ブロックなど、コードの一部だけを整形したい場合に、go/printer を直接利用することが困難でした。これに対応するためには、一時的に *ast.File 構造を構築するなどの手間が必要でした。
  2. ベースインデントの制御: コードスニペットを整形する際、そのスニペットが最終的に配置されるコンテキストに応じて、全体に適用されるベースとなるインデント量を指定したいという要望がありました。例えば、関数本体のコードを整形する際に、その関数が既に特定のインデントレベルにある場合、そのインデントを考慮した整形が必要になります。
  3. ツールチェインの柔軟性向上: go/printer がより多様なASTノードの入力に対応することで、Goのツールチェイン(例: go fmtgoimports、各種IDEのフォーマッタ)がより柔軟に、かつ効率的に動作できるようになります。

これらの背景から、go/printer のAPIを拡張し、より汎用的な整形機能を提供することが求められました。

前提知識の解説

このコミットを理解するためには、Go言語の以下の標準ライブラリに関する基本的な知識が必要です。

  1. go/ast パッケージ (Abstract Syntax Tree):

    • go/ast パッケージは、Goのソースコードを抽象構文木(AST)として表現するためのデータ構造を提供します。Goのコンパイラやツール(go fmt など)は、ソースコードをまずASTにパースし、そのASTを操作することで様々な処理を行います。
    • ast.File: Goのソースファイル全体を表すASTノードです。パッケージ宣言、インポート、トップレベルの宣言(変数、定数、関数、型など)を含みます。
    • ast.Decl: 宣言(Declaration)を表すインターフェースです。ast.GenDecl (汎用宣言: import, const, type, var)、ast.FuncDecl (関数宣言) などがあります。
    • ast.Stmt: ステートメント(Statement)を表すインターフェースです。ast.ExprStmt (式ステートメント)、ast.AssignStmt (代入ステートメント)、ast.IfStmt (ifステートメント)、ast.ForStmt (forステートメント) など、Goのプログラムの実行可能な各行やブロックを構成する要素です。
    • ast.Expr: 式(Expression)を表すインターフェースです。リテラル、変数参照、関数呼び出しなどがあります。
  2. go/token パッケージ:

    • go/token パッケージは、Goのソースコード内のトークン(キーワード、識別子、演算子など)と、それらのソースコード上の位置情報(ファイル名、行番号、列番号、オフセット)を扱うための型と定数を提供します。
    • token.FileSet: 複数のソースファイルをまとめて管理し、各トークンの位置情報を一意に特定するためのコンテキストを提供します。
    • token.Pos: ソースコード内の特定の位置を表します。
  3. go/printer パッケージ:

    • go/printer パッケージは、go/ast パッケージで表現されたASTを、Goの標準的なフォーマットルールに従って整形し、ソースコードとして出力する機能を提供します。go fmt コマンドの内部でも利用されています。
    • printer.Config: go/printer の整形動作を制御するための設定構造体です。タブ幅やインデントモードなどを指定できます。

これらのパッケージは、Goのソースコードをプログラム的に解析、操作、生成する上で不可欠な要素です。

技術的詳細

このコミットによる技術的な変更点は大きく分けて2つあります。

  1. go/printer.Fprint 関数が []ast.Decl[]ast.Stmt を入力として受け入れるようになったこと:

    • 以前の Fprint は、*ast.File*CommentedNode、または単一の ast.Expr, ast.Decl, ast.Spec, ast.Stmt のみを直接整形できました。
    • この変更により、Fprintnode 引数に []ast.Decl (宣言のスライス) や []ast.Stmt (ステートメントのスライス) を渡すことができるようになりました。
    • 内部的には、printer.printNode メソッドがこれらの新しい型 ([]ast.Stmt, []ast.Decl) を処理するための case を追加し、それぞれ p.stmtList および p.declList という新しいヘルパーメソッドを呼び出すように変更されています。
    • p.stmtListp.declList は、それぞれのリスト内の要素をループ処理し、適切な改行やインデントを挿入しながら個々のステートメントや宣言を整形します。特に、p.stmtList では、出力の先頭でない場合にのみ改行を挿入するロジックが追加されており、部分的なコード整形時の不要な改行を防いでいます。
    • p.declList は、宣言の種類が変わる場合や、次の宣言にドキュメントコメントが付いている場合に、トップレベルの宣言間に空行を挿入するロジックをカプセル化しています。これは元々 p.file メソッド内にあったロジックを分離したものです。
  2. printer.ConfigIndent フィールドが追加され、ベースインデントを設定できるようになったこと:

    • printer.Config 構造体に Indent int フィールドが追加されました。このフィールドのデフォルト値は 0 です。
    • Indent フィールドに 0 より大きい値を設定すると、go/printer が出力するすべてのコード行(複数行にわたる生文字列リテラルを除く)が、その値の分だけ追加でインデントされます。
    • このベースインデントは、printer.atLineBegin メソッド内で適用されます。atLineBegin は各行の先頭でインデントを書き込む役割を担っており、既存の p.indent (AST構造に基づくインデント) に加えて、p.Config.Indent の値が加算されてインデントが生成されます。
    • 「複数行にわたる生文字列リテラル(raw string literals)」が例外とされるのは、これらのリテラルはGoのコード内で改行やインデントを含めて記述された内容がそのまま文字列値となるため、go/printer が勝手にインデントを追加すると、文字列リテラルの値が変わってしまうという問題があるためです。これはGoの言語仕様と go/printer の設計原則に基づいた重要な考慮事項です。

これらの変更により、go/printer はより柔軟なコード整形機能を提供し、Goのツールチェインにおけるコード操作の可能性を広げています。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更箇所は以下のファイルに集中しています。

  1. src/pkg/go/printer/nodes.go:

    • func (p *printer) stmtList(...) の変更:
      • p.linebreak の呼び出しに len(p.output) > 0 の条件が追加され、出力の先頭でない場合にのみ改行が挿入されるようになりました。これにより、部分的なステートメントリストを整形する際に、先頭に不要な改行が入るのを防ぎます。
    • func (p *printer) file(src *ast.File) の変更:
      • トップレベルの宣言リストを処理するロジックが p.declList という新しいメソッドに抽出されました。
      • p.file は単に p.declList(src.Decls) を呼び出すようになりました。
    • func (p *printer) declList(list []ast.Decl) の追加:
      • 宣言リストをループ処理し、宣言の種類変更やドキュメントコメントの有無に応じて適切な空行を挿入するロジックが実装されました。
  2. src/pkg/go/printer/printer.go:

    • func (p *printer) atLineBegin(pos token.Position) の変更:
      • インデントを書き込むループの回数が p.indent だけでなく、p.Config.Indent + p.indent となり、Config で指定されたベースインデントが適用されるようになりました。
    • func (p *printer) printNode(node interface{}) error の変更:
      • case []ast.Stmt:case []ast.Decl: の新しい case が追加され、それぞれ p.stmtListp.declList を呼び出すようになりました。これにより、Fprint がこれらの型の入力を直接処理できるようになります。
    • type Config struct の変更:
      • Indent int フィールドが追加されました。
    • func (cfg *Config) Fprint(...) のコメント変更:
      • node 引数に []ast.Decl[]ast.Stmt が受け入れられるようになったことを示すようにドキュメントが更新されました。
  3. src/pkg/go/printer/printer_test.go:

    • TestDeclLists 関数の追加:
      • []ast.DeclFprint に渡して整形できることをテストします。
    • TestStmtLists 関数の追加:
      • []ast.StmtFprint に渡して整形できることをテストします。
    • TestBaseIndent 関数の追加:
      • printer.Config.Indent フィールドが正しく機能し、指定されたベースインデントがコード全体に適用されることを検証します。特に、複数行の生文字列リテラルがインデントされないことを確認するコメントが含まれています。

コアとなるコードの解説

src/pkg/go/printer/nodes.go の変更

  • stmtList の改行ロジック改善:

    // src/pkg/go/printer/nodes.go
    // ...
    func (p *printer) stmtList(list []ast.Stmt, nindent int, nextIsRBrace bool) {
        // ...
        if len(p.output) > 0 { // only print line break if we are not at the beginning of the output
            p.linebreak(p.lineFor(s.Pos()), 1, ignore, i == 0 || nindent == 0 || multiLine)
        }
        // ...
    }
    

    この変更は、stmtList が部分的なステートメントリストを整形する際に、出力バッファ p.output が空でない(つまり、これが最初の出力ではない)場合にのみ改行を挿入するようにします。これにより、例えば関数本体のステートメントだけを整形するような場合に、先頭に余分な改行が入るのを防ぎ、より自然な出力が得られます。

  • declList メソッドの導入と file からの分離:

    // src/pkg/go/printer/nodes.go
    // ...
    func (p *printer) declList(list []ast.Decl) {
        tok := token.ILLEGAL
        for _, d := range list {
            prev := tok
            tok = declToken(d)
            // If the declaration token changed (e.g., from CONST to TYPE)
            // or the next declaration has documentation associated with it,
            // print an empty line between top-level declarations.
            if len(p.output) > 0 { // only print line break if we are not at the beginning of the output
                min := 1
                if prev != tok || getDoc(d) != nil {
                    min = 2
                }
                p.linebreak(p.lineFor(d.Pos()), min, ignore, false)
            }
            p.decl(d)
        }
    }
    
    func (p *printer) file(src *ast.File) {
        p.setComment(src.Doc)
        p.print(src.Pos(), token.PACKAGE, blank)
        p.expr(src.Name)
        p.declList(src.Decls) // Call the new declList method
        p.print(newline)
    }
    

    declList は、トップレベルの宣言(ast.Decl)のリストを整形するための新しいヘルパーメソッドです。以前は file メソッド内に直接記述されていた、宣言の種類(const, type, var, func)が変わる場合や、次の宣言にドキュメントコメントが付いている場合に空行を挿入するロジックがここに移動されました。これにより、コードの関心事が分離され、file メソッドはより簡潔になりました。stmtList と同様に、len(p.output) > 0 の条件で不要な先頭の改行を防いでいます。

src/pkg/go/printer/printer.go の変更

  • ベースインデントの適用:

    // src/pkg/go/printer/printer.go
    // ...
    func (p *printer) atLineBegin(pos token.Position) {
        // ...
        n := p.Config.Indent + p.indent // include base indentation
        for i := 0; i < n; i++ {
            p.output = append(p.output, '\t')
        }
        // ...
    }
    

    atLineBegin は、各行の先頭でインデントを書き込む際に呼び出されます。ここで、p.Config.Indent (新しく追加されたベースインデント) が既存の p.indent (AST構造に基づくインデント) に加算され、その合計値 n の分だけタブ文字 (\t) が出力バッファに追加されます。これにより、Config.Indent で指定された値が、出力されるコード全体の最小インデントとして機能します。

  • printNode での新しい型 ([]ast.Stmt, []ast.Decl) のハンドリング:

    // src/pkg/go/printer/printer.go
    // ...
    func (p *printer) printNode(node interface{}) error {
        // ...
        case []ast.Stmt:
            // A labeled statement will un-indent to position the label.
            // Set p.indent to 1 so we don't get indent "underflow".
            for _, s := range n {
                if _, ok := s.(*ast.LabeledStmt); ok {
                    p.indent = 1
                }
            }
            p.stmtList(n, 0, false)
        case []ast.Decl:
            p.declList(n)
        // ...
    }
    

    printNode は、printer.Fprint に渡されたASTノードの型を判別し、適切な整形メソッドを呼び出す中心的なディスパッチャです。この変更により、[]ast.Stmt[]ast.Decl という新しい型が case 文で直接処理されるようになりました。それぞれ、新しく導入された p.stmtListp.declList メソッドが呼び出されます。[]ast.Stmt のケースでは、ラベル付きステートメントのインデントに関する特別な処理(アンダーフローを防ぐために p.indent1 に設定)も含まれています。

  • Config 構造体への Indent フィールド追加:

    // src/pkg/go/printer/printer.go
    // ...
    type Config struct {
        Mode     Mode // default: 0
        Tabwidth int  // default: 8
        Indent   int  // default: 0 (all code is indented at least by this much)
    }
    

    printer.ConfigIndent フィールドが追加されました。このフィールドは、出力されるコードに適用されるベースインデントのレベルをタブ数で指定します。

src/pkg/go/printer/printer_test.go の変更

  • TestDeclLists, TestStmtLists, TestBaseIndent の追加: これらのテストケースは、新しい機能が期待通りに動作することを確認します。
    • TestDeclListsTestStmtLists は、それぞれ宣言リストとステートメントリストを Fprint に渡して整形し、期待される出力と比較します。
    • TestBaseIndent は、異なる Config.Indent 値を設定して printer.go ファイル自体を整形し、各行のインデントが指定されたベースインデント以上になっていることを検証します。特に、複数行の生文字列リテラルがインデントされないという例外ケースも考慮されています。

これらの変更は、go/printer パッケージの汎用性と柔軟性を大幅に向上させ、Goのコード整形ツールやIDEの機能強化に貢献しています。

関連リンク

  • Go Change-list (CL) 6847086: このコミットの元となったGoのコードレビューシステム上の変更リストです。詳細な議論やレビューコメントが含まれています。 https://golang.org/cl/6847086

参考にした情報源リンク