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

[インデックス 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)を表すノード。

技術的詳細

このコミットの主要な技術的変更点は以下の通りです。

  1. ChainNode の導入:

    • src/pkg/text/template/parse/node.go に新しいノードタイプ ChainNode が追加されました。
    • ChainNode は、あるノード(Node フィールド)に続く一連のフィールドアクセス(Field スライス)を表現します。これにより、(.Y .Z).Field のような「括弧で囲まれた式の結果に対するフィールドアクセス」をAST上で明確に表現できるようになります。
    • Add メソッドは、チェーンに新しいフィールドを追加するために使用されます。
    • String メソッドは、ChainNode を文字列として表現する際に、括弧付きのノードを適切に表示するように変更されています。
  2. 字句解析器(Lexer)の変更:

    • src/pkg/text/template/parse/lex.go において、フィールドアクセスと変数の字句解析ロジックが変更されました。
    • 以前は .X.Y のような連鎖的なフィールドアクセスが itemField として単一のトークンとして扱われていましたが、この変更により、.X.Y がそれぞれ独立した itemField トークンとして生成されるようになりました。これは lexFieldOrVariable 関数によって実現されています。
    • atTerminator 関数が変更され、. もトークンの終端文字として認識されるようになりました。これにより、.X.Y.X.Y に分割されるようになります。
    • itemFielditemIdentifier の定義がより明確になりました。itemField. で始まる英数字識別子、itemIdentifier. で始まらない英数字識別子を指すようになりました。
    • lexIdentifierlexFieldlexVariable に分割され、それぞれの字句解析ロジックがより専門化されました。
  3. 構文解析器(Parser)の変更:

    • src/pkg/text/template/parse/parse.go において、command および operand の解析ロジックが大幅にリファクタリングされました。
    • operand 関数が新しく導入され、これはリテラル、関数、..Field$、そして (' pipeline ')' のような「項(term)」を解析し、それに続く連鎖的なフィールドアクセスを ChainNode として構築する役割を担います。
    • command 関数は、operand 関数を使用してコマンドの引数を解析するように簡素化されました。
    • pipeline 関数は、括弧で囲まれたパイプライン(itemLeftParen)をコマンドの最初の要素として受け入れるように変更されました。
    • NodeField または NodeVariable の場合、互換性のために ChainNode を使用せずに既存のノードにフィールドを追加するロジックが operand 関数内に残されています(TODOコメントで将来的に削除される可能性が示唆されています)。
  4. 実行エンジン(Executor)の変更:

    • src/pkg/text/template/exec.go において、evalCommand 関数が parse.ChainNodeparse.PipeNode を処理するように拡張されました。
    • evalChainNode 関数が新しく追加され、ChainNode を評価し、その基になるパイプラインを評価した後に、連鎖するフィールドアクセスを処理します。
    • evalPipelineparse.PipeNode を処理する際に呼び出されるようになりました。
    • validateType 関数で、typ == nil のチェックが追加され、より堅牢な型検証が行われるようになりました。

これらの変更により、text/template{{(.Y .Z).Field}} のような構文を正しく解析し、実行できるようになりました。

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

このコミットにおける主要な変更ファイルと、その中のコアとなる変更箇所は以下の通りです。

  • src/pkg/text/template/parse/node.go:

    • ChainNode 構造体とその関連メソッド(newChain, Add, String, Copy)の追加。
    • NodeTypeNodeChain の追加。
    • ActionNode のコメントが更新され、括弧で囲まれたパイプラインも含むことが示唆されています。
    • VariableNode のコメントが更新され、連鎖的なフィールドアクセスも含むことが示唆されています。
  • src/pkg/text/template/parse/lex.go:

    • itemFielditemIdentifier のコメント更新。
    • lexInsideAction.$ の処理が lexFieldlexVariable に分岐するように変更。
    • lexIdentifier が削除され、lexFieldlexVariable が新しく追加。
    • 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}}
    • echomakemap というヘルパー関数がテストのために追加されています。
  • 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 を構築します。ここで、互換性のために NodeFieldNodeVariable の場合は 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テンプレートはより複雑な式構造を解析し、実行できるようになり、テンプレートの表現力が大幅に向上しました。

関連リンク

参考にした情報源リンク