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

[インデックス 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 fmtgo 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.Cleanfilepath.IsAbsなどのパス処理を行うことを意味します。

この変更の最も重要な点は、ファイル名が空文字列である場合、go/scannerはファイル名をそのまま空文字列として扱うようになったことです。以前のように、現在のファイル名や直前のファイル名を「推測」して割り当てることはなくなりました。

この変更の技術的意味合い:

  1. 明示的な空ファイル名: //line :line_number のようなディレクティブは、ファイル名が意図的に空であることを明確に示します。これは、特定のツールがファイル名を持たないコードスニペットを生成する場合に役立つ可能性があります。
  2. 推測ロジックの排除: 以前の挙動は、ファイル名が省略された場合に「適切な」ファイル名を推測しようとするものでした。しかし、この推測が常に正しいとは限らず、特にgodocのようなツールが誤った//lineディレクティブを生成した場合に問題を引き起こしていました。推測ロジックを排除することで、go/scannerの挙動がよりシンプルで予測可能になります。
  3. バグの修正: godocのバグによって生成された不適切な//lineディレクティブが、以前の推測ロジックによってさらに誤ったファイルパスにマッピングされる問題を解決します。ファイル名が空であれば、その情報がそのまま伝達され、後続のツールがその情報を適切に処理できるようになります(例えば、エラーメッセージでファイル名を表示しない、または「不明なファイル」として扱うなど)。
  4. テストケースの変更: 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.Cleanfilepath.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ディレクティブをどのように解釈するかという、このコミットの核心的な変更を明確に示しています。

関連リンク

参考にした情報源リンク