[インデックス 19176] ファイルの概要
このコミットは、Go言語のgo/scanner
パッケージにおける//line
ディレクティブの解釈方法を改善するものです。具体的には、ファイル名が指定されていない//line
ディレクティブ(例: //line :42
)が、以前は現在のファイルのディレクトリを指していた不適切な挙動を修正し、C言語のプリプロセッサのように直前の行と同じファイル名を指すように変更します。これにより、デバッグ情報やエラーメッセージの正確性が向上します。
コミット
commit d079144190085da29ace309164e30f5a2bee492e
Author: Alan Donovan <adonovan@google.com>
Date: Wed Apr 16 14:51:33 2014 -0400
go/scanner: interpret //line directives sans filename sensibly
A //line directive without a filename now denotes the same
filename as the previous line (as in C).
Previously it denoted the file's directory (!).
Fixes #7765
LGTM=gri
R=gri
CC=golang-codereviews
https://golang.org/cl/86990044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d079144190085da29ace309164e30f5a2bee492e
元コミット内容
このコミットは、go/scanner
パッケージが//line
ディレクティブを解釈する方法を変更します。以前は、ファイル名が省略された//line
ディレクティブ(例: //line :123
)が、そのディレクティブを含むファイルのディレクトリを不適切に参照していました。この変更により、ファイル名が省略された場合、そのディレクティブは直前の行と同じファイル名を指すようになります。これはC言語のプリプロセッサにおける同様のディレクティブの挙動に合わせたものです。この修正はIssue #7765に対応しています。
変更の背景
Go言語のコンパイラやツールチェーンは、ソースコードの正確な位置情報(ファイル名と行番号)を追跡するために、//line
ディレクティブを使用します。これは、特にコード生成ツールやプリプロセッサが元のソースコードの行番号を保持したい場合に重要です。
このコミット以前は、//line
ディレクティブでファイル名が省略された場合(例: //line :123
)、go/scanner
は誤ってそのディレクティブが記述されているファイルの「ディレクトリ」をファイル名として解釈していました。これは直感的ではなく、デバッグ情報やエラーメッセージが不正確になる原因となっていました。例えば、main.go
というファイル内で//line :123
と記述した場合、その後のコードの行番号はmain.go
の123行目から始まるべきですが、実際には./
(カレントディレクトリ)の123行目として扱われる可能性がありました。
この不整合はIssue #7765として報告されており、このコミットはその問題を解決するために導入されました。C言語のプリプロセッサにおける#line
ディレクティブの挙動(ファイル名が省略された場合は直前のファイル名を継承する)に合わせることで、より自然で期待される動作を実現し、ツールチェーンの正確性を向上させることが目的です。
前提知識の解説
//line
ディレクティブ
Go言語には、C言語の#line
ディレクティブに似た//line
ディレクティブが存在します。これは、コンパイラに対して、その後のコードのソースファイル名と行番号を上書きするよう指示するために使用されます。主な用途は以下の通りです。
- コード生成: テンプレートエンジンやコード生成ツールが、生成されたコードの元のソースファイルと行番号を指し示すために使用します。これにより、生成されたコードでエラーが発生した場合でも、元のテンプレートやソースコードのどこに問題があったかを特定しやすくなります。
- プリプロセッサ: Goのソースコードを変換するプリプロセッサが、変換後のコードの行番号を元のコードにマッピングするために使用します。
//line
ディレクティブの一般的な形式は以下の通りです。
//line filename:line_number
filename
: 新しいソースファイル名。line_number
: 新しい行番号。
例えば、//line my_generated_file.go:100
と記述すると、その後のコードはmy_generated_file.go
の100行目から始まったものとして扱われます。
go/scanner
パッケージ
go/scanner
パッケージは、Go言語のソースコードをスキャン(字句解析)し、トークンに分割するための機能を提供します。コンパイラのフロントエンドの一部であり、ソースコードを行単位で読み込み、コメント、文字列、識別子、キーワードなどの要素を識別します。//line
ディレクティブの解釈もこのパッケージの役割の一部です。
filepath
パッケージ
filepath
パッケージは、ファイルパスを操作するためのユーティリティ関数を提供します。このコミットでは、特に以下の関数が関連します。
filepath.Clean(path string) string
: パスをクリーンアップし、冗長な要素(例:.
、..
、複数のスラッシュ)を削除し、標準的な形式に変換します。filepath.IsAbs(path string) bool
: パスが絶対パスであるかどうかを判定します。filepath.Join(elem ...string) string
: 複数のパス要素を結合して、単一のパスを構築します。
bytes
パッケージ
bytes
パッケージは、バイトスライスを操作するための関数を提供します。このコミットでは、bytes.TrimSpace
が使用されており、これはバイトスライスの先頭と末尾から空白文字を削除します。
strconv
パッケージ
strconv
パッケージは、文字列と数値の間の変換を行うための関数を提供します。このコミットでは、strconv.Atoi
が使用されており、これは文字列を整数に変換します。
技術的詳細
このコミットの核心は、go/scanner
パッケージ内のScanner.interpretLineComment
メソッドの変更にあります。このメソッドは、//line
ディレクティブを含むコメントを解析し、スキャナーの現在のファイル位置情報を更新する役割を担っています。
変更前のコードでは、//line
ディレクティブのファイル名部分が空文字列だった場合(例: //line :123
)、filename
変数は空文字列となり、その後の処理でfilepath.Join(s.dir, filename)
によって、スキャナーが現在処理しているファイルのディレクトリ(s.dir
)と結合されていました。これにより、ファイル名が省略された//line
ディレクティブが、意図せずカレントディレクトリを指すという誤った挙動を引き起こしていました。
変更後のコードでは、このロジックが修正され、ファイル名部分が空文字列である場合の特別なハンドリングが追加されました。
- まず、
text[len(prefix):i]
で抽出されたファイル名部分に対してbytes.TrimSpace
が適用されます。これにより、ファイル名部分に余分な空白が含まれていても正確に処理されます。 filename == ""
(ファイル名が空文字列)の場合、スキャナーは直前の行と同じファイル名を継承するように変更されました。これは、s.file.Position(s.file.Pos(s.lineOffset)).Filename
という式によって実現されます。s.lineOffset
は、現在のコメント行の開始位置を示します。s.file.Pos(s.lineOffset)
は、その位置に対応するtoken.Pos
(ファイル内の絶対位置)を取得します。s.file.Position(...)
は、そのtoken.Pos
からtoken.Position
構造体(ファイル名、行番号、列番号を含む)を取得します。- 最後に
.Filename
を呼び出すことで、直前の行のファイル名を取得します。
- ファイル名が空でない場合、以前と同様に
filepath.Clean
とfilepath.Join
を使用して、ファイルパスが適切に処理されます。
この変更により、//line :line_number
という形式のディレクティブが、C言語の#line
ディレクティブと同様に、直前の行のファイル名を継承し、指定された行番号にジャンプするという、より直感的で正確な動作をするようになりました。
テストコードsrc/pkg/go/scanner/scanner_test.go
にも、この新しい挙動を検証するためのテストケースが追加されています。特に、//line :42
や//line \t :123
のような、ファイル名が省略されたケースが正しく処理されることを確認しています。
コアとなるコードの変更箇所
src/pkg/go/scanner/scanner.go
--- a/src/pkg/go/scanner/scanner.go
+++ b/src/pkg/go/scanner/scanner.go
@@ -148,11 +148,17 @@ func (s *Scanner) interpretLineComment(text []byte) {
// get filename and line number, if any
if i := bytes.LastIndex(text, []byte{':'}); i > 0 {
if line, err := strconv.Atoi(string(text[i+1:])); err == nil && line > 0 {
- // valid //line filename:line comment;
- filename := filepath.Clean(string(text[len(prefix):i]))
- if !filepath.IsAbs(filename) {
- // make filename relative to current directory
- filename = filepath.Join(s.dir, filename)
+ // 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 {
+ filename = filepath.Clean(filename)
+ if !filepath.IsAbs(filename) {
+ // make filename relative to current directory
+ filename = filepath.Join(s.dir, filename)
+ }
}
// update scanner position
s.file.AddLineInfo(s.lineOffset+len(text)+1, filename, line) // +len(text)+1 since comment applies to next line
src/pkg/go/scanner/scanner_test.go
--- a/src/pkg/go/scanner/scanner_test.go
+++ b/src/pkg/go/scanner/scanner_test.go
@@ -493,9 +493,10 @@ 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},
+\t{"\n//line :42\n line1", "dir/File1.go", 42},
{"\n//line File2.go:200\n line200", filepath.Join("dir", "File2.go"), 200},
-\t{"\n//line :1\n line1", "dir", 1},\n-\t{"\n//line foo:42\n line42", filepath.Join("dir", "foo"), 42},\n+\t{"\n//line \t :123\n line1", "dir/File2.go", 123},\n+\t{"\n//line foo\t:42\n line42", filepath.Join("dir", "foo"), 42},\n {"\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
{"\n//line foo:42 extra text\n line48", filepath.Join("dir", "foo"), 48}, // bad line comment, ignored
コアとなるコードの解説
src/pkg/go/scanner/scanner.go
の変更点
Scanner.interpretLineComment
関数内の変更は、//line
ディレクティブのファイル名部分の解釈ロジックを改善しています。
変更前:
filename := filepath.Clean(string(text[len(prefix):i]))
if !filepath.IsAbs(filename) {
// make filename relative to current directory
filename = filepath.Join(s.dir, filename)
}
このコードでは、//line
ディレクティブから抽出されたファイル名部分(text[len(prefix):i]
)を直接string
に変換し、filepath.Clean
で処理していました。もしファイル名部分が空文字列だった場合、filename
も空文字列となり、その後のfilepath.Join(s.dir, filename)
によってs.dir
(現在のファイルのディレクトリ)がファイル名として設定されてしまう問題がありました。
変更後:
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 = filepath.Clean(filename)
if !filepath.IsAbs(filename) {
// make filename relative to current directory
filename = filepath.Join(s.dir, filename)
}
}
-
bytes.TrimSpace
の導入:filename := string(bytes.TrimSpace(text[len(prefix):i]))
まず、抽出したファイル名部分のバイトスライスから、bytes.TrimSpace
を使って先頭と末尾の空白文字を削除します。これにより、//line \t :123
のようにファイル名部分に空白が含まれていても、それが正しく空文字列として認識されるようになります。 -
ファイル名が空の場合の新しいロジック:
if filename == ""
ブロックが追加されました。filename = s.file.Position(s.file.Pos(s.lineOffset)).Filename
この行が、ファイル名が省略された場合の新しい挙動を定義しています。s.lineOffset
: 現在解析中のコメント行のファイル内での開始オフセット(バイト位置)です。s.file.Pos(s.lineOffset)
:s.lineOffset
に対応するtoken.Pos
(Goのトークン位置を表す型)を取得します。s.file.Position(...)
: このtoken.Pos
から、ファイル名、行番号、列番号を含むtoken.Position
構造体を取得します。.Filename
: 取得したtoken.Position
構造体から、現在の(つまり、//line
ディレクティブが記述されている)ファイルのファイル名を取得します。これにより、ファイル名が省略された//line
ディレクティブは、直前の行と同じファイル名を継承するようになります。
-
ファイル名が空でない場合の既存ロジックの維持:
else
ブロックには、ファイル名が指定されている場合の既存のロジックがそのまま残されています。filepath.Clean(filename)
: パスを正規化します。!filepath.IsAbs(filename)
: パスが絶対パスでない場合、filepath.Join(s.dir, filename)
で現在のディレクトリと結合し、相対パスを絶対パスに変換します。
この変更により、//line
ディレクティブのファイル名が省略された場合でも、スキャナーは正確なファイル位置情報を維持できるようになり、デバッグやエラー報告の精度が向上します。
src/pkg/go/scanner/scanner_test.go
の変更点
テストファイルには、新しい挙動を検証するためのテストケースが追加されています。
-
{"\n//line :42\n line1", "dir/File1.go", 42},
このテストケースは、ファイル名が省略された//line
ディレクティブ(:42
)が、直前のファイル名(この場合はFile1.go
)を継承し、行番号が42に設定されることを確認しています。 -
{"\n//line \t :123\n line1", "dir/File2.go", 123},
このテストケースは、ファイル名部分に空白が含まれている場合でも、それが正しく空文字列として解釈され、直前のファイル名(File2.go
)が継承されることを確認しています。
これらのテストケースは、go/scanner
が//line
ディレクティブを正しく解釈し、特にファイル名が省略された場合の挙動が期待通りであることを保証します。
関連リンク
- Go CL 86990044: https://golang.org/cl/86990044
- Go Issue 7765: https://github.com/golang/go/issues/7765
参考にした情報源リンク
- Go言語のソースコード (特に
go/scanner
パッケージ) - Go言語のIssueトラッカー (Issue #7765)
- C言語のプリプロセッサにおける
#line
ディレクティブのドキュメント (一般的な知識として) - Go言語の
filepath
パッケージのドキュメント - Go言語の
bytes
パッケージのドキュメント - Go言語の
strconv
パッケージのドキュメント - Go言語の
token
パッケージのドキュメント (特にtoken.Pos
とtoken.Position
について)