[インデックス 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の基本的なルールは以下の通りです。
- 改行が、識別子、整数リテラル、浮動小数点リテラル、虚数リテラル、ルーンリテラル、文字列リテラル、キーワード(
break
,continue
,fallthrough
,return
)、演算子と区切り文字(++
,--
,)
,]
,}
)の直後に続く場合、その改行の前にセミコロンが挿入されます。 - 複雑な式や文の途中で改行がある場合、セミコロンは挿入されません。
この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.impliedSemi
がtrue
)、かつ次のコメントが改行を含む場合(p.commentNewline
がtrue
)、そのコメントはすぐにプリントされず、セミコロン挿入のルールが適用されないように配置が遅延されます。これにより、不正なセミコロン挿入を防ぎ、常に正しい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
関数のロジックが変更され、impliedSemi
とcommentNewline
を考慮するように。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.commentOffset
とp.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の状態を更新
}
}
}
各トークンやノードがプリントされた後に、impliedSemi
がtrue
になるべきかを判断し、その値を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
ツールに関する一般的な知識