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

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

このコミットは、Go言語の text/template パッケージにおけるテンプレート解析の挙動に関する修正です。具体的には、(A).X のような形式の式が誤って2つの引数として解析されてしまう問題を一時的にエラーとして扱うように変更し、混乱を避けることを目的としています。これは、将来的にこの構文が正しく扱われるまでの暫定的な措置です。

コミット

commit 8b2306623992b584cc18ec4c5287a94349eb5fb3
Author: Rob Pike <r@golang.org>
Date:   Fri Aug 24 13:00:24 2012 -0700

    text/template: catch (A).X as a parse error
    This shouldn't be an error (see issue 3999), but until it's handled
    correctly, treat it as one to avoid confusion. Without this CL,
    (A).X parses as two arguments.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6473059

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

https://github.com/golang/go/commit/8b2306623992b584cc18ec4c5287a94349eb5fb3

元コミット内容

text/template: catch (A).X as a parse error
This shouldn't be an error (see issue 3999), but until it's handled
correctly, treat it as one to avoid confusion. Without this CL,
(A).X parses as two arguments.

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6473059

変更の背景

この変更の背景には、Go言語の text/template パッケージにおけるテンプレートの構文解析の特定の挙動があります。通常、テンプレートエンジンは、与えられた文字列(テンプレート)を解析し、データと結合して最終的な出力を生成します。この解析プロセスにおいて、(A).X のような形式の式が正しく解釈されないという問題が存在していました。

具体的には、(A).X という構文は、本来であれば A という式の評価結果に対してフィールド X にアクセスしようとする意図を持つものと解釈されるべきです。しかし、当時の text/template のパーサーは、この構文を AX という2つの独立した引数として誤って解釈していました。この誤った解釈は、開発者にとって予期せぬ挙動や混乱を引き起こす可能性がありました。

この問題は、GoのIssue 3999として報告されており、将来的にはこの構文が正しく扱われるように修正されることが期待されていました。しかし、その修正が完了するまでの間、誤った解析による混乱を避けるため、一時的な対策として (A).X のような構文を明示的にパースエラーとして扱うようにこのコミットで変更されました。これにより、開発者は意図しない挙動に遭遇する代わりに、明確なエラーメッセージを受け取ることができるようになりました。

前提知識の解説

Go言語の text/template パッケージ

text/template パッケージは、Go言語に組み込まれているテキストテンプレートエンジンです。これは、データ構造(Goの構造体、マップ、スライスなど)をテンプレートと結合して、動的なテキスト出力を生成するために使用されます。ウェブページ、設定ファイル、コード生成など、様々な用途で利用されます。

テンプレートは、プレーンテキストと、{{...}} で囲まれた「アクション」と呼ばれる特殊な構文で構成されます。アクションは、データの表示、条件分岐、ループ、関数呼び出しなど、様々な操作を実行できます。

テンプレートの構文解析 (Parsing)

テンプレートエンジンは、テンプレート文字列を読み込み、それを内部的なデータ構造(抽象構文木 - AST)に変換する「構文解析(Parsing)」のプロセスを実行します。このプロセスは通常、以下の2つのフェーズに分けられます。

  1. 字句解析 (Lexing/Tokenizing): テンプレート文字列を、意味を持つ最小単位である「トークン」のストリームに分解します。例えば、{{printf .}}{{, printf, ., }} といったトークンに分解されます。
  2. 構文解析 (Parsing): トークンのストリームを文法規則に従って解析し、ASTを構築します。このASTは、テンプレートの構造と意味を表現します。

フィールド/メソッドアクセスとドット記法

Goのテンプレートでは、データ構造のフィールドやメソッドにアクセスするためにドット記法 (.) を使用します。例えば、{{.Name}} は現在のコンテキストの Name フィールドの値を出力します。また、{{.Method}} は現在のコンテキストの Method メソッドを呼び出します。

(A).X のような構文

このコミットで問題となっている (A).X のような構文は、プログラミング言語において一般的な「式の評価結果に対するメンバーアクセス」を意図しています。ここで A は何らかの式(例えば、関数呼び出しや別のフィールドアクセス)であり、その A の評価結果に対して、さらに X というフィールドやメソッドにアクセスしようとするものです。

例えば、printf 関数が何らかの値を返すとして、その返り値が構造体であり、その構造体に Field というフィールドがある場合、{{(printf .).Field}} のように記述することで、printf の結果の Field にアクセスすることを意図します。

Niladic Function (引数なし関数)

Niladic Functionとは、引数を取らない関数のことを指します。Goのテンプレートコンテキストでは、データ構造のメソッドが引数を取らない場合、それをNiladic Functionとして呼び出すことができます。Issue 3999では、このようなNiladic Functionの呼び出し結果に対して、さらにフィールドやメソッドにアクセスする際の挙動の曖昧さが議論されていました。

技術的詳細

このコミットが対処している技術的な問題は、text/template パッケージの字句解析器(lexer)と構文解析器(parser)が、(A).X という形式の式を正しく解釈できないという点にあります。

本来、(A).X は、括弧内の式 A をまず評価し、その評価結果に対して . 演算子を用いてフィールド X にアクセスするという、単一の論理的な操作として扱われるべきです。しかし、当時のパーサーは、括弧 () の処理と . 演算子の処理の組み合わせにおいて、この構文を誤って AX という2つの独立した引数として認識していました。

例えば、{{printf (printf .).}} というテンプレートがあった場合、パーサーは (printf .) を一つの引数として認識し、その後に続く . を別の引数として認識してしまう、というような挙動をしていました。これは、テンプレートの意図とは全く異なる解釈であり、結果として予期せぬエラーや、場合によってはサイレントな誤動作を引き起こす可能性がありました。

このコミットでは、この誤った解析を一時的に回避するために、字句解析の段階で特定のパターンを検出した場合に明示的にエラーを発生させるというアプローチが取られています。具体的には、括弧の閉じ())の直後に . が続く場合、これを「括弧で囲まれた式のフィールドにアクセスしようとしている」と判断し、それが現在のパーサーの能力では正しく扱えないため、エラーとして報告するように変更されました。

これは、問題の根本的な解決(つまり、(A).X を正しく解析できるようにパーサーを改良すること)ではなく、あくまで「誤った解析による混乱を避けるための暫定的なエラーハンドリング」です。コミットメッセージにも「This shouldn't be an error (see issue 3999), but until it's handled correctly, treat it as one to avoid confusion.」と明記されており、Issue 3999が解決されればこのエラーチェックは不要になることが示唆されています。

この変更により、開発者は (A).X のような構文を使用した場合に、誤った結果が得られるのではなく、「cannot evaluate field of parenthesized expression」という明確なエラーメッセージを受け取ることができるようになり、問題の特定とデバッグが容易になりました。

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

このコミットでは、以下の2つのファイルが変更されています。

  1. src/pkg/text/template/parse/lex.go
  2. src/pkg/text/template/parse/parse_test.go

src/pkg/text/template/parse/lex.go の変更

このファイルは、text/template パッケージの字句解析器(lexer)を定義しています。変更は lexInsideAction 関数内で行われています。

--- a/src/pkg/text/template/parse/lex.go
+++ b/src/pkg/text/template/parse/lex.go
@@ -354,6 +354,12 @@ func lexInsideAction(l *lexer) stateFn {
 		if l.parenDepth < 0 {
 			return l.errorf("unexpected right paren %#U", r)
 		}
+		// Catch the mistake of (a).X, which will parse as two args.
+		// See issue 3999. TODO: Remove once arg parsing is
+		// better defined.
+		if l.peek() == '.' {
+			return l.errorf("cannot evaluate field of parenthesized expression")
+		}
 		return lexInsideAction
 	case r <= unicode.MaxASCII && unicode.IsPrint(r):
 		l.emit(itemChar)

src/pkg/text/template/parse/parse_test.go の変更

このファイルは、text/template パッケージの構文解析器のテストを定義しています。新しいテストケースが追加されています。

--- a/src/pkg/text/template/parse/parse_test.go
+++ b/src/pkg/text/template/parse/parse_test.go
@@ -232,6 +232,9 @@ var parseTests = []parseTest{\n 	{"invalid punctuation", "{{printf 3, 4}}", hasError, ""},\n 	{"multidecl outside range", "{{with $v, $u := 3}}{{end}}", hasError, ""},\n 	{"too many decls in range", "{{range $u, $v, $w := 3}}{{end}}", hasError, ""},\n+\t// This one should work but doesn't. Caught as a parse error to avoid confusion.\n+\t// TODO: Update after issue 3999 is resolved.\n+\t{\"dot applied to parentheses\", "{{printf (printf .).}}", hasError, ""},\n 	// Equals (and other chars) do not assignments make (yet).\n 	{\"bug0a\", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"},\n 	{\"bug0b\", "{{$x = 1}}{{$x}}", hasError, ""},\

コアとなるコードの解説

src/pkg/text/template/parse/lex.go の変更点

lexInsideAction 関数は、テンプレートのアクションブロック({{...}} の内部)を字句解析するステート関数です。この関数は、様々な文字や記号を読み込み、対応するトークンを生成します。

変更が加えられたのは、r == ')'、つまり閉じ括弧 ) が検出された後の処理です。

  1. if l.parenDepth < 0: これは、括弧の対応が取れていない(閉じ括弧が多すぎる)場合にエラーを報告する既存のチェックです。
  2. 新しい追加コード:
    // Catch the mistake of (a).X, which will parse as two args.
    // See issue 3999. TODO: Remove once arg parsing is
    // better defined.
    if l.peek() == '.' {
        return l.errorf("cannot evaluate field of parenthesized expression")
    }
    
    • l.peek(): これは、現在の読み取り位置から次の文字を「覗き見」する(読み取り位置を進めずに確認する)メソッドです。
    • l.peek() == '.': 閉じ括弧 ) の直後にドット . が続くかどうかをチェックしています。
    • この条件が真の場合、つまり (A).X のようなパターンが検出された場合、l.errorf("cannot evaluate field of parenthesized expression") を呼び出してエラーを発生させます。このエラーメッセージは、「括弧で囲まれた式のフィールドを評価できません」という意味です。
    • コメントには、このチェックがIssue 3999に関連しており、引数解析が改善されたら削除されるべき「TODO」としてマークされています。これは、この変更が一時的な回避策であることを明確に示しています。

この変更により、字句解析の段階で (A).X のような構文が検出されると、パーサーに進む前にエラーとして処理されるようになり、誤った解析を防ぎます。

src/pkg/text/template/parse/parse_test.go の変更点

parseTests 変数は、様々なテンプレート文字列とその期待される解析結果(エラーの有無など)を定義したテストケースのスライスです。

追加されたテストケースは以下の通りです。

// This one should work but doesn't. Caught as a parse error to avoid confusion.
// TODO: Update after issue 3999 is resolved.
{"dot applied to parentheses", "{{printf (printf .).}}", hasError, ""},
  • "dot applied to parentheses": テストケースの名前です。
  • "{{printf (printf .).}}": テスト対象のテンプレート文字列です。これは、(A).X の典型的な例であり、A の部分が printf . という別の printf 呼び出しになっています。
  • hasError: このテンプレートが解析エラーを発生させることを期待していることを示します。
  • "": 期待されるエラーメッセージですが、ここでは空文字列になっています。これは、エラーメッセージの内容自体を厳密にチェックするのではなく、エラーが発生することのみを確認していることを意味します。

このテストケースの追加により、(A).X のような構文が実際にエラーとして扱われることを保証し、将来的にパーサーが修正された際には、このテストケースが noError に変更されることで、修正が正しく行われたことを確認できるようになります。

関連リンク

参考にした情報源リンク

  • Web search results for "Go issue 3999"
  • Go言語の text/template パッケージのドキュメント (一般的な情報源として)
  • Go言語の字句解析器と構文解析器に関する一般的な知識 (一般的な情報源として)