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

[インデックス 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以前)で使われていましたが、現在はほとんど見られません。
  • プログラミングにおける影響: プログラミング言語のパーサーやスキャナーは、これらの改行コードの違いを適切に処理する必要があります。特に、文字列リテラルやコメントなど、ソースコードの一部としてテキストデータが扱われる場合、改行コードの正規化は重要です。正規化が行われないと、異なる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文字の検出と除去のメカニズムを導入したことです。

  1. scanComment()関数の変更:

    • scanComment()関数は、//または/* */形式のコメントを読み取り、その内容を文字列として返します。
    • この関数内にhasCRという新しいブーリアン変数(falseで初期化)が導入されました。
    • コメントの内容をスキャンするループ内で、現在の文字s.ch\rであるかどうかがチェックされます。
    • もしs.ch == '\r'であれば、hasCRtrueに設定されます。これは、コメント内に少なくとも1つのキャリッジリターン文字が存在することを示します。
    • コメントの終端に達した後、スキャンされたコメントリテラル(lit := s.src[offs:s.offset])に対して、hasCRtrueの場合にのみstripCR(lit)関数が呼び出されます。
    • stripCR関数は、Goの標準ライブラリの一部であるbytesパッケージのbytes.ReplaceAllや、あるいはカスタム実装によって、バイトスライスからすべての\r文字を除去する役割を担います。このコミットのdiffにはstripCR関数の定義は含まれていませんが、その存在と機能は文脈から明らかです。
    • 最終的に、\rが除去された(または元々存在しなかった)コメントリテラルが文字列として返されます。
  2. scanner_test.goの変更:

    • TestScan関数は、go/scannerの動作を検証するための主要なテスト関数です。このコミットでは、このテスト関数が大幅にリファクタリングされています。
    • 新しいテストケースの追加: tokens配列に、\rを含むコメントの新しいテストケースが追加されました。
      • {token.COMMENT, "/*\r*/", special}: ブロックコメント内に\rを含むケース。
      • {token.COMMENT, "//\r\n", special}: 行コメント内に\rを含むケース(\rの後に\nが続くCRLF形式)。
    • テストロジックの簡素化と正確化:
      • 以前はsrc_linecountwhitespace_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)文字を確実に除去するためのものです。

  1. hasCRフラグの導入:
    • hasCR := falseという新しいブーリアン変数が導入されました。これは、現在スキャン中のコメント内に\r文字が見つかったかどうかを追跡するためのフラグです。
  2. \r文字の検出:
    • 行コメント(//スタイル)とブロックコメント(/* */スタイル)の両方のスキャンループ内で、現在の文字s.ch\rであるかどうかがチェックされます。
    • if s.ch == '\r' { hasCR = true }
    • このチェックにより、コメントの内容を読み進める過程で\r文字が検出されると、hasCRフラグがtrueに設定されます。
  3. 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文字が含まれないようになります。
  4. 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.goTestScan関数は、go/scannerの字句解析機能が正しく動作するかを検証するための統合テストです。このコミットでは、テストの構造が大幅に改善され、\rを含むコメントの新しいテストケースが追加されました。

  1. 新しいテストケースの追加:

    • var tokens配列に、\rを含むコメントのテストケースが追加されました。
      • {token.COMMENT, "/*\r*/", special}: ブロックコメント内に\r
      • {token.COMMENT, "//\r\n", special}: 行コメント内に\r\n(CRLF)。
    • これらのテストケースは、scanComment()関数が\rを正しく除去することを確認するために不可欠です。
  2. TestScan関数のリファクタリング:

    • 初期化の簡素化: src_linecountwhitespace_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言語の公式ドキュメント
  • Go言語のソースコード(特にgo/scannerパッケージ)
  • GitHubのGoリポジトリのIssueトラッカー
  • 一般的なプログラミングにおける改行コード(CR, LF, CRLF)に関する知識
  • 字句解析(Lexical Analysis)に関する一般的な知識