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

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

このコミットは、Go言語の標準ライブラリであるgo/printerパッケージにおけるコード整形ロジックの改善に関するものです。go/printerパッケージは、Goの抽象構文木(AST: Abstract Syntax Tree)を受け取り、それを整形されたGoのソースコードとして出力する役割を担っています。これはgofmtツールの中核をなすコンポーネントであり、Goコードの標準的なフォーマットを維持するために不可欠です。

この変更の主な目的は、コードの複数行にわたる表現を検出するための既存のロジックを簡素化し、より効率的で保守しやすい実装に置き換えることです。具体的には、多くの関数で引数として渡されていたmultiLine *boolというフラグを削除し、ASTノードの開始位置と終了位置から複数行かどうかを判断するように変更しています。

コミット

commit b1b0ed1e60af93bc83298da80d0293a2b23fcb5e
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Feb 29 08:38:31 2012 -0800

    go/printer: replace multiline logic
    
    This CL mostly deletes code.
    
    Using existing position information is
    just as good to determine if a new section
    is needed; no need to track exact multi-
    line information. Eliminates the need to
    carry around a multiLine parameter with
    practically every function.
    
    Applied gofmt -w src misc resulting in only
    a minor change to godoc.go. In return, a couple
    of test cases are now formatted better.
    
    Not Go1-required, but nice-to-have as it will
    simplify fixes going forward.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5706055

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

https://github.com/golang/go/commit/b1b0ed1e60af93bc83298da80d0293a2b23fcb5e

元コミット内容

go/printer: replace multiline logic

This CL mostly deletes code.

Using existing position information is
just as good to determine if a new section
is needed; no need to track exact multi-
line information. Eliminates the need to
carry around a multiLine parameter with
practically every function.

Applied gofmt -w src misc resulting in only
a minor change to godoc.go. In return, a couple
of test cases are now formatted better.

Not Go1-required, but nice-to-have as it will
simplify fixes going forward.

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

変更の背景

この変更の背景には、go/printerパッケージにおけるコード整形ロジックの複雑性の解消があります。以前の実装では、コードの要素(式、宣言、ステートメントなど)が複数行にわたるかどうかを判断するために、multiLine *boolというポインタ型のブーリアンフラグが多くの関数間で引き回されていました。このフラグは、関数が処理するASTノードが複数行にわたる場合にtrueに設定され、その情報に基づいて改行やインデントの調整が行われていました。

しかし、コミットメッセージが示唆するように、このmultiLineパラメータは冗長であり、コードベース全体にわたって多くの関数シグネチャを複雑化させていました。GoのASTノードは、token.Postoken.Endという位置情報を持っており、これらを利用すれば、ノードがソースコードの何行目から何行目までを占めるかを正確に知ることができます。したがって、明示的なmultiLineフラグを渡す代わりに、既存の位置情報から複数行の状態を推論することが可能であると判断されました。

この変更は、コードの削除と簡素化を主眼としており、将来的なバグ修正や機能追加の際に、よりクリーンで理解しやすいコードベースを提供することを目指しています。また、gofmtの出力にもわずかな改善が見られ、一部のテストケースでより良いフォーマットが適用されるようになりました。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の標準ライブラリと概念に関する知識が役立ちます。

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

    • go/astパッケージは、Goのソースコードを解析して抽象構文木(AST)を構築するためのデータ構造と関数を提供します。ASTは、プログラムの構造を木構造で表現したもので、コンパイラやコード分析ツール、そしてgofmtのようなコード整形ツールにとっての入力となります。
    • ASTの各ノードは、ast.Nodeインターフェースを実装しており、Pos()メソッドとEnd()メソッドを通じて、それがソースコードのどの位置(開始と終了)に対応するかを返します。
  2. go/tokenパッケージ (トークンと位置情報):

    • go/tokenパッケージは、Goのソースコードを構成する個々の要素(キーワード、識別子、演算子など)である「トークン」と、それらのトークンがソースコード内のどこに位置するかを示す「位置情報」を扱います。
    • token.Posはソースコード内の絶対位置を表し、token.Positionはファイル名、行番号、列番号などの人間が読める形式の位置情報を提供します。token.FileSetは、複数のファイルにわたる位置情報を管理するために使用されます。
    • printerパッケージは、これらの位置情報を使用して、整形されたコードの改行やインデントを決定します。
  3. go/printerパッケージの役割:

    • go/printerパッケージは、go/astパッケージによって生成されたASTを受け取り、それをGoの標準的なフォーマット規則に従って整形されたソースコード文字列に変換する役割を担います。gofmtコマンドはこのパッケージを利用しています。
    • コード整形では、改行、インデント、空白の挿入などが重要になります。特に、式や宣言が複数行にわたる場合に、どのように整形するかは、コードの可読性に大きく影響します。
  4. コードフォーマッタにおける「複数行」の概念と整形への影響:

    • コードフォーマッタは、要素が単一行に収まるか、複数行にわたるかによって、異なる整形ルールを適用することがよくあります。例えば、関数呼び出しの引数リストが長い場合、各引数を新しい行に配置し、適切にインデントすることが一般的です。
    • この「複数行」の判定は、整形ロジックの複雑さに直結します。以前はmultiLineフラグで明示的に伝達していましたが、このコミットではASTノードの位置情報からこの状態を推論するように変更されました。

技術的詳細

このコミットの技術的な核心は、go/printerパッケージ内の多くの関数からmultiLine *boolパラメータを削除し、代わりにASTノードの開始位置と終了位置から複数行の情報を動的に判断する新しいアプローチを採用した点にあります。

multiLine *bool パラメータの問題点

以前のgo/printerの実装では、identList, exprList, parameters, signature, binaryExpr, expr1, stmt, decl, funcDecl, funcBody, valueSpec, spec, genDecl, fileなど、ASTノードを処理して整形するほぼすべての主要な関数がmultiLine *boolというポインタ型のブーリアンパラメータを受け取っていました。

このパラメータの目的は、現在処理しているASTノード(またはその一部)がソースコード上で複数行にわたる場合に、呼び出し元にその情報を伝えることでした。例えば、exprListが複数行にわたる式リストを整形した場合、multiLineポインタが指す値をtrueに設定し、その情報がさらに上位の関数に伝播されることで、適切な改行やインデントが適用される仕組みでした。

しかし、このアプローチには以下の問題がありました。

  • APIの複雑化: 多くの関数シグネチャに冗長なmultiLine *boolパラメータが存在し、コードの可読性と保守性を低下させていました。
  • 状態の引き回し: multiLineの状態を関数呼び出しスタックを通じて明示的に引き回す必要があり、ロジックが複雑になりがちでした。
  • ポインタの利用: ブーリアン値をポインタで渡すことで、値渡しに比べてわずかなオーバーヘッドが生じる可能性がありました(ただし、これはパフォーマンス上の大きな問題ではなかったでしょう)。

新しいアプローチ:位置情報からの推論

コミットメッセージが述べているように、「既存の位置情報を使用するだけで、新しいセクションが必要かどうかを判断するのに十分」であるという考えに基づき、multiLineパラメータは削除されました。

新しいアプローチでは、printer構造体にisMultiLineという新しいヘルパーメソッドが追加されました。このメソッドはast.Nodeを受け取り、そのノードがソースコード上で複数行にわたるかどうかを、ノードの開始位置 (n.Pos()) と終了位置 (n.End()) の行番号を比較することで判断します。

func (p *printer) isMultiLine(n ast.Node) bool {
	return p.lineFor(n.End())-p.lineFor(n.Pos()) > 1
}
  • p.lineFor(pos token.Pos): このprinterメソッドは、与えられたtoken.Posに対応する行番号を返します。
  • p.lineFor(n.End()) - p.lineFor(n.Pos()) > 1: ノードの終了位置の行番号から開始位置の行番号を引いた結果が1より大きい場合、そのノードは複数行にわたると判断されます。例えば、開始が1行目、終了が2行目であれば差は1ですが、これは2行にわたることを意味します。差が0であれば単一行、差が1であれば2行にわたる、というように解釈できます。したがって、> 1は3行以上、または開始行と終了行を含めて2行以上を意味します。正確には、p.lineFor(n.End()) > p.lineFor(n.Pos())であれば複数行と判断できます。コミットのコードでは> 1となっているため、厳密には3行以上の場合にtrueを返すことになりますが、これはgo/printerの内部的な整形ロジックの要件に合わせたものと考えられます。

このisMultiLineメソッドの導入により、各関数はmultiLineフラグを明示的に受け渡す必要がなくなり、必要に応じて自身の内部でisMultiLineを呼び出すことで、現在のノードが複数行かどうかを判断できるようになりました。

ignoreMultiLine変数の削除

src/pkg/go/printer/printer.goファイルには、ignoreMultiLineというグローバル変数が存在していました。これは、multiLine情報が不要な場合にダミーとして渡すための*boolポインタでした。multiLineパラメータが削除されたことで、このダミー変数も不要となり、削除されました。

gofmtへの影響とテストケースの改善

この変更は、gofmtの出力に直接的な影響を与えました。コミットメッセージには「Applied gofmt -w src misc resulting in only a minor change to godoc.go. In return, a couple of test cases are now formatted better.」とあります。これは、gofmtを適用した結果、godoc.goにわずかな変更があったものの、いくつかのテストケース(特にdeclarations.goldenのような整形結果を比較するテスト)では、より適切なフォーマットが適用されるようになったことを示しています。これは、新しい複数行検出ロジックが、より正確な整形を可能にした結果と言えます。

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

このコミットでは、主にsrc/pkg/go/printer/nodes.gosrc/pkg/go/printer/printer.goの2つのファイルが大きく変更されています。

src/pkg/go/printer/nodes.go

  • isMultiLine関数の追加:

    func (p *printer) isMultiLine(n ast.Node) bool {
    	return p.lineFor(n.End())-p.lineFor(n.Pos()) > 1
    }
    

    この関数が、ASTノードが複数行にわたるかを判断する新しいロジックの中核です。

  • 多数の関数シグネチャからのmultiLine *boolパラメータの削除: 以下は変更された関数の一部です。multiLine *bool引数が削除され、それに伴い関数内部での*multiLine = trueのような代入も削除されています。

    • func (p *printer) identList(list []*ast.Ident, indent bool, multiLine *bool) -> func (p *printer) identList(list []*ast.Ident, indent bool)
    • func (p *printer) exprList(prev0 token.Pos, list []ast.Expr, depth int, mode exprListMode, multiLine *bool, next0 token.Pos) -> func (p *printer) exprList(prev0 token.Pos, list []ast.Expr, depth int, mode exprListMode, next0 token.Pos)
    • func (p *printer) parameters(fields *ast.FieldList, multiLine *bool) -> func (p *printer) parameters(fields *ast.FieldList)
    • func (p *printer) signature(params, result *ast.FieldList, multiLine *bool) -> func (p *printer) signature(params, result *ast.FieldList)
    • func (p *printer) binaryExpr(x *ast.BinaryExpr, prec1, cutoff, depth int, multiLine *bool) -> func (p *printer) binaryExpr(x *ast.BinaryExpr, prec1, cutoff, depth int)
    • func (p *printer) expr1(expr ast.Expr, prec1, depth int, multiLine *bool) -> func (p *printer) expr1(expr ast.Expr, prec1, depth int)
    • func (p *printer) expr0(x ast.Expr, depth int, multiLine *bool) -> func (p *printer) expr0(x ast.Expr, depth int)
    • func (p *printer) expr(x ast.Expr, multiLine *bool) -> func (p *printer) expr(x ast.Expr)
    • func (p *printer) stmt(stmt ast.Stmt, nextIsRBrace bool, multiLine *bool) -> func (p *printer) stmt(stmt ast.Stmt, nextIsRBrace bool)
    • func (p *printer) decl(decl ast.Decl, multiLine *bool) -> func (p *printer) decl(decl ast.Decl)
    • func (p *printer) funcDecl(d *ast.FuncDecl, multiLine *bool) -> func (p *printer) funcDecl(d *ast.FuncDecl)
    • func (p *printer) funcBody(b *ast.BlockStmt, headerSize int, isLit bool, multiLine *bool) -> func (p *printer) funcBody(b *ast.BlockStmt, headerSize int, isLit bool)
    • func (p *printer) valueSpec(s *ast.ValueSpec, keepType, doIndent bool, multiLine *bool) -> func (p *printer) valueSpec(s *ast.ValueSpec, keepType, doIndent bool)
    • func (p *printer) spec(spec ast.Spec, n int, doIndent bool, multiLine *bool) -> func (p *printer) spec(spec ast.Spec, n int, doIndent bool)
    • func (p *printer) genDecl(d *ast.GenDecl, multiLine *bool) -> func (p *printer) genDecl(d *ast.GenDecl)
    • func (p *printer) file(src *ast.File): src.Nameを処理するp.expr呼び出しからignoreMultiLineが削除。
  • stmtList関数におけるmultiLineの利用方法の変更:

    // 変更前
    var multiLine bool
    for i, s := range list {
        p.linebreak(p.lineFor(s.Pos()), 1, ignore, i == 0 || _indent == 0 || multiLine)
        multiLine = false
        p.stmt(s, nextIsRBrace && i == len(list)-1, &multiLine)
    }
    
    // 変更後
    multiLine := false // multiLineはローカル変数として初期化
    for i, s := range list {
        p.linebreak(p.lineFor(s.Pos()), 1, ignore, i == 0 || _indent == 0 || multiLine)
        // multiLineは次のセクションの開始を決定するために使用される
        p.stmt(s, nextIsRBrace && i == len(list)-1) // multiLine引数が削除された
        multiLine = p.isMultiLine(s) // 新しいisMultiLineを使って更新
    }
    

    ここでは、multiLineがローカル変数として扱われ、各ステートメントの処理後にp.isMultiLine(s)を呼び出して更新されることで、次のステートメントの整形に影響を与えるようになっています。

src/pkg/go/printer/printer.go

  • ignoreMultiLine変数の削除:

    // 削除されたコード
    // Use ignoreMultiLine if the multiLine information is not important.
    // var ignoreMultiLine = new(bool)
    

    この変数は、multiLine *boolパラメータが不要な場合に渡すためのダミーでしたが、パラメータ自体が削除されたため不要になりました。

  • printNode関数におけるignoreMultiLineの利用箇所の変更: p.expr(n, ignoreMultiLine) -> p.expr(n) p.stmt(n, false, ignoreMultiLine) -> p.stmt(n, false) p.decl(n, ignoreMultiLine) -> p.decl(n) p.spec(n, 1, false, ignoreMultiLine) -> p.spec(n, 1, false)

src/cmd/godoc/godoc.go

  • フラグの定義におけるインデントの微調整。これはgofmtの適用による副次的な変更であり、本質的なロジック変更ではありません。

src/pkg/go/printer/testdata/declarations.golden

  • 整形結果のテストデータがわずかに変更されています。これは、新しい整形ロジックによって出力が改善されたことを示しています。例えば、intfloatの前のタブの数が変わっています。

コアとなるコードの解説

このコミットのコアとなる変更は、go/printerパッケージがコードの複数行の状態をどのように扱うかという根本的なアプローチの変更です。

isMultiLine関数の役割

新しく導入されたfunc (p *printer) isMultiLine(n ast.Node) bool関数は、この変更の要です。この関数は、与えられたASTノードnがソースコード上で複数行にわたるかどうかを、ノードの開始位置 (n.Pos()) と終了位置 (n.End()) を比較することで判断します。

具体的には、p.lineFor(pos token.Pos)というヘルパーメソッドを使って、各位置の行番号を取得し、p.lineFor(n.End()) - p.lineFor(n.Pos()) > 1という条件で複数行かどうかを判定します。このシンプルな比較により、明示的なmultiLineフラグを関数間で引き回す必要がなくなりました。

multiLineパラメータの削除による簡素化

以前は、multiLine *boolというポインタが多くの関数シグネチャに存在し、その値を更新することで複数行の状態を伝播させていました。例えば、p.expr(x, multiLine)のように呼び出し、expr関数内で*multiLine = trueと設定していました。

この変更により、これらのパラメータが削除されたことで、関数シグネチャが大幅に簡素化されました。例えば、p.expr(x)のように、よりクリーンな呼び出しが可能になりました。これにより、go/printerパッケージの内部APIがより理解しやすくなり、将来的な開発やデバッグが容易になります。

stmtListにおけるmultiLineの新しい使い方

stmtList関数(ステートメントのリストを整形する関数)では、multiLineというローカル変数が導入され、各ステートメントの整形後にp.isMultiLine(s)を呼び出してその値が更新されるようになりました。

multiLine := false // multiLineはローカル変数として初期化
for i, s := range list {
    // linebreakの第4引数 (newSection) に multiLine を渡す
    p.linebreak(p.lineFor(s.Pos()), 1, ignore, i == 0 || _indent == 0 || multiLine)
    p.stmt(s, nextIsRBrace && i == len(list)-1)
    multiLine = p.isMultiLine(s) // 現在のステートメントが複数行なら multiLine を true に更新
}

ここでlinebreak関数の第4引数newSectionは、新しいセクション(例えば、新しいステートメントや宣言)が開始される際に、強制的に改行を挿入するかどうかを制御します。以前はmultiLineパラメータがこの情報を提供していましたが、新しい実装では、直前のステートメントが複数行にわたっていた場合にmultiLinetrueとなり、次のステートメントの開始時にnewSectionとして機能するようになっています。これにより、コードの論理的な区切りがより適切に表現されるようになります。

この変更は、go/printerの内部ロジックをより洗練させ、ASTノードの構造と位置情報から整形ルールをより自然に導き出すことを可能にしました。

関連リンク

参考にした情報源リンク