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

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

このコミットは、Go言語の公式フォーマッタであるgofmtおよびその基盤となるgo/printerパッケージにおける、コードの行間(空白行)の整形ルールを微調整するものです。具体的には、空の構造体やインターフェース内の不要な空白行を削除し、トップレベルの宣言(const, type, var, funcなど)間の空白行の挿入ルールを改善しています。特に、異なる種類の宣言間、またはドキュメンテーションコメントが付随する宣言の前に空白行を挿入する新しいルールが導入されました。これにより、Goコードの可読性と一貫性が向上します。

コミット

commit 541b67d051fbd26f3727d4d13c6d2b025af8a775
Author: Robert Griesemer <gri@golang.org>
Date:   Fri Dec 16 15:43:06 2011 -0800

    go/printer, gofmt: fine tuning of line spacing
    
    - no empty lines inside empty structs and interfaces
    - top-level declarations are separated by a blank line if
      a) they are of different kind (e.g. const vs type); or
      b) there are documentation comments associated with a
         declaration (this is new)
    - applied gofmt -w misc src
    
    The actual changes are in go/printer/nodes.go:397-400 (empty structs/interfaces),
    and go/printer/printer.go:307-309 (extra line break). The remaining
    changes are cleanups w/o changing the existing functionality.
    
    Fixes issue  2570.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5493057

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

https://github.com/golang/go/commit/541b67d051fbd26f3727d4d13c6d2b025af8a775

元コミット内容

このコミットは、go/printerパッケージとgofmtツールにおける行間(空白行)の整形ロジックを微調整することを目的としています。主な変更点は以下の通りです。

  1. 空の構造体およびインターフェース内の空白行の削除: {}のような空の宣言内に余分な空白行が挿入されないようにします。
  2. トップレベル宣言間の空白行の調整:
    • 異なる種類のトップレベル宣言(例: consttype)の間には空白行を挿入します。
    • 新規ルール: ドキュメンテーションコメントが付随する宣言の前には、空白行を挿入します。
  3. gofmt -w misc srcコマンドを適用し、既存のGoソースコードベース全体にこれらの新しい整形ルールを適用しています。

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

変更の背景

このコミットの背景には、Go言語のコード整形ツールgofmtの出力の一貫性と可読性をさらに向上させるという目的があります。特に、Issue 2570で報告された問題に対処しています。

Issue 2570は、gofmtがトップレベルの宣言(const, type, var, funcなど)の前にドキュメンテーションコメントがある場合に、そのコメントと前の宣言との間に適切な空白行を挿入しないことがあるという問題でした。これにより、生成されるコードの可読性が損なわれる可能性がありました。

Go言語では、gofmtによってコードが自動的に整形されることで、プロジェクト全体で一貫したコーディングスタイルが保たれることが重視されています。しかし、既存のルールでは、特にドキュメンテーションコメントと宣言の間の空白行の扱いに不整合が生じることがありました。このコミットは、これらのエッジケースを修正し、より自然で読みやすいコードレイアウトを実現するために行われました。

また、空の構造体やインターフェース(例: struct{}interface{})の内部に不要な空白行が挿入される問題も修正対象でした。これもまた、gofmtの出力の「美しさ」と一貫性を高めるための微調整です。

前提知識の解説

このコミットを理解するためには、以下のGo言語のツールと概念に関する知識が必要です。

  1. gofmt:

    • gofmtはGo言語の公式なコード整形ツールです。Goのソースコードを解析し、Goコミュニティで推奨される標準的なスタイルに自動的に整形します。
    • その目的は、Goコードの可読性を高め、スタイルに関する議論を減らすことです。gofmtは、インデント、空白行、括弧の配置など、多くの整形ルールを自動的に適用します。
    • このコミットは、gofmtが適用する整形ルールの一部、特に空白行の挿入ロジックを改善するものです。
  2. go/printerパッケージ:

    • go/printerは、Goの抽象構文木(AST)を整形し、Goソースコードとして出力するためのパッケージです。gofmtはこのパッケージを内部的に利用しています。
    • このパッケージは、ASTノードをトラバースし、コメント、空白、インデントなどを考慮しながら、最終的なソースコード文字列を生成します。
    • このコミットの主要な変更は、このgo/printerパッケージ内のロジックにあります。
  3. 抽象構文木 (AST - Abstract Syntax Tree):

    • Goコンパイラやツール(gofmtなど)は、Goのソースコードを直接操作するのではなく、まずソースコードを解析してASTと呼ばれるツリー構造に変換します。
    • ASTは、プログラムの構造を抽象的に表現したもので、各ノードがコードの要素(宣言、式、文など)に対応します。
    • go/printerは、このASTを受け取り、それを整形して出力します。
  4. go/astパッケージ:

    • Goの標準ライブラリの一部で、GoソースコードのASTを定義しています。ast.File, ast.Decl (宣言), ast.Expr (式), ast.Stmt (文) などの型が含まれます。
    • このコミットでは、ast.FieldList (構造体やインターフェースのフィールドリスト) や ast.Decl (トップレベル宣言) などのASTノードの処理が変更されています。
  5. go/tokenパッケージ:

    • Goの標準ライブラリの一部で、Go言語の字句要素(トークン)とソースコード上の位置(token.Pos)を定義しています。
    • go/printerは、トークンの位置情報を使用して、コメントの位置や空白行の挿入を決定します。
  6. ドキュメンテーションコメント:

    • Goでは、パッケージ、関数、型、変数などの宣言の直前に書かれたコメントは、その宣言のドキュメンテーションとして扱われます。
    • これらのコメントはgodocツールによって抽出され、ドキュメントとして表示されます。
    • このコミットでは、ドキュメンテーションコメントが付随する宣言の前に、適切な空白行を挿入するルールが追加されました。

技術的詳細

このコミットの技術的な変更は、主にsrc/pkg/go/printer/nodes.gosrc/pkg/go/printer/printer.goの2つのファイルに集中しています。

1. 空の構造体およびインターフェース内の空白行の削除 (nodes.go)

nodes.go(*printer).fieldListメソッドは、構造体やインターフェースのフィールドリストを整形する役割を担っています。このコミットでは、空の構造体やインターフェース(例: struct{}interface{})が1行で記述されている場合に、{}の間に不要な空白行が挿入されないようにロジックが変更されました。

変更前は、len(list) == 0(フィールドがない)かつ!isIncomplete(不完全な宣言ではない)の場合でも、p.print(blank, lbrace, token.LBRACE, indent, formfeed)という行でformfeed(改行とインデント)が強制的に挿入される可能性がありました。

変更後、hasCommentsという新しいフラグが導入されました。これは、isIncompleteであるか、または閉じ括弧}の前にコメントがある場合にtrueになります。 if !hasComments && srcIsOneLineという条件が追加され、コメントがなく、かつソースコードが1行で書かれている場合に、特別な処理(// no blank between keyword and {} in this case)が適用され、formfeedが抑制されます。

これにより、type S struct{}のような宣言が、type S struct{\n}のように整形されるのを防ぎ、よりコンパクトな出力が実現されます。

2. トップレベル宣言間の空白行の調整 (nodes.goprinter.go)

この変更は、nodes.go(*printer).fileメソッドとprinter.go(*printer).writeCommentPrefixメソッドに関連しています。

nodes.goの変更点:

(*printer).fileメソッドは、ファイルのトップレベル宣言を整形します。変更前は、異なる種類の宣言(例: constからtypeへ)に切り替わる場合にのみ、宣言間に空白行(min = 2)が挿入されていました。

変更後、以下の条件が追加されました。 if prev != tok || getDoc(d) != nil

これは、前の宣言の種類と現在の宣言の種類が異なる場合(prev != tok)に加えて、**現在の宣言にドキュメンテーションコメントが付随している場合(getDoc(d) != nil)**にも、宣言間に空白行を挿入する(min = 2)ようにします。

getDoc(d)は、与えられたASTノードdに関連付けられたドキュメンテーションコメントグループを返します。これにより、ドキュメンテーションコメントを持つ宣言が、その前の宣言から視覚的に区切られるようになり、可読性が向上します。

printer.goの変更点:

(*printer).writeCommentPrefixメソッドは、コメントを書き出す前に必要な空白(特に改行)を処理します。このコミットでは、パッケージスコープ(p.indent == 0)でドキュメンテーションコメントの前に空白行が適切に挿入されるようにロジックが調整されました。

特に重要なのは、droppedLinebreakという新しいフラグと、以下のロジックです。 if p.indent == 0 && droppedLinebreak { n++ }

これは、writeCommentPrefixが以前の空白行を「ドロップ」した場合(つまり、整形処理の過程で改行が失われた場合)に、パッケージスコープのコメントの前に追加の改行を挿入することで、Issue 2570で報告された問題を修正します。これにより、ドキュメンテーションコメントが前のコード行に密着しすぎるのを防ぎ、常に適切な空白行が確保されるようになります。

また、getDoc関数が*ast.FieldDocフィールドも参照するように拡張され、構造体フィールドのドキュメンテーションコメントも適切に扱えるようになりました。

これらの変更は、gofmtが生成するコードの視覚的な一貫性と可読性を高めるための、細部にわたる調整です。

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

このコミットの主要な変更は以下のファイルと行範囲にあります。

  • src/pkg/go/printer/nodes.go:
    • fieldListメソッド内の空の構造体/インターフェースの整形ロジック: 約397-400行目付近
    • fileメソッド内のトップレベル宣言間の空白行挿入ロジック: 約1512-1517行目付近
  • src/pkg/go/printer/printer.go:
    • writeCommentPrefixメソッド内のコメント前の空白行処理ロジック: 約289-309行目付近
    • getDoc関数への*ast.Fieldケースの追加: 約812-814行目付近

コアとなるコードの解説

src/pkg/go/printer/nodes.go

fieldListメソッドの変更 (空の構造体/インターフェースの整形)

--- a/src/pkg/go/printer/nodes.go
+++ b/src/pkg/go/printer/nodes.go
@@ -364,9 +364,10 @@ func (p *printer) fieldList(fields *ast.FieldList, isStruct, isIncomplete bool)\
 	lbrace := fields.Opening
 	list := fields.List
 	rbrace := fields.Closing
+\thasComments := isIncomplete || p.commentBefore(p.fset.Position(rbrace))\
 	srcIsOneLine := lbrace.IsValid() && rbrace.IsValid() && p.fset.Position(lbrace).Line == p.fset.Position(rbrace).Line
 
-\tif !isIncomplete && !p.commentBefore(p.fset.Position(rbrace)) && srcIsOneLine {\
+\tif !hasComments && srcIsOneLine {\
 		// possibly a one-line struct/interface
 		if len(list) == 0 {\
 			// no blank between keyword and {} in this case
@@ -391,9 +392,13 @@ func (p *printer) fieldList(fields *ast.FieldList, isStruct, isIncomplete bool)\
 			return
 		}
 	}\
+\t// hasComments || !srcIsOneLine
+\
+\tp.print(blank, lbrace, token.LBRACE, indent)\
+\tif hasComments || len(list) > 0 {\
+\t\tp.print(formfeed)\
+\t}
 \n-\t// at least one entry or incomplete
-\tp.print(blank, lbrace, token.LBRACE, indent, formfeed)\
 \tif isStruct {
 
 		sep := vtab
  • hasCommentsの導入: isIncompleteであるか、または閉じ括弧rbraceの前にコメントがある場合にtrueとなるhasComments変数が追加されました。これは、空の構造体/インターフェースであっても、コメントが存在する場合は特別な整形ルールを適用するためのものです。
  • 条件式の変更: 以前は!isIncomplete && !p.commentBefore(p.fset.Position(rbrace)) && srcIsOneLineという条件で1行整形を試みていましたが、!hasComments && srcIsOneLineに変更されました。これにより、コメントがある場合は1行整形を避けるようになります。
  • p.print呼び出しの変更: 以前は無条件にformfeed(改行とインデント)を伴って{を出力していましたが、新しいロジックでは、hasCommentsがあるか、またはフィールドリストが空でない場合(len(list) > 0)にのみformfeedを挿入するようになりました。これにより、struct{}のような空の宣言が1行で出力されるようになります。

fileメソッドの変更 (トップレベル宣言間の空白行)

--- a/src/pkg/go/printer/nodes.go
+++ b/src/pkg/go/printer/nodes.go
@@ -1512,9 +1517,14 @@ func (p *printer) file(src *ast.File) {\
 			prev := tok
 			tok = declToken(d)
 			// if the declaration token changed (e.g., from CONST to TYPE)
+\t\t\t// or the next declaration has documentation associated with it,
 \t\t\t// print an empty line between top-level declarations
+\t\t\t// (because p.linebreak is called with the position of d, which
+\t\t\t// is past any documentation, the minimum requirement is satisfied
+\t\t\t// even w/o the extra getDoc(d) nil-check - leave it in case the
+\t\t\t// linebreak logic improves - there\'s already a TODO).\
 			min := 1
-\t\t\tif prev != tok {\
+\t\t\tif prev != tok || getDoc(d) != nil {\
 				min = 2
 			}
 			p.linebreak(p.fset.Position(d.Pos()).Line, min, ignore, false)\
  • 空白行挿入条件の追加: トップレベル宣言間に空白行(min = 2)を挿入する条件に、getDoc(d) != nilが追加されました。これは、現在の宣言dにドキュメンテーションコメントが付随している場合に、前の宣言との間に空白行を強制的に挿入することを意味します。これにより、ドキュメンテーションコメントを持つ宣言がより明確に区切られます。

src/pkg/go/printer/printer.go

writeCommentPrefixメソッドの変更 (コメント前の空白行処理)

--- a/src/pkg/go/printer/printer.go
+++ b/src/pkg/go/printer/printer.go
@@ -257,6 +257,7 @@ func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *as\
 	} else {
 		// comment on a different line:
 		// separate with at least one line break
+\t\tdroppedLinebreak := false
 		if prev == nil {
 			// first comment of a comment group
 			j := 0
@@ -282,6 +283,7 @@ func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *as\
 				case newline, formfeed:
 					// TODO(gri): may want to keep formfeed info in some cases
 					p.wsbuf[i] = ignore
+\t\t\t\t\tdroppedLinebreak = true
 				}
 				j = i
 				break
@@ -289,25 +291,41 @@ func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *as\
 			p.writeWhitespace(j)
 		}
 
-\t\t// turn off indent if we\'re about to print a line directive.\
-\t\tindent := p.indent
-\t\tif strings.HasPrefix(comment.Text, linePrefix) {\
-\t\t\tp.indent = 0
+\t\t// determine number of linebreaks before the comment
+\t\tn := 0
+\t\tif pos.IsValid() && p.last.IsValid() {\
+\t\t\tn = pos.Line - p.last.Line
+\t\t\tif n < 0 { // should never happen
+\t\t\t\tn = 0
+\t\t\t}\
+\t\t}
+\n+\t\t// at the package scope level only (p.indent == 0),\
+\t\t// add an extra newline if we dropped one before:\
+\t\t// this preserves a blank line before documentation\
+\t\t// comments at the package scope level (issue 2570)\
+\t\tif p.indent == 0 && droppedLinebreak {\
+\t\t\tn++
 \t\t}\
 
-\t\t// use formfeeds to break columns before a comment;\
-\t\t// this is analogous to using formfeeds to separate\
-\t\t// individual lines of /*-style comments - but make\n-\t\t// sure there is at least one line break if the previous\n-\t\t// comment was a line comment\n-\t\tn := pos.Line - p.last.Line // if !pos.IsValid(), pos.Line == 0, and n will be 0\n-\t\tif n <= 0 && prev != nil && prev.Text[1] == \'/\' {\
+\t\t// make sure there is at least one line break\n+\t\t// if the previous comment was a line comment\n+\t\tif n == 0 && prev != nil && prev.Text[1] == \'/\' {\
 \t\t\tn = 1
 \t\t}\
+\n \t\tif n > 0 {\
+\t\t\t// turn off indent if we\'re about to print a line directive\n+\t\t\tindent := p.indent\n+\t\t\tif strings.HasPrefix(comment.Text, linePrefix) {\
+\t\t\t\tp.indent = 0\n+\t\t\t}\n+\t\t\t// use formfeeds to break columns before a comment;\n+\t\t\t// this is analogous to using formfeeds to separate\n+\t\t\t// individual lines of /*-style comments\n \t\t\tp.writeByteN(\'\\f\', nlimit(n))\n+\t\t\t\tp.indent = indent // restore indent\n \t\t}\
-\t\tp.indent = indent\
 \t}\
 }\
  • droppedLinebreakフラグ: コメントの前にあった改行が整形処理中に失われたかどうかを追跡するためのdroppedLinebreakフラグが追加されました。
  • パッケージスコープでの追加改行: p.indent == 0(パッケージスコープ)かつdroppedLinebreaktrueの場合に、n++によって改行数を1つ増やしています。これは、Issue 2570で報告された、ドキュメンテーションコメントの前に空白行が不足する問題を修正するためのものです。これにより、パッケージレベルのドキュメンテーションコメントが常に適切な空白行で区切られるようになります。
  • インデントの扱い: 行ディレクティブ(//line)をプリントする際に一時的にインデントをオフにし、その後復元するロジックが、n > 0のブロック内に移動し、より適切に管理されるようになりました。

getDoc関数の変更

--- a/src/pkg/go/printer/printer.go
+++ b/src/pkg/go/printer/printer.go
@@ -812,7 +830,8 @@ func (p *printer) flush(next token.Position, tok token.Token) (wroteNewline, dro\
 // getNode returns the ast.CommentGroup associated with n, if any.\
 func getDoc(n ast.Node) *ast.CommentGroup {\
 	switch n := n.(type) {\
-\t// *ast.Fields cannot be printed separately - ignore for now\
+\tcase *ast.Field:\
+\t\treturn n.Doc\
 	case *ast.ImportSpec:\
 		return n.Doc
 	case *ast.ValueSpec:\
  • *ast.Fieldのサポート: getDoc関数が*ast.Field型も処理できるようになりました。これにより、構造体やインターフェースのフィールドに付随するドキュメンテーションコメントもgo/printerによって適切に認識され、整形ロジックに反映されるようになります。

これらの変更は、Goのコード整形における細かな視覚的品質と一貫性を向上させるための重要なステップです。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (go/printer, go/ast, go/tokenパッケージ)
  • gofmtの設計思想に関するGoブログ記事やトーク
  • Go言語のIssueトラッカー (Issue 2570の詳細)
  • Go言語のソースコード (特にsrc/cmd/gofmtsrc/pkg/go/printerディレクトリ)
  • https://go.dev/blog/gofmt (gofmtに関する公式ブログ記事)
  • https://pkg.go.dev/go/printer (go/printerパッケージのドキュメント)
  • https://pkg.go.dev/go/ast (go/astパッケージのドキュメント)
  • https://pkg.go.dev/go/token (go/tokenパッケージのドキュメント)