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

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

このコミットは、Go言語のgofixツールに新しいルールを追加するものです。具体的には、text/templateおよびhtml/templateパッケージにおけるAPIの変更に対応するため、template.ParseFileの呼び出しをtemplate.ParseFilesに自動的に書き換える機能と、非推奨となったtemplate.Set型およびその関連メソッドの使用に対して警告を発する機能が導入されています。これにより、古いAPIを使用しているGoプログラムを新しいAPIに適合させ、将来の互換性の問題を回避することを目的としています。

コミット

  • コミットハッシュ: da62104169c7b31f8b2917b24232dd349b769c8f
  • 作者: Rob Pike r@golang.org
  • コミット日時: 2011年11月23日(水) 20:17:41 -0800

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

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

元コミット内容

gofix: trivial rewrite for template.ParseFiles
Also warn about uses of Set.

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

変更の背景

この変更の背景には、Go言語の標準ライブラリであるtext/templateおよびhtml/templateパッケージのAPI進化があります。初期のGo言語開発段階では、APIが頻繁に変更されることがありました。特に、テンプレートファイルのパースに関する関数は、単一ファイルをパースするParseFileから、複数のファイルを一度にパースできるParseFilesへと変更されました。これは、複数のテンプレートファイルを扱う際の利便性と効率性を向上させるための変更です。

また、template.Set型は、テンプレートのセットを管理するためのものでしたが、その後のAPI設計の見直しにより、template.Template型自体が複数のテンプレートを管理する機能を内包するようになり、template.Setは冗長または非推奨となりました。

gofixツールは、このようなGo言語のAPI変更や慣習の進化に対応するために開発された自動コード修正ツールです。APIの変更に伴い、既存のコードベースを新しいAPIに手動で移行するのは手間がかかり、エラーの元となる可能性があります。gofixは、このような移行作業を自動化し、開発者がGo言語の進化に追従しやすくすることを目的としています。

このコミットは、上記のtemplateパッケージのAPI変更(ParseFileからParseFilesへの変更、およびtemplate.Setの非推奨化)に対応するために、gofixに新しい修正ルールを追加するものです。これにより、ユーザーはgofixを実行するだけで、古いtemplateパッケージのAPIを使用しているコードを自動的に更新できるようになります。

前提知識の解説

gofixツール

gofixは、Go言語のソースコードを自動的に修正するためのコマンドラインツールです。Go言語のバージョンアップに伴うAPIの変更や、言語仕様の変更、あるいは推奨されるコーディングスタイルへの準拠など、様々な理由で既存のコードベースを更新する必要がある場合に利用されます。gofixは、Goの抽象構文木(AST: Abstract Syntax Tree)を解析し、定義されたルールに基づいてコードを変換します。これにより、手動での修正に比べて、はるかに効率的かつ正確にコードの移行を行うことができます。

text/templateおよびhtml/templateパッケージ

これらはGo言語の標準ライブラリで提供されるテンプレートエンジンです。

  • text/template: テキストベースの出力を生成するための汎用テンプレートエンジンです。
  • html/template: text/templateをベースにしており、HTML出力に特化しています。クロスサイトスクリプティング(XSS)攻撃を防ぐための自動エスケープ機能など、Webアプリケーション開発に不可欠なセキュリティ機能を提供します。

これらのパッケージは、Webアプリケーションのビュー層や、設定ファイルの生成、コード生成など、様々な場面で利用されます。

抽象構文木 (AST: Abstract Syntax Tree)

ASTは、プログラミング言語のソースコードを抽象的な構文構造で表現したツリー構造のデータです。コンパイラやリンター、コード分析ツール、そしてgofixのようなコード変換ツールは、ソースコードを直接操作するのではなく、一度ASTに変換してから処理を行います。ASTは、コードの構造を明確に表現するため、プログラムの意味を理解し、変更を加えるのに適しています。

go/astパッケージ

Go言語の標準ライブラリの一部であり、GoのソースコードのASTを表現するための型と関数を提供します。gofixのようなツールは、このパッケージを利用してGoのソースコードを解析し、ASTを構築し、そのASTを操作することでコードの変換を行います。

技術的詳細

このコミットで追加されたgofixの修正は、主にsrc/cmd/gofix/template.goに実装されています。

  1. template.ParseFileからtemplate.ParseFilesへの書き換え:

    • gofixは、GoのソースファイルをASTとして読み込みます。
    • ASTを走査し、template.ParseFileという関数呼び出し、または*template.Template型のレシーバーを持つParseFileメソッドの呼び出しを特定します。
    • これらの識別子(ParseFile)をParseFilesに書き換えます。
    • この変更は、template.ParseFileが単一のファイルパスを引数にとるのに対し、template.ParseFilesが可変長引数で複数のファイルパスを受け取るように変更されたことに対応しています。gofixは、引数の数や型を変更するわけではなく、単にメソッド/関数名を変更するだけです。これは、ParseFileParseFilesの単一ファイル版として機能するため、名前の変更だけで互換性が保たれるケースを想定しています。
  2. template.Setの使用に対する警告:

    • gofixは、template.Set型、またはその関連するグローバル関数(ParseSetFiles, ParseSetGlob, ParseTemplateFiles, ParseTemplateGlob, Set, SetMust)やメソッドの使用を検出します。
    • これらの使用箇所が見つかった場合、gofixは警告メッセージを出力し、手動での修正が必要であることをユーザーに通知します。これは、template.Setが非推奨となり、その機能がtemplate.Templateに統合されたため、単純な自動修正では対応できない複雑な変更が必要となる可能性があるためです。警告は、コードのどの位置で問題が検出されたかを示す行番号情報を含みます。

template.goのコード構造

  • init()関数でtemplateFixgofixのレジストリに登録します。
  • templateFix構造体は、修正の名前、適用日、修正関数(template)、および説明を含みます。
  • templateSetGlobalstemplateSetMethodsは、template.Setに関連するグローバル関数名とメソッド名を定義した文字列スライスです。これらは警告を生成する際に使用されます。
  • templateTypeConfigは、go/typesパッケージのような型チェックを行うための設定です。template.Templatetemplate.Setのメソッドシグネチャを定義しており、gofixがASTを走査する際に、セレクタ式(sel.X.sel.Nameのような形式)の型を特定するために利用されます。
  • template(f *ast.File) bool関数が実際の修正ロジックを実装しています。
    • まず、ファイルがtext/templateまたはhtml/templateパッケージをインポートしているかを確認します。インポートしていない場合は修正対象外です。
    • typecheck関数(gofixの内部関数)を使用して、AST内の各ノードの型情報を取得します。
    • walk関数(gofixの内部関数)を使用してASTを再帰的に走査します。
    • 走査中にast.SelectorExpr(例: template.ParseFilet.ParseFile)が見つかった場合、以下のチェックを行います。
      • template.ParseFileというグローバル関数呼び出しの場合、ParseFileParseFilesに書き換えます。
      • *template.Template型のレシーバーを持つParseFileメソッド呼び出しの場合、ParseFileParseFilesに書き換えます。
      • templateSetGlobalsに含まれるグローバル関数(例: template.Set)が呼び出されている場合、警告を発します。
      • *template.Set型のレシーバーを持つtemplateSetMethodsに含まれるメソッドが呼び出されている場合、警告を発します。

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

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

  1. src/cmd/gofix/Makefile:

    --- a/src/cmd/gofix/Makefile
    +++ b/src/cmd/gofix/Makefile
    @@ -31,6 +31,7 @@ GOFILES=\
     	sorthelpers.go\
     	sortslice.go\
     	stringssplit.go\
    +\ttemplate.go\
     	typecheck.go\
     	url.go\
    

    gofixのビルドプロセスにtemplate.goを追加しています。

  2. src/cmd/gofix/template.go: (新規ファイル) このファイル全体が新規追加されており、template.ParseFileからtemplate.ParseFilesへの書き換えロジックと、template.Setの使用に対する警告ロジックが含まれています。

  3. src/cmd/gofix/template_test.go: (新規ファイル) このファイル全体が新規追加されており、template.goで実装された修正ロジックのテストケースが含まれています。template.ParseFiletemplate.ParseFilesに書き換えられること、およびtemplate.Set関連の呼び出しが警告の対象となることを検証しています。

コアとなるコードの解説

src/cmd/gofix/template.go

// Copyright 2011 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"go/ast"
)

func init() {
	register(templateFix)
}

var templateFix = fix{
	"template",
	"2011-11-22",
	template,
	`Rewrite calls to template.ParseFile to template.ParseFiles

http://codereview.appspot.com/5433048
`,
}

var templateSetGlobals = []string{
	"ParseSetFiles",
	"ParseSetGlob",
	"ParseTemplateFiles",
	"ParseTemplateGlob",
	"Set",
	"SetMust",
}

var templateSetMethods = []string{
	"ParseSetFiles",
	"ParseSetGlob",
	"ParseTemplateFiles",
	"ParseTemplateGlob",
}

var templateTypeConfig = &TypeConfig{
	Type: map[string]*Type{
		"template.Template": &Type{
			Method: map[string]string{
				"Funcs":      "func() *template.Template",
				"Delims":     "func() *template.Template",
				"Parse":      "func() (*template.Template, error)",
				"ParseFile":  "func() (*template.Template, error)",
				"ParseInSet": "func() (*template.Template, error)",
			},
		},
		"template.Set": &Type{
			Method: map[string]string{
				"ParseSetFiles":      "func() (*template.Set, error)",
				"ParseSetGlob":       "func() (*template.Set, error)",
				"ParseTemplateFiles": "func() (*template.Set, error)",
				"ParseTemplateGlob":  "func() (*template.Set, error)",
			},
		},
	},

	Func: map[string]string{
		"template.New":     "*template.Template",
		"template.Must":    "(*template.Template, error)",
		"template.SetMust": "(*template.Set, error)",
	},
}

func template(f *ast.File) bool {
	// text/template または html/template をインポートしているか確認
	if !imports(f, "text/template") && !imports(f, "html/template") {
		return false
	}

	fixed := false

	// 型情報を取得
	typeof, _ := typecheck(templateTypeConfig, f)

	// ASTを走査して名前を更新
	walk(f, func(n interface{}) {
		if sel, ok := n.(*ast.SelectorExpr); ok {
			// トップレベル関数 template.ParseFile の参照
			if isPkgDot(sel, "template", "ParseFile") {
				sel.Sel.Name = "ParseFiles" // ParseFile を ParseFiles に書き換え
				fixed = true
				return
			}
			// ParseFile メソッドの参照 (template.Template 型のレシーバー)
			if typeof[sel.X] == "*template.Template" && sel.Sel.Name == "ParseFile" {
				sel.Sel.Name = "ParseFiles" // ParseFile を ParseFiles に書き換え
				fixed = true
				return
			}
			// Set 型とその関数は現在廃止されているため警告
			for _, name := range templateSetGlobals {
				if isPkgDot(sel, "template", name) {
					warn(sel.Pos(), "reference to template.%s must be fixed manually", name)
					return
				}
			}
			// Set のメソッドも現在廃止されているため警告
			for _, name := range templateSetMethods {
				if typeof[sel.X] == "*template.Set" && sel.Sel.Name == name {
					warn(sel.Pos(), "reference to template.*Set.%s must be fixed manually", name)
					return
				}
			}
		}
	})

	return fixed
}
  • template(f *ast.File) bool関数が、このgofixルールの主要なロジックを含んでいます。
  • imports(f, "text/template")imports(f, "html/template")は、現在のファイルがtext/templateまたはhtml/templateパッケージをインポートしているかどうかをチェックするgofixのヘルパー関数です。これにより、関連のないファイルでの処理をスキップし、パフォーマンスを向上させます。
  • typecheck(templateTypeConfig, f)は、templateTypeConfigで定義された型情報に基づいて、AST内の各式の型を推論します。これにより、例えばsel.X*template.Template型であるかどうかを正確に判断できます。
  • walk(f, func(n interface{}) { ... })は、ASTを深さ優先で走査するためのgofixのヘルパー関数です。匿名関数内で、各ASTノードnが処理されます。
  • if sel, ok := n.(*ast.SelectorExpr); okは、現在のノードがast.SelectorExpr(例: pkg.Nameexpr.Methodのような形式)であるかをチェックします。
  • isPkgDot(sel, "template", "ParseFile")は、セレクタ式がtemplate.ParseFileのような形式であるかをチェックするgofixのヘルパー関数です。これが真の場合、sel.Sel.Name = "ParseFiles"によってParseFileParseFilesに書き換えられます。
  • typeof[sel.X] == "*template.Template" && sel.Sel.Name == "ParseFile"は、セレクタ式のレシーバー(sel.X)が*template.Template型であり、かつ選択された名前がParseFileであるかをチェックします。これも真の場合、ParseFileParseFilesに書き換えられます。
  • templateSetGlobalstemplateSetMethodsのループでは、template.Setに関連するグローバル関数やメソッドの使用を検出し、warn()関数を使って警告メッセージを出力します。warn()は、指定された位置(sel.Pos())に警告メッセージを表示するgofixのヘルパー関数です。

src/cmd/gofix/template_test.go

// Copyright 2011 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

func init() {
	addTestCases(templateTests, template)
}

var templateTests = []testCase{
	{
		Name: "template.0",
		In: `package main

import (
	"text/template"
)

func f() {
	template.ParseFile(a)
	var t template.Template
	x, y := template.ParseFile()
	template.New("x").Funcs(m).ParseFile(a) // chained method
	// Output should complain about these as functions or methods.
	var s *template.Set
	s.ParseSetFiles(a)
	template.ParseSetGlob(a)
	s.ParseTemplateFiles(a)
	template.ParseTemplateGlob(a)
	x := template.SetMust(a())
}
`,
		Out: `package main

import (
	"text/template"
)

func f() {
	template.ParseFiles(a)
	var t template.Template
	x, y := template.ParseFiles()
	template.New("x").Funcs(m).ParseFiles(a) // chained method
	// Output should complain about these as functions or methods.
	var s *template.Set
	s.ParseSetFiles(a)
	template.ParseSetGlob(a)
	s.ParseTemplateFiles(a)
	template.ParseTemplateGlob(a)
	x := template.SetMust(a())
}
`,
	},
}
  • addTestCases(templateTests, template)は、templateTestsで定義されたテストケースを、template関数(修正ロジック)に対して実行するように登録します。
  • templateTeststestCase型のスライスで、各要素が1つのテストケースを表します。
  • Inフィールドは修正前の入力コード、Outフィールドはgofixによる修正後の期待される出力コードです。
  • このテストケースでは、template.ParseFile(a)template.ParseFiles(a)に、x, y := template.ParseFile()x, y := template.ParseFiles()に、そしてチェーンされたメソッド呼び出しtemplate.New("x").Funcs(m).ParseFile(a)template.New("x").Funcs(m).ParseFiles(a)にそれぞれ書き換えられることを確認しています。
  • また、template.Setに関連する呼び出し(s.ParseSetFiles(a)など)はOutコードでは変更されていませんが、gofixの実行時にはこれらの行に対して警告が出力されることが期待されます。これは、gofixのテストフレームワークが、コードの変更だけでなく、警告メッセージの出力も検証する能力を持っていることを示唆しています。

関連リンク

参考にした情報源リンク

  • Go言語のgofixツールに関する一般的な情報:
  • Go言語のtext/templateおよびhtml/templateパッケージに関する情報:
  • Go言語のASTに関する情報:
  • Go言語の歴史的なAPI変更に関する情報 (一般的な知識として):
    • Go言語のリリースノートや、Goの公式ブログの過去記事などが参考になりますが、特定のParseFileからParseFilesへの変更に関する詳細なブログ記事は特定できませんでした。これは、Goの初期開発段階でのAPI変更はドキュメント化が追いつかない場合があったためと考えられます。