Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 が追加されました。これは、nilNode が渡された場合も空のツリーとして扱うことを明示しています。これにより、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 関数にあります。この関数は、新しいテンプレートを既存のテンプレートグループに関連付ける役割を担っています。

  1. associate 関数のシグネチャ変更:

    • 変更前: func (t *Template) associate(new *Template) error
    • 変更後: func (t *Template) associate(new *Template, tree *parse.Tree) (bool, error) 新しい tree 引数が追加され、戻り値に bool 型が追加されました。この bool は、新しいテンプレートのツリーを t.Tree に格納すべきかどうかを示します。
  2. Parse メソッド内の associate 呼び出しの変更: Parse メソッド内で t.associate(tmpl) の呼び出しが replace, err := t.associate(tmpl, tree) に変更され、replace の値に基づいて tmpl.Tree = tree が実行されるようになりました。これにより、associate 関数がテンプレートツリーの置き換えを制御できるようになります。

  3. associate 関数内の再定義ロジックの改善:

    • 以前は new.Tree != nil && parse.IsEmptyTree(new.Root) を使って新しいテンプレートが空かどうかを判断していましたが、これは new テンプレートが既にパースされたツリーを持っていることを前提としていました。
    • 修正後は newIsEmpty := parse.IsEmptyTree(tree.Root) となり、associate 関数に直接渡された tree 引数(まだ new テンプレートに割り当てられていない可能性のあるパースツリー)のルートが空かどうかを判断するようになりました。これにより、再定義のチェックがより正確になります。
    • 最も重要な変更は、既存のテンプレート old が空でなく (!oldIsEmpty)、かつ新しいテンプレート new も空でない (!newIsEmpty) 場合にエラーを返すロジックが変更された点です。
    • 新しいロジックでは、newIsEmpty (新しいテンプレートが空であるか) のチェックが先に行われます。
      • もし newIsEmptytrue であれば、新しいテンプレートは空なので、既存のテンプレートを置き換える理由がないため false, nil を返します(置き換えは行わない)。
      • もし newIsEmptyfalse であり、かつ oldIsEmptyfalse (既存のテンプレートが空でない) であれば、これは非空のテンプレートの再定義となるため、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 関数は、テンプレートの再定義ロジックの核心部分です。

  1. 引数の追加と戻り値の変更:

    • tree *parse.Tree 引数が追加されたことで、associate 関数は、new テンプレートにまだ割り当てられていない、パースされたばかりのツリーを直接受け取れるようになりました。これにより、new.Tree がまだ設定されていない状態でも、新しいテンプレートの内容が空かどうかを正確に判断できます。
    • bool の戻り値が追加されたことで、associate 関数は、呼び出し元(Parse メソッド)に対して、実際に tmpl.Tree = tree を実行して新しいツリーをテンプレートに割り当てるべきかどうかを指示できるようになりました。これは、新しいテンプレートが空で、既存のテンプレートを上書きする必要がない場合に false を返すことで、不要なツリーの割り当てを防ぎます。
  2. 再定義ロジックの改善:

    • 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 newIsEmptyfalse と判断されたため)場合、これは非空のテンプレートによる非空のテンプレートの再定義となるため、明確なエラーを返します。これにより、開発者は意図しないテンプレートの上書きを防ぐことができます。

これらの変更により、text/template パッケージは、テンプレートの再定義に関してより厳密で予測可能な挙動を示すようになり、開発者がテンプレートを扱う際のバグを減らすことに貢献します。

関連リンク

参考にした情報源リンク

  • Go text/template パッケージの公式ドキュメント
  • Go言語のコミット履歴と関連する議論
  • Go言語のソースコード