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

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

このコミットは、Go言語の標準ライブラリである text/template パッケージ内の parse サブパッケージに関連するものです。text/template パッケージは、Goプログラム内でテキストベースのテンプレートを解析し、実行するための機能を提供します。ウェブアプリケーションのHTML生成や、設定ファイルの動的な生成など、様々な用途で利用されます。

parse サブパッケージは、テンプレート文字列をトークンに分解する「字句解析(lexing)」と、そのトークン列から構文木を構築する「構文解析(parsing)」を担当します。このコミットは、特に字句解析器(lexer)におけるコメントの処理に関するバグ修正に焦点を当てています。

コミット

このコミットは、text/template/parse パッケージにおけるコメントの誤った構文(具体的には /*/)が誤って受け入れられてしまうバグを修正します。これにより、テンプレートの字句解析器が不正なコメント形式を正しくエラーとして認識するようになります。

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

https://github.com/golang/go/commit/2253f671577c5302096f679d9dfe80218fdff99d

元コミット内容

commit 2253f671577c5302096f679d9dfe80218fdff99d
Author: Rob Pike <r@golang.org>
Date:   Thu Aug 9 19:24:46 2012 -0700

    text/template/parse: fix bug handling /*/
    Incorrect syntax for comment was erroneously accepted.
    Fixes #3919.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6453105

変更の背景

text/template パッケージでは、テンプレート内でコメントを記述するために {{/* ... */}} のような構文を使用します。このコミットが修正するバグは、字句解析器が {{/*/}} のような不正なコメント構文を誤って有効なコメントとして扱ってしまうというものでした。

本来、コメントは {{/* で始まり */}} で閉じられる必要があります。しかし、バグのある状態では、{{/*/}} のように /* の直後に */ が続く形式でも、字句解析器が /* をコメント開始と認識し、その後の */ をコメント終了と認識してしまい、結果として空のコメントとして処理されていました。これは、テンプレートの構文規則に違反しており、開発者が意図しない動作を引き起こす可能性がありました。

このバグは、GoのIssue #3919として報告されており、このコミットはその問題を解決するために作成されました。

前提知識の解説

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

text/template パッケージは、Go言語でテキストベースの出力を生成するための強力なツールです。主な機能は以下の通りです。

  • テンプレートの定義: プレースホルダーや制御構造(条件分岐、ループなど)を含むテキストテンプレートを定義できます。
  • データの結合: Goのデータ構造(構造体、マップ、スライスなど)をテンプレートに渡し、プレースホルダーを実際のデータで置き換えることができます。
  • 関数: テンプレート内で利用できる組み込み関数やカスタム関数を定義できます。
  • 字句解析と構文解析: テンプレート文字列を解析し、実行可能な内部表現に変換します。

字句解析器 (Lexer) と構文解析器 (Parser)

コンパイラやインタプリタの基本的な構成要素として、字句解析器と構文解析器があります。

  • 字句解析器 (Lexer/Scanner/Tokenizer): 入力文字列(この場合はテンプレート文字列)を読み込み、意味のある最小単位である「トークン(token)」のストリームに分解します。例えば、{{ は左デリミタ、/* はコメント開始、}} は右デリミタといった具合に認識します。
  • 構文解析器 (Parser): 字句解析器が生成したトークンのストリームを受け取り、そのトークン列が言語の文法規則に合致しているかを検証し、通常は「抽象構文木(Abstract Syntax Tree: AST)」と呼ばれるツリー構造を構築します。このASTが、その後のコード生成や解釈の基盤となります。

ステート関数 (State Function) パターン

Goの字句解析器の実装では、しばしば「ステート関数」パターンが用いられます。これは、字句解析器の状態を関数として表現し、各関数が次の状態(次の字句解析を行う関数)を返すことで、複雑な字句解析ロジックを簡潔かつ読みやすく記述する手法です。

lex.go ファイルでは、stateFn 型が定義されており、lexText, lexLeftDelim, lexComment などがそれぞれの状態に対応する関数として実装されています。

技術的詳細

このバグは、text/template/parse パッケージの字句解析器 (lex.go) におけるコメントの処理ロジックに起因していました。具体的には、lexLeftDelim 関数と lexComment 関数の連携に問題がありました。

  1. lexLeftDelim 関数: この関数は、テンプレートのアクション開始デリミタ(デフォルトでは {{)を検出した後に呼び出されます。この関数内で、コメント開始デリミタ /* が続くかどうかをチェックしていました。

    • 変更前: l.input[l.pos:] (現在の位置から入力文字列の残りの部分)に対して l.leftDelim+leftComment (例: {{/*)というプレフィックスがあるかをチェックしていました。このチェックが成功すると、lexComment 関数に処理を移していました。
    • 問題点: l.pos はまだ l.leftDelim の直前を指しているため、l.leftDelim の長さを考慮せずに strings.HasPrefix を呼び出すと、{{ の部分もチェックに含まれてしまいます。しかし、strings.HasPrefix はあくまでプレフィックスのチェックなので、{{/* があれば true を返します。問題は、l.pos を進める処理が if 文の外にあったため、lexComment に移る前に l.leftDelim の分だけ l.pos が進んでいなかったことです。
  2. lexComment 関数: この関数は、コメントの本体を解析し、コメント終了デリミタ */ を探します。

    • 変更前: lexLeftDelim から呼び出された際、l.pos{{ の直後ではなく、{{ の開始位置を指したままになっている可能性がありました。これにより、l.input[l.pos:] の先頭が /* ではなく {{/* となってしまい、strings.Index(l.input[l.pos:], rightComment+l.rightDelim) (例: */}})の検索が正しく行われない可能性がありました。

この組み合わせにより、{{/*/}} のような入力に対して、lexLeftDelim{{ を処理した後、/* をコメント開始と誤認識し、lexComment に処理を渡していました。lexComment*/}} を探しますが、/* の直後に */ があるため、これをコメント終了と誤認識し、結果として不正な構文がエラーにならずに処理されてしまっていました。

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

src/pkg/text/template/parse/lex.golexLeftDelim 関数と lexComment 関数に修正が加えられました。

--- a/src/pkg/text/template/parse/lex.go
+++ b/src/pkg/text/template/parse/lex.go
@@ -264,16 +264,17 @@ func lexText(l *lexer) stateFn {
 
 // lexLeftDelim scans the left delimiter, which is known to be present.
 func lexLeftDelim(l *lexer) stateFn {
-	if strings.HasPrefix(l.input[l.pos:], l.leftDelim+leftComment) {
+	l.pos += len(l.leftDelim)
+	if strings.HasPrefix(l.input[l.pos:], leftComment) {
 		return lexComment
 	}
-	l.pos += len(l.leftDelim)
 	l.emit(itemLeftDelim)
 	return lexInsideAction
 }
 
 // lexComment scans a comment. The left comment marker is known to be present.
 func lexComment(l *lexer) stateFn {
+	l.pos += len(leftComment)
 	i := strings.Index(l.input[l.pos:], rightComment+l.rightDelim)
 	if i < 0 {
 		return l.errorf("unclosed comment")

また、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
@@ -203,6 +203,10 @@ var lexTests = []lexTest{\n 		tRight,\n 		tEOF,\n 	}},\n+\t{\"text with bad comment\", \"hello-{{/*/}}-world\", []item{\n+\t\t{itemText, 0, \"hello-\"},\n+\t\t{itemError, 0, `unclosed comment`},\n+\t}},\n }\n```

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

### `lexLeftDelim` 関数の変更

*   **変更前**:
    ```go
    if strings.HasPrefix(l.input[l.pos:], l.leftDelim+leftComment) {
        return lexComment
    }
    l.pos += len(l.leftDelim) // ここでデリミタの長さを進める
    ```
*   **変更後**:
    ```go
    l.pos += len(l.leftDelim) // まずデリミタの長さを進める
    if strings.HasPrefix(l.input[l.pos:], leftComment) {
        return lexComment
    }
    ```
この変更のポイントは、`l.pos += len(l.leftDelim)` の行が `if` 文の**前に移動**したことです。これにより、`lexLeftDelim` が呼び出された時点で `l.pos` は既に左デリミタ(`{{`)の直後を指すようになります。その上で、`l.input[l.pos:]` に対して `leftComment`(`/*`)が続くかをチェックすることで、より正確にコメントの開始を判断できるようになりました。

### `lexComment` 関数の変更

*   **変更前**: (変更なし)
*   **変更後**:
    ```go
    func lexComment(l *lexer) stateFn {
        l.pos += len(leftComment) // コメント開始マーカーの長さを進める
        i := strings.Index(l.input[l.pos:], rightComment+l.rightDelim)
        // ...
    }
    ```
`lexComment` 関数が呼び出された時点で、`l.pos` は既に左デリミタの直後を指しています。この変更により、`lexComment` の冒頭で `l.pos += len(leftComment)` を実行することで、`l.pos` がコメント開始マーカー(`/*`)の直後を指すように調整されます。これにより、`strings.Index` でコメント終了マーカー(`*/}}`)を検索する際に、正しい位置から検索が開始されるようになり、`/*/` のような不正な構文が正しく「閉じられていないコメント」として検出されるようになりました。

### テストケースの追加

`lex_test.go` に追加されたテストケース `{"text with bad comment", "hello-{{/*/}}-world", ...}` は、このバグが修正されたことを検証するためのものです。このテストケースでは、`{{/*/}}` という不正なコメント構文を含む文字列が与えられたときに、字句解析器が `unclosed comment` というエラーを正しく発生させることを期待しています。これにより、修正が意図通りに機能していることが確認できます。

これらの変更により、字句解析器はテンプレート内のコメント構文をより厳密にチェックし、不正な `/*/` 形式を正しくエラーとして扱うようになりました。

## 関連リンク

*   Go言語 `text/template` パッケージ公式ドキュメント: [https://pkg.go.dev/text/template](https://pkg.go.dev/text/template)
*   Go言語のIssueトラッカー: [https://github.com/golang/go/issues](https://github.com/golang/go/issues) (Issue #3919は古いGoogle CodeのIssueトラッカーに存在した可能性が高いですが、現在のGitHubリポジトリでも関連する議論が見つかる場合があります。)

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

*   Go言語のソースコード: `src/pkg/text/template/parse/lex.go` および `src/pkg/text/template/parse/lex_test.go`
*   Go言語の字句解析器の実装パターンに関する一般的な知識
*   Go言語のIssue #3919に関する情報(コミットメッセージに記載されている情報に基づく)
*   Go言語のコードレビューシステム (Gerrit) の変更リスト: [https://golang.org/cl/6453105](https://golang.org/cl/6453105) (現在はGitHubに移行しているため、直接アクセスしてもリダイレクトされる可能性があります。)