[インデックス 13915] ファイルの概要
このコミットは、Go言語の text/template パッケージにおけるテンプレート構文の解析器(パーサー)と字句解析器(レキサー)の改善に関するものです。具体的には、括弧で囲まれた式(parenthesized expressions)に対するフィールドアクセス(.Field)を許可し、より柔軟なテンプレート記述を可能にすることを目的としています。
コミット
commit 9050550c12e2d09cf8f0c22a270cfa90120cdf6d
Author: Rob Pike <r@golang.org>
Date: Mon Sep 24 13:23:15 2012 +1000
text/template: allow .Field access to parenthesized expressions
Change the grammar so that field access is a proper operator.
This introduces a new node, ChainNode, into the public (but
actually internal) API of text/template/parse. For
compatibility, we only use the new node type for the specific
construct, which was not parseable before. Therefore this
should be backward-compatible.
Before, .X.Y was a token in the lexer; this CL breaks it out
into .Y applied to .X. But for compatibility we mush them
back together before delivering. One day we might remove
that hack; it's the simple TODO in parse.go/operand.
This change also provides grammatical distinction between
f
and
(f)
which might permit function values later, but not now.
Fixes #3999.
R=golang-dev, dsymonds, gri, rsc, mikesamuel
CC=golang-dev
https://golang.org/cl/6494119
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9050550c12e2d09cf8f0c22a270cfa90120cdf6d
元コミット内容
text/template パッケージにおいて、括弧で囲まれた式に対するフィールドアクセスを許可する。
文法を変更し、フィールドアクセスを適切な演算子として扱うようにする。これにより、text/template/parse の公開(ただし実際には内部的な)APIに新しいノードタイプ ChainNode が導入される。互換性のために、この新しいノードタイプは、以前は解析できなかった特定の構文に対してのみ使用される。したがって、これは後方互換性があるはずである。
以前は、.X.Y はレキサーにおける単一のトークンであった。この変更により、これを .X に適用される .Y として分解する。しかし、互換性のために、最終的に提供する前にこれらを再び結合する。将来的にはこのハックを削除するかもしれない。これは parse.go/operand にある単純なTODOである。
この変更はまた、f と (f) の間に文法的な区別を提供する。これにより、将来的には関数値を許可する可能性があるが、現時点ではそうではない。
Issue #3999 を修正する。
変更の背景
このコミットは、Go言語の text/template パッケージにおけるテンプレートエンジンの柔軟性を向上させることを目的としています。具体的には、GitHub Issue #3999「allow field/method reference on result of niladic function」に関連しています。
以前の text/template の実装では、フィールドアクセス(例: .Field)は、直接的な変数やドット(.)に対してのみ許可されていました。しかし、より複雑な式、特に括弧で囲まれた式の結果に対してフィールドアクセスを行いたいというニーズがありました。例えば、{{(.Y .Z).Field}} のような構文は、以前のパーサーでは正しく解釈できませんでした。
この制限は、テンプレートの表現力を低下させ、開発者がより複雑なデータ構造や関数呼び出しの結果を扱う際に不便をもたらしていました。このコミットは、この制約を取り除き、フィールドアクセスをより汎用的な演算子として扱うことで、テンプレートの記述をより直感的で強力なものにすることを目指しています。
また、.X.Y のような連鎖的なフィールドアクセスがレキサーで単一のトークンとして扱われていたため、パーサーがその内部構造を理解し、括弧付きの式と組み合わせることが困難でした。このコミットは、この連鎖的なフィールドアクセスをレキサーレベルで分解し、パーサーがより細かく制御できるようにすることで、この問題を解決しています。
前提知識の解説
このコミットの理解には、以下の概念に関する基本的な知識が役立ちます。
- テンプレートエンジン: テキストやHTMLなどの出力に、動的なデータを埋め込むためのシステム。Go言語の
text/templateおよびhtml/templateパッケージがこれに該当します。 - 字句解析器(Lexer/Scanner): ソースコードや入力文字列を、意味のある最小単位(トークン)のシーケンスに分解するプログラムのコンポーネント。例えば、
{{.X.Y}}という文字列を、{{、.X.Y、}}といったトークンに分解します。 - 構文解析器(Parser): 字句解析器が生成したトークンのシーケンスを受け取り、その言語の文法規則に従って、プログラムの構造を表現するツリー(抽象構文木、AST)を構築するプログラムのコンポーネント。
- 抽象構文木(Abstract Syntax Tree, AST): ソースコードの抽象的な構文構造を木構造で表現したもの。各ノードは、ソースコード内の構成要素(式、文、宣言など)を表します。
- トークン(Token): 字句解析器によって識別される、意味のある最小単位。例えば、キーワード、識別子、演算子、リテラルなどがあります。
- フィールドアクセス(Field Access): オブジェクトや構造体のメンバー(フィールドやメソッド)にアクセスする操作。Goテンプレートでは、
.FieldNameの形式で表現されます。 - 括弧で囲まれた式(Parenthesized Expressions): 演算の優先順位を明示したり、複雑な式をグループ化したりするために使用される括弧
()で囲まれた式。 - ノード(Node): ASTを構成する基本的な要素。各ノードは、特定の構文要素(例: 変数、関数呼び出し、フィールドアクセス)を表します。
PipeNode: GoテンプレートのASTにおけるパイプライン(|で連結された一連のコマンド)を表すノード。FieldNode: GoテンプレートのASTにおけるフィールドアクセス(例:.Name)を表すノード。VariableNode: GoテンプレートのASTにおける変数(例:$var)を表すノード。
技術的詳細
このコミットの主要な技術的変更点は以下の通りです。
-
ChainNodeの導入:src/pkg/text/template/parse/node.goに新しいノードタイプChainNodeが追加されました。ChainNodeは、あるノード(Nodeフィールド)に続く一連のフィールドアクセス(Fieldスライス)を表現します。これにより、(.Y .Z).Fieldのような「括弧で囲まれた式の結果に対するフィールドアクセス」をAST上で明確に表現できるようになります。Addメソッドは、チェーンに新しいフィールドを追加するために使用されます。Stringメソッドは、ChainNodeを文字列として表現する際に、括弧付きのノードを適切に表示するように変更されています。
-
字句解析器(Lexer)の変更:
src/pkg/text/template/parse/lex.goにおいて、フィールドアクセスと変数の字句解析ロジックが変更されました。- 以前は
.X.Yのような連鎖的なフィールドアクセスがitemFieldとして単一のトークンとして扱われていましたが、この変更により、.Xと.Yがそれぞれ独立したitemFieldトークンとして生成されるようになりました。これはlexFieldOrVariable関数によって実現されています。 atTerminator関数が変更され、.もトークンの終端文字として認識されるようになりました。これにより、.X.Yが.Xと.Yに分割されるようになります。itemFieldとitemIdentifierの定義がより明確になりました。itemFieldは.で始まる英数字識別子、itemIdentifierは.で始まらない英数字識別子を指すようになりました。lexIdentifierがlexFieldとlexVariableに分割され、それぞれの字句解析ロジックがより専門化されました。
-
構文解析器(Parser)の変更:
src/pkg/text/template/parse/parse.goにおいて、commandおよびoperandの解析ロジックが大幅にリファクタリングされました。operand関数が新しく導入され、これはリテラル、関数、.、.Field、$、そして(' pipeline ')'のような「項(term)」を解析し、それに続く連鎖的なフィールドアクセスをChainNodeとして構築する役割を担います。command関数は、operand関数を使用してコマンドの引数を解析するように簡素化されました。pipeline関数は、括弧で囲まれたパイプライン(itemLeftParen)をコマンドの最初の要素として受け入れるように変更されました。NodeFieldまたはNodeVariableの場合、互換性のためにChainNodeを使用せずに既存のノードにフィールドを追加するロジックがoperand関数内に残されています(TODOコメントで将来的に削除される可能性が示唆されています)。
-
実行エンジン(Executor)の変更:
src/pkg/text/template/exec.goにおいて、evalCommand関数がparse.ChainNodeとparse.PipeNodeを処理するように拡張されました。evalChainNode関数が新しく追加され、ChainNodeを評価し、その基になるパイプラインを評価した後に、連鎖するフィールドアクセスを処理します。evalPipelineがparse.PipeNodeを処理する際に呼び出されるようになりました。validateType関数で、typ == nilのチェックが追加され、より堅牢な型検証が行われるようになりました。
これらの変更により、text/template は {{(.Y .Z).Field}} のような構文を正しく解析し、実行できるようになりました。
コアとなるコードの変更箇所
このコミットにおける主要な変更ファイルと、その中のコアとなる変更箇所は以下の通りです。
-
src/pkg/text/template/parse/node.go:ChainNode構造体とその関連メソッド(newChain,Add,String,Copy)の追加。NodeTypeにNodeChainの追加。ActionNodeのコメントが更新され、括弧で囲まれたパイプラインも含むことが示唆されています。VariableNodeのコメントが更新され、連鎖的なフィールドアクセスも含むことが示唆されています。
-
src/pkg/text/template/parse/lex.go:itemFieldとitemIdentifierのコメント更新。lexInsideActionで.と$の処理がlexFieldとlexVariableに分岐するように変更。lexIdentifierが削除され、lexFieldとlexVariableが新しく追加。lexFieldOrVariable関数が追加され、フィールドと変数の共通の字句解析ロジックをカプセル化。atTerminator関数に.が終端文字として追加。
-
src/pkg/text/template/parse/parse.go:pipeline関数で、itemLeftParenがコマンドの開始として許可されるように変更。command関数のロジックが大幅に簡素化され、operand関数に依存するように変更。operand関数が新しく追加され、項(term)とそれに続く連鎖的なフィールドアクセスを解析する主要なロジックを実装。term関数が新しく追加され、リテラル、関数、.、$、フィールド、ブール値、数値、文字列、そして括弧で囲まれたパイプラインを解析。
-
src/pkg/text/template/exec.go:evalCommand関数にcase *parse.ChainNode:とcase *parse.PipeNode:の処理が追加。evalChainNode関数が新しく追加。validateType関数でtyp == nilのチェックが追加。
-
src/pkg/text/template/exec_test.go:- 括弧で囲まれた式とフィールドアクセスを組み合わせた新しいテストケースが追加されています。例:
{{($).X}},{{($.GetU).V}},{{($ | echo).X}}。 echoとmakemapというヘルパー関数がテストのために追加されています。
- 括弧で囲まれた式とフィールドアクセスを組み合わせた新しいテストケースが追加されています。例:
-
src/pkg/text/template/parse/lex_test.go:{{.x . .2 .x.y.z}}のような連鎖的なフィールドアクセスが、個別のitemFieldトークンに分割されることを検証するテストが追加されています。{{($x 23)}}のような変数呼び出しのテストが更新されています。{{(.X).Y}}のような括弧で囲まれた式に対するフィールドアクセスのテストが追加されています。
コアとなるコードの解説
src/pkg/text/template/parse/node.go における ChainNode
// ChainNode holds a term followed by a chain of field accesses (identifier starting with '.').
// The names may be chained ('.x.y').
// The periods are dropped from each ident.
type ChainNode struct {
NodeType
Node Node
Field []string // The identifiers in lexical order.
}
func newChain(node Node) *ChainNode {
return &ChainNode{NodeType: NodeChain, Node: node}
}
// Add adds the named field (which should start with a period) to the end of the chain.
func (c *ChainNode) Add(field string) {
if len(field) == 0 || field[0] != '.' {
panic("no dot in field")
}
field = field[1:] // Remove leading dot.
if field == "" {
panic("empty field")
}
c.Field = append(c.Field, field)
}
func (c *ChainNode) String() string {
s := c.Node.String()
if _, ok := c.Node.(*PipeNode); ok {
s = "(" + s + ")"
}
for _, field := range c.Field {
s += "." + field
}
return s
}
ChainNode は、Node フィールドに基になる式(例えば、パイプラインや変数)を持ち、Field スライスにその式に適用される一連のフィールド名(例: ["Field1", "Field2"])を保持します。これにより、(.Y .Z).Field のような複雑な式を単一のASTノードとして表現できるようになります。Add メソッドは、字句解析器から受け取ったフィールド名(.Field の形式)から先頭の . を取り除いて追加します。String メソッドは、デバッグや表示のためにノードを文字列に変換する際に、基になるノードが PipeNode の場合は括弧で囲んで表示し、その後に連鎖するフィールド名を付加します。
src/pkg/text/template/parse/lex.go における字句解析の変更
// lexField scans a field: .Alphanumeric.
// The . has been scanned.
func lexField(l *lexer) stateFn {
return lexFieldOrVariable(l, itemField)
}
// lexVariable scans a Variable: $Alphanumeric.
// The $ has been scanned.
func lexVariable(l *lexer) stateFn {
if l.atTerminator() { // Nothing interesting follows -> "$".
l.emit(itemVariable)
return lexInsideAction
}
return lexFieldOrVariable(l, itemVariable)
}
// lexVariable scans a field or variable: [.$]Alphanumeric.
// The . or $ has been scanned.
func lexFieldOrVariable(l *lexer, typ itemType) stateFn {
if l.atTerminator() { // Nothing interesting follows -> "." or "$".
if typ == itemVariable {
l.emit(itemVariable)
} else {
l.emit(itemDot)
}
return lexInsideAction
}
var r rune
for {
r = l.next()
if !isAlphaNumeric(r) {
l.backup()
break
}
}
if !l.atTerminator() {
return l.errorf("bad character %#U", r)
}
l.emit(typ)
return lexInsideAction
}
// atTerminator reports whether the input is at valid termination character to
// appear after an identifier. Breaks .X.Y into two pieces. Also catches cases
// like "$x+2" not being acceptable without a space, in case we decide one
// day to implement arithmetic.
func (l *lexer) atTerminator() bool {
r := l.peek()
if isSpace(r) || isEndOfLine(r) {
return true
}
switch r {
case eof, '.', ',', '|', ':', ')', '(': // '.' is new here
return true
}
// ... (rest of the function)
}
lexFieldOrVariable 関数は、. または $ の後に続く英数字を読み取り、itemField または itemVariable トークンとして発行します。重要な変更は atTerminator 関数に . が追加されたことです。これにより、{{.x.y}} のような入力があった場合、以前は単一の itemField トークン .x.y が生成されていましたが、この変更後は .x と .y という2つの独立した itemField トークンが生成されるようになります。これは、パーサーが連鎖的なフィールドアクセスをより細かく制御し、ChainNode を構築するための基盤となります。
src/pkg/text/template/parse/parse.go における構文解析の変更
// command:
// operand (space operand)*
// space-separated arguments up to a pipeline character or right delimiter.
// we consume the pipe character but leave the right delim to terminate the action.
func (t *Tree) command() *CommandNode {
cmd := newCommand()
for {
t.peekNonSpace() // skip leading spaces.
operand := t.operand()
if operand != nil {
cmd.append(operand)
}
switch token := t.next(); token.typ {
case itemSpace:
continue
case itemError:
t.errorf("%s", token.val)
case itemRightDelim, itemRightParen:
t.backup()
case itemPipe:
break
default:
t.errorf("unexpected %s in operand; missing space?", token)
}
break
}
if len(cmd.Args) == 0 {
t.errorf("empty command")
}
return cmd
}
// operand:
// term .Field*
// An operand is a space-separated component of a command,
// a term possibly followed by field accesses.
// A nil return means the next item is not an operand.
func (t *Tree) operand() Node {
node := t.term()
if node == nil {
return nil
}
if t.peek().typ == itemField {
chain := newChain(node)
for t.peek().typ == itemField {
chain.Add(t.next().val)
}
// Compatibility with original API: If the term is of type NodeField
// or NodeVariable, just put more fields on the original.
// Otherwise, keep the Chain node.
// TODO: Switch to Chains always when we can.
switch node.Type() {
case NodeField:
node = newField(chain.String())
case NodeVariable:
node = newVariable(chain.String())
default:
node = chain
}
}
return node
}
// term:
// literal (number, string, nil, boolean)
// function (identifier)
// .
// .Field
// $
// '(' pipeline ')' // This is the key change for parenthesized expressions
// A term is a simple "expression".
// A nil return means the next item is not a term.
func (t *Tree) term() Node {
switch token := t.nextNonSpace(); token.typ {
// ... (other cases)
case itemLeftParen: // New case for parenthesized expressions
pipe := t.pipeline("parenthesized pipeline")
if token := t.next(); token.typ != itemRightParen {
t.errorf("unclosed right paren: unexpected %s", token)
}
return pipe
// ... (other cases)
}
t.backup()
return nil
}
parse.go の変更は、このコミットの核心部分です。
command関数は、コマンドの引数を解析するために新しく導入されたoperand関数を使用するように簡素化されました。operand関数は、まずtermを解析し、その後に続くitemFieldトークンをすべて消費してChainNodeを構築します。ここで、互換性のためにNodeFieldやNodeVariableの場合はChainNodeを直接使用せず、既存のノードにフィールドを追加するロジックが残されています。これは将来的に削除される可能性のある「ハック」とコメントされています。term関数は、itemLeftParen(左括弧)を新しいケースとして追加しました。これにより、(が現れた場合、内部の式をパイプラインとして解析し、対応する)が現れるまで処理を続けます。この変更により、{{(.Y .Z)}}のような括弧で囲まれたパイプラインが単一の「項」として扱われ、その後にフィールドアクセスを適用できるようになります。
src/pkg/text/template/exec.go における実行エンジンの変更
func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final reflect.Value) reflect.Value {
firstWord := cmd.Args[0]
switch n := firstWord.(type) {
case *parse.FieldNode:
return s.evalFieldNode(dot, n, cmd.Args, final)
case *parse.ChainNode: // New case for ChainNode
return s.evalChainNode(dot, n, cmd.Args, final)
case *parse.IdentifierNode:
// Must be a function.
return s.evalFunction(dot, n.Ident, cmd.Args, final)
case *parse.PipeNode: // New case for PipeNode (parenthesized pipeline)
// Parenthesized pipeline. The arguments are all inside the pipeline; final is ignored.
// TODO: is this right?
return s.evalPipeline(dot, n)
// ... (other cases)
}
// ... (rest of the function)
}
func (s *state) evalChainNode(dot reflect.Value, chain *parse.ChainNode, args []parse.Node, final reflect.Value) reflect.Value {
// (pipe).Field1.Field2 has pipe as .Node, fields as .Field. Eval the pipeline, then the fields.
pipe := s.evalArg(dot, nil, chain.Node)
if len(chain.Field) == 0 {
s.errorf("internal error: no fields in evalChainNode")
}
return s.evalFieldChain(dot, pipe, chain.Field, args, final)
}
evalCommand 関数は、コマンドの最初の引数のタイプに基づいて処理を分岐します。このコミットでは、*parse.ChainNode と *parse.PipeNode の新しいケースが追加されました。
*parse.ChainNodeの場合、evalChainNodeが呼び出されます。evalChainNodeは、まずchain.Node(基になる式、例えば括弧で囲まれたパイプライン)を評価し、その結果に対してchain.Fieldに含まれる連鎖的なフィールドアクセスを適用します。これにより、{{(.Y .Z).Field}}のような式が正しく評価されるようになります。*parse.PipeNodeの場合、evalPipelineが呼び出され、括弧で囲まれたパイプラインが評価されます。
これらの変更により、Goテンプレートはより複雑な式構造を解析し、実行できるようになり、テンプレートの表現力が大幅に向上しました。
関連リンク
- Go言語の
text/templateパッケージ公式ドキュメント: https://pkg.go.dev/text/template - Go言語の
html/templateパッケージ公式ドキュメント: https://pkg.go.dev/html/template
参考にした情報源リンク
- GitHub Issue #3999: allow field/method reference on result of niladic function: https://github.com/golang/go/issues/3999
- Go CL 6494119: text/template: allow .Field access to parenthesized expressions: https://golang.org/cl/6494119
- Go言語のソースコード (text/template/parse): https://github.com/golang/go/tree/master/src/text/template/parse
- Go言語のソースコード (text/template): https://github.com/golang/go/tree/master/src/text/template