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

[インデックス 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 関数が nilRoot フィールドにアクセスしようとしてパニックを引き起こす可能性がありました。

このバグは、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 パッケージにあるヘルパー関数で、与えられた解析ツリーが空であるかどうかを判定します。空のツリーとは、実質的に何も出力しないテンプレートを指します。
  • テンプレートの再定義: 既に存在する名前のテンプレートを、NewParse を使って再度定義しようとすることです。text/template パッケージは、通常、空でないテンプレートによる再定義をエラーとして扱いますが、空のテンプレートによる再定義は許可される場合があります。

技術的詳細

このコミットの核心は、Template 構造体の associate メソッド内のロジック変更にあります。associate メソッドは、新しいテンプレートが既存のテンプレートセットに追加される際に呼び出され、名前の衝突(再定義)を検出する役割を担っています。

変更前のコードでは、新しいテンプレートが空であるかどうかを判定するために parse.IsEmptyTree(new.Root) を直接呼び出していました。しかし、new.Rootnew.Treenil の場合に nil となる可能性があり、その状態で IsEmptyTree を呼び出すと nil ポインタ参照エラーが発生していました。これは、Parse メソッドがエラーを返した場合など、テンプレートの解析が不完全な場合に new.Treenil のままになることがあるためです。

修正後のコードでは、newIsEmpty の計算方法が newIsEmpty := new.Tree != nil && parse.IsEmptyTree(new.Root) に変更されました。この変更により、parse.IsEmptyTree(new.Root) が呼び出される前に new.Treenil でないことが保証されます。つまり、new.Treenil の場合は newIsEmptyfalse となり、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.Treenil になる可能性があります。
  • parse.IsEmptyTree(new.Root): new テンプレートのルートノード (new.Root) が空のツリーを構成するかどうかを判定します。

この論理積 (&&) により、new.Treenil の場合は parse.IsEmptyTree(new.Root) が評価されることなく newIsEmptyfalse に設定されます。これにより、nil ポインタ参照エラーが回避され、IsEmptyTree が安全に呼び出されるようになります。

その後の if !oldIsEmpty && !newIsEmpty の条件は、既存のテンプレートも新しいテンプレートも空でない場合に再定義エラーを発生させるためのものです。この修正により、newIsEmpty の計算がより堅牢になり、new.Treenil の場合でも安全に処理できるようになりました。

multi_test.go の変更

TestRedefinition という新しいテスト関数が追加されました。

  1. New("tmpl1").Parse({{define "test"}}foo{{end}}): まず、tmpl1 という名前のテンプレートを作成し、その中に test という名前のサブテンプレートを定義します。このサブテンプレートは "foo" を出力します。
  2. tmpl.New("tmpl2").Parse({{define "test"}}bar{{end}}): 次に、tmpl1 から派生した tmpl2 という新しいテンプレートを作成し、その中で同じ名前の test サブテンプレートを "bar" として再定義しようとします。
  3. if _, err = ...; err == nil { t.Fatal("expected error") }: この行は、test サブテンプレートの再定義がエラーになることを期待しています。text/template の設計上、空でないテンプレートの再定義はエラーとなるべきだからです。
  4. if !strings.Contains(err.Error(), "redefinition") { ... }: 最後に、発生したエラーメッセージが "redefinition" という文字列を含んでいることを確認し、期待通りの再定義エラーであることを検証しています。

このテストケースは、text/template パッケージがテンプレートの再定義を正しく処理し、期待されるエラーを返すことを保証します。特に、このコミットで修正された nil エラーが発生しないことを間接的に確認する役割も果たします。

関連リンク

参考にした情報源リンク

  • GitHubコミットページ (上記と同じ)
  • Go言語の text/template パッケージのドキュメント (一般的な概念理解のため)
  • Go言語の text/template/parse パッケージのドキュメント (IsEmptyTree の理解のため)
  • Go言語の Issue Tracker (Issue #2720 の詳細確認のため、ただし直接的な情報は見つからず、コミットメッセージからの推測に頼った)