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

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

このコミットは、Go言語のgo/printerパッケージとgofmtツールにおける、不正なプログラムの出力に関するバグ修正と改善を目的としています。特に、コメントの配置と自動セミコロン挿入(ASI)の挙動が原因で発生する問題に対処し、プリンタの堅牢性と正確性を向上させています。

コミット

commit 3d6b368514f2b72538c23a27f248684dd9cca227
Author: Robert Griesemer <gri@golang.org>
Date:   Tue Feb 7 15:19:52 2012 -0800

    go/printer, gofmt: don't print incorrect programs
    
    Be careful when printing line comments with incorrect
    position information. Maintain additional state
    impliedSemi: when set, a comment containing a newline
    would imply a semicolon and thus placement must be
    delayed.
    
    Precompute state information pertaining to the next
    comment for faster checks (the printer is marginally
    faster now despite additional checks for each comment).
    
    No effect on existing src, misc sources.
    
    Fixes #1505.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5598054

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

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

元コミット内容

go/printer, gofmt: don't print incorrect programs

不正な位置情報を持つ行コメントをプリントする際に注意を払う。追加の状態impliedSemiを維持する。これが設定されている場合、改行を含むコメントはセミコロンを意味するため、配置を遅延させる必要がある。

次のコメントに関する状態情報を事前に計算することで、チェックを高速化する(各コメントに対する追加チェックにもかかわらず、プリンタはわずかに高速化された)。

既存のsrc、その他のソースには影響なし。

Fixes #1505.

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

変更の背景

このコミットは、Go言語のコードフォーマッタであるgofmt(およびその基盤となるgo/printerパッケージ)が、特定の状況下で不正なGoプログラムを出力してしまうという問題に対処するために行われました。具体的には、コメント、特に改行を含むコメントが、Goの自動セミコロン挿入(Automatic Semicolon Insertion: ASI)のルールと予期せぬ相互作用を起こし、構文的に誤ったコードを生成してしまうケースがありました。

Go言語では、特定のトークンの後に改行がある場合、コンパイラが自動的にセミコロンを挿入します。しかし、コメントがこの自動挿入の挙動に影響を与え、開発者の意図しないセミコロンが挿入されたり、逆に挿入されるべきセミコロンが挿入されなかったりすることがありました。これにより、gofmtが整形したコードがコンパイルエラーを引き起こすという、ツールとしては致命的な問題が発生していました。

コミットメッセージにあるFixes #1505は、この問題がGoのIssueトラッカーで報告されていたことを示しています。このコミットは、go/printerがコメントを処理する際のロジックを改善し、コメントの位置情報が不正確な場合でも、常に正しいGoプログラムを出力できるようにすることを目的としています。

前提知識の解説

Goの自動セミコロン挿入 (Automatic Semicolon Insertion: ASI)

Go言語の構文はC言語に似ていますが、文の終わりにセミコロンを明示的に記述する必要がある場面が少ないという特徴があります。これは、Goコンパイラが特定のルールに基づいて自動的にセミコロンを挿入するためです。この機能は「自動セミコロン挿入 (Automatic Semicolon Insertion: ASI)」と呼ばれます。

ASIの基本的なルールは以下の通りです。

  1. 改行が、識別子、整数リテラル、浮動小数点リテラル、虚数リテラル、ルーンリテラル、文字列リテラル、キーワード(break, continue, fallthrough, return)、演算子と区切り文字(++, --, ), ], })の直後に続く場合、その改行の前にセミコロンが挿入されます。
  2. 複雑な式や文の途中で改行がある場合、セミコロンは挿入されません。

このASIの挙動は、コードの可読性を高め、記述量を減らす一方で、コメントの配置によっては予期せぬ結果を招く可能性がありました。特に、行コメントがコードの途中に挿入され、そのコメントの後に改行が続く場合、ASIのルールが誤って適用され、意図しないセミコロンが挿入されることが問題となっていました。

go/printerパッケージ

go/printerパッケージは、Goの抽象構文木(AST: Abstract Syntax Tree)を整形されたGoソースコードに変換するためのパッケージです。gofmtツールはこのパッケージを利用してGoのソースコードを標準的なスタイルに整形します。go/printerは、ASTの構造を解析し、Goの公式スタイルガイドに従ってインデント、スペース、改行、コメントなどを適切に配置する役割を担っています。

gofmtツール

gofmtは、Go言語のソースコードを自動的に整形するツールです。Goの公式ツールチェインに含まれており、Goコミュニティ全体でコードの一貫性を保つために広く利用されています。gofmtは、コードのスタイルに関する議論を不要にし、開発者がより本質的な問題に集中できるようにすることを目的としています。しかし、このツールが不正なコードを出力してしまうことは、その目的を損なう重大な問題でした。

技術的詳細

このコミットの技術的な核心は、go/printerがコメントを処理する際のロジックをより洗練させ、特に「不正なプログラムをプリントしない」という目標を達成することにあります。

impliedSemi状態の導入

最も重要な変更点の一つは、printer構造体にimpliedSemi boolという新しいフィールドが追加されたことです。このフラグは、直前にプリントされたトークンが、その後に改行が続く場合にセミコロンを自動挿入する可能性があるかどうかを示します。

GoのASIルールでは、特定のトークン(例えば、識別子、リテラル、returnなどのキーワード、})などの区切り文字)の後に改行が来るとセミコロンが挿入されます。impliedSemiは、まさにこの「セミコロンが暗黙的に挿入される可能性がある状態」を追跡します。

コメントがコードの途中に挿入され、そのコメント自体が改行を含む場合、go/printerはコメントを整形する際に改行を挿入します。この改行が、直前のトークンがimpliedSemi状態であった場合に、意図しないセミコロン挿入を引き起こす可能性がありました。このコミットでは、impliedSemiの状態を考慮し、コメントの配置を遅延させることで、この問題を回避しています。

コメントの状態情報の事前計算

コミットメッセージには「Precompute state information pertaining to the next comment for faster checks」とあります。これは、printerが次に処理するコメントグループに関する情報を事前に計算し、キャッシュするメカニズムが導入されたことを指します。具体的には、printer構造体に以下のフィールドが追加されました。

  • comment *ast.CommentGroup: 現在処理対象のコメントグループ。
  • commentOffset int: コメントグループの最初のコメントのファイルオフセット。
  • commentNewline bool: コメントグループ内に改行が含まれているかどうかを示すフラグ。

これらの情報は、nextComment()という新しいヘルパー関数によって計算され、printerがコメントを処理する前に準備されます。特にcommentNewlineフラグは、コメントがASIに影響を与える可能性があるかどうかを判断するために重要です。コメントが改行を含む場合、それがimpliedSemi状態のトークンの後に続くならば、セミコロン挿入の挙動に注意を払う必要があります。

commentBeforeロジックの改善

commentBefore関数は、次にプリントされるトークンの位置の前に、処理すべきコメントが存在するかどうかを判断します。このコミットでは、この関数のロジックが変更され、impliedSemiの状態とcommentNewlineフラグが考慮されるようになりました。

変更前: return p.cindex < len(p.comments) && p.posFor(p.comments[p.cindex].List[0].Pos()).Offset < next.Offset これは単に、次のコメントが現在の位置よりも前にあるかどうかをチェックしていました。

変更後: return p.commentOffset < next.Offset && (!p.impliedSemi || !p.commentNewline) この新しいロジックでは、次のコメントが現在の位置よりも前にあることに加えて、以下の条件が追加されました。

  • !p.impliedSemi: 現在のプリンタの状態がセミコロンを暗黙的に挿入しない状態である。
  • !p.commentNewline: または、次のコメントが改行を含まない。

この条件により、もし現在の状態がセミコロンを暗黙的に挿入する可能性があり(p.impliedSemitrue)、かつ次のコメントが改行を含む場合(p.commentNewlinetrue)、そのコメントはすぐにプリントされず、セミコロン挿入のルールが適用されないように配置が遅延されます。これにより、不正なセミコロン挿入を防ぎ、常に正しいGoプログラムが出力されるようになります。

printメソッドの変更

printer.printメソッドは、ASTノードやトークンを実際に文字列として出力する中心的なメソッドです。このメソッドも、impliedSemiの状態を適切に管理するように変更されました。各引数(トークンやASTノード)を処理する際に、その引数がプリントされた後にimpliedSemiがどのような状態になるべきかを計算し、impliedSemi変数に格納します。そして、実際にwriteItemで出力する直前に、このimpliedSemiの値をp.impliedSemiに設定します。

また、printメソッド内で改行を挿入するロジックも変更され、!p.impliedSemiの条件が追加されました。これにより、セミコロンが暗黙的に挿入されるべきではない状況でのみ、ソースコード中の余分な改行が反映されるようになります。

これらの変更により、go/printerはコメントの不正確な位置情報や、コメントが含む改行がASIに与える影響をより正確に考慮し、常に構文的に正しいGoコードを生成できるようになりました。

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

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

  • src/cmd/fix/timefileinfo_test.go: テストケースの追加。特に、Issue 1505に関連するコメントの整形に関するテストが追加されています。
  • src/pkg/go/printer/nodes.go: printer.setComment関数にp.nextComment()の呼び出しが追加され、コメント処理の準備が強化されました。
  • src/pkg/go/printer/printer.go: go/printerパッケージの主要なロジックが含まれるファイルで、最も多くの変更が行われています。
    • printer構造体にimpliedSemi, comment, commentOffset, commentNewlineフィールドが追加。
    • commentsHaveNewline関数とnextComment関数が新規追加。
    • printメソッドのロジックが大幅に変更され、impliedSemiの管理とコメントの整形ロジックが改善。
    • commentBefore関数のロジックが変更され、impliedSemicommentNewlineを考慮するように。
    • flush関数とprintNode関数にも関連する変更が加えられています。
  • src/pkg/go/printer/printer_test.go: go/printerのテストファイル。
    • TestLineCommentsの修正。
    • TestBadNodesの修正。
    • TestBadCommentsという新しいテスト関数が追加され、コメントの位置情報が不正な場合でも正しいプログラムが生成されることを検証しています。

コアとなるコードの解説

src/pkg/go/printer/printer.go

printer構造体の変更

type printer struct {
	// ... 既存のフィールド ...
	impliedSemi bool         // if set, a linebreak implies a semicolon
	lastTok     token.Token  // the last token printed (token.ILLEGAL if it's whitespace)
	wsbuf       []whiteSpace // delayed white space

	// The (possibly estimated) position in the generated output;
	// ... 既存のフィールド ...

	// Information about p.comments[p.cindex]; set up by nextComment.
	comment        *ast.CommentGroup // = p.comments[p.cindex]; or nil
	commentOffset  int               // = p.posFor(p.comments[p.cindex].List[0].Pos()).Offset; or infinity
	commentNewline bool              // true if the comment group contains newlines
}

impliedSemiは、直前のトークンが改行によってセミコロンを暗黙的に挿入する可能性がある場合にtrueになります。 comment, commentOffset, commentNewlineは、次に処理されるコメントグループに関する事前計算された情報です。

commentsHaveNewline関数の追加

func (p *printer) commentsHaveNewline(list []*ast.Comment) bool {
	// len(list) > 0
	line := p.lineFor(list[0].Pos())
	for i, c := range list {
		if i > 0 && p.lineFor(list[i].Pos()) != line {
			// not all comments on the same line
			return true
		}
		if t := c.Text; len(t) >= 2 && (t[1] == '/' || strings.Contains(t, "\n")) {
			return true
		}
	}
	_ = line
	return false
}

この関数は、与えられたコメントリスト(ast.CommentGroupの一部)に改行が含まれているかどうかを判定します。コメントが複数行にわたる場合や、コメントテキスト自体に改行文字が含まれる場合にtrueを返します。これは、コメントがASIに影響を与えるかどうかを判断するために使用されます。

nextComment関数の追加

func (p *printer) nextComment() {
	for p.cindex < len(p.comments) {
		c := p.comments[p.cindex]
		p.cindex++
		if list := c.List; len(list) > 0 {
			p.comment = c
			p.commentOffset = p.posFor(list[0].Pos()).Offset
			p.commentNewline = p.commentsHaveNewline(list)
			return
		}
		// we should not reach here (correct ASTs don't have empty
		// ast.CommentGroup nodes), but be conservative and try again
	}
	// no more comments
	p.commentOffset = infinity
}

この関数は、次に処理すべきコメントグループをp.commentに設定し、そのオフセットと改行の有無をp.commentOffsetp.commentNewlineに事前計算して格納します。これにより、コメント処理の効率が向上し、後続のチェックでこれらの情報をすぐに利用できるようになります。

printメソッドの変更

printメソッドは、各引数(トークン、ASTノードなど)を処理する際に、impliedSemiの状態を適切に更新するように変更されました。

func (p *printer) print(args ...interface{}) {
	for _, arg := range args {
		// ...
		var impliedSemi bool // value for p.impliedSemi after this arg
		switch x := arg.(type) {
		// ...
		case *ast.Ident:
			data = x.Name
			impliedSemi = true // 識別子の後に改行があればセミコロンが挿入される
			p.lastTok = token.IDENT
		// ...
		case token.Token:
			// ...
			switch x {
			case token.BREAK, token.CONTINUE, token.FALLTHROUGH, token.RETURN,
				token.INC, token.DEC, token.RPAREN, token.RBRACK, token.RBRACE:
				impliedSemi = true // これらのトークンの後に改行があればセミコロンが挿入される
			}
			p.lastTok = x
		// ...
		}

		// ...
		if data != "" {
			wroteNewline, droppedFF := p.flush(next, p.lastTok)

			// intersperse extra newlines if present in the source and
			// if they don't cause extra semicolons (don't do this in
			// flush as it will cause extra newlines at the end of a file)
			if !p.impliedSemi { // ここでimpliedSemiをチェック
				n := nlimit(next.Line - p.pos.Line)
				// ...
				if n > 0 {
					// ...
					impliedSemi = false // 改行が挿入されたので、セミコロンは暗黙的に挿入されない
				}
			}

			p.writeItem(next, data, isLit)
			p.impliedSemi = impliedSemi // 最終的なimpliedSemiの状態を更新
		}
	}
}

各トークンやノードがプリントされた後に、impliedSemitrueになるべきかを判断し、その値をp.impliedSemiに設定します。また、ソースコード中の余分な改行を挿入する際にも!p.impliedSemiをチェックすることで、不正なセミコロン挿入を防ぎます。

commentBefore関数の変更

func (p *printer) commentBefore(next token.Position) (result bool) {
	return p.commentOffset < next.Offset && (!p.impliedSemi || !p.commentNewline)
}

この変更により、コメントが次にプリントされるトークンの位置よりも前にあるだけでなく、現在の状態がセミコロンを暗黙的に挿入する可能性がないか、またはコメント自体が改行を含まない場合にのみ、コメントが処理されるようになりました。これにより、コメントがASIの挙動を誤ってトリガーするのを防ぎます。

src/pkg/go/printer/nodes.go

setComment関数の変更

func (p *printer) setComment(g *ast.CommentGroup) {
	// ...
	p.comments[0] = g
	p.cindex = 0
	p.nextComment() // get comment ready for use
}

コメントが設定された直後にp.nextComment()を呼び出すことで、コメントに関する状態情報がすぐに利用可能になり、後続の処理で効率的に利用できるようになります。

src/pkg/go/printer/printer_test.go

TestBadCommentsの追加

func TestBadComments(t *testing.T) {
	const src = `
// first comment - text and position changed by test
package p
import "fmt"
const pi = 3.14 // rough circle
var (
	x, y, z int = 1, 2, 3
	u, v float64
)
func fibo(n int) {
	if n < 2 {
		return n /* seed values */
	}
	return fibo(n-1) + fibo(n-2)
}
`
	// ...
	testComment(t, f, len(src), &ast.Comment{pos, "//-style comment"})
	testComment(t, f, len(src), &ast.Comment{pos, "/*-style comment */"})
	testComment(t, f, len(src), &ast.Comment{pos, "/*-style \n comment */"}) // 改行を含むコメントのテスト
	testComment(t, f, len(src), &ast.Comment{pos, "/*-style comment \n\n\n */"}) // 複数の改行を含むコメントのテスト
}

この新しいテストケースは、コメントの位置情報が不正確な場合や、コメントが改行を含む場合でも、go/printerが常に構文的に正しいGoプログラムを生成することを検証します。特に、/*-style \n comment */のような改行を含むブロックコメントが、ASIのルールを誤ってトリガーしないことを確認しています。

これらの変更により、go/printerはコメントの整形においてより賢明になり、Goの自動セミコロン挿入のルールと適切に連携することで、gofmtが常に有効なGoコードを出力することを保証しています。

関連リンク

  • Go CL: https://golang.org/cl/5598054

参考にした情報源リンク

  • コミットメッセージ: 3d6b368514f2b72538c23a27f248684dd9cca227
  • Go言語の自動セミコロン挿入に関する一般的な知識
  • Go言語のgo/printerパッケージに関する一般的な知識
  • Go言語のgofmtツールに関する一般的な知識