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

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

このコミットは、Go言語のコードフォーマッタであるgofmtと、その基盤となるgo/printerパッケージにおける、複数行にわたるreturn文のフォーマット改善に関するものです。特に、複数の戻り値が複数行にわたる場合に、より見やすく整形されるように変更が加えられました。

コミット

commit 49d2d986978f3b3654ff284fbcbeb4c32ec55fad
Author: Robert Griesemer <gri@golang.org>
Date:   Fri Mar 9 11:05:50 2012 -0800

    go/printer, gofmt: nicer formatting of multi-line returns
    
    This affects corner (test) cases only; gofmt -w src misc
    doesn't cause any changes.
    
    - added additional test cases
    - removed doIndent parameter from printer.valueSpec
      (was always false)
    - gofmt -w src misc causes no changes
    
    Fixes #1207.
    
    R=dsymonds, rsc
    CC=golang-dev
    https://golang.org/cl/5786060

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

https://github.com/golang/go/commit/49d2d986978f3b3654ff284fbcbeb4c32ec55fad

元コミット内容

このコミットの目的は、go/printerパッケージとgofmtツールにおいて、複数行にわたるreturn文のフォーマットを改善することです。具体的には、複数の戻り値が複数行にまたがるような特殊なケース(主にテストケース)において、より適切なインデントが適用されるように修正されました。

変更点としては、以下の点が挙げられています。

  • 追加のテストケースが追加された。
  • printer.valueSpec関数からdoIndentパラメータが削除された(常にfalseであったため)。
  • gofmt -w src miscを実行しても、既存のコードベースに大きな変更は発生しないことが確認された。

この変更は、Go言語のIssue 1207を修正するものです。

変更の背景

このコミットは、Go言語のIssue 1207「go/printer: odd layout of multiple return values that span lines」に対応するものです。このIssueは、gofmtが複数行にわたるreturn文の戻り値を整形する際に、期待されるインデントが適用されず、読みにくいレイアウトになるという問題点を指摘していました。

具体的には、以下のようなコードが問題とされていました。

func F() (*T, os.Error) {
       return &T{
               X: 1,
               Y: 2,
       },
               nil
}

この場合、nil&T{...}と同じインデントレベルになってしまい、視覚的に戻り値のリストがどこから始まるのか、どこで終わるのかが分かりにくいという問題がありました。このコミットは、このようなケースにおいて、より適切なインデントを適用し、コードの可読性を向上させることを目的としています。

前提知識の解説

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

  • go/printerパッケージ: Go言語のソースコードを整形(フォーマット)するための低レベルな機能を提供するパッケージです。抽象構文木(AST)を受け取り、整形されたコードを出力します。gofmtはこのパッケージを利用して実装されています。
  • gofmt: Go言語の公式なコードフォーマッタです。Goのコードを標準的なスタイルに自動的に整形します。これにより、Goコミュニティ全体で一貫したコードスタイルが保たれ、コードの可読性が向上します。gofmtは、コードの構文解析を行い、ASTを構築し、それをgo/printerパッケージに渡して整形されたコードを生成します。
  • 抽象構文木 (AST: Abstract Syntax Tree): ソースコードの構造を木構造で表現したものです。コンパイラやリンタ、フォーマッタなどのツールは、ソースコードを直接操作するのではなく、一度ASTに変換してから処理を行います。
  • インデント: コードの階層構造を視覚的に表現するために、行頭に空白(スペースやタブ)を入れることです。適切なインデントはコードの可読性を大きく向上させます。
  • return: 関数の実行を終了し、呼び出し元に値を返すためのGo言語のキーワードです。Go言語では、複数の値を返すことができます。

技術的詳細

このコミットの主要な変更は、go/printerパッケージ内のstmtメソッド(特にtoken.RETURNの場合)と、新しく導入されたindentListヘルパー関数にあります。

indentList関数の導入

indentList関数は、与えられたast.Exprのリスト(この場合はreturn文の戻り値のリスト)が、全体的にインデントされるべきかどうかをヒューリスティックに基づいて判断します。

  • 目的: 複数行にわたる戻り値のリストが、より見やすく整形されるべきかを判定する。
  • ヒューリスティック:
    • リストに2つ以上の要素がある場合。
    • リストが複数行にわたる場合。
    • リスト内の要素が、前の要素の終了行と同じ行から始まっていない場合(つまり、要素間に改行がある場合)。
    • リスト内に複数の複数行要素がある場合。

これらの条件のいずれかが満たされる場合、indentListtrueを返し、そのリスト全体をインデントする必要があることを示します。

stmtメソッドにおけるreturn文の処理変更

printer.stmtメソッドは、Goの各ステートメント(文)を整形する役割を担っています。このコミットでは、return文の処理が以下のように変更されました。

変更前:

		case *ast.ReturnStmt:
			p.print(token.RETURN)
			if s.Results != nil {
				p.print(blank)
				p.exprList(s.Pos(), s.Results, 1, 0, token.NoPos)
			}

変更後:

		case *ast.ReturnStmt:
			p.print(token.RETURN)
			if s.Results != nil {
				p.print(blank)
				// Use indentList heuristic to make corner cases look
				// better (issue 1207). A more systematic approach would
				// always indent, but this would cause significant
				// reformatting of the code base and not necessarily
				// lead to more nicely formatted code in general.
				if p.indentList(s.Results) {
					p.print(indent)
					p.exprList(s.Pos(), s.Results, 1, noIndent, token.NoPos)
					p.print(unindent)
				} else {
					p.exprList(s.Pos(), s.Results, 1, 0, token.NoPos)
				}
			}

この変更により、return文の戻り値リストを整形する際に、まずindentList関数が呼び出されます。

  • indentListtrueを返した場合、p.print(indent)でインデントを開始し、p.exprListで戻り値リストを整形した後、p.print(unindent)でインデントを解除します。これにより、戻り値リスト全体が一段階インデントされて出力されます。
  • indentListfalseを返した場合、以前と同様にインデントなしでp.exprListが呼び出されます。

この条件付きインデントの導入により、Issue 1207で指摘されたような、複数行にわたる戻り値のレイアウト問題が解決されました。コミットメッセージにもあるように、より体系的なアプローチとして常にインデントすることも考えられますが、それは既存のコードベースに大規模な再フォーマットを引き起こすため、ヒューリスティックに基づいたアプローチが採用されました。

valueSpec関数の変更

printer.valueSpec関数からdoIndentパラメータが削除されました。これは、このパラメータが常にfalseとして渡されていたため、冗長であったためです。この変更は、コードのクリーンアップと簡素化を目的としています。

変更前:

func (p *printer) valueSpec(s *ast.ValueSpec, keepType, doIndent bool) {
	p.setComment(s.Doc)
	p.identList(s.Names, doIndent) // always present

変更後:

func (p *printer) valueSpec(s *ast.ValueSpec, keepType bool) {
	p.setComment(s.Doc)
	p.identList(s.Names, false) // always present

そして、genDecl関数での呼び出しも変更されています。

変更前:

					p.valueSpec(s.(*ast.ValueSpec), keepType[i], false)

変更後:

					p.valueSpec(s.(*ast.ValueSpec), keepType[i])

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

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

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

    • indentList関数の新規追加。
    • stmtメソッド内の*ast.ReturnStmtケースにおける、indentListを用いた条件付きインデントロジックの追加。
    • valueSpec関数のシグネチャ変更と、それに伴うidentList呼び出しの修正。
    • genDecl関数内のvalueSpec呼び出しの修正。
  2. src/pkg/go/printer/testdata/statements.golden および src/pkg/go/printer/testdata/statements.input:

    • return文の新しいフォーマットを検証するためのテストケースの追加と、それに対応する期待される出力(.goldenファイル)の更新。

コアとなるコードの解説

src/pkg/go/printer/nodes.go

indentList関数

func (p *printer) indentList(list []ast.Expr) bool {
	// Heuristic: indentList returns true if there are more than one multi-
	// line element in the list, or if there is any element that is not
	// starting on the same line as the previous one ends.
	if len(list) >= 2 {
		var b = p.lineFor(list[0].Pos())
		var e = p.lineFor(list[len(list)-1].End())
		if 0 < b && b < e {
			// list spans multiple lines
			n := 0 // multi-line element count
			line := b
			for _, x := range list {
				xb := p.lineFor(x.Pos())
				xe := p.lineFor(x.End())
				if line < xb {
					// x is not starting on the same
					// line as the previous one ended
					return true
				}
				if xb < xe {
					// x is a multi-line element
					n++
				}
				line = xe
			}
			return n > 1
		}
	}
	return false
}

この関数は、ast.Exprのスライス(list)を受け取り、そのリストが複数行にわたる場合に、全体をインデントすべきかどうかを判断します。

  • リストの要素が2つ以上あり、かつリスト全体が複数行にわたる場合に処理を進めます。
  • 各要素について、開始行と終了行を取得し、前の要素の終了行と比較します。
  • もし要素が前の要素の終了行と同じ行から始まっていない場合(つまり、要素間に改行がある場合)、即座にtrueを返します。
  • また、複数行にわたる要素の数をカウントし、それが1より大きい場合(つまり、2つ以上の複数行要素がある場合)もtrueを返します。
  • これらの条件に合致しない場合はfalseを返します。

stmtメソッド内のreturn文処理

		case *ast.ReturnStmt:
			p.print(token.RETURN)
			if s.Results != nil {
				p.print(blank)
				// Use indentList heuristic to make corner cases look
				// better (issue 1207).
				if p.indentList(s.Results) {
					p.print(indent)
					p.exprList(s.Pos(), s.Results, 1, noIndent, token.NoPos)
					p.print(unindent)
				} else {
					p.exprList(s.Pos(), s.Results, 1, 0, token.NoPos)
				}
			}

この部分が、return文の戻り値の整形ロジックの核心です。

  • returnキーワードを出力した後、戻り値がある場合に処理を進めます。
  • p.indentList(s.Results)を呼び出し、戻り値のリストをインデントすべきかを判断します。
  • indentListtrueを返した場合、p.print(indent)で現在のインデントレベルを一段階増やし、p.exprListで戻り値の式リストを整形します。整形後、p.print(unindent)でインデントレベルを元に戻します。これにより、戻り値全体がインデントされたブロックとして出力されます。
  • indentListfalseを返した場合は、従来のインデントなしのp.exprList呼び出しが行われます。

src/pkg/go/printer/testdata/statements.golden および src/pkg/go/printer/testdata/statements.input

これらのファイルは、gofmtのテストスイートの一部です。

  • .inputファイルは、gofmtに与えられる整形前のコードスニペットを含みます。
  • .goldenファイルは、.inputファイルがgofmtによって整形された後の期待される出力を含みます。

このコミットでは、Issue 1207で問題とされた複数行のreturn文のケースが追加され、それらが適切にインデントされるように.goldenファイルが更新されています。これにより、gofmtが意図した通りに動作することを保証しています。

関連リンク

参考にした情報源リンク

  • GitHub: golang/go repository (特にコミット履歴とIssueトラッカー)
  • Go言語公式ドキュメント
  • Go言語のソースコード