[インデックス 12250] ファイルの概要
このコミットは、Go言語の標準ライブラリ text/template パッケージにおけるテンプレートの再定義に関するバグを修正するものです。具体的には、既に定義されているテンプレートを誤って再定義しようとした際に、適切なエラーハンドリングが行われるように改善されています。
コミット
text/template: fix redefinition bugs
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5696087
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/180541b2b1bde56f31d0f895a12c25bb01d8c58b
元コミット内容
commit 180541b2b1bde56f31d0f895a12c25bb01d8c58b
Author: Rob Pike <r@golang.org>
Date: Tue Feb 28 14:23:57 2012 +1100
text/template: fix redefinition bugs
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5696087
変更の背景
Go言語の text/template パッケージでは、define アクションを使用して名前付きテンプレートを定義できます。しかし、このコミット以前は、既に存在する名前でテンプレートを再定義しようとした際に、常に期待通りのエラーが発生するわけではありませんでした。特に、空のテンプレートと非空のテンプレートの間での再定義の挙動に一貫性がなく、バグとして認識されていました。
このバグは、開発者が意図せず同じ名前のテンプレートを複数回定義してしまい、予期せぬテンプレートの挙動やデバッグの困難さを引き起こす可能性がありました。このコミットは、このような再定義のシナリオにおいて、より堅牢で予測可能なエラーハンドリングを提供することを目的としています。具体的には、非空のテンプレートが既に存在する場合に、その名前で別のテンプレートを再定義しようとすると、明確なエラーを返すように修正されています。
前提知識の解説
Go言語の text/template パッケージ
text/template パッケージは、Go言語でテキストベースのテンプレートを生成するための機能を提供します。これは、HTML、XML、プレーンテキストなど、様々な形式の出力に対応しています。主な機能は以下の通りです。
- テンプレートのパース: テンプレート文字列を解析し、内部的なツリー構造に変換します。
- アクション: テンプレート内でデータにアクセスしたり、制御フローを記述したりするための構文(例:
{{.Field}},{{if .Condition}},{{range .Slice}})。 defineアクション: テンプレート内で名前付きのサブテンプレートを定義するために使用されます。これにより、テンプレートの再利用性やモジュール化が可能になります。例:{{define "myTemplate"}}...{{end}}。- テンプレートの関連付け:
Template型は、複数の名前付きテンプレートを管理できます。ParseメソッドやNewメソッドを使って、新しいテンプレートを既存のテンプレートセットに追加したり、既存のテンプレートを更新したりできます。
テンプレートの再定義
text/template パッケージでは、同じ名前のテンプレートを複数回定義することが可能です。しかし、その挙動は定義されているテンプレートが「空」であるかどうかに依存します。
- 空のテンプレート:
{{define "name"}}{{end}}のように、内容が空のテンプレート。 - 非空のテンプレート:
{{define "name"}}Hello{{end}}のように、内容を持つテンプレート。
従来の挙動では、空のテンプレートは再定義によって上書きされることが許容されていましたが、非空のテンプレートの再定義はエラーとなるべきでした。しかし、このエラーハンドリングに不整合があったため、今回の修正が必要となりました。
parse.IsEmptyTree 関数
text/template/parse パッケージは、テンプレートのパースツリーを扱います。IsEmptyTree 関数は、与えられたテンプレートのパースツリーが実質的に空であるかどうかを判断するために使用されます。これは、テンプレートが define されたものの、実際には何も内容を含んでいない場合に true を返します。この関数は、テンプレートの再定義ロジックにおいて、既存のテンプレートが上書き可能かどうかを判断する上で重要な役割を果たします。
技術的詳細
このコミットは、主に src/pkg/text/template/template.go 内の associate 関数と、src/pkg/text/template/parse/parse.go 内の IsEmptyTree 関数の変更に焦点を当てています。
src/pkg/text/template/parse/parse.go の変更
IsEmptyTree 関数に case nil: return true が追加されました。これは、nil の Node が渡された場合も空のツリーとして扱うことを明示しています。これにより、IsEmptyTree のロバスト性が向上し、nil チェックが不要になります。
src/pkg/text/template/parse/parse_test.go の変更
TestIsEmpty 関数に if !IsEmptyTree(nil) { t.Errorf("nil tree is not empty") } というテストケースが追加されました。これは、nil が空のツリーとして正しく扱われることを検証するためのものです。
src/pkg/text/template/multi_test.go の変更
TestRedefinition 関数に新しいテストケースが追加されました。
tmpl, err = New("tmpl1").Parse({{define "test"}}foo{{end}}) で test という名前の非空テンプレートを定義した後、
_, err = tmpl.Parse({{define "test"}}bar{{end}}) で同じ名前の非空テンプレートを再定義しようとすると、エラーが発生することを検証しています。
さらに、そのエラーメッセージが "redefinition" を含むことを確認し、期待されるエラーが返されていることを保証しています。
src/pkg/text/template/template.go の変更
最も重要な変更は associate 関数にあります。この関数は、新しいテンプレートを既存のテンプレートグループに関連付ける役割を担っています。
-
associate関数のシグネチャ変更:- 変更前:
func (t *Template) associate(new *Template) error - 変更後:
func (t *Template) associate(new *Template, tree *parse.Tree) (bool, error)新しいtree引数が追加され、戻り値にbool型が追加されました。このboolは、新しいテンプレートのツリーをt.Treeに格納すべきかどうかを示します。
- 変更前:
-
Parseメソッド内のassociate呼び出しの変更:Parseメソッド内でt.associate(tmpl)の呼び出しがreplace, err := t.associate(tmpl, tree)に変更され、replaceの値に基づいてtmpl.Tree = treeが実行されるようになりました。これにより、associate関数がテンプレートツリーの置き換えを制御できるようになります。 -
associate関数内の再定義ロジックの改善:- 以前は
new.Tree != nil && parse.IsEmptyTree(new.Root)を使って新しいテンプレートが空かどうかを判断していましたが、これはnewテンプレートが既にパースされたツリーを持っていることを前提としていました。 - 修正後は
newIsEmpty := parse.IsEmptyTree(tree.Root)となり、associate関数に直接渡されたtree引数(まだnewテンプレートに割り当てられていない可能性のあるパースツリー)のルートが空かどうかを判断するようになりました。これにより、再定義のチェックがより正確になります。 - 最も重要な変更は、既存のテンプレート
oldが空でなく (!oldIsEmpty)、かつ新しいテンプレートnewも空でない (!newIsEmpty) 場合にエラーを返すロジックが変更された点です。 - 新しいロジックでは、
newIsEmpty(新しいテンプレートが空であるか) のチェックが先に行われます。- もし
newIsEmptyがtrueであれば、新しいテンプレートは空なので、既存のテンプレートを置き換える理由がないためfalse, nilを返します(置き換えは行わない)。 - もし
newIsEmptyがfalseであり、かつoldIsEmptyがfalse(既存のテンプレートが空でない) であれば、これは非空のテンプレートの再定義となるため、false, fmt.Errorf("template: redefinition of template %q", name)を返してエラーを発生させます。
- もし
- この変更により、非空のテンプレートが既に存在する場合に、別の非空のテンプレートで再定義しようとすると、常に明確なエラーが返されるようになります。
- 以前は
コアとなるコードの変更箇所
src/pkg/text/template/parse/parse.go
--- a/src/pkg/text/template/parse/parse.go
+++ b/src/pkg/text/template/parse/parse.go
@@ -193,6 +193,8 @@ func (t *Tree) add(treeSet map[string]*Tree) {
// IsEmptyTree reports whether this tree (node) is empty of everything but space.
func IsEmptyTree(n Node) bool {
switch n := n.(type) {
+ case nil:
+ return true
case *ActionNode:
case *IfNode:
case *ListNode:
src/pkg/text/template/template.go
--- a/src/pkg/text/template/template.go
+++ b/src/pkg/text/template/template.go
@@ -178,10 +178,11 @@ func (t *Template) Parse(text string) (*Template, error) {
// Even if t == tmpl, we need to install it in the common.tmpl map.
- if err := t.associate(tmpl); err != nil {
+ if replace, err := t.associate(tmpl, tree); err != nil {
return nil, err
+ } else if replace {
+ tmpl.Tree = tree
}
- tmpl.Tree = tree
tmpl.leftDelim = t.leftDelim
tmpl.rightDelim = t.rightDelim
}
@@ -191,22 +192,23 @@ func (t *Template) Parse(text string) (*Template, error) {
// associate installs the new template into the group of templates associated
// with t. It is an error to reuse a name except to overwrite an empty
// template. The two are already known to share the common structure.
-func (t *Template) associate(new *Template) error {
+// The boolean return value reports wither to store this tree as t.Tree.
+func (t *Template) associate(new *Template, tree *parse.Tree) (bool, error) {
if new.common != t.common {
panic("internal error: associate not common")
}
name := new.name
if old := t.tmpl[name]; old != nil {
oldIsEmpty := parse.IsEmptyTree(old.Root)
- newIsEmpty := new.Tree != nil && parse.IsEmptyTree(new.Root)
- if !oldIsEmpty && !newIsEmpty {
- return fmt.Errorf("template: redefinition of template %q", name)
- }
+ newIsEmpty := parse.IsEmptyTree(tree.Root)
if newIsEmpty {
// Whether old is empty or not, new is empty; no reason to replace old.
- return nil
+ return false, nil
+ }
+ if !oldIsEmpty {
+ return false, fmt.Errorf("template: redefinition of template %q", name)
}
}
t.tmpl[name] = new
- return nil
+ return true, nil
}
コアとなるコードの解説
IsEmptyTree の変更
IsEmptyTree 関数は、テンプレートのノードが nil の場合も空であると判断するように修正されました。これは、テンプレートのパースツリーがまだ構築されていない、あるいは意図的に空のノリーフノードが渡された場合でも、安全に空として扱えるようにするための改善です。これにより、associate 関数のような呼び出し元で nil チェックを減らし、コードの簡潔性と堅牢性を向上させます。
associate 関数の変更
associate 関数は、テンプレートの再定義ロジックの核心部分です。
-
引数の追加と戻り値の変更:
tree *parse.Tree引数が追加されたことで、associate関数は、newテンプレートにまだ割り当てられていない、パースされたばかりのツリーを直接受け取れるようになりました。これにより、new.Treeがまだ設定されていない状態でも、新しいテンプレートの内容が空かどうかを正確に判断できます。boolの戻り値が追加されたことで、associate関数は、呼び出し元(Parseメソッド)に対して、実際にtmpl.Tree = treeを実行して新しいツリーをテンプレートに割り当てるべきかどうかを指示できるようになりました。これは、新しいテンプレートが空で、既存のテンプレートを上書きする必要がない場合にfalseを返すことで、不要なツリーの割り当てを防ぎます。
-
再定義ロジックの改善:
newIsEmpty := parse.IsEmptyTree(tree.Root): 新しいテンプレートが空であるかどうかの判断が、associateに渡されたtreeのルートノードに基づいて行われるようになりました。これにより、newテンプレートオブジェクト自体のTreeフィールドがまだ設定されていなくても、正確な判断が可能になります。if newIsEmpty { return false, nil }: 新しいテンプレートが空の場合、既存のテンプレートが空であろうとなかろうと、新しい空のテンプレートで上書きする意味はないため、置き換えを行わず(false)、エラーも返しません。これは、空のテンプレートによる再定義が許容されるという元の意図を維持しつつ、無駄な処理を省きます。if !oldIsEmpty { return false, fmt.Errorf("template: redefinition of template %q", name) }: この行が、非空のテンプレートの再定義バグを修正する主要な部分です。もし既存のテンプレート (old) が空でなく (!oldIsEmpty)、かつ新しいテンプレート (new) も空でない(前のif newIsEmptyでfalseと判断されたため)場合、これは非空のテンプレートによる非空のテンプレートの再定義となるため、明確なエラーを返します。これにより、開発者は意図しないテンプレートの上書きを防ぐことができます。
これらの変更により、text/template パッケージは、テンプレートの再定義に関してより厳密で予測可能な挙動を示すようになり、開発者がテンプレートを扱う際のバグを減らすことに貢献します。
関連リンク
- Go CL (Change List): https://golang.org/cl/5696087
- Go
text/templateパッケージドキュメント: https://pkg.go.dev/text/template
参考にした情報源リンク
- Go
text/templateパッケージの公式ドキュメント - Go言語のコミット履歴と関連する議論
- Go言語のソースコード