[インデックス 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言語のソースコード