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

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

このコミットは、Go言語のgo/printerパッケージにおけるコードフォーマットの挙動を改善するものです。特に、一行に収まる関数(one-line functions)のコメントの扱いを洗練し、不必要な改行や空白の挿入を防ぐことを目的としています。これにより、gofmtなどのツールが生成するコードの可読性と一貫性が向上します。

コミット

commit 3d4c12d9d091fcbe3941534af5057d453758650e
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Feb 26 13:39:49 2014 -0800

    go/printer: refine handling of one-line functions

    Functions that "fit" on one line and were on one
    line in the original source are not broken up into
    two lines anymore simply because they contain a comment.

    - Fine-tuned use of separating blanks after /*-style comments, so:

    ( /* extra blank after this comment */ )
    (a int /* no extra blank after this comment*/)

    - Factored out comment state (from printer state) into commentInfo.
    - No impact on $GOROOT/src, misc formatting.

    Fixes #5543.

    LGTM=r
    R=golang-codereviews, r
    CC=golang-codereviews
    https://golang.org/cl/68630043

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

https://github.com/golang/go/commit/3d4c12d9d091fcbe3941534af5057d453758650e

元コミット内容

このコミットは、go/printerパッケージにおける一行関数(one-line functions)の処理を改善します。具体的には、元のソースコードで一行に収まっていた関数が、コメントが含まれているという理由だけで不必要に複数行に分割されることを防ぎます。

主な変更点は以下の通りです。

  • /*-styleコメントの後の空白の挿入ルールを微調整しました。これにより、例えば( /* extra blank after this comment */ )のようにコメントの後に空白が必要な場合と、(a int /* no extra blank after this comment*/)のように不要な場合を区別して処理します。
  • プリンタの状態からコメント関連の状態をcommentInfoという新しい構造体に分離し、コードの整理を行いました。
  • $GOROOT/srcへの影響はなく、その他のフォーマットに関する変更も含まれています。
  • Issue #5543を修正します。

変更の背景

Go言語にはgofmtという標準のコードフォーマッタが存在し、Goコードのスタイルを一貫させる上で非常に重要な役割を担っています。go/printerパッケージは、このgofmtが内部的に利用している、Goの抽象構文木(AST)を整形してソースコードに戻す(pretty-printする)ためのライブラリです。

このコミットが行われる以前は、一行に収まるように書かれた関数であっても、その内部にコメントが含まれていると、go/printerが不必要にその関数を複数行に分割してしまう問題がありました。これは、特に短く簡潔な関数定義において、コードの可読性を損ねる可能性がありました。

例えば、以下のようなコードがあったとします。

func _() { /* comment */ }

これがgo/printerによって以下のようにフォーマットされてしまうことがありました。

func _() { /* comment */
}

このような挙動は、開発者が意図した一行の表現を維持したい場合に不便であり、gofmtの出力が期待と異なるというフィードバック(Issue #5543)があったと考えられます。このコミットは、このような不必要な改行を防ぎ、より賢明なフォーマットルールを適用することで、go/printerの出力を改善し、開発者の期待に応えることを目的としています。

前提知識の解説

Go言語のgo/printerパッケージ

go/printerパッケージは、Go言語の標準ライブラリの一部であり、Goの抽象構文木(AST)を「整形して出力する」(pretty-print)機能を提供します。これは、go/parserパッケージがソースコードを解析してASTを生成するのとは逆のプロセスです。gofmtツールは、このgo/printerパッケージを内部的に利用して、Goのソースコードを標準的なスタイルに自動的に整形しています。

go/printerの主な役割は以下の通りです。

  • ASTからソースコードへの変換: go/parserによって生成されたASTを受け取り、それを人間が読めるGoのソースコード文字列に変換します。
  • フォーマットの適用: Goの公式スタイルガイド(Go Fmt)に従って、インデント、空白、改行などのフォーマットルールを適用します。これにより、異なる開発者が書いたコードでも一貫した見た目になります。
  • コメントの扱い: ソースコード内のコメントも適切に配置し、フォーマットの一部として扱います。

抽象構文木(AST: Abstract Syntax Tree)

ASTは、プログラミング言語のソースコードの抽象的な構文構造を木構造で表現したものです。コンパイラやインタプリタがソースコードを処理する際の中間表現として広く利用されます。Go言語では、go/astパッケージがASTの型定義を提供し、go/parserパッケージがソースコードを解析してASTを構築します。

ASTの各ノードは、変数宣言、関数定義、式、文などの言語構造に対応します。ASTは、ソースコードの具体的な構文(括弧、セミコロンなど)を抽象化し、プログラムの意味的な構造に焦点を当てます。これにより、コードの分析、変換、生成などの操作が容易になります。

go/printerは、このASTをトラバースし、各ノードに対応するGoの構文要素を適切なフォーマットで出力することで、最終的なソースコードを生成します。コメントはASTの一部として扱われ、go/printerはコメントが元のコードのどの位置にあったかを考慮して、整形後のコードにコメントを再配置します。

/*-styleコメントと//-styleコメント

Go言語には2種類のコメントスタイルがあります。

  • //-styleコメント: 行の残りの部分をコメントアウトします。通常、コードの行末や単独の行で使用されます。
  • /*-styleコメント: /*で始まり*/で終わるブロックコメントです。複数行にわたるコメントや、コードの途中に挿入される短いコメントによく使用されます。

このコミットでは、特に/*-styleコメントの後の空白の扱いに焦点を当てています。これは、/*-styleコメントがコードの途中に挿入されることが多く、その前後の空白がコードの見た目に大きく影響するためです。

技術的詳細

このコミットの技術的な核心は、go/printerがASTを整形する際の、特にコメントと空白、そして一行関数の判断ロジックの改善にあります。

  1. 一行関数の判断ロジックの改善: 以前は、関数本体(ast.BlockStmt)内にコメントが存在すると、その関数が一行に収まるかどうかを判断するbodySize関数が、コメントの存在だけで「一行に収まらない」と判断してしまう傾向がありました。 変更前:

    if len(b.List) > 5 || p.commentBefore(p.posFor(pos2)) {
        // too many statements or there is a comment inside - don't make it a one-liner
        return maxSize + 1
    }
    

    変更後:

    if len(b.List) > 5 {
        // too many statements - don't make it a one-liner
        return maxSize + 1
    }
    // otherwise, estimate body size
    bodySize := p.commentSizeBefore(p.posFor(pos2)) // コメントのサイズを考慮するが、コメントの存在だけで改行しない
    

    この変更により、コメントの存在自体が一行関数を複数行に分割する直接的な理由ではなくなり、コメントの「サイズ」が考慮されるようになりました。commentSizeBefore関数が導入され、コメントが同じ行に収まる場合にそのサイズを推定し、全体の行サイズがmaxSizeを超えない限り一行に維持されるようになります。

  2. /*-styleコメント後の空白挿入の微調整: go/printerは、コードの要素間に適切な空白を挿入する役割を担っています。特に/*-styleコメントの直後に続くトークンによっては、追加の空白が必要な場合と不要な場合があります。 変更前は、/*-styleコメントの後に続くトークンがカンマ、右括弧、右角括弧、右波括弧でない場合に一律に空白を挿入していました。 変更後:

    if p.mode&noExtraBlank == 0 &&
        last.Text[1] == '*' && p.lineFor(last.Pos()) == next.Line &&
        tok != token.COMMA &&
        (tok != token.RPAREN || p.prevOpen == token.LPAREN) &&
        (tok != token.RBRACK || p.prevOpen == token.LBRACK) {
        p.writeByte(' ', 1)
    }
    

    この変更では、noExtraBlankモードが導入され、明示的に空白挿入を無効にできるようになったほか、右括弧(RPAREN)や右角括弧(RBRACK)が、それぞれ対応する左括弧(LPAREN)や左角括弧(LBRACK)の直後に続く場合に空白を挿入しないという条件が追加されました。これは、例えば( /* comment */ )のような場合に、コメントと右括弧の間に不必要な空白が入るのを防ぐためです。p.prevOpenという新しいフィールドが導入され、直前の「開き」トークンを追跡することで、この判断を可能にしています。

  3. commentInfo構造体の導入: printer構造体からコメント関連の状態(cindex, comment, commentOffset, commentNewline)をcommentInfoという独立した構造体に切り出しました。これにより、printer構造体の責務が明確になり、コードの可読性と保守性が向上します。 commentInfoは、現在のコメントグループのインデックス、コメントグループ自体、そのオフセット、そしてコメントグループが改行を含むかどうかを管理します。

これらの変更により、go/printerはより賢明にコメントと空白を扱い、特に一行関数のフォーマットにおいて、開発者の意図をより正確に反映できるようになりました。

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

このコミットで変更された主要なファイルは以下の通りです。

  • src/pkg/go/printer/nodes.go: ASTノードの整形ロジック、特に複合リテラル(composite literals)やブロックステートメント(block statements)の処理に関する変更が含まれます。一行関数の判断ロジックが修正されました。
  • src/pkg/go/printer/printer.go: go/printerパッケージの主要なロジックが含まれるファイルです。commentInfo構造体の導入、コメント後の空白挿入ロジックの微調整、およびcommentBeforecommentSizeBeforeといったコメント関連のヘルパー関数の変更が含まれます。
  • src/pkg/go/printer/testdata/comments.golden: go/printerのテストデータのうち、コメントの整形結果の期待値(goldenファイル)です。今回の変更による出力の修正が反映されています。
  • src/pkg/go/printer/testdata/comments.input: go/printerのテストデータのうち、コメントの整形入力(inputファイル)です。新しいテストケースが追加されています。
  • src/pkg/go/printer/testdata/declarations.golden: 宣言の整形結果の期待値ファイルです。一行関数のコメントに関するテストケースの出力が更新されています。
  • src/pkg/go/printer/testdata/declarations.input: 宣言の整形入力ファイルです。一行関数のコメントに関するテストケースが追加されています。

コアとなるコードの解説

src/pkg/go/printer/nodes.go

このファイルでは、主にASTノードの整形に関するロジックが定義されています。

  • func (p *printer) expr1(expr ast.Expr, prec1, depth int) 内の変更: 複合リテラル(ast.CompositeLit)の処理において、閉じ波括弧(})の前のコメントに関する改行と空白の挿入モードが調整されました。 以前は、閉じ波括弧の前にコメントがある場合に不必要な改行を避けるためのnoExtraLinebreakモードのみが考慮されていました。 変更後:

    		mode := noExtraLinebreak
    		// do not insert extra blank following a /*-style comment
    		// before the closing '}' unless the literal is empty
    		if len(x.Elts) > 0 {
    			mode |= noExtraBlank
    		}
    		p.print(mode, x.Rbrace, token.RBRACE, mode)
    

    noExtraBlankモードが追加され、リテラルが空でない限り、閉じ波括弧の前の/*-styleコメントの後に余分な空白を挿入しないようになりました。これにより、{ /* comment */ }のような場合に、コメントと閉じ波括弧の間に空白が入るのを防ぎます。

  • func (p *printer) bodySize(b *ast.BlockStmt, maxSize int) int の変更: 関数本体(ast.BlockStmt)が一行に収まるかどうかを推定するロジックが変更されました。 以前は、ステートメントが多すぎる場合(len(b.List) > 5)またはブロック内にコメントがある場合(p.commentBefore(p.posFor(pos2)))に、そのブロックを一行にしないと判断していました。 変更後:

    	if len(b.List) > 5 {
    		// too many statements - don't make it a one-liner
    		return maxSize + 1
    	}
    	// otherwise, estimate body size
    	bodySize := p.commentSizeBefore(p.posFor(pos2))
    	for i, s := range b.List {
    		if bodySize > maxSize {
    			break // no need to continue
    		}
    		// ... (既存のロジック)
    	}
    

    コメントの存在自体が一行にしない理由ではなくなり、代わりにp.commentSizeBeforeを呼び出して、ブロック開始位置の前に存在するコメントの推定サイズをbodySizeに加算するようになりました。これにより、コメントの「サイズ」が考慮され、コメントが短ければ一行に収まる可能性が残ります。

src/pkg/go/printer/printer.go

このファイルはgo/printerパッケージの主要なロジックを含んでいます。

  • pmode 定数の追加:

    const (
    	noExtraBlank     pmode = 1 << iota // disables extra blank after /*-style comment
    	noExtraLinebreak                   // disables extra line break after /*-style comment
    )
    

    noExtraBlankという新しいモードが追加されました。これは、/*-styleコメントの後に余分な空白を挿入するのを無効にするためのフラグです。

  • commentInfo 構造体の導入:

    type commentInfo struct {
    	cindex         int               // current comment index
    	comment        *ast.CommentGroup // = printer.comments[cindex]; or nil
    	commentOffset  int               // = printer.posFor(printer.comments[cindex].List[0].Pos()).Offset; or infinity
    	commentNewline bool              // true if the comment group contains newlines
    }
    

    コメントに関する状態をカプセル化するためのcommentInfo構造体が新しく定義されました。これにより、printer構造体からコメント関連のフィールドが移動し、コードの整理が図られました。

  • printer 構造体の変更:

    type printer struct {
    	// ... (既存のフィールド)
    	impliedSemi bool         // if set, a linebreak implies a semicolon
    	lastTok     token.Token  // last token printed (token.ILLEGAL if it's whitespace)
    	prevOpen    token.Token  // previous non-brace "open" token (, [, or token.ILLEGAL
    	wsbuf       []whiteSpace // delayed white space
    
    	// ... (コメント関連のフィールドが commentInfo に置き換えられた)
    	commentInfo
    	// ... (既存のフィールド)
    }
    

    prevOpenという新しいフィールドが追加されました。これは、直前の「開き」トークン(([など)を記録するために使用され、/*-styleコメント後の空白挿入ロジックで利用されます。また、コメント関連のフィールドがcommentInfo構造体として埋め込まれました。

  • func (p *printer) commentBefore(next token.Position) bool の変更: この関数は、現在のコメントグループが次の位置の前にあり、かつ暗黙のセミコロンを導入しない場合にtrueを返します。以前はprinter構造体のフィールドとして定義されていましたが、commentInfoの導入に伴い、commentInfoのメソッドとしてではなく、printerのメソッドとして再定義されました。ロジック自体は大きく変わっていません。

  • func (p *printer) commentSizeBefore(next token.Position) int の追加:

    func (p *printer) commentSizeBefore(next token.Position) int {
    	// save/restore current p.commentInfo (p.nextComment() modifies it)
    	defer func(info commentInfo) {
    		p.commentInfo = info
    	}(p.commentInfo)
    
    	size := 0
    	for p.commentBefore(next) {
    		for _, c := range p.comment.List {
    			size += len(c.Text)
    		}
    		p.nextComment()
    	}
    	return size
    }
    

    この新しい関数は、次の位置の前に同じ行に存在するコメントの推定サイズを返します。bodySize関数から呼び出され、一行関数の判断ロジックで利用されます。コメントのサイズを計算するために、一時的にcommentInfoの状態を保存・復元するロジックが含まれています。

  • func (p *printer) intersperseComments(...) の変更: コメントと次のトークンの間に空白を挿入するロジックが変更されました。

    		if p.mode&noExtraBlank == 0 &&
    			last.Text[1] == '*' && p.lineFor(last.Pos()) == next.Line &&
    			tok != token.COMMA &&
    			(tok != token.RPAREN || p.prevOpen == token.LPAREN) &&
    			(tok != token.RBRACK || p.prevOpen == token.LBRACK) {
    			p.writeByte(' ', 1)
    		}
    

    noExtraBlankモードが設定されていないこと、およびprevOpenフィールドを利用して、右括弧や右角括弧が対応する開き括弧の直後に続く場合に余分な空白を挿入しないという条件が追加されました。

  • func (p *printer) writeWhitespace(n int) の変更: wsbuf(空白バッファ)の処理がcopy関数を使ってより効率的に書き換えられました。

  • func (p *printer) print(args ...interface{}) の変更:

    		// record previous opening token, if any
    		switch p.lastTok {
    		case token.ILLEGAL:
    			// ignore (white space)
    		case token.LPAREN, token.LBRACK:
    			p.prevOpen = p.lastTok
    		default:
    			// other tokens followed any opening token
    			p.prevOpen = token.ILLEGAL
    		}
    

    print関数が各引数を処理する前に、lastTok(直前に出力されたトークン)に基づいてprevOpenフィールドを更新するロジックが追加されました。これにより、intersperseComments関数で正確な空白挿入の判断が可能になります。

これらの変更は、go/printerがGoコードを整形する際の、特にコメントと空白の扱いの精度を大幅に向上させ、より自然で期待通りのフォーマット結果を生成することを可能にしています。

関連リンク

参考にした情報源リンク