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

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

このコミットは、Go言語の標準ライブラリである text/template パッケージにおける、テンプレートの字句解析(lexing)に関する改善です。具体的には、テンプレートのプレーンテキスト部分に、対応する左デリミタ(例: {{)なしに右デリミタ(例: }})が出現した場合に、これをエラーとして適切に捕捉するよう修正が加えられました。

コミット

commit caa462137a41f68bbb6d604ab6fa14c3d89fca5b
Author: Rob Pike <r@golang.org>
Date:   Thu Sep 12 13:22:56 2013 +1000

    text/template: catch unmatched right delimiter
    It was simply a missing error case: when scanning plain text
    outside of an action, a right delimiter should be an error.
    
    R=golang-dev, dsymonds
    CC=golang-dev
    https://golang.org/cl/13468045

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

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

元コミット内容

text/template: catch unmatched right delimiter
It was simply a missing error case: when scanning plain text
outside of an action, a right delimiter should be an error.

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

変更の背景

text/template パッケージは、Goアプリケーションで動的なコンテンツを生成するための強力なツールです。テンプレートは、プレーンテキストと「アクション」(Goのコードや制御構造を埋め込む部分)で構成されます。アクションは通常、特定のデリミタ(デフォルトでは {{}})で囲まれています。

このコミットが導入される前は、テンプレートの字句解析器(lexer)がプレーンテキスト部分をスキャンしている際に、対応する左デリミタなしに右デリミタ(例: }})が出現した場合の挙動が未定義、または適切にエラーとして扱われていませんでした。これにより、開発者が意図しないテンプレート文字列を記述した場合に、パーサーが予期せぬ動作をしたり、エラーが報告されずにサイレントに処理されたりする可能性がありました。

この変更の背景にあるのは、テンプレートの堅牢性を高め、開発者に対してより明確なエラーメッセージを提供することです。テンプレートの構文エラーは、可能な限り早い段階(この場合は字句解析の段階)で捕捉されるべきであり、これによりデバッグが容易になります。

前提知識の解説

このコミットを理解するためには、以下の概念を把握しておく必要があります。

  1. テンプレートエンジン: テンプレートエンジンは、テンプレート(静的なテキストと動的なプレースホルダーの組み合わせ)とデータソースを組み合わせて、最終的な出力(HTML、XML、テキストなど)を生成するソフトウェアコンポーネントです。Goの text/template パッケージはその一例です。

  2. 字句解析(Lexical Analysis / Lexing): コンパイラやインタプリタの最初のフェーズであり、入力文字列(この場合はテンプレート文字列)を意味のある最小単位である「トークン」のストリームに分解するプロセスです。このプロセスを担当するプログラムは「字句解析器(lexer)」または「スキャナ」と呼ばれます。例えば、{{.Name}} というテンプレート文字列は、{{(左デリミタ)、.Name(識別子)、}}(右デリミタ)といったトークンに分解されます。

  3. 構文解析(Syntactic Analysis / Parsing): 字句解析によって生成されたトークンのストリームが、言語の文法規則に従っているかを検証し、通常は抽象構文木(AST)を構築するプロセスです。このプロセスを担当するプログラムは「構文解析器(parser)」と呼ばれます。

  4. デリミタ(Delimiters): テンプレート言語において、動的なコードブロックやアクションの開始と終了を示す特殊な文字列です。text/template のデフォルトデリミタは {{}} ですが、これらは Template.Delims メソッドで変更可能です。

  5. 状態関数(State Function)パターン: Go言語で字句解析器を実装する際によく用いられるデザインパターンです。字句解析器の状態を表現する関数(stateFn)が、次の状態関数を返すことで、入力ストリームの異なる部分を処理します。例えば、テキストを解析する状態関数、左デリミタを解析する状態関数などがあります。

このコミットは、字句解析フェーズ、特にプレーンテキストをスキャンする lexText 状態関数における挙動の修正に焦点を当てています。

技術的詳細

このコミットの核心は、src/pkg/text/template/parse/lex.go ファイル内の lexText 関数に新しいエラーチェックを追加した点にあります。

lexText 関数は、テンプレートのプレーンテキスト部分を字句解析する役割を担っています。この関数は、入力ストリームを文字ごとに読み進め、左デリミタ(l.leftDelim)が見つかった場合には lexLeftDelim 状態に遷移します。

追加されたコードは以下の通りです。

		// Check for right after left in case they're the same.
		if strings.HasPrefix(l.input[l.pos:], l.rightDelim) {
			return l.errorf("unmatched right delimiter")
		}

このコードは、lexText 関数がプレーンテキストをスキャンしている最中に、現在の読み取り位置 l.pos から始まる文字列が、右デリミタ l.rightDelim と一致するかどうかを strings.HasPrefix を用いてチェックします。

  • もし一致した場合、それはプレーンテキスト中に対応する左デリミタなしに右デリミタが出現したことを意味します。これはテンプレートの構文として不正な状態です。
  • この不正な状態を検出すると、l.errorf("unmatched right delimiter") を呼び出して、"unmatched right delimiter" というエラーメッセージを伴うエラーを生成し、字句解析プロセスを停止させます。

この変更により、以下のようなテンプレート文字列が与えられた場合に、以前は予期せぬ動作をしたり、エラーが報告されなかったりしたものが、明確なエラーとして捕捉されるようになります。

例: Hello, World! }} (右デリミタが }} の場合)

この修正は、テンプレートの堅牢性を向上させ、開発者がテンプレートの記述ミスを早期に発見できるようにするために非常に重要です。

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

	{"unmatched right delimiter", "hello-{.}}-world", []item{
		{itemError, 0, `unmatched right delimiter`},
	}},

このテストケースは、hello-{.}}-world という文字列が与えられたときに、unmatched right delimiter というエラーが正しく発生することを確認します。ここで {.}} は、左デリミタが {.、右デリミタが }} であると仮定した場合の例です。

src/pkg/text/template/parse/parse_test.go の変更は、既存のテストケース {"definitions and text", ...} から末尾の }} を削除するものです。これは、新しいエラーハンドリングが導入されたことで、このテストケースが意図せずエラーを発生させる可能性があったため、テストの整合性を保つための修正と考えられます。つまり、このテストケースは本来、有効なテンプレート文字列をテストするものであり、新しいエラーチェックによって不正と判断されるような文字列を含んでいてはならない、という意図があったと推測されます。

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

src/pkg/text/template/parse/lex.go

--- a/src/pkg/text/template/parse/lex.go
+++ b/src/pkg/text/template/parse/lex.go
@@ -217,6 +217,10 @@ func lexText(l *lexer) stateFn {
 			}
 			return lexLeftDelim
 		}
+		// Check for right after left in case they're the same.
+		if strings.HasPrefix(l.input[l.pos:], l.rightDelim) {
+			return l.errorf("unmatched right delimiter")
+		}
 		if l.next() == eof {
 			break
 		}

src/pkg/text/template/parse/lex_test.go

--- a/src/pkg/text/template/parse/lex_test.go
+++ b/src/pkg/text/template/parse/lex_test.go
@@ -340,6 +340,9 @@ var lexTests = []lexTest{
 		{itemText, 0, "hello-"},
 		{itemError, 0, `comment ends before closing delimiter`},
 	}},
+	{"unmatched right delimiter", "hello-{.}}-world", []item{
+		{itemError, 0, `unmatched right delimiter`},
+	}},
 }

src/pkg/text/template/parse/parse_test.go

--- a/src/pkg/text/template/parse/parse_test.go
+++ b/src/pkg/text/template/parse/parse_test.go
@@ -312,7 +312,7 @@ var isEmptyTests = []isEmptyTest{\n 	{"spaces only", " \t\n \t\n", true},\n 	{"definition", `{{define "x"}}something{{end}}`, true},\n 	{"definitions and space", "{{define `x`}}something{{end}}\n\n{{define `y`}}something{{end}}\n\n", true},\n-\t{"definitions and text", "{{define `x`}}something{{end}}\nx\n{{define `y`}}something{{end}}\ny\n}}", false},\n+\t{"definitions and text", "{{define `x`}}something{{end}}\nx\n{{define `y`}}something{{end}}\ny\n", false},\n 	{"definition and action", "{{define `x`}}something{{end}}{{if 3}}foo{{end}}", false},\n }\n```

## コアとなるコードの解説

`lex.go` の `lexText` 関数は、テンプレートのプレーンテキスト部分を解析する字句解析器の状態関数です。この関数は、入力文字列を走査し、左デリミタ(`l.leftDelim`)が見つかるまでテキストトークンを生成します。

追加された `if strings.HasPrefix(l.input[l.pos:], l.rightDelim)` の行は、この `lexText` 関数内で、左デリミタが見つかる前に、現在の位置から右デリミタが始まるかどうかをチェックします。

-   `l.input`: テンプレート全体の入力文字列。
-   `l.pos`: 字句解析器が現在読み取っている入力文字列内の位置。
-   `l.rightDelim`: テンプレートの右デリミタ文字列(例: `}}`)。

`strings.HasPrefix` 関数は、`l.input[l.pos:]`(現在の位置から入力文字列の最後まで)が `l.rightDelim` で始まるかどうかを効率的に判定します。もし始まる場合、それはプレーンテキスト中に予期せぬ右デリミタが出現したことを意味します。

この条件が真の場合、`l.errorf("unmatched right delimiter")` が呼び出されます。`l.errorf` は字句解析器のメソッドで、指定されたエラーメッセージを含むエラーを生成し、字句解析プロセスを終了させます。これにより、不正なテンプレートが実行時エラーではなく、解析時エラーとして早期に捕捉されるようになります。

この変更は、テンプレートの字句解析のロバスト性を高め、開発者がテンプレートの構文エラーをより迅速かつ明確に特定できるようにするための重要な改善です。

## 関連リンク

-   Go `text/template` パッケージのドキュメント: [https://pkg.go.dev/text/template](https://pkg.go.dev/text/template)
-   Go `text/template/parse` パッケージのドキュメント: [https://pkg.go.dev/text/template/parse](https://pkg.go.dev/text/template/parse)
-   Go言語における字句解析器の実装パターン(State Function Pattern)に関する一般的な情報源。例えば、Rob Pikeの "Lexical Scanning in Go" など。

## 参考にした情報源リンク

-   Go言語の公式ドキュメント
-   Go言語のソースコード(特に `text/template` パッケージ)
-   コミットメッセージと関連するコードレビュー(`https://golang.org/cl/13468045`)
-   Go言語における字句解析器の実装に関する一般的な知識とパターン。