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

[インデックス 18261] ファイルの概要

このコミットは、Go言語のgo/scannerパッケージにおけるエスケープシーケンスの解析とエラー報告の改善に焦点を当てています。特に、不完全なエスケープシーケンスや不正な文字を含むエスケープシーケンスに対するエラーメッセージをより詳細かつ正確にすることで、コンパイラやツールがよりユーザーフレンドリーな診断を提供できるようにしています。

コミット

commit fc80ce81946b009dd826e260fe4fc1fbcc19f133
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Jan 15 09:50:55 2014 -0800

    go/scanner: report too short escape sequences
    
    Generally improve error messages for escape sequences.
    
    R=adonovan
    CC=golang-codereviews
    https://golang.org/cl/49430046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/fc80ce81946b009dd826e260fe4fc1fbcc19f133

元コミット内容

go/scanner: report too short escape sequences Generally improve error messages for escape sequences.

変更の背景

Go言語のコンパイラは、ソースコードを解析する際に字句解析(lexical analysis)を行います。この過程で、文字列リテラルやruneリテラル内のエスケープシーケンス(例: \n, \x41, \u0041)を正しく解釈する必要があります。以前の実装では、エスケープシーケンスが不完全であったり、不正な文字を含んでいたりする場合のエラーメッセージが曖昧であったり、不十分であったりする可能性がありました。

このコミットの背景には、開発者がコードを記述する際に遭遇する可能性のあるエスケープシーケンスの誤りに対して、より具体的で分かりやすいエラーメッセージを提供することで、デバッグの効率を向上させるという目的があります。特に、\x\u\Uといった固定長の16進数エスケープシーケンスにおいて、必要な桁数が満たされていない場合に「too short escape sequences」として明確に報告できるようにすることが狙いです。これにより、ユーザーは問題の箇所と原因を迅速に特定できるようになります。

前提知識の解説

Go言語の字句解析とgo/scannerパッケージ

Go言語のコンパイラは、ソースコードをトークン(識別子、キーワード、演算子、リテラルなど)のストリームに変換する字句解析器(lexerまたはscanner)を使用します。go/scannerパッケージは、この字句解析の機能を提供します。ソースコードを読み込み、Go言語の文法規則に従ってトークンを生成し、エラーがあれば報告します。

エスケープシーケンス

Go言語では、文字列リテラルやruneリテラル内で特殊文字や非表示文字を表現するためにエスケープシーケンスを使用します。主なエスケープシーケンスには以下の種類があります。

  • 単純なエスケープシーケンス: \a (ベル), \b (バックスペース), \f (フォームフィード), \n (改行), \r (キャリッジリターン), \t (水平タブ), \v (垂直タブ), \\ (バックスラッシュ), \' (シングルクォート), \" (ダブルクォート)。
  • 8進数エスケープシーケンス: \ooo の形式で、3桁の8進数でバイト値を表現します。例: \077
  • 16進数エスケープシーケンス: \xhh の形式で、2桁の16進数でバイト値を表現します。例: \x41
  • Unicodeエスケープシーケンス:
    • \uhhhh の形式で、4桁の16進数でUnicodeコードポイントを表現します。これはUTF-16のコードユニットに対応しますが、Goでは直接Unicodeコードポイントとして扱われます。例: \u0041
    • \Uhhhhhhhh の形式で、8桁の16進数でUnicodeコードポイントを表現します。例: \U00000041

これらのエスケープシーケンスは、それぞれ特定の形式と桁数を要求します。このコミットは、これらの要件が満たされない場合に、より適切なエラーを報告するように改善しています。

runestringリテラル

Go言語では、シングルクォートで囲まれた文字はruneリテラル(Unicodeコードポイントを表す整数値)であり、ダブルクォートで囲まれた文字はstringリテラル(UTF-8エンコードされたバイト列)です。どちらのリテラルもエスケープシーケンスを含むことができます。

技術的詳細

このコミットの主要な変更点は、src/pkg/go/scanner/scanner.goファイル内のscanEscape関数とscanRune関数の挙動にあります。

scanEscape関数の変更

  • 戻り値の追加: 以前はvoid(何も返さない)関数でしたが、boolを返すように変更されました。このbool値は、エスケープシーケンスの解析が成功したかどうかを示します。trueは成功、falseはエラーが発生したことを意味します。これにより、呼び出し元(scanRuneなど)がエスケープシーケンスの解析結果に基づいて後続の処理を調整できるようになります。
  • エラーメッセージの改善:
    • defaultケース(未知のエスケープシーケンス)において、s.ch < 0(ファイルの終端に到達した場合など)であれば「escape sequence not terminated」というより具体的なエラーメッセージを報告するようになりました。それ以外の場合は「unknown escape sequence」を報告します。
    • 16進数やUnicodeエスケープシーケンスの桁数が不足している場合や、不正な文字が含まれている場合に、fmt.Sprintfを使用して「illegal character %#U in escape sequence」のように、どの文字が不正であるかを明示するエラーメッセージを生成するようになりました。また、この場合もs.ch < 0であれば「escape sequence not terminated」を報告します。
  • エラー発生時の挙動: エラーが発生した場合、以前は残りの文字を消費しようとしていましたが、新しい実装ではfalseを返してすぐに処理を終了します。これにより、エラーの連鎖を防ぎ、最初の明確なエラーを報告することに集中します。

scanRune関数の変更

  • validフラグの導入: scanRune関数内にvalidというbool型の変数が導入されました。これは、runeリテラル全体の解析中にエラーが発生したかどうかを追跡するために使用されます。
  • scanEscapeの戻り値の利用: scanEscapefalseを返した場合(エスケープシーケンスの解析に失敗した場合)、validフラグをfalseに設定します。これにより、scanRuneはエスケープシーケンスのエラーを認識し、後続の処理で重複するエラー報告を避けることができます。
  • エラー報告の調整: rune literal not terminatedのエラー報告が、validtrueの場合にのみ行われるようになりました。これは、既にエスケープシーケンスの解析中にエラーが報告されている場合、runeリテラル全体の終了に関するエラーは報告しないようにするためです。
  • n != 1チェックの変更: 以前はn != 1(runeリテラルが1文字でない場合)であれば常に「illegal rune literal」を報告していましたが、valid && n != 1という条件に変更されました。これにより、エスケープシーケンスのエラーによってvalidfalseになっている場合は、このエラーは報告されなくなります。

scanner_test.goの変更

テストファイルsrc/pkg/go/scanner/scanner_test.goには、新しいエラーケースが多数追加されています。これらは、\x\u\Uエスケープシーケンスが不完全な場合や、不正な文字を含む場合の挙動を検証するためのものです。例えば、\'\\x\'\'\\u\'\'\\U\'のような短いエスケープシーケンスや、\'\\x0g\'のように不正な文字を含むエスケープシーケンスに対する期待されるエラーメッセージが定義されています。これにより、変更が正しく機能していることが保証されます。

コアとなるコードの変更箇所

src/pkg/go/scanner/scanner.go

--- a/src/pkg/go/scanner/scanner.go
+++ b/src/pkg/go/scanner/scanner.go
@@ -358,60 +358,77 @@ exit:
 	return tok, string(s.src[offs:s.offset])
 }

-func (s *Scanner) scanEscape(quote rune) {
+// scanEscape parses an escape sequence where rune is the accepted
+// escaped quote. In case of a syntax error, it stops at the offending
+// character (without consuming it) and returns false. Otherwise
+// it returns true.
+func (s *Scanner) scanEscape(quote rune) bool {
 	offs := s.offset

-	var i, base, max uint32
+	var n int
+	var base, max uint32
 	switch s.ch {
 	case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', quote:
 		s.next()
-		return
+		return true
 	case '0', '1', '2', '3', '4', '5', '6', '7':
-		i, base, max = 3, 8, 255
+		n, base, max = 3, 8, 255
 	case 'x':
 		s.next()
-		i, base, max = 2, 16, 255
+		n, base, max = 2, 16, 255
 	case 'u':
 		s.next()
-		i, base, max = 4, 16, unicode.MaxRune
+		n, base, max = 4, 16, unicode.MaxRune
 	case 'U':
 		s.next()
-		i, base, max = 8, 16, unicode.MaxRune
+		n, base, max = 8, 16, unicode.MaxRune
 	default:
-		s.next() // always make progress
-		s.error(offs, "unknown escape sequence")
-		return
+		msg := "unknown escape sequence"
+		if s.ch < 0 {
+			msg = "escape sequence not terminated"
+		}
+		s.error(offs, msg)
+		return false
 	}

 	var x uint32
-	for ; i > 0 && s.ch != quote && s.ch >= 0; i-- {
+	for n > 0 {
 		d := uint32(digitVal(s.ch))
 		if d >= base {
-			s.error(s.offset, "illegal character in escape sequence")
-			break
+			msg := fmt.Sprintf("illegal character %#U in escape sequence", s.ch)
+			if s.ch < 0 {
+				msg = "escape sequence not terminated"
+			}
+			s.error(s.offset, msg)
+			return false
 		}
 		x = x*base + d
 		s.next()
-	}
-	// in case of an error, consume remaining chars
-	for ; i > 0 && s.ch != quote && s.ch >= 0; i-- {
-		s.next()
-	}
+		n--
+	}
+
 	if x > max || 0xD800 <= x && x < 0xE000 {
 		s.error(offs, "escape sequence is invalid Unicode code point")
+		return false
 	}
+
+	return true
 }

 func (s *Scanner) scanRune() string {
 	// '\'' opening already consumed
 	offs := s.offset - 1

+	valid := true
 	n := 0
 	for {
 		ch := s.ch
 		if ch == '\n' || ch < 0 {
-			s.error(offs, "rune literal not terminated")
-			n = 1 // avoid further errors
+			// only report error if we don't have one already
+			if valid {
+				s.error(offs, "rune literal not terminated")
+				valid = false
+			}
 			break
 		}
 		s.next()
@@ -420,11 +437,14 @@ func (s *Scanner) scanRune() string {
 		}
 		n++
 		if ch == '\\' {
-			s.scanEscape('\'')
+			if !s.scanEscape('\'') {
+				valid = false
+			}
+			// continue to read to closing quote
 		}
 	}

-	if n != 1 {
+	if valid && n != 1 {
 		s.error(offs, "illegal rune literal")
 	}

src/pkg/go/scanner/scanner_test.go

--- a/src/pkg/go/scanner/scanner_test.go
+++ b/src/pkg/go/scanner/scanner_test.go
@@ -641,13 +641,9 @@ func checkError(t *testing.T, src string, tok token.Token, pos int, lit, err str
 	}\n\ts.Init(fset.AddFile(\"\", fset.Base(), len(src)), []byte(src), eh, ScanComments|dontInsertSemis)\n\t_, tok0, lit0 := s.Scan()\n-\t_, tok1, _ := s.Scan()\n \tif tok0 != tok {\n \t\tt.Errorf(\"%q: got %s, expected %s\", src, tok0, tok)\n \t}\n-\tif tok1 != token.EOF {\n-\t\tt.Errorf(\"%q: got %s, expected EOF\", src, tok1)\n-\t}\n \tif tok0 != token.ILLEGAL && lit0 != lit {\n \t\tt.Errorf(\"%q: got literal %q, expected %q\", src, lit0, lit)\n \t}\n@@ -678,12 +674,34 @@ var errors = []struct {\n \t{`…`, token.ILLEGAL, 0, \"\", \"illegal character U+2026 \'…\'\"},\n \t{`\' \'`, token.CHAR, 0, `\' \'`, \"\"},\n \t{`\'\'`, token.CHAR, 0, `\'\'`, \"illegal rune literal\"},\n+\t{`\'12\'`, token.CHAR, 0, `\'12\'`, \"illegal rune literal\"},\n \t{`\'123\'`, token.CHAR, 0, `\'123\'`, \"illegal rune literal\"},\n+\t{`\'\\0\'`, token.CHAR, 3, `\'\\0\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\07\'`, token.CHAR, 4, `\'\\07\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n \t{`\'\\8\'`, token.CHAR, 2, `\'\\8\'`, \"unknown escape sequence\"},\n-\t{`\'\\08\'`, token.CHAR, 3, `\'\\08\'`, \"illegal character in escape sequence\"},\n-\t{`\'\\x0g\'`, token.CHAR, 4, `\'\\x0g\'`, \"illegal character in escape sequence\"},\n+\t{`\'\\08\'`, token.CHAR, 3, `\'\\08\'`, \"illegal character U+0038 \'8\' in escape sequence\"},\n+\t{`\'\\x\'`, token.CHAR, 3, `\'\\x\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\x0\'`, token.CHAR, 4, `\'\\x0\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\x0g\'`, token.CHAR, 4, `\'\\x0g\'`, \"illegal character U+0067 \'g\' in escape sequence\"},\n+\t{`\'\\u\'`, token.CHAR, 3, `\'\\u\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\u0\'`, token.CHAR, 4, `\'\\u0\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\u00\'`, token.CHAR, 5, `\'\\u00\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\u000\'`, token.CHAR, 6, `\'\\u000\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\u000`, token.CHAR, 6, `\'\\u000`, \"escape sequence not terminated\"},\n+\t{`\'\\u0000\'`, token.CHAR, 0, `\'\\u0000\'`, \"\"},\n+\t{`\'\\U\'`, token.CHAR, 3, `\'\\U\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\U0\'`, token.CHAR, 4, `\'\\U0\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\U00\'`, token.CHAR, 5, `\'\\U00\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\U000\'`, token.CHAR, 6, `\'\\U000\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\U0000\'`, token.CHAR, 7, `\'\\U0000\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\U00000\'`, token.CHAR, 8, `\'\\U00000\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\U000000\'`, token.CHAR, 9, `\'\\U000000\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\U0000000\'`, token.CHAR, 10, `\'\\U0000000\'`, \"illegal character U+0027 \'\'\' in escape sequence\"},\n+\t{`\'\\U0000000`, token.CHAR, 10, `\'\\U0000000`, \"escape sequence not terminated\"},\n+\t{`\'\\U00000000\'`, token.CHAR, 0, `\'\\U00000000\'`, \"\"},\n \t{`\'\\Uffffffff\'`, token.CHAR, 2, `\'\\Uffffffff\'`, \"escape sequence is invalid Unicode code point\"},\n \t{`\'`, token.CHAR, 0, `\'`, \"rune literal not terminated\"},\n+\t{`\'\\`, token.CHAR, 2, `\'\\`, \"escape sequence not terminated\"},\n \t{\"\'\\n\", token.CHAR, 0, \"\'\", \"rune literal not terminated\"},\n \t{\"\'\\n   \", token.CHAR, 0, \"\'\", \"rune literal not terminated\"},\n \t{`\"\"`, token.STRING, 0, `\"\"`, \"\"},\n```

## コアとなるコードの解説

### `scanEscape`関数の変更点

`scanEscape`関数は、Goの字句解析器が文字列リテラルやruneリテラル内でバックスラッシュ(`\`)に遭遇した際に呼び出され、その後の文字を解析してエスケープシーケンスを処理します。

1.  **戻り値の追加 (`bool`)**:
    変更前: `func (s *Scanner) scanEscape(quote rune)`
    変更後: `func (s *Scanner) scanEscape(quote rune) bool`
    この変更により、エスケープシーケンスの解析が成功したか(`true`)失敗したか(`false`)を呼び出し元に伝えることができるようになりました。これにより、呼び出し元はエラー発生時に適切な処理を行うことができます。

2.  **`n`変数の導入**:
    以前は`i`という変数が残りの桁数を管理していましたが、`n`という変数に置き換えられました。これはセマンティックな変更であり、機能的な違いはありません。

3.  **エラーメッセージの改善と早期リターン**:
    *   `default`ケース(`\a`, `\b`などの単純なエスケープシーケンスや`\x`, `\u`, `\U`以外の文字に遭遇した場合)において、`s.ch < 0`(ファイルの終端など)であれば「escape sequence not terminated」というエラーメッセージを生成し、`false`を返して早期に終了します。これにより、不完全なエスケープシーケンスに対するより正確な診断が可能になります。
    *   16進数やUnicodeエスケープシーケンスの解析ループ内で、`d >= base`(不正な桁の文字に遭遇した場合)のチェックが強化されました。以前は`break`してループを抜けるだけでしたが、新しいコードでは`fmt.Sprintf`を使用して「illegal character %#U in escape sequence」のように、どの文字が不正であるかを具体的に示すエラーメッセージを生成し、`false`を返して早期に終了します。これにより、エラーの連鎖を防ぎ、最初の問題点を明確に報告します。

4.  **エラー発生時の残りの文字の消費の削除**:
    以前のコードには、エラーが発生した場合でも残りの文字を消費しようとするループがありました。
    ```go
    // in case of an error, consume remaining chars
    for ; i > 0 && s.ch != quote && s.ch >= 0; i-- {
    	s.next()
    }
    ```
    この部分は削除されました。`scanEscape`がエラーを検出した際に`false`を返して早期に終了するため、残りの文字を消費する必要がなくなりました。これは、エラー処理のロジックを簡素化し、よりクリーンなエラー報告を可能にします。

### `scanRune`関数の変更点

`scanRune`関数は、runeリテラル(例: `'A'`, `'\n'`)を解析する役割を担っています。

1.  **`valid`フラグの導入**:
    `valid := true`という新しい変数が導入されました。このフラグは、runeリテラル全体の解析中にエラーが発生したかどうかを追跡します。初期値は`true`で、エラーが検出されると`false`に設定されます。

2.  **`scanEscape`の戻り値の利用**:
    `ch == '\\'`(エスケープシーケンスに遭遇した場合)のブロック内で、`s.scanEscape('\'')`の呼び出しが`if !s.scanEscape('\'')`に変更されました。`scanEscape`が`false`を返した場合(エスケープシーケンスの解析に失敗した場合)、`valid`フラグを`false`に設定します。これにより、`scanRune`はエスケープシーケンスのエラーを認識し、後続の処理で重複するエラー報告を避けることができます。

3.  **エラー報告の条件の変更**:
    *   `ch == '\n' || ch < 0`(runeリテラルが終了していない場合)のエラー報告が、`if valid`という条件で囲まれました。
        ```diff
        -			s.error(offs, "rune literal not terminated")
        -			n = 1 // avoid further errors
        +			if valid {
        +				s.error(offs, "rune literal not terminated")
        +				valid = false
        +			}
        ```
        これにより、既にエスケープシーケンスのエラーによって`valid`が`false`になっている場合は、この「rune literal not terminated」エラーは報告されなくなります。これは、単一のリテラルに対して複数のエラーメッセージが出力されるのを防ぎ、ユーザーにとってより分かりやすいエラー報告を実現します。
    *   `if n != 1`(runeリテラルが1文字でない場合)のエラー報告も、`if valid && n != 1`という条件に変更されました。
        ```diff
        -	if n != 1 {
        +	if valid && n != 1 {
        ```
        同様に、エスケープシーケンスのエラーによって`valid`が`false`になっている場合は、この「illegal rune literal」エラーは報告されなくなります。

これらの変更により、`go/scanner`はエスケープシーケンスの解析においてより堅牢になり、エラー発生時にはより具体的で重複のないエラーメッセージを提供するようになりました。

## 関連リンク

*   Go言語の字句解析器のソースコード: [https://github.com/golang/go/tree/master/src/go/scanner](https://github.com/golang/go/tree/master/src/go/scanner)
*   Go言語の仕様 - 字句要素: [https://go.dev/ref/spec#Lexical_elements](https://go.dev/ref/spec#Lexical_elements)
*   Go言語の仕様 - 文字列リテラル: [https://go.dev/ref/spec#String_literals](https://go.dev/ref/spec#String_literals)

## 参考にした情報源リンク

*   [https://golang.org/cl/49430046](https://golang.org/cl/49430046) (Gerrit Code Review)
*   Go言語の公式ドキュメント
*   Go言語のソースコード (go/scannerパッケージ)
*   Unicodeの仕様 (特にエスケープシーケンスとコードポイントに関する部分)
*   字句解析器に関する一般的な情報(コンパイラ理論)