[インデックス 13119] ファイルの概要
このコミットは、Go言語のgo/scanner
パッケージにおけるコメント処理の改善と、関連するテストのクリーンアップを目的としています。具体的には、コメント内に含まれるキャリッジリターン(\r
)文字を適切に除去するようにスキャナーの動作が修正され、これに伴いテストコードも更新されています。
コミット
commit 7b9a6d8ddafea4c72f507f7254c3526fdcbbd543
Author: Robert Griesemer <gri@golang.org>
Date: Tue May 22 10:03:53 2012 -0700
go/scanner: strip carriage returns from commments
Also:
- cleaned up and simplified TestScan
- added tests for comments containing carriage returns
Fixes #3647.
R=rsc
CC=golang-dev
https://golang.org/cl/6225047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7b9a6d8ddafea4c72f507f7254c3526fdcbbd543
元コミット内容
このコミットの元のメッセージは以下の通りです。
go/scanner: strip carriage returns from commments
Also:
- cleaned up and simplified TestScan
- added tests for comments containing carriage returns
Fixes #3647.
R=rsc
CC=golang-dev
https://golang.org/cl/6225047
変更の背景
この変更の主な背景は、Go言語のソースコードスキャナー(go/scanner
パッケージ)がコメントを処理する際に、キャリッジリターン(\r
)文字を適切に扱っていなかったバグ(Issue #3647)の修正です。
多くのテキストファイル、特にWindows環境で作成されたファイルでは、改行コードとしてCRLF(\r\n
)が使用されます。一方、Unix/Linux環境ではLF(\n
)が一般的です。Go言語のソースコードは、プラットフォームに依存しない形で解析されるべきであり、コメント内の改行コードの違いがスキャナーの出力に影響を与えるべきではありませんでした。
以前のgo/scanner
の実装では、コメント内の\r
文字がそのままコメントリテラルに含まれてしまう可能性がありました。これは、ツールがGoソースコードを解析する際に予期せぬ動作を引き起こしたり、コメントの内容がプラットフォームによって異なって解釈される原因となる可能性がありました。このコミットは、この不整合を解消し、コメントリテラルから\r
文字を確実に除去することで、スキャナーの堅牢性とクロスプラットフォーム互換性を向上させています。
また、この修正と並行して、go/scanner
パッケージのテストスイートであるTestScan
関数が大幅にクリーンアップされ、簡素化されています。これにより、テストの可読性と保守性が向上し、将来的な変更に対する安定性が確保されています。
前提知識の解説
Go言語のgo/scanner
パッケージ
go/scanner
パッケージは、Go言語の標準ライブラリの一部であり、Goソースコードを字句解析(lexical analysis)するための機能を提供します。字句解析とは、ソースコードをトークン(識別子、キーワード、演算子、リテラルなど)のストリームに分解するプロセスです。これはコンパイラやリンター、フォーマッターなどのGoツールチェーンの基盤となる重要なステップです。
- スキャナーの役割:
go/scanner
は、Goの文法規則に従ってソースコードを読み込み、各文字シーケンスがどの種類のトークンに属するかを識別します。例えば、func
はキーワード、main
は識別子、"hello"
は文字列リテラル、// This is a comment
はコメントとして認識されます。 - トークンとリテラル: スキャナーは、トークンの種類(
token.Token
型)と、そのトークンに対応するソースコード上の実際の文字列(リテラル)を返します。例えば、文字列リテラル"hello"
の場合、トークンはtoken.STRING
、リテラルは"hello"
となります。コメントもtoken.COMMENT
というトークンタイプを持ち、そのリテラルはコメントの内容全体です。 - コメントの扱い: Go言語では、
//
による行コメントと/* */
によるブロックコメントの2種類があります。スキャナーはこれらのコメントを認識し、通常はトークンストリームから除外しますが、ScanComments
フラグが設定されている場合はコメントもトークンとして扱います。このコミットは、コメントがトークンとして扱われる際のリテラル内容の正確性に関わるものです。
キャリッジリターン(\r
)とラインフィード(\n
)
- ASCII制御文字:
\r
(Carriage Return, CR, ASCIIコード13)と\n
(Line Feed, LF, ASCIIコード10)は、テキストファイルにおける改行を表すために使用される制御文字です。 - 改行コードの歴史:
- LF (
\n
): Unix系システム(Linux, macOSなど)で標準的な改行コードです。 - CRLF (
\r\n
): Windows系システムで標準的な改行コードです。タイプライターの「キャリッジを戻し(CR)、紙を一行送る(LF)」という動作に由来します。 - CR (
\r
): 古いMac OS(OS 9以前)で使われていましたが、現在はほとんど見られません。
- LF (
- プログラミングにおける影響: プログラミング言語のパーサーやスキャナーは、これらの改行コードの違いを適切に処理する必要があります。特に、文字列リテラルやコメントなど、ソースコードの一部としてテキストデータが扱われる場合、改行コードの正規化は重要です。正規化が行われないと、異なるOSで作成されたソースファイルが異なる結果を生む可能性があります。
Go言語のIssueトラッカー(Fixes #3647
)
Fixes #3647
は、このコミットがGoプロジェクトのIssueトラッカー(通常はGitHub IssuesまたはGoの独自のIssueトラッカー)で報告された3647番のバグを修正したことを示します。Issue #3647は、go/scanner
がコメント内の\r
文字を適切に処理しないという問題に関するものでした。コミットメッセージにFixes #<issue_number>
と記述することで、コミットがマージされた際に自動的に該当するIssueがクローズされる仕組みがGoプロジェクトでは採用されています。
技術的詳細
このコミットの技術的詳細な変更点は、go/scanner
パッケージがコメントをスキャンするロジックに\r
文字の検出と除去のメカニズムを導入したことです。
-
scanComment()
関数の変更:scanComment()
関数は、//
または/* */
形式のコメントを読み取り、その内容を文字列として返します。- この関数内に
hasCR
という新しいブーリアン変数(false
で初期化)が導入されました。 - コメントの内容をスキャンするループ内で、現在の文字
s.ch
が\r
であるかどうかがチェックされます。 - もし
s.ch == '\r'
であれば、hasCR
がtrue
に設定されます。これは、コメント内に少なくとも1つのキャリッジリターン文字が存在することを示します。 - コメントの終端に達した後、スキャンされたコメントリテラル(
lit := s.src[offs:s.offset]
)に対して、hasCR
がtrue
の場合にのみstripCR(lit)
関数が呼び出されます。 stripCR
関数は、Goの標準ライブラリの一部であるbytes
パッケージのbytes.ReplaceAll
や、あるいはカスタム実装によって、バイトスライスからすべての\r
文字を除去する役割を担います。このコミットのdiffにはstripCR
関数の定義は含まれていませんが、その存在と機能は文脈から明らかです。- 最終的に、
\r
が除去された(または元々存在しなかった)コメントリテラルが文字列として返されます。
-
scanner_test.go
の変更:TestScan
関数は、go/scanner
の動作を検証するための主要なテスト関数です。このコミットでは、このテスト関数が大幅にリファクタリングされています。- 新しいテストケースの追加:
tokens
配列に、\r
を含むコメントの新しいテストケースが追加されました。{token.COMMENT, "/*\r*/", special}
: ブロックコメント内に\r
を含むケース。{token.COMMENT, "//\r\n", special}
: 行コメント内に\r
を含むケース(\r
の後に\n
が続くCRLF形式)。
- テストロジックの簡素化と正確化:
- 以前は
src_linecount
やwhitespace_linecount
といった変数を事前に計算していましたが、これらが削除され、テストのフローがより直接的になりました。 checkPos
関数の呼び出しが簡素化され、token.EOF
の場合のepos
(期待される位置)の計算が修正されました。elit
(期待されるリテラル)の計算ロジックがswitch
文を使って整理されました。特に、token.COMMENT
の場合にstripCR
が適用されること、そして行コメントの場合に末尾の\n
がリテラルに含まれないようにする処理が明示されました。- 生文字列リテラル(バッククォートで囲まれた文字列)についても、
\r
が除去されるべきであることがテストロジックに反映されました。 - テストの各ステップでの位置(
epos.Offset
,epos.Line
)の更新ロジックがより正確に、かつ簡潔に記述されるようになりました。
- 以前は
これらの変更により、go/scanner
はコメント内の\r
文字を透過的に処理し、常に正規化されたコメントリテラルを返すようになります。これにより、Goソースコードの解析ツールがプラットフォームの改行コードの違いに影響されることなく、一貫した動作を保証できるようになります。
コアとなるコードの変更箇所
src/pkg/go/scanner/scanner.go
--- a/src/pkg/go/scanner/scanner.go
+++ b/src/pkg/go/scanner/scanner.go
@@ -157,11 +157,15 @@ func (s *Scanner) interpretLineComment(text []byte) {
func (s *Scanner) scanComment() string {
// initial '/' already consumed; s.ch == '/' || s.ch == '*'
offs := s.offset - 1 // position of initial '/'
+ hasCR := false
if s.ch == '/' {
//-style comment
s.next()
for s.ch != '\n' && s.ch >= 0 {
+ if s.ch == '\r' {
+ hasCR = true
+ }
s.next()
}
if offs == s.lineOffset {
@@ -175,6 +179,9 @@ func (s *Scanner) scanComment() string {
s.next()
for s.ch >= 0 {
ch := s.ch
+ if ch == '\r' {
+ hasCR = true
+ }
s.next()
if ch == '*' && s.ch == '/' {
s.next()
@@ -185,7 +192,12 @@ func (s *Scanner) scanComment() string {
s.error(offs, "comment not terminated")
exit:
- return string(s.src[offs:s.offset])
+ lit := s.src[offs:s.offset]
+ if hasCR {
+ lit = stripCR(lit)
+ }
+
+ return string(lit)
}
func (s *Scanner) findLineEnd() bool {
@@ -527,6 +539,8 @@ func (s *Scanner) switch4(tok0, tok1 token.Token, ch2 rune, tok2, tok3 token.Tok
// token.IMAG, token.CHAR, token.STRING) or token.COMMENT, the literal string
// has the corresponding value.
//
+// If the returned token is a keyword, the literal string is the keyword.
+//
// If the returned token is token.SEMICOLON, the corresponding
// literal string is ";" if the semicolon was present in the source,
// and "\n" if the semicolon was inserted because of a newline or
src/pkg/go/scanner/scanner_test.go
--- a/src/pkg/go/scanner/scanner_test.go
+++ b/src/pkg/go/scanner/scanner_test.go
@@ -43,6 +43,8 @@ var tokens = [...]elt{\n // Special tokens\n {token.COMMENT, "/* a comment */", special},\n {token.COMMENT, "// a comment \n", special},\n+\t{token.COMMENT, "/*\r*/", special},\n+\t{token.COMMENT, "//\r\n", special},\n \n // Identifiers and basic type literals\n {token.IDENT, "foobar", literal},\
@@ -214,8 +216,6 @@ func checkPos(t *testing.T, lit string, p token.Pos, expected token.Position) {\
// Verify that calling Scan() provides the correct results.\n func TestScan(t *testing.T) {\n-\t// make source\n-\tsrc_linecount := newlineCount(string(source))\n \twhitespace_linecount := newlineCount(whitespace)\n \n \t// error handler\n@@ -226,59 +226,81 @@ func TestScan(t *testing.T) {\
\t// verify scan\n \tvar s Scanner\n \ts.Init(fset.AddFile("", fset.Base(), len(source)), source, eh, ScanComments|dontInsertSemis)\
-\tindex := 0\n-\t// epos is the expected position\n+\n+\t// set up expected position\n \tepos := token.Position{\n \t\tFilename: "",\n \t\tOffset: 0,\n \t\tLine: 1,\n \t\tColumn: 1,\n \t}\n+\n+\tindex := 0\
\tfor {\n \t\tpos, tok, lit := s.Scan()\
-\t\tif lit == "" {\n-\t\t\t// no literal value for non-literal tokens\n-\t\t\tlit = tok.String()\n+\n+\t\t// check position\n+\t\tif tok == token.EOF {\n+\t\t\t// correction for EOF\n+\t\t\tepos.Line = newlineCount(string(source))\n+\t\t\tepos.Column = 2\n \t\t}\n+\t\tcheckPos(t, lit, pos, epos)\n+\n+\t\t// check token\n \t\te := elt{token.EOF, "", special}\n \t\tif index < len(tokens) {\n \t\t\te = tokens[index]\n+\t\t\tindex++\
\t\t}\n-\t\tif tok == token.EOF {\n-\t\t\tlit = "<EOF>"\n-\t\t\tepos.Line = src_linecount\n-\t\t\tepos.Column = 2\n-\t\t}\n-\t\tcheckPos(t, lit, pos, epos)\
\t\tif tok != e.tok {\n \t\t\tt.Errorf("bad token for %q: got %s, expected %s", lit, tok, e.tok)\
\t\t}\n-\t\tif e.tok.IsLiteral() {\n-\t\t\t// no CRs in raw string literals\n-\t\t\telit := e.lit\n-\t\t\tif elit[0] == '`' {\n-\t\t\t\telit = string(stripCR([]byte(elit)))\n-\t\t\t\tepos.Offset += len(e.lit) - len(lit) // correct position\n-\t\t\t}\n-\t\t\tif lit != elit {\n-\t\t\t\tt.Errorf("bad literal for %q: got %q, expected %q", lit, lit, elit)\n-\t\t\t}\n-\t\t}\n+\n+\t\t// check token class\n \t\tif tokenclass(tok) != e.class {\n \t\t\tt.Errorf("bad class for %q: got %d, expected %d", lit, tokenclass(tok), e.class)\
\t\t}\n-\t\tepos.Offset += len(lit) + len(whitespace)\n-\t\tepos.Line += newlineCount(lit) + whitespace_linecount\n-\t\tif tok == token.COMMENT && lit[1] == '/' {\n-\t\t\t// correct for unaccounted '/n' in //-style comment\n-\t\t\tepos.Offset++\n-\t\t\tepos.Line++\n+\n+\t\t// check literal\n+\t\telit := ""\n+\t\tswitch e.tok {\n+\t\tcase token.COMMENT:\n+\t\t\t// no CRs in comments\n+\t\t\telit = string(stripCR([]byte(e.lit)))\n+\t\t\t//-style comment literal doesn't contain newline\n+\t\t\tif elit[1] == '/' {\n+\t\t\t\telit = elit[0 : len(elit)-1]\n+\t\t\t}\n+\t\tcase token.IDENT:\n+\t\t\telit = e.lit\n+\t\tcase token.SEMICOLON:\n+\t\t\telit = ";"\n+\t\tdefault:\n+\t\t\tif e.tok.IsLiteral() {\n+\t\t\t\t// no CRs in raw string literals\n+\t\t\t\telit = e.lit\n+\t\t\t\tif elit[0] == '`' {\n+\t\t\t\t\telit = string(stripCR([]byte(elit)))\n+\t\t\t\t}\n+\t\t\t} else if e.tok.IsKeyword() {\n+\t\t\t\telit = e.lit\n+\t\t\t}\n+\t\t}\n+\t\tif lit != elit {\n+\t\t\tt.Errorf("bad literal for %q: got %q, expected %q", lit, lit, elit)\
\t\t}\n-\t\tindex++\n+\n \t\tif tok == token.EOF {\n \t\t\tbreak\n \t\t}\n+\n+\t\t// update position\n+\t\tepos.Offset += len(e.lit) + len(whitespace)\n+\t\tepos.Line += newlineCount(e.lit) + whitespace_linecount\n+\n \t}\n+\n \tif s.ErrorCount != 0 {\n \t\tt.Errorf("found %d errors", s.ErrorCount)\
\t}\
コアとなるコードの解説
src/pkg/go/scanner/scanner.go
の変更点
scanComment()
関数は、Goソースコード内のコメントを字句解析する役割を担っています。この関数への変更は、コメントリテラルからキャリッジリターン(\r
)文字を確実に除去するためのものです。
hasCR
フラグの導入:hasCR := false
という新しいブーリアン変数が導入されました。これは、現在スキャン中のコメント内に\r
文字が見つかったかどうかを追跡するためのフラグです。
\r
文字の検出:- 行コメント(
//
スタイル)とブロックコメント(/* */
スタイル)の両方のスキャンループ内で、現在の文字s.ch
が\r
であるかどうかがチェックされます。 if s.ch == '\r' { hasCR = true }
- このチェックにより、コメントの内容を読み進める過程で
\r
文字が検出されると、hasCR
フラグがtrue
に設定されます。
- 行コメント(
stripCR
関数の適用:- コメントのスキャンが完了し、
exit:
ラベルに到達した後、コメントのバイトスライスlit
が取得されます(lit := s.src[offs:s.offset]
)。 if hasCR { lit = stripCR(lit) }
という条件文が追加されました。これは、コメント内に\r
文字が検出された場合にのみ、stripCR
関数を呼び出してlit
からすべての\r
文字を除去することを意味します。stripCR
関数は、Goのbytes
パッケージのbytes.ReplaceAll(b, []byte{'\r'}, []byte{})
のような実装を持つユーティリティ関数であると推測されます。これにより、コメントリテラルが正規化され、\r
文字が含まれないようになります。
- コメントのスキャンが完了し、
switch4
関数のコメント更新:switch4
関数のドキュメンテーションコメントに// If the returned token is a keyword, the literal string is the keyword.
という行が追加されました。これは、スキャナーがキーワードを返す際のlit
(リテラル)の振る舞いに関する説明を明確にするものです。直接的な機能変更ではありませんが、ドキュメンテーションの改善です。
これらの変更により、go/scanner
は、ソースコードの改行コード形式(LF, CRLFなど)に関わらず、コメントの内容を常に\r
を含まない形で提供するようになります。これは、Goツールチェーン全体の一貫性と堅牢性を高める上で重要です。
src/pkg/go/scanner/scanner_test.go
の変更点
scanner_test.go
のTestScan
関数は、go/scanner
の字句解析機能が正しく動作するかを検証するための統合テストです。このコミットでは、テストの構造が大幅に改善され、\r
を含むコメントの新しいテストケースが追加されました。
-
新しいテストケースの追加:
var tokens
配列に、\r
を含むコメントのテストケースが追加されました。{token.COMMENT, "/*\r*/", special}
: ブロックコメント内に\r
。{token.COMMENT, "//\r\n", special}
: 行コメント内に\r
と\n
(CRLF)。
- これらのテストケースは、
scanComment()
関数が\r
を正しく除去することを確認するために不可欠です。
-
TestScan
関数のリファクタリング:- 初期化の簡素化:
src_linecount
やwhitespace_linecount
といった初期計算が削除され、テストのセットアップがより簡潔になりました。 - 位置チェックの改善:
epos
(期待される位置)の更新ロジックが、より正確かつ簡潔になりました。特に、token.EOF
の場合の行と列の計算が修正されました。checkPos(t, lit, pos, epos)
の呼び出しが、各トークンの処理の早い段階で行われるようになりました。
- リテラルチェックの強化:
elit := ""
で期待されるリテラルを初期化し、switch e.tok
文を使って、トークンの種類に応じてelit
を動的に決定するようになりました。case token.COMMENT:
ブロックでは、stripCR
関数がe.lit
に適用され、さらに行コメント(//
スタイル)の場合は末尾の\n
が除去されるようにelit = elit[0 : len(elit)-1]
が適用されます。これは、go/scanner
が行コメントのリテラルを// comment text
のように、末尾の改行を含まない形で返すという仕様に合わせたものです。- 生文字列リテラル(
elit[0] == '
'の条件で
stripCRが適用されるようになりました。これは、生文字列リテラルも
\r`を含まない形で扱われるべきであることをテストで確認するためです。 if lit != elit
で、実際にスキャンされたリテラルlit
と期待されるリテラルelit
が一致するかを厳密にチェックします。
- 位置更新ロジックの整理:
epos.Offset += len(e.lit) + len(whitespace)
epos.Line += newlineCount(e.lit) + whitespace_linecount
- これらの行は、各トークンが消費する文字数と改行数を正確に反映するように調整され、テストの堅牢性が向上しました。
- 初期化の簡素化:
これらのテストの変更は、go/scanner
のコメント処理の修正が正しく機能していることを検証するだけでなく、パッケージ全体のテストスイートの品質と信頼性を向上させるものです。
関連リンク
- Go Issue 3647: https://github.com/golang/go/issues/3647 (このコミットが修正したバグのIssueページ)
- Go
go/scanner
パッケージドキュメント: https://pkg.go.dev/go/scanner (Go言語のgo/scanner
パッケージの公式ドキュメント) - Go
token
パッケージドキュメント: https://pkg.go.dev/go/token (Go言語のtoken
パッケージの公式ドキュメント。token.COMMENT
などのトークンタイプが定義されています)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
go/scanner
パッケージ) - GitHubのGoリポジトリのIssueトラッカー
- 一般的なプログラミングにおける改行コード(CR, LF, CRLF)に関する知識
- 字句解析(Lexical Analysis)に関する一般的な知識