[インデックス 12615] ファイルの概要
このコミットは、Go言語の標準ライブラリである text/template
パッケージにおける、識別子(変数名など)のパースに関する複数のバグを修正するものです。具体的には、変数宣言時の不適切なエラーチェックと、識別子の後に適切な区切り文字がない場合のパースの問題に対処しています。
コミット
commit 8170d81f4f12db0c5d40bb550639026ee850fe25
Author: Rob Pike <r@golang.org>
Date: Wed Mar 14 07:03:11 2012 +1100
text/template: fix a couple of parse bugs around identifiers.
1) Poor error checking in variable declarations admitted
$x=2 or even $x%2.
2) Need white space or suitable termination character
after identifiers, so $x+2 doesn't parse, in case we want it
to mean something one day.
Number 2 in particular prevents mistakes that we will have
to honor later and so is necessary for Go 1.
Fixes #3270.
Fixes #3271.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5795073
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8170d81f4f12db0c5d40bb550639026ee850fe25
元コミット内容
text/template: fix a couple of parse bugs around identifiers.
- 変数宣言における不十分なエラーチェックにより、
$x=2
や$x%2
のような不正な構文が許容されていた。 - 識別子の後に空白または適切な終端文字が必要。これにより、将来的に算術演算子などを導入する可能性を考慮し、
$x+2
のような構文がパースされないようにする。 特に2番目の修正は、将来的に対応しなければならない誤りを防ぐため、Go 1のリリースにとって不可欠である。
Fixes #3270. Fixes #3271.
変更の背景
このコミットは、Go言語の text/template
パッケージにおけるテンプレートのパース処理の堅牢性を向上させるために行われました。主な背景は以下の2点です。
-
不正な変数宣言の許容: 従来のパーサーは、テンプレート内で変数を宣言する際に、Go言語の慣習である
:=
(短い変数宣言演算子) ではなく、=
や%
のような他の演算子を使用してもエラーを報告しませんでした。これは、テンプレートの記述ミスを誘発し、予期せぬ動作やデバッグの困難さにつながる可能性がありました。Go言語の設計思想として、明確で厳格な構文が重視されるため、このような曖昧さは修正されるべきでした。 -
識別子と後続文字の曖昧性: テンプレートエンジンでは、
$x
のような識別子の直後に+
や-
といった文字が続く場合、これらをどのように解釈するかが問題となります。例えば、$x+2
という記述があった場合、$x
と+2
を別々の要素として認識すべきか、それとも$x+2
全体を一つの意味のある塊として認識すべきか、という曖昧さがありました。このコミットの時点ではtext/template
は算術演算をサポートしていませんでしたが、将来的にサポートする可能性を考慮すると、現在の段階で$x+2
のような構文をエラーとして扱うことで、将来の互換性問題や予期せぬパース結果を防ぐ必要がありました。Go 1の安定版リリースに向けて、このような将来的な拡張性を阻害しないよう、厳密なパースルールを確立することが重要視されました。
これらの問題は、それぞれGoのIssue #3270と #3271として報告されており、このコミットによって解決されました。
前提知識の解説
このコミットを理解するためには、以下の概念についての知識が役立ちます。
-
Go言語の
text/template
パッケージ:- Go言語に組み込まれているテキストテンプレートエンジンです。HTMLやプレーンテキストの生成に利用されます。
- テンプレートは、Goのデータ構造を埋め込むためのプレースホルダーや制御構造(条件分岐、ループなど)を含むテキストです。
- テンプレートの構文は、
{{.FieldName}}
のようなデータアクセス、{{if .Condition}}...{{end}}
のような制御フロー、{{range .Slice}}...{{end}}
のような繰り返し処理、そして{{$variable := .Value}}
のような変数宣言を含みます。
-
字句解析 (Lexical Analysis / Lexing):
- コンパイラやインタープリタの最初の段階です。ソースコード(この場合はテンプレート文字列)を読み込み、意味のある最小単位である「トークン (Token)」のストリームに変換するプロセスです。
- 例えば、
$x := .SI
という文字列は、$
、x
、:=
、.
、SI
といったトークンに分割されます。 lex.go
ファイルは、この字句解析器(lexer)の実装を含んでいます。
-
構文解析 (Syntactic Analysis / Parsing):
- 字句解析によって生成されたトークンのストリームを受け取り、それらが言語の文法規則に従っているかを確認し、抽象構文木 (Abstract Syntax Tree: AST) を構築するプロセスです。
- ASTは、プログラムの構造を階層的に表現したものです。
parse.go
ファイルは、この構文解析器(parser)の実装を含んでいます。
-
Go言語の変数宣言:
- Go言語では、変数の宣言と初期化には主に
var
キーワードを使用する方法と、短い変数宣言演算子:=
を使用する方法があります。 name := expression
は、var name = expression
と同じ意味で、型推論が行われます。テンプレート内でもこの:=
が変数宣言の標準的な構文として採用されています。
- Go言語では、変数の宣言と初期化には主に
-
Go 1の互換性保証:
- Go言語は、バージョン1(Go 1)のリリース以降、後方互換性を非常に重視しています。これは、Go 1で導入されたAPIや言語仕様が、将来のバージョンでも変更されないことを意味します。
- このコミットの背景にある「Go 1にとって不可欠」という記述は、将来的な変更によって既存のテンプレートが壊れることを防ぐため、初期段階で厳密なパースルールを確立する必要があったことを示しています。
技術的詳細
このコミットは、text/template
パッケージの字句解析器(lexer)と構文解析器(parser)の両方に変更を加えています。
1. 変数宣言の厳格化 (parse.go
の変更)
- 問題点: 以前は、
{{$x=2}}
や{{$x%2}}
のように、変数名の後に:=
以外の文字(例えば=
や%
)が続いても、パーサーがこれを変数宣言として誤って解釈してしまう可能性がありました。 - 修正:
parse.go
のpipeline
関数内で、変数宣言を処理する部分が修正されました。- 変更前:
if next := t.peek(); next.typ == itemColonEquals || next.typ == itemChar {
- 変更後:
if next := t.peek(); next.typ == itemColonEquals || (next.typ == itemChar && next.val == ",") {
- この変更により、変数宣言の
:=
の後に続く文字としてitemColonEquals
(つまり:=
) またはitemChar
かつその値が,
(複数の変数宣言{{$x, $y := ...}}
の場合) のみが有効とされました。これにより、=
や%
のような不正な文字が変数名の直後に来た場合に、パーサーがエラーを報告するようになります。
- 変更前:
2. 識別子の終端チェックの追加 (lex.go
の変更)
- 問題点: 以前の字句解析器は、識別子(例:
$x
)の直後に空白や適切な区切り文字がない場合でも、その識別子を正しく終端させず、後続の文字と結合して誤ったトークンを生成する可能性がありました。例えば、$x+2
のようなケースで$x
と+
を適切に分離できない可能性がありました。これは、将来的にテンプレート内で算術演算子などを導入する際に、構文の曖昧さを生む原因となります。 - 修正:
lex.go
にatTerminator()
という新しいヘルパー関数が追加されました。- この関数は、現在の字句解析器の位置が、識別子の有効な終端文字(空白、EOF、コンマ、パイプ、コロン、または右デリミタの開始文字)であるかどうかを判断します。
lexInsideAction
関数内の識別子を処理するロジックに、l.atTerminator()
のチェックが追加されました。- 変更前は、識別子を読み取った後、すぐに
l.emit(itemIdentifier)
を呼び出していました。 - 変更後: 識別子を読み取った後、
if !l.atTerminator() { return l.errorf("unexpected character %+U", r) }
というチェックが追加されました。 - これにより、識別子の直後に有効な終端文字がない場合、字句解析器は「予期せぬ文字」としてエラーを報告し、不正な構文を早期に検出できるようになりました。
テストの追加 (multi_test.go
, parse_test.go
の変更)
- これらの変更を検証するために、既存のテストファイルに新しいテストケースが追加されています。
multi_test.go
では、{{template "nested" $x=.SI}}
のような不正な変数宣言が{{template "nested" $x:=.SI}}
に修正されています。これは、テスト自体が正しい構文を使用するように更新されたことを示しています。parse_test.go
では、特に以下の新しいテストケースが追加されています。bug0b
:{{$x = 1}}
がエラーになることを確認。bug0c
:{{$x ! 2}}
がエラーになることを確認。bug0d
:{{$x % 3}}
がエラーになることを確認。bug1a
:{{$x:=.}}{{$x!2}}
がエラーになることを確認。bug1b
:{{$x:=.}}{{$x+2}}
がエラーになることを確認。bug1c
:{{$x:=.}}{{$x +2}}
がエラーにならないことを確認(空白があればOK)。- これらのテストは、新しいパースルールが期待通りに機能し、不正な構文を正しく拒否することを確認しています。
これらの変更により、text/template
パッケージのパース処理はより厳密になり、将来的な拡張性も考慮された堅牢なものとなりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
-
src/pkg/text/template/parse/lex.go
:lexInsideAction
関数内で、識別子を処理する部分にl.atTerminator()
のチェックが追加されました。- 新しく
atTerminator()
ヘルパー関数が追加されました。この関数は、識別子の後に続く文字が有効な終端文字であるかを判断します。
// lex.go // ... func lexInsideAction(l *lexer) stateFn { Loop: for { // ... (既存のコード) default: l.backup() word := l.input[l.start:l.pos] // 新しいチェック: 識別子の後に有効な終端文字がない場合はエラー if !l.atTerminator() { return l.errorf("unexpected character %+U", r) } // ... (既存のコード) } } // ... } // 新しく追加されたヘルパー関数 // atTerminator reports whether the input is at valid termination character to // appear after an identifier. Mostly to catch cases like "$x+2" not being // acceptable without a space, in case we decide one day to implement // arithmetic. func (l *lexer) atTerminator() bool { r := l.peek() if isSpace(r) { return true } switch r { case eof, ',', '|', ':': return true } // Does r start the delimiter? This can be ambiguous (with delim=="//", $x/2 will // succeed but should fail) but only in extremely rare cases caused by willfully // bad choice of delimiter. if rd, _ := utf8.DecodeRuneInString(l.rightDelim); rd == r { return true } return false }
-
src/pkg/text/template/parse/parse.go
:pipeline
関数内で、変数宣言のパースロジックが修正されました。
// parse.go // ... func (t *Tree) pipeline(context string) (pipe *PipeNode) { // ... for { if v := t.peek(); v.typ == itemVariable { t.next() // 変更点: itemChar の場合、その値が "," であることを確認 if next := t.peek(); next.typ == itemColonEquals || (next.typ == itemChar && next.val == ",") { t.next() variable := newVariable(v.val) if len(variable.Ident) != 1 { t.errorf("cannot declare multiple variables in a single declaration") } pipe.Decl = append(pipe.Decl, variable) // ... } // ... } // ... } // ... }
-
src/pkg/text/template/parse/parse_test.go
:- 新しいテストケースが
parseTests
スライスに追加され、不正な変数宣言や識別子と後続文字の結合に関するエラーが正しく検出されることを検証しています。
// parse_test.go // ... var parseTests = []parseTest{ // ... (既存のテスト) // Equals (and other chars) do not assignments make (yet). {"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"}, {"bug0b", "{{$x = 1}}{{$x}}", hasError, ""}, // $x = 1 はエラーになることを確認 {"bug0c", "{{$x ! 2}}{{$x}}", hasError, ""}, // $x ! 2 はエラーになることを確認 {"bug0d", "{{$x % 3}}{{$x}}", hasError, ""}, // $x % 3 はエラーになることを確認 // Check the parse fails for := rather than comma. {"bug0e", "{{range $x := $y := 3}}{{end}}", hasError, ""}, // Another bug: variable read must ignore following punctuation. {"bug1a", "{{$x:=.}}{{$x!2}}", hasError, ""}, // ! はここでは不正 {"bug1b", "{{$x:=.}}{{$x+2}}", hasError, ""}, // $x+2 はパースされない {"bug1c", "{{$x:=.}}{{$x +2}}", noError, "{{$x := .}}{{$x +2}}"}, // スペースがあればOK }
- 新しいテストケースが
コアとなるコードの解説
src/pkg/text/template/parse/lex.go
の変更
lex.go
の変更は、字句解析器が識別子をトークン化する際の厳密性を高めるものです。
-
atTerminator()
関数の導入:- この関数は、字句解析器が現在見ている文字が、識別子の終わりを示す有効な区切り文字であるかどうかを判断します。
- 有効な区切り文字には、空白文字、ファイルの終端 (EOF)、コンマ (
,
)、パイプ (|
)、コロン (:
)、そしてテンプレートの右デリミタの開始文字が含まれます。 - 例えば、
{{$x+2}}
の場合、$x
を読み取った後、+
がatTerminator()
で定義された有効な終端文字ではないため、l.errorf("unexpected character %+U", r)
が呼び出され、パースエラーが発生します。 - これにより、
$x
と+
が誤って結合されたり、+
が予期せぬ形で識別子の一部として解釈されたりするのを防ぎます。これは、将来的にテンプレート言語に算術演算子などの新しい構文要素が追加された場合に、既存のテンプレートとの互換性問題を避ける上で非常に重要です。
-
lexInsideAction
内でのatTerminator()
の利用:lexInsideAction
は、{{...}}
のアクションブロック内の字句解析を担当します。- 識別子(変数名など)を読み取った後、
l.atTerminator()
を呼び出して、その識別子の直後に有効な終端文字が続いているかを確認します。 - もし有効な終端文字でなければ、即座にエラーを発生させます。これにより、
$x=2
や$x%2
のような不正な構文が字句解析の段階で捕捉され、より早い段階でユーザーにエラーを通知できるようになります。
src/pkg/text/template/parse/parse.go
の変更
parse.go
の変更は、構文解析器が変数宣言を処理する際の厳密性を高めるものです。
pipeline
関数内の変数宣言ロジックの修正:pipeline
関数は、テンプレートのパイプライン({{. | func}}
のような構造)をパースします。この中で変数宣言も処理されます。- 変更前は、
itemVariable
の後にitemColonEquals
(:=
) または任意のitemChar
が続く場合を変数宣言としていました。これは、=
や%
のような文字もitemChar
として扱われるため、$x=2
のような不正な構文を許容してしまう原因となっていました。 - 変更後、
itemChar
が続く場合は、その文字が具体的にコンマ (,
) であることをnext.val == ","
で明示的にチェックするようになりました。 - これにより、
{{$x := .Value}}
や{{$x, $y := .Values}}
のような正しい変数宣言のみが許可され、{{$x = .Value}}
のような不正な構文は構文解析の段階でエラーとして扱われるようになります。これは、Go言語の変数宣言の慣習にテンプレート構文をより厳密に合わせるための重要な修正です。
これらの変更は、text/template
パッケージの堅牢性と将来の拡張性を確保するために、パース処理の初期段階(字句解析)と中期段階(構文解析)の両方で厳密なチェックを導入したものです。
関連リンク
- Go Issue #3270: https://github.com/golang/go/issues/3270
- Go Issue #3271: https://github.com/golang/go/issues/3271
- Go CL 5795073: https://golang.org/cl/5795073 (このコミットに対応するGoの変更リスト)
参考にした情報源リンク
- Go言語公式ドキュメント:
text/template
パッケージ: https://pkg.go.dev/text/template - Go言語公式ドキュメント:
text/template/parse
パッケージ: https://pkg.go.dev/text/template/parse - Go言語の変数宣言に関する公式ドキュメントやチュートリアル (一般的なGo言語の知識として)
- コンパイラの基本原理(字句解析、構文解析、ASTなど)に関する一般的な情報源
- GitHubのGoリポジトリのIssueトラッカー
- Goのコードレビューシステム (Gerrit) の変更リスト (CL)