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

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

このコミットは、Go言語の公式フォーマッタであるgofmtと、その基盤となるgo/printerパッケージにおけるカンマの配置ロジックの改善に関するものです。特に、コメントが隣接する場合のカンマの扱いが修正され、より自然で読みやすいコードフォーマットが実現されています。

コミット

commit 8b7cdb7f25ff1e97150ee4648ff4f7764454ccd5
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Feb 22 11:27:45 2012 -0800

    go/printer, gofmt: improved comma placement
    
    Not a Go 1 issue, but appeared to be fairly easy to fix.
    
    - Note that a few existing test cases look slightly worse but
      those cases were not representative for real code. All real
      code looks better now.
    
    - Manual move of the comment in go/scanner/example_test.go
      before applying gofmt.
    
    - gofmt -w $GOROOT/src $GOROOT/misc
    
    Fixes #3062.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5674093

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

https://github.com/golang/go/commit/8b7cdb7f25ff1e97150ee4648ff4f7764454ccd5

元コミット内容

go/printer, gofmt: improved comma placement

このコミットは、Go言語のコードフォーマッタであるgofmtと、その内部で使用されるgo/printerパッケージにおけるカンマの配置を改善するものです。Go 1のリリースには直接関係ない問題でしたが、比較的簡単に修正できると判断されました。

変更によって、既存のいくつかのテストケースでは見た目がわずかに悪くなるものがありましたが、それらは実際のコードを代表するものではなく、実際のコードではすべて改善が見られるとのことです。

また、go/scanner/example_test.go内のコメントは、gofmt適用前に手動で移動されました。そして、$GOROOT/src$GOROOT/miscに対してgofmt -wが実行されました。

この変更は、Issue #3062を修正するものです。

変更の背景

Go言語では、コードの可読性と一貫性を保つために、gofmtという自動フォーマッタが提供されています。gofmtは、Goのソースコードを標準的なスタイルに整形し、開発者がスタイルガイドラインについて議論する時間を削減することを目的としています。

しかし、特定の状況、特にコード内にコメントが挿入されている場合において、gofmtによるカンマの配置が最適ではないという問題がありました。例えば、カンマの直後にコメントがある場合、カンマがコメントの前に移動してしまうことで、コードの意図が分かりにくくなったり、視覚的な整合性が損なわれたりすることがありました。

このコミットは、このようなgofmtの既存の振る舞いを改善し、コメントとカンマの間の関係をより適切に処理することで、生成されるコードの可読性と美しさを向上させることを目的としています。特に、カンマがリスト内の各要素に属するように、その要素の直後に配置されるべきという原則を強化しています。

前提知識の解説

gofmt

gofmtは、Go言語のソースコードを自動的に整形するツールです。Go言語のツールチェインに標準で含まれており、Goコミュニティ全体でコードスタイルの一貫性を保つために広く利用されています。gofmtは、インデント、スペース、改行、カンマなどの配置をGoの公式スタイルガイドラインに従って自動的に調整します。これにより、開発者はコードのスタイルについて悩むことなく、本質的なロジックの実装に集中できます。

go/printerパッケージ

go/printerパッケージは、Goの抽象構文木(AST: Abstract Syntax Tree)をGoのソースコードとして出力するためのパッケージです。gofmtは、このgo/printerパッケージを内部的に利用して、ASTを整形されたコードに変換しています。つまり、gofmtの出力スタイルは、主にgo/printerパッケージのロジックによって決定されます。このパッケージは、コメントの扱い、改行の挿入、カンマの配置など、コードの視覚的な表現に関する詳細なルールを実装しています。

Go言語のコードフォーマットにおけるカンマの役割

Go言語では、リスト、引数、構造体リテラル、マップリテラルなどの要素を区切るためにカンマが使用されます。一般的に、カンマは区切られる要素の直後に配置され、その後にスペースが続きます。このコミットの背景にある問題は、この「要素の直後にカンマ」という原則が、コメントが介在する場合に適切に適用されないケースがあったことです。

token.Posとコメントの関連性

Goのパーサーは、ソースコード内の各トークン(キーワード、識別子、演算子など)とコメントに、その位置情報(token.Pos)を割り当てます。go/printerは、この位置情報とASTを基にコードを整形します。コメントはコードの論理的な構造の一部ではないため、go/printerがコメントをどこに配置するかは、そのコメントが関連するコード要素のtoken.Posと、go/printerが持つ整形ルールに依存します。このコミットでは、カンマとコメントの相対的な位置関係をより適切に処理するために、token.Posの利用方法が調整されています。

技術的詳細

このコミットの主要な変更点は、go/printerパッケージにおけるカンマの配置ロジック、特にコメントが隣接する場合の挙動の改善です。

従来のgo/printerでは、カンマの後にコメントが続く場合、カンマがコメントの前に移動してしまうことがありました。これは、カンマが区切る要素の直後に配置されるべきというGoの一般的なスタイルガイドラインに反するものでした。

このコミットでは、以下の主要な変更が導入されています。

  1. カンマの位置情報の調整:

    • exprList関数(式リストの整形)とparameters関数(関数パラメータの整形)において、カンマをプリントする際に、そのカンマが区切る次の要素のtoken.Pos()を使用するように変更されました。
    • 具体的には、p.print(token.COMMA)の代わりにp.print(x.Pos(), token.COMMA)xは次の要素)やp.print(par.Pos(), token.COMMA)parは次のパラメータ)が使用されています。
    • これにより、カンマは次の要素の開始位置に「紐付けられる」ようになり、その要素に続くコメントがあっても、カンマがコメントの前に適切に配置されるようになります。
    • ただし、次の要素が新しい行にある場合は、この位置情報の調整は行われません(!needsLinebreakの条件)。これは、改行によってカンマと次の要素の関連性が薄れるため、コメントの配置に影響を与えないようにするためです。
  2. printer.intersperseComments関数のロファクタリング:

    • この関数は、コードとコメントの間に適切なスペースや改行を挿入する役割を担っています。
    • 変更前は、/*-styleのコメントが次の要素と同じ行にある場合、無条件にスペースを追加していました。
    • 変更後は、次のトークンがカンマ (token.COMMA)、右括弧 (token.RPAREN)、右角括弧 (token.RBRACK)、または右波括弧 (token.RBRACE) でない場合にのみスペースを追加するように条件が追加されました。
    • これにより、カンマや閉じ括弧の直後にコメントがある場合に、不要なスペースが挿入されるのを防ぎ、よりコンパクトで自然なフォーマットを実現しています。
  3. テストケースの更新:

    • src/pkg/go/printer/testdata/comments.goldensrc/pkg/go/printer/testdata/comments.inputが更新され、新しいカンマ配置ロジックの挙動を反映しています。特に、カンマの直後にコメントがある場合の整形結果が改善されていることが示されています。

これらの変更により、gofmtは、特にコメントがコード内に散りばめられている場合に、より一貫性があり、視覚的に優れたコードを生成できるようになりました。

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

このコミットのコアとなる変更は、主に以下のファイルに集中しています。

  • src/pkg/go/printer/nodes.go: 式リスト、関数パラメータ、for-range文におけるカンマの配置ロジックが変更されています。
  • src/pkg/go/printer/printer.go: コメントと次のトークンの間にスペースを挿入するロジックが変更されています。
  • src/pkg/go/printer/testdata/comments.golden: 期待される整形結果のテストデータが更新されています。
  • src/pkg/go/printer/testdata/comments.input: 整形前の入力テストデータが更新されています。

具体的な変更行は以下の通りです。

src/pkg/go/printer/nodes.go

--- a/src/pkg/go/printer/nodes.go
+++ b/src/pkg/go/printer/nodes.go
@@ -132,7 +132,9 @@ func (p *printer) exprList(prev0 token.Pos, list []ast.Expr, depth int, mode exp
 		for i, x := range list {
 			if i > 0 {
 				if mode&commaSep != 0 {
-					p.print(token.COMMA)
+					// use position of expression following the comma as
+					// comma position for correct comment placement
+					p.print(x.Pos(), token.COMMA)
 				}
 				p.print(blank)
 			}
@@ -212,11 +214,18 @@ func (p *printer) exprList(prev0 token.Pos, list []ast.Expr, depth int, mode exp
 		}
 
 		if i > 0 {
+			needsLinebreak := prevLine < line && prevLine > 0 && line > 0
 			if mode&commaSep != 0 {
+				// use position of expression following the comma as
+				// comma position for correct comment placement, but
+				// only if the expression is on the same line
+				if !needsLinebreak {
+					p.print(x.Pos())
+				}
 				p.print(token.COMMA)
 			}
 			needsBlank := true
-			if prevLine < line && prevLine > 0 && line > 0 {
+			if needsLinebreak {
 				// lines are broken using newlines so comments remain aligned
 				// unless forceFF is set or there are multiple expressions on
 				// the same line in which case formfeed is used
@@ -283,11 +292,18 @@ func (p *printer) parameters(fields *ast.FieldList, multiLine *bool) {
 				parLineBeg = parLineEnd
 			}
 			// separating "," if needed
+			needsLinebreak := 0 < prevLine && prevLine < parLineBeg
 			if i > 0 {
+				// use position of parameter following the comma as
+				// comma position for correct comma placement, but
+				// only if the next parameter is on the same line
+				if !needsLinebreak {
+					p.print(par.Pos())
+				}
 				p.print(token.COMMA)
 			}
 			// separator if needed (linebreak or blank)
-			if 0 < prevLine && prevLine < parLineBeg && p.linebreak(parLineBeg, 0, ws, true) {
+			if needsLinebreak && p.linebreak(parLineBeg, 0, ws, true) {
 				// break line if the opening "(" or previous parameter ended on a different line
 				// or if there are multiple expressions on the same line in which case formfeed is used
 				ws = ignore
@@ -312,7 +328,7 @@ func (p *printer) parameters(fields *ast.FieldList, multiLine *bool) {
 		// if the closing ")" is on a separate line from the last parameter,
 		// print an additional "," and line break
 		if closing := p.lineFor(fields.Closing); 0 < prevLine && prevLine < closing {
-			p.print(",")
+			p.print(token.COMMA)
 			p.linebreak(closing, 0, ignore, true)
 		}
 		// unindent if we indented
@@ -393,6 +409,7 @@ func (p *printer) fieldList(fields *ast.FieldList, isStruct, isIncomplete bool)
 			f := list[0]
 			for i, x := range f.Names {
 				if i > 0 {
+					// no comments so no need for comma position
 					p.print(token.COMMA, blank)
 				}
 				p.expr(x, ignoreMultiLine)
@@ -1125,7 +1142,9 @@ func (p *printer) stmt(stmt ast.Stmt, nextIsRBrace bool, multiLine *bool) {
 		p.print(token.FOR, blank)
 		p.expr(s.Key, multiLine)
 		if s.Value != nil {
-			p.print(token.COMMA, blank)
+			// use position of value following the comma as
+			// comma position for correct comment placement
+			p.print(s.Value.Pos(), token.COMMA, blank)
 			p.expr(s.Value, multiLine)
 		}
 		p.print(blank, s.TokPos, s.Tok, blank, token.RANGE, blank

src/pkg/go/printer/printer.go

--- a/src/pkg/go/printer/printer.go
+++ b/src/pkg/go/printer/printer.go
@@ -686,9 +686,11 @@ func (p *printer) intersperseComments(next token.Position, tok token.Token) (wro
 	}
 
 	if last != nil {
-		if last.Text[1] == '*' && p.lineFor(last.Pos()) == next.Line {
-			// the last comment is a /*-style comment and the next item
-			// follows on the same line: separate with an extra blank
+		// if the last comment is a /*-style comment and the next item
+		// follows on the same line but is not a comma or a "closing"
+		// token, add an extra blank for separation
+		if last.Text[1] == '*' && p.lineFor(last.Pos()) == next.Line && tok != token.COMMA &&
+			tok != token.RPAREN && tok != token.RBRACK && tok != token.RBRACE {
 			p.writeByte(' ', 1)
 		}
 		// ensure that there is a line break after a //-style comment,

コアとなるコードの解説

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

このファイルでは、GoのASTノードを整形する際のカンマの配置ロジックが修正されています。

  • exprList関数とparameters関数におけるp.print(x.Pos(), token.COMMA):

    • 以前は単にp.print(token.COMMA)としてカンマを出力していました。これは、カンマ自体の位置情報を持たず、プリンタが内部的に決定する位置に配置されることを意味します。
    • 変更後は、x.Pos()(またはpar.Pos())を引数として追加しています。xはリスト内の次の要素(式やパラメータ)を表します。
    • この変更の意図は、カンマを次の要素の開始位置に「紐付ける」ことです。これにより、カンマの直後にコメントがある場合でも、カンマがコメントの前に適切に配置されるようになります。go/printerは、与えられた位置情報に基づいて、コメントを考慮した上で最適なカンマの配置を決定します。
    • needsLinebreakの導入により、次の要素が新しい行にある場合は、この位置情報の調整を行わないようにしています。これは、改行がある場合はカンマと次の要素の視覚的な関連性が薄れるため、コメントの配置に影響を与えないようにするためです。
  • fieldList関数におけるコメントの追加:

    • // no comments so no need for comma positionというコメントが追加されています。これは、構造体のフィールドリストなど、カンマの直後にコメントが来る可能性が低い箇所では、カンマの位置情報を特別に指定する必要がないことを示しています。
  • stmt関数(for-range文)におけるp.print(s.Value.Pos(), token.COMMA, blank):

    • for-range文のキーと値のペアを区切るカンマについても、同様に値のtoken.Pos()を使用してカンマの位置情報を指定するように変更されています。これにより、for a /* comment */, b := range xのようなケースで、カンマがaの直後に適切に配置されるようになります。

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

このファイルでは、コメントと次のトークンの間にスペースを挿入するロジックが修正されています。

  • intersperseComments関数における条件の追加:
    • /*-styleのコメントが次のトークンと同じ行にある場合にスペースを追加する既存のロジックに、新しい条件が追加されました。
    • 追加された条件は、次のトークンがtoken.COMMA(カンマ)、token.RPAREN(右括弧)、token.RBRACK(右角括弧)、またはtoken.RBRACE(右波括弧)ではない場合にのみスペースを追加するというものです。
    • この変更の目的は、カンマや閉じ括弧の直後にコメントがある場合に、gofmtが不要なスペースを挿入するのを防ぐことです。例えば、f( /* no args */)のようなコードで、(/*の間にスペースが挿入されないようにします。これにより、よりコンパクトで自然なコードフォーマットが実現されます。

これらの変更は、go/printerがGoのコードを整形する際の「賢さ」を向上させ、特にコメントが絡む複雑なケースにおいて、より直感的でGoのスタイルガイドラインに沿った出力を生成することを可能にしています。

関連リンク

参考にした情報源リンク