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

[インデックス 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ディレクティブが、意図せずカレントディレクトリを指すという誤った挙動を引き起こしていました。

変更後のコードでは、このロジックが修正され、ファイル名部分が空文字列である場合の特別なハンドリングが追加されました。

  1. まず、text[len(prefix):i]で抽出されたファイル名部分に対してbytes.TrimSpaceが適用されます。これにより、ファイル名部分に余分な空白が含まれていても正確に処理されます。
  2. 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を呼び出すことで、直前の行のファイル名を取得します。
  3. ファイル名が空でない場合、以前と同様にfilepath.Cleanfilepath.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)
					}
				}
  1. bytes.TrimSpaceの導入: filename := string(bytes.TrimSpace(text[len(prefix):i])) まず、抽出したファイル名部分のバイトスライスから、bytes.TrimSpaceを使って先頭と末尾の空白文字を削除します。これにより、//line \t :123のようにファイル名部分に空白が含まれていても、それが正しく空文字列として認識されるようになります。

  2. ファイル名が空の場合の新しいロジック: 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ディレクティブは、直前の行と同じファイル名を継承するようになります。
  3. ファイル名が空でない場合の既存ロジックの維持: 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言語のソースコード (特に go/scanner パッケージ)
  • Go言語のIssueトラッカー (Issue #7765)
  • C言語のプリプロセッサにおける#lineディレクティブのドキュメント (一般的な知識として)
  • Go言語のfilepathパッケージのドキュメント
  • Go言語のbytesパッケージのドキュメント
  • Go言語のstrconvパッケージのドキュメント
  • Go言語のtokenパッケージのドキュメント (特にtoken.Postoken.Positionについて)