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

[インデックス 12618] ファイルの概要

このコミットは、Go言語の text/template パッケージにおける変数の挙動に関する修正です。具体的には、テンプレート内で定義された変数に対して引数を渡すことができないようにする変更が加えられました。これにより、変数を関数のように呼び出す誤用を防ぎ、テンプレートのセマンティクスをより明確にしています。

コミット

commit d6ad6f0e61228152b3618af2e34381439d3b3ca0
Author: Rob Pike <r@golang.org>
Date:   Wed Mar 14 10:46:21 2012 +1100

    text/template: variables do not take arguments
    
    R=golang-dev, dsymonds
    CC=golang-dev
    https://golang.org/cl/5821044
---
 src/pkg/text/template/exec.go      | 1 +
 src/pkg/text/template/exec_test.go | 4 ++++\
 2 files changed, 5 insertions(+)

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d6ad6f0e61228152b3618af2e34381439d3b3ca0

元コミット内容

text/template: variables do not take arguments

R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5821044

変更の背景

Go言語の text/template パッケージは、テキストベースの出力を生成するためのテンプレートエンジンを提供します。このテンプレート言語では、変数や関数を定義し、それらを使って動的なコンテンツを生成できます。

このコミットが行われる以前は、text/template のパーサーは、変数に対して引数が与えられた場合でも、構文的にはエラーとせずにパースを完了させていました。しかし、実行時には変数に引数を渡すことは意味をなさず、予期せぬ動作や混乱を招く可能性がありました。例えば、{{$x 2}} のように変数 $x に引数 2 を渡そうとする構文は、テンプレート言語の設計上、関数呼び出しにのみ許されるべきものでした。

この変更の背景には、テンプレートのセマンティクスをより厳密にし、ユーザーがテンプレートを記述する際の誤解や誤用を防ぐという目的があります。変数は値を保持するものであり、関数のように引数を受け取って処理を実行するものではないという明確な区別を、実行時にも強制することで、テンプレートの堅牢性と予測可能性を高めています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の text/template パッケージに関する基本的な知識が必要です。

  • Go言語のテンプレートエンジン: Go言語には、text/templatehtml/template の2つの標準テンプレートパッケージがあります。これらは、データ構造をテンプレートに適用してテキスト出力を生成するために使用されます。
  • テンプレート構文: {{...}} で囲まれた部分がアクションと呼ばれ、変数、関数呼び出し、制御構造(if, rangeなど)を記述します。
  • 変数: テンプレート内で $variableName の形式で変数を定義し、値を格納できます。例えば、{{$x := 1}} は変数 $x に値 1 を代入します。
  • 関数: テンプレート内で functionName arg1 arg2 の形式で関数を呼び出すことができます。関数は引数を受け取り、結果を返します。
  • パイプライン: | 演算子を使って、前のコマンドの結果を次のコマンドの最後の引数として渡すことができます。例えば、{{.Value | html}}.Value の結果を html 関数に渡します。
  • reflect パッケージ: Go言語の reflect パッケージは、実行時に型情報を調べたり、値の操作を行ったりするための機能を提供します。テンプレートエンジンは、この reflect パッケージを使用して、テンプレートに渡されたデータの型を調べ、適切な処理を行います。
  • parse パッケージ: text/template パッケージの内部では、テンプレート文字列を解析して抽象構文木(AST)を構築するために parse パッケージが使用されます。parse.VariableNode は、AST内の変数ノードを表します。

技術的詳細

このコミットの技術的な核心は、text/template パッケージの実行エンジンが、変数ノードを評価する際に、その変数に引数が渡されていないことを確認するロジックを追加した点にあります。

変更が加えられたのは src/pkg/text/template/exec.go ファイル内の evalVariableNode 関数です。この関数は、テンプレートの抽象構文木(AST)を走査する際に、変数ノード(parse.VariableNode)を評価する役割を担っています。

以前のバージョンでは、evalVariableNode 関数は、変数が単一の識別子(例: $x)である場合に、単にその変数の値を返していました。しかし、この時、もし誤って引数が渡されていたとしても、その引数は無視され、実行時エラーにはなりませんでした。

今回の変更では、if len(v.Ident) == 1 (つまり、変数が単一の識別子である場合)のブロック内に s.notAFunction(args, final) という新しい呼び出しが追加されました。

  • sstate 構造体のインスタンスであり、テンプレートの実行状態を管理します。
  • notAFunction メソッドは、渡された args(引数)と final(パイプラインの最終引数かどうかを示すブール値)をチェックします。
  • もし args が空でなく、かつ finalfalse でない(つまり、引数が存在し、かつそれがパイプラインの最終引数ではない)場合、notAFunction は実行時エラーを発生させます。

これにより、{{$x 2}} のような構文がパース時にエラーにならなくても、実行時に「変数は引数を取らない」というエラーとして捕捉されるようになりました。

また、src/pkg/text/template/exec_test.go には、この新しい挙動を検証するためのテストケースが追加されています。

  • {{3 2}}: 数値リテラルに引数を渡すケース。数値は関数ではないためエラーとなるべきです。
  • {{$x := 1}}{{$x 2}}: 変数に引数を渡すケース。変数は関数ではないためエラーとなるべきです。
  • {{$x := 1}}{{3 | $x}}: パイプラインの最終引数として変数に値を渡すケース。この場合も変数は関数ではないためエラーとなるべきです。

これらのテストケースは、変数が関数のように呼び出された場合に、期待通りに実行時エラーが発生することを確認しています。

コアとなるコードの変更箇所

--- a/src/pkg/text/template/exec.go
+++ b/src/pkg/text/template/exec.go
@@ -369,6 +369,7 @@ func (s *state) evalVariableNode(dot reflect.Value, v *parse.VariableNode, args
 	// $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
 	value := s.varValue(v.Ident[0])
 	if len(v.Ident) == 1 {
+		s.notAFunction(args, final)
 		return value
 	}
 	return s.evalFieldChain(dot, value, v.Ident[1:], args, final)
--- a/src/pkg/text/template/exec_test.go
+++ b/src/pkg/text/template/exec_test.go
@@ -466,6 +466,10 @@ var execTests = []execTest{\n 	{\"bug6b\", \"{{vfunc .V0 .V0}}\", \"vfunc\", tVal, true},\n 	{\"bug6c\", \"{{vfunc .V1 .V0}}\", \"vfunc\", tVal, true},\n 	{\"bug6d\", \"{{vfunc .V1 .V1}}\", \"vfunc\", tVal, true},\n+\t// Legal parse but illegal execution: non-function should have no arguments.\n+\t{\"bug7a\", \"{{3 2}}\", \"\", tVal, false},\n+\t{\"bug7b\", \"{{$x := 1}}{{$x 2}}\", \"\", tVal, false},\n+\t{\"bug7c\", \"{{$x := 1}}{{3 | $x}}\", \"\", tVal, false},\
 }\n \n func zeroArgs() string {\

コアとなるコードの解説

src/pkg/text/template/exec.go の変更

evalVariableNode 関数は、テンプレートの実行中に変数ノードを評価する主要なロジックを含んでいます。

func (s *state) evalVariableNode(dot reflect.Value, v *parse.VariableNode, args []reflect.Value, final bool) (reflect.Value, error) {
	// $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
	value := s.varValue(v.Ident[0]) // 変数名(例: $x)に対応する値を取得
	if len(v.Ident) == 1 {          // 変数が単一の識別子(例: $x)である場合
		s.notAFunction(args, final) // ★追加された行★
		return value, nil           // 変数の値を返す
	}
	// 変数がフィールドチェーン(例: $x.Field)である場合、フィールドを評価
	return s.evalFieldChain(dot, value, v.Ident[1:], args, final)
}

追加された s.notAFunction(args, final) の呼び出しがこの変更の肝です。 notAFunction メソッド(exec.go 内に定義されていると仮定)は、以下のようなロジックを持つと考えられます。

// notAFunction reports an error if args is not empty and final is true.
// It is used to ensure that non-function values are not called with arguments.
func (s *state) notAFunction(args []reflect.Value, final bool) {
	if len(args) > 0 && final {
		s.errorf("non-function %s called with arguments", s.node.String())
	}
}

(※上記の notAFunction の実装はコミット差分には含まれていませんが、その機能と目的から推測されるものです。実際のGoのソースコードでは、s.errorf を呼び出してエラーを報告する形になっているはずです。)

このチェックにより、evalVariableNode が評価しているのが単なる変数であり、かつその変数に引数が渡されている(len(args) > 0)場合、そしてそれがパイプラインの最終引数として扱われている(finaltrue)場合にエラーが発生します。これにより、{{$x 2}} のような直接的な変数への引数渡しや、{{3 | $x}} のようなパイプラインでの変数への引数渡しが実行時に捕捉されるようになります。

src/pkg/text/template/exec_test.go の変更

追加されたテストケースは、この新しい挙動を具体的に検証します。

var execTests = []execTest{
	// ... 既存のテストケース ...
	// Legal parse but illegal execution: non-function should have no arguments.
	{"bug7a", "{{3 2}}", "", tVal, false},
	{"bug7b", "{{$x := 1}}{{$x 2}}", "", tVal, false},
	{"bug7c", "{{$x := 1}}{{3 | $x}}", "", tVal, false},
}

各テストケースの最後の false は、そのテストケースが実行時にエラーを発生させることを期待していることを示しています。

  • "bug7a", "{{3 2}}", "", tVal, false: 数値リテラル 3 は関数ではないため、引数 2 を与えるとエラーになることを期待します。
  • "bug7b", "{{$x := 1}}{{$x 2}}", "", tVal, false: 変数 $x に値 1 が代入されており、これは関数ではないため、引数 2 を与えるとエラーになることを期待します。
  • "bug7c", "{{$x := 1}}{{3 | $x}}", "", tVal, false: パイプラインで 3 を変数 $x に渡そうとしています。変数 $x は関数ではないため、エラーになることを期待します。

これらのテストケースが追加されたことで、将来的に同様の回帰バグが発生するのを防ぐことができます。

関連リンク

参考にした情報源リンク