[インデックス 18457] ファイルの概要
このコミットは、Go言語のパーサー(go/parser
パッケージ)における3インデックススライス(s[low:high:max]
)の構文解析に関する修正です。具体的には、3インデックススライスにおいて2番目と3番目のインデックスが欠落している場合に、パーサーが適切なエラーを報告するように改善されています。これにより、不正なスライス式がgofmt
を通過してしまう問題(Issue 7305)が解決されました。
コミット
commit 13a5958db328a74f863d8391f453a81ae326fd31
Author: Robert Griesemer <gri@golang.org>
Date: Tue Feb 11 13:40:37 2014 -0800
go/parser: check presence of 2nd and 3rd index in 3-index slice
Fixes #7305.
LGTM=adonovan
R=bradfitz, adonovan
CC=golang-codereviews
https://golang.org/cl/58950045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/13a5958db328a74f863d8391f453a81ae326fd31
元コミット内容
go/parser: check presence of 2nd and 3rd index in 3-index slice
Fixes #7305.
LGTM=adonovan
R=bradfitz, adonovan
CC=golang-codereviews
https://golang.org/cl/58950045
変更の背景
Go言語には、スライス式としてs[low:high]
(2インデックススライス)とs[low:high:max]
(3インデックススライス)の2種類があります。3インデックススライスは、スライスの容量(capacity)を明示的に指定するために使用されます。
このコミットが修正する問題(Issue 7305)は、go/parser
が3インデックススライスを解析する際に、2番目(high
)または3番目(max
)のインデックスが欠落している場合に、構文エラーとして適切に検出していなかったことに起因します。例えば、s[i::k]
やs[i:j:]
のような不正なスライス式が、パーサーによってエラーと認識されずに抽象構文木(AST)が生成されてしまうことがありました。
このような不正なASTが生成されると、gofmt
のようなツールがそのASTを整形しようとした際に、予期せぬ動作やクラッシュを引き起こす可能性がありました。本来、構文エラーはパーサーの段階で検出され、それ以降の処理に進むべきではありません。このコミットは、このパーサーの不備を修正し、より堅牢な構文解析を実現することを目的としています。
前提知識の解説
Go言語のスライス
Go言語のスライスは、配列の一部を参照する軽量なデータ構造です。スライスは、内部的にポインタ、長さ(length)、容量(capacity)の3つの要素で構成されます。
- 長さ (length): スライスが現在参照している要素の数。
len()
関数で取得できます。 - 容量 (capacity): スライスの基底配列の先頭から、スライスが拡張できる最大の要素数。
cap()
関数で取得できます。
スライス式
Go言語では、既存の配列やスライスから新しいスライスを作成するためにスライス式を使用します。
- 2インデックススライス:
a[low:high]
low
からhigh-1
までの要素を含む新しいスライスを作成します。- 長さは
high - low
、容量は基底配列のlow
から最後までとなります。
- 3インデックススライス:
a[low:high:max]
low
からhigh-1
までの要素を含む新しいスライスを作成します。- 長さは
high - low
、容量はmax - low
となります。 max
はhigh
以上でなければならず、基底配列の範囲内である必要があります。- 3インデックススライスは、新しいスライスの容量を明示的に制限したい場合に使用されます。
go/parser
パッケージ
go/parser
パッケージは、Go言語のソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を生成するための標準ライブラリです。ASTは、プログラムの構造を木構造で表現したもので、コンパイラ、リンター、コード整形ツール(gofmt
など)がコードを理解し、処理するために利用します。
パーサーは、字句解析器(lexer)から受け取ったトークンストリームを基に、Go言語の文法規則に従って構文解析を行います。構文エラーが検出された場合、パーサーは通常、エラーを報告し、解析を停止するか、エラーリカバリを試みます。
ast.SliceExpr
とast.BadExpr
ast.SliceExpr
:go/ast
パッケージで定義されている、スライス式を表すASTノードです。X
(基底となる式)、Lbrack
([
の位置)、Low
、High
、Max
(それぞれlow
,high
,max
の式)、Slice3
(3インデックススライスかどうかを示すブール値)、Rbrack
(]
の位置)などのフィールドを持ちます。ast.BadExpr
:go/ast
パッケージで定義されている、不正な式を表すASTノードです。パーサーが構文エラーを検出したが、解析を続行するためにプレースホルダーとして使用されます。From
とTo
フィールドは、不正な式の範囲を示します。
gofmt
gofmt
は、Go言語のソースコードを標準的なスタイルに自動的に整形するツールです。go/parser
が生成したASTを読み込み、それを基に整形されたコードを出力します。パーサーが不正なASTを生成すると、gofmt
が正しく動作しない原因となることがあります。
技術的詳細
このコミットの主要な変更点は、src/pkg/go/parser/parser.go
内のparseIndexOrSlice
関数にあります。この関数は、配列やスライスのインデックス操作やスライス式を解析する役割を担っています。
変更前は、3インデックススライスを解析する際に、コロンの数(ncolons
)が2であること(つまりs[low:high:max]
形式であること)はチェックしていましたが、high
(2番目のインデックス)やmax
(3番目のインデックス)に対応する式が実際に存在するかどうかは、型チェックの段階に任せていました。しかし、このアプローチでは、パーサーが不正な構文を許容し、不完全なASTを生成してしまう問題がありました。
今回の修正では、ncolons == 2
の場合、つまり3インデックススライスであると判断された場合に、以下の追加チェックが導入されました。
- 2番目のインデックスの存在チェック:
index[1] == nil
の場合、つまりhigh
に相当する式が解析されなかった場合、p.error(colons[0], "2nd index required in 3-index slice")
を呼び出してエラーを報告します。同時に、index[1]
にはast.BadExpr
が割り当てられ、ASTの整合性を保ちつつ、不正な部分をマークします。colons[0]
は最初のコロンの位置を示します。 - 3番目のインデックスの存在チェック:
index[2] == nil
の場合、つまりmax
に相当する式が解析されなかった場合、p.error(colons[1], "3rd index required in 3-index slice")
を呼び出してエラーを報告します。同様に、index[2]
にはast.BadExpr
が割り当てられます。colons[1]
は2番目のコロンの位置を示します。
これらのチェックをパーサーの段階で導入することで、不正な3インデックススライスが早期に検出され、エラーが報告されるようになります。これにより、gofmt
のような後続のツールが不完全なASTを処理しようとして問題を起こすことを防ぎます。
また、src/pkg/go/parser/short_test.go
には、この修正を検証するための新しいテストケースが追加されました。これらのテストケースは、2番目または3番目のインデックスが欠落している様々な不正な3インデックススライス式を意図的に含み、パーサーが期待通りにエラーを報告することを確認します。
コアとなるコードの変更箇所
src/pkg/go/parser/parser.go
--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -1168,16 +1168,19 @@ func (p *parser) parseIndexOrSlice(x ast.Expr) ast.Expr {
defer un(trace(p, "IndexOrSlice"))
}
+ const N = 3 // change the 3 to 2 to disable 3-index slices
lbrack := p.expect(token.LBRACK)
p.exprLev++
- var index [3]ast.Expr // change the 3 to 2 to disable slice expressions w/ cap
+ var index [N]ast.Expr
+ var colons [N - 1]token.Pos
if p.tok != token.COLON {
index[0] = p.parseRhs()
}
ncolons := 0
- for p.tok == token.COLON && ncolons < len(index)-1 {
- p.next()
+ for p.tok == token.COLON && ncolons < len(colons) {
+ colons[ncolons] = p.pos
ncolons++
+ p.next()
if p.tok != token.COLON && p.tok != token.RBRACK && p.tok != token.EOF {
index[ncolons] = p.parseRhs()
}
@@ -1187,7 +1190,21 @@ func (p *parser) parseIndexOrSlice(x ast.Expr) ast.Expr {
if ncolons > 0 {
// slice expression
- return &ast.SliceExpr{X: x, Lbrack: lbrack, Low: index[0], High: index[1], Max: index[2], Slice3: ncolons == 2, Rbrack: rbrack}
+ slice3 := false
+ if ncolons == 2 {
+ slice3 = true
+ // Check presence of 2nd and 3rd index here rather than during type-checking
+ // to prevent erroneous programs from passing through gofmt (was issue 7305).
+ if index[1] == nil {
+ p.error(colons[0], "2nd index required in 3-index slice")
+ index[1] = &ast.BadExpr{From: colons[0] + 1, To: colons[1]}
+ }
+ if index[2] == nil {
+ p.error(colons[1], "3rd index required in 3-index slice")
+ index[2] = &ast.BadExpr{From: colons[1] + 1, To: rbrack}
+ }
+ }
+ return &ast.SliceExpr{X: x, Lbrack: lbrack, Low: index[0], High: index[1], Max: index[2], Slice3: slice3, Rbrack: rbrack}
}
return &ast.IndexExpr{X: x, Lbrack: lbrack, Index: index[0], Rbrack: rbrack}
src/pkg/go/parser/short_test.go
--- a/src/pkg/go/parser/short_test.go
+++ b/src/pkg/go/parser/short_test.go
@@ -76,7 +76,11 @@ var invalids = []string{
`package p; func f() { _ = x = /* ERROR "expected '=='" */ 0 {}};`,
`package p; func f() { _ = 1 == func()int { var x bool; x = x = /* ERROR "expected '=='" */ true; return x }() };`,
`package p; func f() { var s []int; _ = s[] /* ERROR "expected operand" */ };`,
- `package p; func f() { var s []int; _ = s[::: /* ERROR "expected ']'" */ ] };`,
+ `package p; func f() { var s []int; _ = s[i:j: /* ERROR "3rd index required" */ ] };`,
+ `package p; func f() { var s []int; _ = s[i: /* ERROR "2nd index required" */ :k] };`,
+ `package p; func f() { var s []int; _ = s[i: /* ERROR "2nd index required" */ :] };`,
+ `package p; func f() { var s []int; _ = s[: /* ERROR "2nd index required" */ :] };`,
+ `package p; func f() { var s []int; _ = s[: /* ERROR "2nd index required" */ ::] };`,
`package p; func f() { var s []int; _ = s[i:j:k: /* ERROR "expected ']'" */ l] };`,
}
コアとなるコードの解説
src/pkg/go/parser/parser.go
の変更
-
const N = 3
とvar colons [N - 1]token.Pos
の追加:N
はスライス式の最大インデックス数(low
,high
,max
の3つ)を定義する定数です。将来的に2インデックススライスのみをサポートするように変更する場合に、この値を2に変更するだけで済むように設計されています。colons
配列は、スライス式内のコロン(:
)の位置を記録するために追加されました。これにより、エラーメッセージで正確な位置を指し示すことができます。
-
コロンの解析ロジックの変更:
- 変更前は
for p.tok == token.COLON && ncolons < len(index)-1
という条件でコロンを処理していましたが、変更後はfor p.tok == token.COLON && ncolons < len(colons)
となり、colons
配列の長さを基準に処理するようになりました。 colons[ncolons] = p.pos
が追加され、各コロンのトークン位置が記録されるようになりました。
- 変更前は
-
3インデックススライスにおけるインデックス存在チェックの追加:
if ncolons == 2
のブロック内で、slice3 = true
が設定され、これが3インデックススライスであることを示します。if index[1] == nil
: 2番目のインデックス(high
)が解析されなかった場合に実行されます。p.error(colons[0], "2nd index required in 3-index slice")
: 最初のコロンの位置(colons[0]
)を指し示し、「2nd index required in 3-index slice」(3インデックススライスでは2番目のインデックスが必要です)というエラーメッセージを出力します。index[1] = &ast.BadExpr{From: colons[0] + 1, To: colons[1]}
: 不正な式を表すast.BadExpr
ノードをindex[1]
に割り当てます。これにより、ASTの構造を維持しつつ、エラー箇所をマークします。From
は最初のコロンの直後、To
は2番目のコロンの位置を示します。
if index[2] == nil
: 3番目のインデックス(max
)が解析されなかった場合に実行されます。p.error(colons[1], "3rd index required in 3-index slice")
: 2番目のコロンの位置(colons[1]
)を指し示し、「3rd index required in 3-index slice」(3インデックススライスでは3番目のインデックスが必要です)というエラーメッセージを出力します。index[2] = &ast.BadExpr{From: colons[1] + 1, To: rbrack}
: 同様にast.BadExpr
をindex[2]
に割り当てます。From
は2番目のコロンの直後、To
は右角括弧(]
)の位置を示します。
これらの変更により、パーサーは3インデックススライスの構文規則をより厳密にチェックし、不正な形式を早期に検出してエラーを報告するようになりました。
src/pkg/go/parser/short_test.go
の変更
invalids
という文字列スライスに、以下の新しいテストケースが追加されました。これらはすべて、2番目または3番目のインデックスが欠落している不正な3インデックススライス式です。
`package p; func f() { var s []int; _ = s[i:j: /* ERROR "3rd index required" */ ] };`
: 3番目のインデックスが欠落しているケース。`package p; func f() { var s []int; _ = s[i: /* ERROR "2nd index required" */ :k] };`
: 2番目のインデックスが欠落しているケース。`package p; func f() { var s []int; _ = s[i: /* ERROR "2nd index required" */ :] };`
: 2番目と3番目のインデックスが両方欠落しているが、コロンが2つあるケース。`package p; func f() { var s []int; _ = s[: /* ERROR "2nd index required" */ :] };`
:low
が省略され、2番目と3番目のインデックスが両方欠落しているケース。`package p; func f() { var s []int; _ = s[: /* ERROR "2nd index required" */ ::] };`
:low
が省略され、2番目と3番目のインデックスが両方欠落しており、コロンが3つあるように見える不正なケース。
これらのテストケースは、パーサーが期待されるエラーメッセージ("3rd index required"
または"2nd index required"
)を正確に出力することを確認します。
関連リンク
- Go Issue 7305: go/parser: 3-index slice expressions with missing 2nd or 3rd index pass through gofmt
- Go Code Review 58950045: go/parser: check presence of 2nd and 3rd index in 3-index slice
参考にした情報源リンク
- Go言語の公式ドキュメント: The Go Programming Language Specification - Slice expressions
- Go言語の公式ドキュメント: The Go Programming Language Specification - Abstract syntax tree (AST)
go/parser
パッケージのドキュメント: pkg.go.dev/go/parsergo/ast
パッケージのドキュメント: pkg.go.dev/go/astgofmt
の概要: Go: gofmt (The Go Programming Language)- Go言語のスライスに関する解説記事 (例: A Tour of Go - Slices)
- Go言語のASTに関する解説記事 (例: Go AST: A Practical Guide)
- Go言語のIssueトラッカー (GitHub): github.com/golang/go/issues
- Go言語のCode Reviewシステム (Gerrit): go.googlesource.com/go/+/refs/heads/master