[インデックス 18229] ファイルの概要
このコミットは、Go言語のgo/scanner
パッケージにおける、終端されていないリテラル(rune、string、raw string)のスキャン処理の改善に関するものです。具体的には、エラー発生時にスキャナーが余分な入力を消費しすぎないように修正し、エラーリカバリの精度を向上させています。また、関連するエラーメッセージの一貫性も改善されています。
コミット
commit 11740e19a4cad2732a196c2eeb8cc990af258165
Author: Robert Griesemer <gri@golang.org>
Date: Mon Jan 13 11:10:45 2014 -0800
go/scanner: minimal non-terminated literals
Consume as little as possible input when encountering
non-terminated rune, string, and raw string literals.
The old code consumed at least one extra character
which could lead to worse error recovery when parsing
erroneous sources.
Also made error messages in those cases more consistent.
Fixes #7091.
R=adonovan
CC=golang-codereviews
https://golang.org/cl/50630043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/11740e19a4cad2732a196c2eeb8cc990af258165
元コミット内容
go/scanner: minimal non-terminated literals
終端されていないrune、string、raw stringリテラルに遭遇した際に、可能な限り少ない入力を消費するように変更。以前のコードは少なくとも1文字余分に消費しており、これが誤ったソースをパースする際のエラーリカバリを悪化させる可能性があった。 また、これらのケースでのエラーメッセージをより一貫性のあるものにした。
Fixes #7091.
変更の背景
この変更の背景には、Go言語のコンパイラやツールがソースコードを解析する際のエラーハンドリングとエラーリカバリの品質向上が挙げられます。特に、字句解析(lexical analysis)の段階で、文字列リテラルや文字リテラルが正しく終端されていない場合に、スキャナーがどのように振る舞うかが問題となっていました。
従来のgo/scanner
の実装では、終端されていないリテラル(例えば、閉じクォートがない文字列)に遭遇した際に、エラーを報告した後も余分な文字を読み進めてしまう傾向がありました。これにより、以下のような問題が発生していました。
- エラーリカバリの悪化: スキャナーが余分な文字を消費してしまうと、その後の字句解析や構文解析において、本来のエラー箇所とは異なる場所でさらなるエラーが報告されたり、本来検出できたはずの有効なトークンを見逃したりする可能性がありました。これは、開発者がコードのバグを特定し修正するのを困難にします。
- エラーメッセージの不正確さ: 余分な文字を消費することで、エラーメッセージが指し示す位置が実際の終端されていないリテラルの開始位置からずれてしまうことがありました。
- 一貫性の欠如: 終端されていないリテラルに対するエラーメッセージが、リテラルの種類(rune、string、raw string)によって一貫性を欠いていた可能性があります。
このコミットは、Go issue #7091「go/scanner
: better error recovery for unterminated literals」に対応するものです。このIssueでは、特に終端されていない文字列リテラルが原因で、スキャナーが不必要に多くの入力を消費し、その後のエラーリカバリが損なわれるという問題が報告されていました。例えば、"
で始まるが閉じられていない文字列があった場合、スキャナーが改行文字まで読み進めてしまい、その後のコードの解析に悪影響を与えることが指摘されていました。
この修正の目的は、スキャナーがエラーを検出した際に、必要最小限の入力だけを消費するようにすることで、エラーリカバリの精度を高め、より正確で役立つエラーメッセージを提供することにあります。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と、コンパイラの字句解析に関する基本的な知識が必要です。
-
Go言語の字句解析 (Lexical Analysis):
- 字句解析は、コンパイラの最初のフェーズであり、ソースコードをトークン(意味のある最小単位)のストリームに変換するプロセスです。例えば、
var x = "hello"
というコードは、var
(キーワード),x
(識別子),=
(演算子),"hello"
(文字列リテラル) といったトークンに分割されます。 go/scanner
パッケージは、Go言語のソースコードを字句解析するための標準ライブラリです。- トークン (Token): 字句解析器によって識別される、プログラムの最小単位。
token.Token
型で表現されます。例:token.IDENT
(識別子),token.INT
(整数リテラル),token.STRING
(文字列リテラル),token.CHAR
(文字リテラル),token.EOF
(ファイルの終端),token.ILLEGAL
(不正なトークン)。 - リテラル (Literal): ソースコードに直接記述された定数値。
- Rune Literal (文字リテラル): 単一のUnicodeコードポイントを表す。例:
'a'
,'世'
. シングルクォートで囲まれます。 - String Literal (文字列リテラル): 文字列を表す。
- Interpreted String Literal (解釈済み文字列リテラル): ダブルクォートで囲まれ、エスケープシーケンスが解釈される。例:
"hello\nworld"
. - Raw String Literal (生文字列リテラル): バッククォートで囲まれ、エスケープシーケンスが解釈されない。改行を含むことができる。例:
`hello\nworld`
.
- Interpreted String Literal (解釈済み文字列リテラル): ダブルクォートで囲まれ、エスケープシーケンスが解釈される。例:
- Rune Literal (文字リテラル): 単一のUnicodeコードポイントを表す。例:
- スキャナー (Scanner): 字句解析を行うコンポーネント。入力ソースコードを読み込み、トークンを生成します。エラーが発生した際には、エラーハンドラを呼び出します。
- エラーリカバリ (Error Recovery): コンパイラがソースコード中のエラーを検出した際に、そのエラーを報告した後も解析を続行し、可能な限り多くのエラーを検出したり、残りのコードを正しく解析しようとする能力。良いエラーリカバリは、開発者が一度のコンパイルで複数のエラーを発見し、修正するのに役立ちます。
- 字句解析は、コンパイラの最初のフェーズであり、ソースコードをトークン(意味のある最小単位)のストリームに変換するプロセスです。例えば、
-
token.Position
: ソースコード内の位置(ファイル名、行番号、列番号)を表す構造体。エラーメッセージなどで使用されます。 -
s.next()
: スキャナーの内部メソッドで、次の文字を読み込み、現在の文字(s.ch
)とオフセット(s.offset
)を更新します。 -
s.error(offs, msg)
: スキャナーの内部メソッドで、指定されたオフセットとメッセージでエラーを報告します。 -
s.src
: スキャン対象のソースコードのバイトスライス。 -
s.offset
: 現在スキャンしている文字のソースコード内でのオフセット(バイト単位)。
これらの知識が、コミットの変更内容、特にscanRune
、scanString
、scanRawString
といったメソッドの修正が、どのようにスキャナーの振る舞いを改善しているかを理解する上で重要となります。
技術的詳細
このコミットの技術的詳細は、go/scanner
パッケージ内のscanRune
(旧scanChar
)、scanString
、scanRawString
の各メソッドにおける、終端されていないリテラルを検出した際の入力消費ロジックの変更に集約されます。
変更の核心
以前の実装では、これらのスキャン関数は、リテラルの終端文字(例: '
、"
、`
)が見つかるまで、または改行文字やファイルの終端に達するまで、無条件にs.next()
を呼び出して文字を消費していました。エラーが発生した場合(例えば、終端文字が見つからない場合)、エラーを報告した後も、ループの次のイテレーションでさらに1文字余分にs.next()
が呼び出されていました。
新しい実装では、この「余分な1文字の消費」を防ぐために、ループの条件とs.next()
の呼び出し順序が変更されています。
scanRune
(旧 scanChar
) の変更点
- 関数名の変更:
scanChar
からscanRune
に変更されました。これは、Go言語の文字リテラルがUnicodeのrune(コードポイント)を表すという事実をより正確に反映しています。 - ループ条件の変更:
- 旧:
for s.ch != '\''
- 新:
for { ... if ch == '\'' { break } ... }
- 新しいループは無限ループ
for {}
となり、内部で明示的に終端文字'
をチェックし、見つかったらbreak
でループを抜けるようになりました。
- 旧:
s.next()
の移動:- 旧:
s.next()
は、s.ch
をチェックする前に呼び出されていました。これにより、終端文字が見つかった後も、ループの最後に一度余分にs.next()
が呼び出されていました。 - 新:
s.next()
は、現在の文字s.ch
(ch
変数にコピーされた後)が終端文字でないことが確認された後に呼び出されるようになりました。これにより、終端文字を検出した直後にループを抜け、余分な文字を消費しなくなります。
- 旧:
- エラーメッセージの改善:
- 旧:
"character literal not terminated"
- 新:
"rune literal not terminated"
- より正確な用語を使用しています。
- 旧:
n = 1
の追加: 終端されていないエラーが発生した場合にn = 1
を設定することで、その後のif n != 1
チェックで「illegal rune literal」という追加のエラーが報告されるのを防いでいます。これは、終端されていないこと自体がエラーであり、それ以上「不正なリテラル」として報告する必要がないためです。
scanString
の変更点
- ループ条件の変更:
- 旧:
for s.ch != '"'
- 新:
for { ... if ch == '"' { break } ... }
scanRune
と同様に、無限ループと明示的なbreak
を使用します。
- 旧:
s.next()
の移動:scanRune
と同様に、s.next()
の呼び出し位置が変更され、終端文字"
を検出した直後にループを抜けるようになりました。
- エラーメッセージの改善:
- 旧:
"string not terminated"
- 新:
"string literal not terminated"
- より明確な表現になっています。
- 旧:
scanRawString
の変更点
- ループ条件の変更:
- 旧:
for s.ch != '
'` - 新:
for { ... if ch == '
'{ break } ... }
scanRune
、scanString
と同様のパターンです。
- 旧:
s.next()
の移動:scanRune
、scanString
と同様に、s.next()
の呼び出し位置が変更され、終端文字`
を検出した直後にループを抜けるようになりました。
- エラーメッセージの改善:
- 旧:
"string not terminated"
- 新:
"raw string literal not terminated"
- より具体的なリテラルタイプを明示しています。
- 旧:
ch < 0
チェックの移動:ch < 0
(ファイルの終端を示す)のチェックがループの先頭に移動し、終端文字のチェックよりも前に行われるようになりました。これにより、ファイルの終端に達した場合に、終端文字を待たずに即座にエラーを報告できるようになります。
テストコードの変更
scanner_test.go
では、checkError
関数のシグネチャが変更され、期待されるリテラル値(lit
)も引数として受け取るようになりました。これにより、エラーが発生した際にスキャナーが実際にどの範囲の文字列をリテラルとして認識したか(つまり、どこまで文字を消費したか)をテストできるようになり、修正が正しく機能していることを検証しています。
また、errors
テストデータには、終端されていないリテラルに関する新しいテストケースが追加され、スキャナーが最小限の文字しか消費しないこと、およびエラーメッセージが正しいことを確認しています。例えば、'\\n
のようなケースで、以前は余分な文字を消費していたものが、修正後は'
までで停止し、rune literal not terminated
というエラーを報告するようになっています。
これらの変更により、go/scanner
は終端されていないリテラルに対してより堅牢になり、その後の解析プロセスへの悪影響を最小限に抑えることができるようになりました。
コアとなるコードの変更箇所
変更は主に以下の2つのファイルにあります。
src/pkg/go/scanner/scanner.go
: スキャナーの主要なロジックが含まれるファイル。scanChar
関数がscanRune
にリネームされ、その内部ロジックが変更されました。scanString
関数の内部ロジックが変更されました。scanRawString
関数の内部ロジックが変更されました。scanAgain
ラベルのcase '\''
ブロックで、s.scanChar()
の呼び出しがs.scanRune()
に変更されました。
src/pkg/go/scanner/scanner_test.go
: スキャナーのテストコードが含まれるファイル。checkError
関数のシグネチャが変更され、lit
引数が追加されました。errors
テストデータに、終端されていないリテラルに関する新しいテストケースが追加され、既存のテストケースもlit
引数に合わせて更新されました。TestScanErrors
関数内のcheckError
の呼び出しが、新しいシグネチャに合わせて更新されました。
src/pkg/go/scanner/scanner.go
の変更点
--- a/src/pkg/go/scanner/scanner.go
+++ b/src/pkg/go/scanner/scanner.go
@@ -402,29 +402,30 @@ func (s *Scanner) scanEscape(quote rune) {
}
}
-func (s *Scanner) scanChar() string {
+func (s *Scanner) scanRune() string {
// '\'' opening already consumed
offs := s.offset - 1
n := 0
- for s.ch != '\'' {
+ for {
ch := s.ch
- n++
- s.next()
if ch == '\n' || ch < 0 {
- s.error(offs, "character literal not terminated")
- n = 1
+ s.error(offs, "rune literal not terminated")
+ n = 1 // avoid further errors
break
}
+ s.next()
+ if ch == '\'' {
+ break
+ }
+ n++
if ch == '\\' {
s.scanEscape('\'')
}
}
- s.next()
-
if n != 1 {
- s.error(offs, "illegal character literal")
+ s.error(offs, "illegal rune literal")
}
return string(s.src[offs:s.offset])
@@ -434,11 +435,14 @@ func (s *Scanner) scanString() string {
// '"' opening already consumed
offs := s.offset - 1
- for s.ch != '"' {
+ for {
ch := s.ch
- s.next()
if ch == '\n' || ch < 0 {
- s.error(offs, "string not terminated")
+ s.error(offs, "string literal not terminated")
+ break
+ }
+ s.next()
+ if ch == '"' {
break
}
if ch == '\\' {
@@ -446,8 +450,6 @@ func (s *Scanner) scanString() string {
}
}
- s.next()
-
return string(s.src[offs:s.offset])
}
@@ -468,20 +470,21 @@ func (s *Scanner) scanRawString() string {
offs := s.offset - 1
hasCR := false
- for s.ch != '`' {
+ for {
ch := s.ch
+ if ch < 0 {
+ s.error(offs, "raw string literal not terminated")
+ break
+ }
s.next()
+ if ch == '`' {
+ break
+ }
if ch == '\r' {
hasCR = true
}
- if ch < 0 {
- s.error(offs, "string not terminated")
- break
- }
}
- s.next()
-
lit := s.src[offs:s.offset]
if hasCR {
lit = stripCR(lit)
@@ -617,7 +620,7 @@ scanAgain:
case '\'':
insertSemi = true
tok = token.CHAR
- lit = s.scanChar()
+ lit = s.scanRune()
case '`':
insertSemi = true
tok = token.STRING
src/pkg/go/scanner/scanner_test.go
の変更点
--- a/src/pkg/go/scanner/scanner_test.go
+++ b/src/pkg/go/scanner/scanner_test.go
@@ -631,7 +631,7 @@ type errorCollector struct {
pos token.Position // last error position encountered
}
-func checkError(t *testing.T, src string, tok token.Token, pos int, err string) {
+func checkError(t *testing.T, src string, tok token.Token, pos int, lit, err string) {
var s Scanner
var h errorCollector
eh := func(pos token.Position, msg string) {
@@ -640,7 +640,7 @@ func checkError(t *testing.T, src string, tok token.Token, pos int, err string)
\th.pos = pos
}\n \ts.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), eh, ScanComments|dontInsertSemis)\n-\t_, tok0, _ := s.Scan()\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@@ -648,6 +648,9 @@ func checkError(t *testing.T, src string, tok token.Token, pos int, err string)
\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 \tcnt := 0\n \tif err != "" {\n \t\tcnt = 1\n@@ -667,43 +670,49 @@ var errors = []struct {\n src string\n tok token.Token\n pos int\n+\tlit string\n err string\n }{\n-\t{"\a", token.ILLEGAL, 0, "illegal character U+0007"},\n-\t{`#`, token.ILLEGAL, 0, "illegal character U+0023 '#'"},\n-\t{`…`, token.ILLEGAL, 0, "illegal character U+2026 '…'"},\n-\t{`' '`, token.CHAR, 0, ""},\n-\t{`''`, token.CHAR, 0, "illegal character literal"},\n-\t{`'\8'`, token.CHAR, 2, "unknown escape sequence"},\n-\t{`'\08'`, token.CHAR, 3, "illegal character in escape sequence"},\n-\t{`'\x0g'`, token.CHAR, 4, "illegal character in escape sequence"},\n-\t{`'\Uffffffff'`, token.CHAR, 2, "escape sequence is invalid Unicode code point"},\n-\t{`'`, token.CHAR, 0, "character literal not terminated"},\n-\t{`""`, token.STRING, 0, ""},\n-\t{`"`, token.STRING, 0, "string not terminated"},\n-\t{"``", token.STRING, 0, ""},\n-\t{"`", token.STRING, 0, "string not terminated"},\n-\t{"/**/", token.COMMENT, 0, ""},\n-\t{"/*", token.COMMENT, 0, "comment not terminated"},\n-\t{"077", token.INT, 0, ""},\n-\t{"078.", token.FLOAT, 0, ""},\n-\t{"07801234567.", token.FLOAT, 0, ""},\n-\t{"078e0", token.FLOAT, 0, ""},\n-\t{"078", token.INT, 0, "illegal octal number"},\n-\t{"07800000009", token.INT, 0, "illegal octal number"},\n-\t{"0x", token.INT, 0, "illegal hexadecimal number"},\n-\t{"0X", token.INT, 0, "illegal hexadecimal number"},\n-\t{"\"abc\x00def\"", token.STRING, 4, "illegal character NUL"},\n-\t{"\"abc\x80def\"", token.STRING, 4, "illegal UTF-8 encoding"},\n-\t{"\ufeff\ufeff", token.ILLEGAL, 3, "illegal byte order mark"}, // only first BOM is ignored\n-\t{"//\ufeff", token.COMMENT, 2, "illegal byte order mark"}, // only first BOM is ignored\n-\t{"'\ufeff" + `'`, token.CHAR, 1, "illegal byte order mark"}, // only first BOM is ignored\n-\t{`"` + "abc\ufeffdef" + `"`, token.STRING, 4, "illegal byte order mark"}, // only first BOM is ignored\n+\t{"\a", token.ILLEGAL, 0, "", "illegal character U+0007"},\n+\t{`#`, token.ILLEGAL, 0, "", "illegal character U+0023 '#'"},\n+\t{`…`, token.ILLEGAL, 0, "", "illegal character U+2026 '…'"},\n+\t{`' '`, token.CHAR, 0, `' '`, ""},\n+\t{`''`, token.CHAR, 0, `''`, "illegal rune literal"},\n+\t{`'123'`, token.CHAR, 0, `'123'`, "illegal rune literal"},\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{`'\Uffffffff'`, token.CHAR, 2, `'\Uffffffff'`, "escape sequence is invalid Unicode code point"},\n+\t{`'`, token.CHAR, 0, `'`, "rune literal 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+\t{`"abc`, token.STRING, 0, `"abc`, "string literal not terminated"},\n+\t{"\"abc\n", token.STRING, 0, `"abc`, "string literal not terminated"},\n+\t{"\"abc\n ", token.STRING, 0, `"abc`, "string literal not terminated"},\n+\t{"``", token.STRING, 0, "``", ""},\n+\t{"`", token.STRING, 0, "`", "raw string literal not terminated"},\n+\t{"/**/", token.COMMENT, 0, "/**/", ""},\n+\t{"/*", token.COMMENT, 0, "/*", "comment not terminated"},\n+\t{"077", token.INT, 0, "077", ""},\n+\t{"078.", token.FLOAT, 0, "078.", ""},\n+\t{"07801234567.", token.FLOAT, 0, "07801234567.", ""},\n+\t{"078e0", token.FLOAT, 0, "078e0", ""},\n+\t{"078", token.INT, 0, "078", "illegal octal number"},\n+\t{"07800000009", token.INT, 0, "07800000009", "illegal octal number"},\n+\t{"0x", token.INT, 0, "0x", "illegal hexadecimal number"},\n+\t{"0X", token.INT, 0, "0X", "illegal hexadecimal number"},\n+\t{"\"abc\x00def\"", token.STRING, 4, "\"abc\x00def\"", "illegal character NUL"},\n+\t{"\"abc\x80def\"", token.STRING, 4, "\"abc\x80def\"", "illegal UTF-8 encoding"},\n+\t{"\ufeff\ufeff", token.ILLEGAL, 3, "\ufeff\ufeff", "illegal byte order mark"}, // only first BOM is ignored\n+\t{"//\ufeff", token.COMMENT, 2, "//\ufeff", "illegal byte order mark"}, // only first BOM is ignored\n+\t{"'\ufeff" + `'`, token.CHAR, 1, "'\ufeff" + `'`, "illegal byte order mark"}, // only first BOM is ignored\n+\t{`"` + "abc\ufeffdef" + `"`, token.STRING, 4, `"` + "abc\ufeffdef" + `"`, "illegal byte order mark"}, // only first BOM is ignored\n }\n \n func TestScanErrors(t *testing.T) {\n \tfor _, e := range errors {\n-\t\tcheckError(t, e.src, e.tok, e.pos, e.err)\n+\t\tcheckError(t, e.src, e.tok, e.pos, e.lit, e.err)\n \t}\n }\n```
## コアとなるコードの解説
このコミットの核心は、`go/scanner`パッケージ内のリテラルスキャン関数(`scanRune`, `scanString`, `scanRawString`)における、エラー発生時の入力消費ロジックの変更です。
### 変更の意図
以前の実装では、終端されていないリテラル(例: 閉じクォートがない文字列)に遭遇した場合、スキャナーはエラーを報告した後も、ループの最後に余分な`s.next()`呼び出しによって1文字余分に読み進めていました。この「余分な1文字の消費」が、その後の字句解析や構文解析におけるエラーリカバリを悪化させる原因となっていました。
例えば、`"abc`という終端されていない文字列があった場合、スキャナーは`abc`を読み込み、改行文字に到達した時点で「string not terminated」エラーを報告します。しかし、その後も`s.next()`が呼び出され、改行文字の次の文字まで読み進めてしまうことがありました。これにより、パーサーが期待するトークンストリームが乱れ、本来検出できるはずの次の有効なトークンを見逃したり、誤ったエラーを報告したりする可能性がありました。
このコミットの目的は、この余分な消費をなくし、エラーを検出した時点で可能な限り最小限の入力(つまり、エラーの原因となったリテラル自体)のみを消費するようにすることです。これにより、スキャナーはより「クリーン」な状態で停止し、パーサーがより正確なエラーリカバリを行えるようになります。
### 具体的な変更点と効果
1. **`for s.ch != ...` から `for { ... if ch == ... { break } ... }` への変更**:
* これは、ループの制御フローをより細かく制御するための重要な変更です。
* 以前の`for s.ch != ...`ループでは、ループ条件の評価後に`s.next()`が呼び出されるため、終端文字が見つかった場合でも、その終端文字を消費した後にループが終了していました。そして、エラーが発生した場合(終端文字が見つからずにループが終了した場合)、さらに次の文字を消費してしまう可能性がありました。
* 新しい`for {}`ループと内部の`if ch == ... { break }`の組み合わせでは、現在の文字`ch`が終端文字であるかをチェックし、もしそうであれば、**その文字を消費した直後に**ループを抜けることができます。これにより、余分な文字の消費を防ぎます。
2. **`s.next()` の呼び出し位置の変更**:
* 以前は、`s.next()`はループの最後に呼び出されていました。
* 新しいコードでは、`ch := s.ch`で現在の文字を一時変数に保存し、その文字が終端文字でないことを確認した後に`s.next()`を呼び出すように変更されました。
* この順序の変更により、終端文字を検出した場合、`s.next()`が呼び出される前に`break`が実行されるため、終端文字の次の文字を誤って消費することがなくなります。
3. **エラーメッセージの具体化と一貫性**:
* `"character literal not terminated"` → `"rune literal not terminated"`
* `"string not terminated"` → `"string literal not terminated"`
* `"string not terminated"` → `"raw string literal not terminated"`
* これらの変更は、エラーメッセージがより具体的になり、どの種類のリテラルが問題であるかを明確に示します。これにより、開発者はエラーの原因をより迅速に特定できます。
4. **`n = 1` の追加 (`scanRune`)**:
* `scanRune`関数で終端されていないエラーが発生した場合に`n = 1`を設定する変更は、エラーリカバリのロジックを改善します。
* `n`はリテラル内の文字数をカウントしており、`if n != 1 { s.error(offs, "illegal rune literal") }`というチェックがあります。
* 終端されていないリテラルは、それ自体がエラーです。もし`n`が1でなければ、さらに「illegal rune literal」というエラーが報告されてしまいます。`n = 1`とすることで、終端されていないエラーが報告された後に、この追加の「illegal rune literal」エラーが重複して報告されるのを防ぎ、エラーメッセージの冗長性を減らします。
これらの変更は、Go言語の字句解析器が、不正な入力に対してより賢く、より効率的に振る舞うようにするための重要な改善です。結果として、コンパイラのエラーメッセージがより正確になり、開発者のデバッグ体験が向上します。
## 関連リンク
* Go issue #7091: `go/scanner`: better error recovery for unterminated literals - [https://github.com/golang/go/issues/7091](https://github.com/golang/go/issues/7091)
* Gerrit Change-Id: `50630043` - [https://golang.org/cl/50630043](https://golang.org/cl/50630043)
## 参考にした情報源リンク
* Go言語の公式ドキュメント(`go/scanner`パッケージ): [https://pkg.go.dev/go/scanner](https://pkg.go.dev/go/scanner)
* Go言語の仕様(リテラルに関するセクション): [https://go.dev/ref/spec#Literals](https://go.dev/ref/spec#Literals)
* コンパイラの設計に関する一般的な情報(字句解析、エラーリカバリなど)
* Go言語のIssueトラッカー: [https://github.com/golang/go/issues](https://github.com/golang/go/issues)
* Go言語のコードレビューシステム (Gerrit): [https://go-review.googlesource.com/](https://go-review.googlesource.com/)
* Go言語のソースコード: [https://github.com/golang/go](https://github.com/golang/go)