[インデックス 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 の詳細確認のため、ただし直接的な情報は見つからず、コミットメッセージからの推測に頼った)