[インデックス 19178] ファイルの概要
このコミットは、Go言語のgo/scanner
パッケージにおける//line
ディレクティブの解釈方法に関する修正です。特に、ファイル名が指定されていない//line
ディレクティブの扱いを改善し、以前の挙動(Go 1.2でのカレントディレクトリ、またはCL 86990044以降の直前の//line
のファイル名)から、空のファイル名として解釈するように変更しました。これにより、godoc
などのツールにおけるバグによって発生する不適切な//line
ディレクティブの解釈問題が解決されます。
コミット
commit 0de521d111b88b2bc6466a4e1f29a8a284c0b3ee
Author: Alan Donovan <adonovan@google.com>
Date: Wed Apr 16 16:17:50 2014 -0400
go/scanner: interpret //line directives sans filename sensibly, second try.
A //line directive without a filename now denotes the empty
filename, not the current directory (the Go 1.2 behaviour) nor
the previous //line's filename (the behaviour since CL
86990044).
They should never appear (but they do, e.g. due to a bug in godoc).
Fixes #7765
LGTM=gri, rsc
R=rsc, gri
CC=golang-codereviews
https://golang.org/cl/88160050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0de521d111b88b2bc6466a4e1f29a8a284c0b3ee
元コミット内容
このコミットは、go/scanner
パッケージが//line
ディレクティブをどのように解釈するかについて、特にファイル名が省略された場合に焦点を当てています。これは「2回目の試み」とされており、以前の修正が完全ではなかったことを示唆しています。
具体的には、ファイル名が指定されていない//line
ディレクティブが、Go 1.2ではカレントディレクトリを指し、CL 86990044以降では直前の//line
ディレクティブのファイル名を指していた挙動を修正し、代わりに「空のファイル名」として解釈するように変更します。
このようなファイル名のない//line
ディレクティブは、本来は出現すべきではないとされていますが、godoc
のバグなどによって実際に存在することが指摘されています。この修正は、GoのIssue #7765を解決することを目的としています。
変更の背景
Go言語のコンパイラやツールは、ソースコードのファイル名と行番号を正確に追跡することが重要です。これは、エラーメッセージの表示、デバッグ情報の生成、プロファイリングなど、様々な場面で利用されます。特に、コード生成ツール(例: go generate
によって生成されるコード)を使用する場合、生成されたコード内のエラーを元のテンプレートファイルやソースファイルにマッピングするために、//line
ディレクティブが利用されます。
//line
ディレクティブは、コンパイラやツールに対して、その後のコードが特定のファイルと行番号から来ているかのように扱わせるための特別なコメントです。通常は //line filename:line_number
の形式で記述されます。
しかし、ファイル名が省略された //line :line_number
のような形式のディレクティブが存在する場合、その解釈には一貫性のない挙動がありました。
- Go 1.2の挙動: ファイル名が省略された場合、
go/scanner
はカレントディレクトリをファイル名として使用していました。これは、意図しないファイルパスが生成される可能性がありました。 - CL 86990044以降の挙動: 以前の変更(CL 86990044)では、ファイル名が省略された場合、直前の
//line
ディレクティブで指定されたファイル名を再利用するようになりました。これは、一部のシナリオでは適切でしたが、ファイル名が完全に欠落している状況では、依然として不適切なファイルパスが割り当てられる可能性がありました。
このコミットの背景には、godoc
のようなツールが、誤ってファイル名のない//line
ディレクティブを生成してしまうバグが存在したことが挙げられています。このような不適切なディレクティブが存在すると、go/scanner
がそれをどのように解釈するかによって、予期せぬ動作やエラーが発生する可能性がありました。
Issue #7765は、この不適切な//line
ディレクティブの解釈に関する問題を報告していたと考えられます。このコミットは、ファイル名が省略された//line
ディレクティブを「空のファイル名」として扱うことで、この問題を修正し、より堅牢で予測可能な挙動を提供することを目的としています。これにより、godoc
などのツールが生成する不完全な//line
ディレクティブに対しても、go/scanner
が適切に対応できるようになります。
前提知識の解説
Go言語の//line
ディレクティブ
Go言語における//line
ディレクティブは、コンパイラやデバッガなどのツールが、ソースコードの特定の行を別のファイルや行番号に関連付けるための特別なコメントです。これは主に、コード生成ツールによって生成されたGoコードのデバッグ体験を向上させるために使用されます。
目的:
- デバッグとエラー報告の改善: コードが別のソース(例: テンプレート、プロトコルバッファ定義)から生成された場合、生成されたGoファイル内のエラーは通常、その生成されたファイル内の行を指します。
//line
ディレクティブを使用することで、コンパイラとデバッガは、生成されたコードの元となった元の入力ファイル内の位置を報告するように指示され、開発者が問題の根本原因を特定しやすくなります。
構文と使用法:
//line
ディレクティブはコメントとして記述され、特定の形式に従います。
//line
または/*line
で始まる必要があります。- 少なくとも1つのコロン(
:
)を含む必要があります。
一般的な形式は以下の通りです:
//line filename:line
//line filename:line:col
//line :line
(現在のファイル名を使用)//line :line:col
(現在のファイル名を使用)
動作:
//line
コメントの場合、ディレクティブは次の行の最初の文字に適用されます。/*line
コメントの場合、ディレクティブは閉じ*/
の直後の文字に適用されます。filename
が省略された場合、コンパイラは現在のファイル名、または直前に指定されたファイル名を使用します。- 行番号と列番号は、ディレクティブ文字列の末尾から解析されます。
例:
//line original_template.tmpl:10
package main
import "fmt"
func main() {
// Some generated code
fmt.Println("Hello, Go line directives!")
//line original_template.tmpl:25
// More generated code
_ = 1 / 0 // This line will cause a runtime panic
}
上記の例で_ = 1 / 0
の行でパニックが発生した場合、スタックトレースは生成されたGoファイル内の実際の行番号ではなく、original_template.tmpl:25
からエラーが発生したと報告します。
go/scanner
パッケージ
go/scanner
パッケージは、Go言語のソースコードを字句解析(スキャン)するためのパッケージです。字句解析とは、ソースコードをトークン(キーワード、識別子、演算子、リテラルなど)のストリームに分解するプロセスです。コンパイラのフロントエンドの一部であり、Goのソースファイルを解析する最初のステップです。
go/scanner
は、//line
ディレクティブのような特別なコメントを認識し、それらに基づいてファイルの位置情報を調整する役割も担っています。このパッケージは、Goの標準ライブラリの一部であり、Goのツールチェイン(コンパイラ、go fmt
、go vet
など)で広く使用されています。
技術的詳細
このコミットの技術的な核心は、go/scanner
パッケージ内のinterpretLineComment
関数における//line
ディレクティブのファイル名解析ロジックの変更です。
変更前のinterpretLineComment
関数では、//line
ディレクティブからファイル名を抽出した後、そのファイル名が空文字列(""
)であるかどうかのチェックを行っていました。
変更前のロジック:
filename := string(bytes.TrimSpace(text[len(prefix):i]))
if filename == "" {
// assume same file as for previous line
filename = s.file.Position(s.file.Pos(s.lineOffset)).Filename
} else {
// ... (既存のファイル名がある場合の処理)
このコードでは、//line
ディレクティブにファイル名が明示的に指定されていない場合(filename == ""
)、s.file.Position(s.file.Pos(s.lineOffset)).Filename
を使用して、現在のファイル名、または直前の//line
ディレクティブで設定されたファイル名を再利用していました。これは、CL 86990044で導入された挙動であり、Go 1.2の「カレントディレクトリを使用する」挙動からの変更でした。
変更後のロジック:
filename := string(bytes.TrimSpace(text[len(prefix):i]))
if filename != "" {
filename = filepath.Clean(filename)
if !filepath.IsAbs(filename) {
// make filename relative to current directory
// ...
}
}
変更後では、if filename == ""
のブロックが完全に削除され、代わりに if filename != ""
のブロックが導入されています。これは、ファイル名が空文字列でない場合にのみ、filepath.Clean
やfilepath.IsAbs
などのパス処理を行うことを意味します。
この変更の最も重要な点は、ファイル名が空文字列である場合、go/scanner
はファイル名をそのまま空文字列として扱うようになったことです。以前のように、現在のファイル名や直前のファイル名を「推測」して割り当てることはなくなりました。
この変更の技術的意味合い:
- 明示的な空ファイル名:
//line :line_number
のようなディレクティブは、ファイル名が意図的に空であることを明確に示します。これは、特定のツールがファイル名を持たないコードスニペットを生成する場合に役立つ可能性があります。 - 推測ロジックの排除: 以前の挙動は、ファイル名が省略された場合に「適切な」ファイル名を推測しようとするものでした。しかし、この推測が常に正しいとは限らず、特に
godoc
のようなツールが誤った//line
ディレクティブを生成した場合に問題を引き起こしていました。推測ロジックを排除することで、go/scanner
の挙動がよりシンプルで予測可能になります。 - バグの修正:
godoc
のバグによって生成された不適切な//line
ディレクティブが、以前の推測ロジックによってさらに誤ったファイルパスにマッピングされる問題を解決します。ファイル名が空であれば、その情報がそのまま伝達され、後続のツールがその情報を適切に処理できるようになります(例えば、エラーメッセージでファイル名を表示しない、または「不明なファイル」として扱うなど)。 - テストケースの変更:
scanner_test.go
の変更もこの新しい挙動を反映しています。特に、//line :42
のようなテストケースが、以前はdir/File1.go
のようなファイル名にマッピングされていたものが、空文字列""
にマッピングされるように修正されています。これは、このコミットが意図する挙動の変更を明確に示しています。
この修正は、Goのツールチェインにおける//line
ディレクティブの堅牢性を高め、特にコード生成やドキュメント生成のシナリオにおける予期せぬファイルパスの問題を軽減するものです。
コアとなるコードの変更箇所
diff --git a/src/pkg/go/scanner/scanner.go b/src/pkg/go/scanner/scanner.go
index c83e4fa816..cec82ea10e 100644
--- a/src/pkg/go/scanner/scanner.go
+++ b/src/pkg/go/scanner/scanner.go
@@ -150,10 +150,7 @@ func (s *Scanner) interpretLineComment(text []byte) {
if line, err := strconv.Atoi(string(text[i+1:])); err == nil && line > 0 {
// valid //line filename:line comment
filename := string(bytes.TrimSpace(text[len(prefix):i]))
- if filename == "" {
- // assume same file as for previous line
- filename = s.file.Position(s.file.Pos(s.lineOffset)).Filename
- } else {
+ if filename != "" {
filename = filepath.Clean(filename)
if !filepath.IsAbs(filename) {
// make filename relative to current directory
diff --git a/src/pkg/go/scanner/scanner_test.go b/src/pkg/go/scanner/scanner_test.go
index 55e55abaec..fc450d8a6e 100644
--- a/src/pkg/go/scanner/scanner_test.go
+++ b/src/pkg/go/scanner/scanner_test.go
@@ -493,9 +493,8 @@ var segments = []segment{
{"\nline3 //line File1.go:100", filepath.Join("dir", "TestLineComments"), 3}, // bad line comment, ignored
{"\nline4", filepath.Join("dir", "TestLineComments"), 4},
{"\n//line File1.go:100\n line100", filepath.Join("dir", "File1.go"), 100},
- {"\n//line :42\n line1", "dir/File1.go", 42},
+ {"\n//line \t :42\n line1", "", 42},
{"\n//line File2.go:200\n line200", filepath.Join("dir", "File2.go"), 200},
- {"\n//line \t :123\n line1", "dir/File2.go", 123},
{"\n//line foo\t:42\n line42", filepath.Join("dir", "foo"), 42},
{"\n //line foo:42\n line44", filepath.Join("dir", "foo"), 44}, // bad line comment, ignored
{"\n//line foo 42\n line46", filepath.Join("dir", "foo"), 46}, // bad line comment, ignored
コアとなるコードの解説
src/pkg/go/scanner/scanner.go
の変更
このファイルの変更は、Scanner
構造体のinterpretLineComment
メソッド内にあります。このメソッドは、//line
ディレクティブを含むコメントを解析し、ファイルの位置情報を更新する役割を担っています。
変更前は以下のロジックがありました。
filename := string(bytes.TrimSpace(text[len(prefix):i]))
if filename == "" {
// assume same file as for previous line
filename = s.file.Position(s.file.Pos(s.lineOffset)).Filename
} else {
// ... (既存のファイル名がある場合の処理)
filename := string(bytes.TrimSpace(text[len(prefix):i]))
: これは、//line
ディレクティブからファイル名部分を抽出し、前後の空白をトリムしてfilename
変数に格納しています。if filename == ""
: ここが変更の核心です。以前は、抽出されたfilename
が空文字列である場合(つまり、//line :line_number
のようにファイル名が省略されている場合)、s.file.Position(s.file.Pos(s.lineOffset)).Filename
を使用して、現在のファイル名または直前の//line
ディレクティブで設定されたファイル名を「推測」してfilename
に割り当てていました。コメント// assume same file as for previous line
がその意図を示しています。else { ... }
: ファイル名が空でない場合は、filepath.Clean
やfilepath.IsAbs
などを用いてパスの正規化や絶対パス化を行っていました。
変更後、このif filename == ""
のブロックが削除され、代わりにif filename != ""
のブロックが導入されました。
filename := string(bytes.TrimSpace(text[len(prefix):i]))
if filename != "" {
filename = filepath.Clean(filename)
if !filepath.IsAbs(filename) {
// make filename relative to current directory
// ...
}
}
if filename != ""
: この変更により、抽出されたfilename
が空文字列でない場合にのみ、パスの正規化や絶対パス化の処理が行われるようになりました。- 重要な変更点:
filename
が空文字列である場合、以前のように他のファイル名を推測して割り当てることはなくなり、filename
はそのまま空文字列として扱われます。これにより、//line :line_number
のようなディレクティブは、ファイル名が明示的に空であることを示すようになりました。
src/pkg/go/scanner/scanner_test.go
の変更
このファイルは、go/scanner
パッケージのテストケースを含んでいます。変更は、segments
というテストデータスライスにあります。
変更前は以下のテストケースがありました。
{"\n//line :42\n line1", "dir/File1.go", 42},
{"\n//line \t :123\n line1", "dir/File2.go", 123},
{"\n//line :42\n line1", "dir/File1.go", 42}
: このテストケースは、ファイル名が省略された//line :42
ディレクティブが、dir/File1.go
というファイル名にマッピングされることを期待していました。これは、以前の「直前のファイル名を再利用する」挙動をテストしていました。{"\n//line \t :123\n line1", "dir/File2.go", 123}
: 同様に、このテストケースもファイル名が省略されたディレクティブがdir/File2.go
にマッピングされることを期待していました。
変更後、これらのテストケースは以下のように修正されました。
{"\n//line \t :42\n line1", "", 42},
// {"\n//line \t :123\n line1", "dir/File2.go", 123}, // 削除された行
{"\n//line \t :42\n line1", "", 42}
: このテストケースは、ファイル名が省略された//line :42
ディレクティブが、**空文字列""
**にマッピングされることを期待するように変更されました。これは、scanner.go
の変更によって導入された新しい挙動を直接テストしています。{"\n//line \t :123\n line1", "dir/File2.go", 123}
: このテストケースは完全に削除されました。これは、もはやこの挙動が期待されないためです。
これらのテストケースの変更は、go/scanner
がファイル名のない//line
ディレクティブをどのように解釈するかという、このコミットの核心的な変更を明確に示しています。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/0de521d111b88b2bc6466a4e1f29a8a284c0b3ee
- Go Gerrit Code Review (CL 88160050): https://golang.org/cl/88160050
参考にした情報源リンク
- Go
//line
directives: https://go.dev/doc/go1.4#line-directives (Go 1.4のリリースノートに//line
ディレクティブに関する記述があります) - Go
//line
directives (general explanation): https://lemoda.net/go/line-directives/ - Go
//line
directives (Go.dev blog): https://go.dev/blog/go1.4-line-directives - Go
//line
directives (GitHub source): https://github.com/golang/go/blob/master/src/go/scanner/scanner.go (現在のgo/scanner
のソースコード) - Go issue #7765 (直接的な検索結果は見つかりませんでしたが、コミットメッセージからその存在と修正対象であることが示唆されています)
- Go CL 86990044 (直接的な検索結果は見つかりませんでしたが、コミットメッセージからその挙動が示唆されています)