[インデックス 11213] ファイルの概要
このコミットは、Go言語の text/template
パッケージにおけるテンプレートの再定義に関するバグ修正です。具体的には、define
アクションを使用してテンプレートを再定義する際に発生する可能性のある nil
エラーを修正し、関連するテストケースを追加しています。
コミット
commit 4985ee3dcb76bd0f9d8aba800e97ba29b535997f
Author: Rob Pike <r@golang.org>
Date: Tue Jan 17 13:24:59 2012 -0800
text/template: fix nil error on redefinition
Fixes #2720.
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5545072
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4985ee3dcb76bd0f9d8aba800e97ba29b535997f
元コミット内容
text/template
: 再定義時の nil
エラーを修正。
Issue #2720 を修正。
変更の背景
Go言語の text/template
パッケージでは、{{define "name"}}...{{end}}
構文を使って名前付きテンプレートを定義できます。また、Template
オブジェクトの New
メソッドを使って新しい名前のテンプレートを作成し、Parse
メソッドでその内容を解析することも可能です。
このコミットが修正しようとしている問題は、既存のテンプレートと同じ名前で新しいテンプレートを定義(再定義)しようとした際に、特定の条件下で nil
ポインタ参照エラーが発生するというものでした。特に、新しいテンプレートが空である(つまり、define
された内容が実質的に空のツリーを生成する)場合に、IsEmptyTree
関数が nil
の Root
フィールドにアクセスしようとしてパニックを引き起こす可能性がありました。
このバグは、Issue #2720 として報告されており、このコミットはその問題を解決することを目的としています。テンプレートの再定義は、例えば、複数のファイルからテンプレートを読み込む際や、共通のレイアウトテンプレートを上書きする際などに発生しうるシナリオです。
前提知識の解説
text/template
パッケージ: Go言語の標準ライブラリの一部で、テキストベースのテンプレートを生成するための機能を提供します。HTMLの生成にも利用できますが、セキュリティ上の理由からHTML生成にはhtml/template
パッケージが推奨されます。- テンプレートの定義 (
define
アクション): テンプレート内で{{define "name"}}...{{end}}
構文を使用すると、name
という名前のサブテンプレートを定義できます。このサブテンプレートは、後で{{template "name"}}
を使って呼び出すことができます。 Template
構造体:text/template
パッケージの中心となる型で、解析されたテンプレートのツリー構造や、関連する名前付きテンプレートのマップを保持します。New(name string) *Template
メソッド: 既存のTemplate
オブジェクトから、新しい名前を持つTemplate
オブジェクトを作成します。この新しいテンプレートは、元のテンプレートと同じ関数マップやオプションを継承しますが、独自の解析ツリーを持ちます。Parse(text string) (*Template, error)
メソッド: テンプレート文字列を解析し、その結果をTemplate
オブジェクトに格納します。解析が成功すると、Template
オブジェクト自身が返され、失敗するとエラーが返されます。parse.IsEmptyTree(tree *parse.Tree) bool
関数:text/template/parse
パッケージにあるヘルパー関数で、与えられた解析ツリーが空であるかどうかを判定します。空のツリーとは、実質的に何も出力しないテンプレートを指します。- テンプレートの再定義: 既に存在する名前のテンプレートを、
New
とParse
を使って再度定義しようとすることです。text/template
パッケージは、通常、空でないテンプレートによる再定義をエラーとして扱いますが、空のテンプレートによる再定義は許可される場合があります。
技術的詳細
このコミットの核心は、Template
構造体の associate
メソッド内のロジック変更にあります。associate
メソッドは、新しいテンプレートが既存のテンプレートセットに追加される際に呼び出され、名前の衝突(再定義)を検出する役割を担っています。
変更前のコードでは、新しいテンプレートが空であるかどうかを判定するために parse.IsEmptyTree(new.Root)
を直接呼び出していました。しかし、new.Root
は new.Tree
が nil
の場合に nil
となる可能性があり、その状態で IsEmptyTree
を呼び出すと nil
ポインタ参照エラーが発生していました。これは、Parse
メソッドがエラーを返した場合など、テンプレートの解析が不完全な場合に new.Tree
が nil
のままになることがあるためです。
修正後のコードでは、newIsEmpty
の計算方法が newIsEmpty := new.Tree != nil && parse.IsEmptyTree(new.Root)
に変更されました。この変更により、parse.IsEmptyTree(new.Root)
が呼び出される前に new.Tree
が nil
でないことが保証されます。つまり、new.Tree
が nil
の場合は newIsEmpty
は false
となり、parse.IsEmptyTree
は呼び出されません。これにより、nil
ポインタ参照エラーが回避されます。
この修正は、テンプレートの再定義ロジックの堅牢性を高め、特定の条件下でのクラッシュを防ぎます。
コアとなるコードの変更箇所
src/pkg/text/template/template.go
--- a/src/pkg/text/template/template.go
+++ b/src/pkg/text/template/template.go
@@ -198,7 +198,7 @@ func (t *Template) associate(new *Template) error {
name := new.name
if old := t.tmpl[name]; old != nil {
oldIsEmpty := parse.IsEmptyTree(old.Root)
- newIsEmpty := parse.IsEmptyTree(new.Root)
+ newIsEmpty := new.Tree != nil && parse.IsEmptyTree(new.Root)
if !oldIsEmpty && !newIsEmpty {
return fmt.Errorf("template: redefinition of template %q", name)
}
src/pkg/text/template/multi_test.go
--- a/src/pkg/text/template/multi_test.go
+++ b/src/pkg/text/template/multi_test.go
@@ -9,6 +9,7 @@ package template
import (
"bytes"
"fmt"
+ "strings"
"testing"
"text/template/parse"
)
@@ -257,3 +258,17 @@ func TestAddParseTree(t *testing.T) {
t.Errorf("expected %q got %q", "broot", b.String())
}\n}\n+\n+func TestRedefinition(t *testing.T) {\n+\tvar tmpl *Template\n+\tvar err error\n+\tif tmpl, err = New(\"tmpl1\").Parse(`{{define \"test\"}}foo{{end}}`); err != nil {\n+\t\tt.Fatalf(\"parse 1: %v\", err)\n+\t}\n+\tif _, err = tmpl.New(\"tmpl2\").Parse(`{{define \"test\"}}bar{{end}}`); err == nil {\n+\t\tt.Fatal(\"expected error\")\n+\t}\n+\tif !strings.Contains(err.Error(), \"redefinition\") {\n+\t\tt.Fatalf(\"expected redefinition error; got %v\", err)\n+\t}\n+}\n```
## コアとなるコードの解説
### `template.go` の変更
`associate` メソッドは、新しいテンプレート `new` を既存のテンプレートマップ `t.tmpl` に関連付ける際に呼び出されます。
変更された行は以下の通りです。
```go
newIsEmpty := new.Tree != nil && parse.IsEmptyTree(new.Root)
new.Tree != nil
: これはガード条件です。new
テンプレートの解析ツリー (new.Tree
) がnil
でないことを確認します。Parse
メソッドがエラーを返した場合など、テンプレートの解析が失敗するとnew.Tree
はnil
になる可能性があります。parse.IsEmptyTree(new.Root)
:new
テンプレートのルートノード (new.Root
) が空のツリーを構成するかどうかを判定します。
この論理積 (&&
) により、new.Tree
が nil
の場合は parse.IsEmptyTree(new.Root)
が評価されることなく newIsEmpty
が false
に設定されます。これにより、nil
ポインタ参照エラーが回避され、IsEmptyTree
が安全に呼び出されるようになります。
その後の if !oldIsEmpty && !newIsEmpty
の条件は、既存のテンプレートも新しいテンプレートも空でない場合に再定義エラーを発生させるためのものです。この修正により、newIsEmpty
の計算がより堅牢になり、new.Tree
が nil
の場合でも安全に処理できるようになりました。
multi_test.go
の変更
TestRedefinition
という新しいテスト関数が追加されました。
New("tmpl1").Parse(
{{define "test"}}foo{{end}})
: まず、tmpl1
という名前のテンプレートを作成し、その中にtest
という名前のサブテンプレートを定義します。このサブテンプレートは "foo" を出力します。tmpl.New("tmpl2").Parse(
{{define "test"}}bar{{end}})
: 次に、tmpl1
から派生したtmpl2
という新しいテンプレートを作成し、その中で同じ名前のtest
サブテンプレートを "bar" として再定義しようとします。if _, err = ...; err == nil { t.Fatal("expected error") }
: この行は、test
サブテンプレートの再定義がエラーになることを期待しています。text/template
の設計上、空でないテンプレートの再定義はエラーとなるべきだからです。if !strings.Contains(err.Error(), "redefinition") { ... }
: 最後に、発生したエラーメッセージが "redefinition" という文字列を含んでいることを確認し、期待通りの再定義エラーであることを検証しています。
このテストケースは、text/template
パッケージがテンプレートの再定義を正しく処理し、期待されるエラーを返すことを保証します。特に、このコミットで修正された nil
エラーが発生しないことを間接的に確認する役割も果たします。
関連リンク
参考にした情報源リンク
- GitHubコミットページ (上記と同じ)
- Go言語の
text/template
パッケージのドキュメント (一般的な概念理解のため) - Go言語の
text/template/parse
パッケージのドキュメント (IsEmptyTree の理解のため) - Go言語の Issue Tracker (Issue #2720 の詳細確認のため、ただし直接的な情報は見つからず、コミットメッセージからの推測に頼った)