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

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

このコミットは、Go言語のgo/printerパッケージにおけるバグ修正です。具体的には、ast.FuncType構造体のParamsフィールドがnilである場合にgo/printerがクラッシュする問題を解決します。go/astパッケージの仕様ではFuncType.Paramsnilである可能性が許容されているにもかかわらず、go/printerがそのケースを適切に処理していなかったため、この修正が必要となりました。

コミット

commit a7c74d52dc49a6b6e448e301165a76bf3fb0a3c7
Author: Robert Griesemer <gri@golang.org>
Date:   Thu Jul 26 17:09:11 2012 -0700

    go/printer: don't crash if ast.FuncType.Params is nil
    
    The go/ast comment for FuncType.Params says that the field may be nil.
    Make sure the printer accepts such a value. The go/parser always sets
    the field (to provide parenthesis position information), but a program
    creating a Go AST from scatch may not.
    
    Added corresponding test case.
    
    Fixes #3870.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6448060

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

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

元コミット内容

このコミットは、go/printerパッケージがast.FuncType.Paramsフィールドがnilの場合にクラッシュしないように修正します。go/astのコメントではFuncType.Paramsフィールドがnilである可能性があると明記されており、go/parserは常にこのフィールドを設定するものの(括弧の位置情報を提供するため)、スクラッチからGoのASTを生成するプログラムではnilになる可能性があるため、go/printerがそのような値を許容するように変更されました。関連するテストケースも追加され、Issue #3870を修正します。

変更の背景

この変更の背景には、Go言語の抽象構文木(AST)を扱う際の、go/astパッケージとgo/printerパッケージ間の整合性の問題がありました。

  1. go/astの仕様: go/astパッケージはGoプログラムのASTを定義しており、ast.FuncType構造体には関数のパラメータリストを表すParamsフィールドがあります。このフィールドに関するコメントでは、パラメータがない関数型の場合にParamsnilになる可能性があると記述されていました。
  2. go/parserの挙動: 一方、go/parserパッケージはGoのソースコードを解析してASTを生成しますが、このパーサーはパラメータがない関数であっても、Paramsフィールドをnilではなく、空のast.FieldList(つまり、Listが空のスライス)として設定していました。これは、関数の括弧()の位置情報などを保持するためです。
  3. 問題の発生: go/printerパッケージはASTをGoのソースコードに整形して出力する役割を担っています。しかし、go/parserが生成するASTとは異なり、手動でASTを構築するプログラムや、他のツールが生成したASTでは、ast.FuncType.Paramsが実際にnilになる可能性がありました。go/printerParamsnilであるケースを想定しておらず、その結果、nilポインタ参照によるクラッシュが発生していました。
  4. Issue #3870: この問題はGitHubのGoリポジトリでIssue #3870として報告されました。報告された問題は、「go/printerFuncTypeParamsフィールドがnilの場合にクラッシュする」というものでした。

このコミットは、go/astの仕様に準拠し、go/printernilParamsフィールドを安全に処理できるようにすることで、ASTを扱うツールの堅牢性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語のパッケージと概念に関する知識が必要です。

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

    • ASTは、プログラミング言語のソースコードの抽象的な構文構造を木構造で表現したものです。コンパイラやリンター、コード整形ツールなどがソースコードを解析する際に内部的に使用します。
    • Go言語では、go/astパッケージがGoプログラムのASTを定義しています。
  2. go/astパッケージ:

    • Go言語のソースコードの抽象構文木(AST)を表現するための型と関数を提供します。
    • ast.Node: ASTのすべてのノードが実装するインターフェース。
    • ast.FuncType: 関数型(例: func(int, string) (bool, error))を表す構造体。
      • Params: 関数の引数リストを表す*ast.FieldList型のフィールド。
      • Results: 関数の戻り値リストを表す*ast.FieldList型のフィールド。
    • ast.FieldList: 識別子と型を持つフィールドのリスト(例: 関数の引数や戻り値、構造体のフィールドなど)を表す構造体。
      • List: *ast.Fieldのポインタのスライス。各*ast.Fieldは単一のフィールド(変数名と型)を表します。
  3. go/parserパッケージ:

    • Go言語のソースコードを解析し、go/astパッケージで定義されたASTを生成するパーサーを提供します。
    • go/parserは、たとえパラメータがなくても、ast.FuncType.Paramsフィールドをnilではなく、空のast.FieldListListが空のスライス)として初期化する傾向があります。これは、ソースコード上の括弧()の位置情報などを保持するためです。
  4. go/printerパッケージ:

    • go/astパッケージで定義されたASTを受け取り、それをGoのソースコードとして整形して出力する機能を提供します。
    • このパッケージは、ASTを人間が読める形式のGoコードに変換する役割を担っています。
  5. tokenパッケージ:

    • Go言語の字句解析(lexical analysis)で使用されるトークン(キーワード、識別子、演算子など)を定義します。
    • token.LPARENは左括弧 ( を、token.RPARENは右括弧 ) を表します。

これらのパッケージが連携して、Goのソースコードの解析、操作、そして再生成を行います。このコミットは、特にgo/printergo/astの仕様のすべてのケース(特にnilParamsフィールド)を適切に処理できるようにするためのものです。

技術的詳細

このコミットの技術的な詳細は、go/printerパッケージ内のsignatureメソッドの挙動の変更と、それに対応するテストケースの追加に集約されます。

問題点: 従来のgo/printersignatureメソッドは、関数のパラメータリストを処理する際に、params*ast.FieldList型)が常に非nilであると仮定していました。そのため、もしparamsnilであった場合、p.parameters(params)の呼び出し内でnilポインタ参照が発生し、プログラムがクラッシュしていました。これは、go/astのドキュメントがFuncType.Paramsnilである可能性を示唆しているにもかかわらず、go/printerがそのケースを適切に扱っていなかったためです。go/parserParamsnilにしないため、この問題は手動でASTを構築するシナリオでのみ顕在化しました。

解決策: コミットは、signatureメソッド内でparamsnilであるかどうかを明示的にチェックするロジックを追加することでこの問題を解決します。

  1. paramsnilチェック:
    • if params != nil { ... }という条件分岐が追加されました。
    • もしparamsが非nilであれば、これまで通りp.parameters(params)を呼び出して通常のパラメータリストを整形します。
  2. paramsnilの場合の処理:
    • もしparamsnilであれば、これはパラメータが全くない関数型を意味します。Goの構文では、パラメータがない関数は()と表記されます。
    • この場合、p.print(token.LPAREN, token.RPAREN)を呼び出して、明示的に空の括弧()を出力するように変更されました。これにより、nilポインタ参照を回避しつつ、正しいGoの構文を生成できます。

テストケースの追加: この修正の正しさを検証するために、src/pkg/go/printer/printer_test.goTestFuncTypeという新しいテスト関数が追加されました。

  • このテストでは、ast.FuncType{}という形で、Paramsフィールドが明示的に設定されていない(つまりnilである)ast.FuncTypeを持つast.FuncDecl(関数宣言)を含むASTをプログラム的に構築します。
  • 構築されたASTをgo/printer.Fprint関数で整形し、その出力が期待される文字列func f()と一致するかどうかを検証します。
  • このテストケースは、go/printernilParamsフィールドを適切に処理し、クラッシュせずに正しい出力を生成することを確認します。

この変更により、go/printergo/astの仕様に完全に準拠し、より堅牢になりました。

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

--- a/src/pkg/go/printer/nodes.go
+++ b/src/pkg/go/printer/nodes.go
@@ -325,9 +325,14 @@ func (p *printer) parameters(fields *ast.FieldList) {
 }
 
 func (p *printer) signature(params, result *ast.FieldList) {
-	p.parameters(params)
+	if params != nil {
+		p.parameters(params)
+	} else {
+		p.print(token.LPAREN, token.RPAREN)
+	}
 	n := result.NumFields()
 	if n > 0 {
+		// result != nil
 		p.print(blank)
 		if n == 1 && result.List[0].Names == nil {
 			// single anonymous result; no ()'s
--- a/src/pkg/go/printer/printer_test.go
+++ b/src/pkg/go/printer/printer_test.go
@@ -385,6 +385,35 @@ func (t *t) foo(a, b, c int) int {\n 	}\n }\n \n+// TestFuncType tests that an ast.FuncType with a nil Params field\n+// can be printed (per go/ast specification). Test case for issue 3870.\n+func TestFuncType(t *testing.T) {\n+\tsrc := &ast.File{\n+\t\tName: &ast.Ident{Name: \"p\"},\n+\t\tDecls: []ast.Decl{\n+\t\t\t&ast.FuncDecl{\n+\t\t\t\tName: &ast.Ident{Name: \"f\"},\n+\t\t\t\tType: &ast.FuncType{},\n+\t\t\t},\n+\t\t},\n+\t}\n+\n+\tvar buf bytes.Buffer\n+\tif err := Fprint(&buf, fset, src); err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\tgot := buf.String()\n+\n+\tconst want = `package p\n+\n+func f()\n+`\n+\n+\tif got != want {\n+\t\tt.Fatalf(\"got:\\n%s\\nwant:\\n%s\\n\", got, want)\n+\t}\n+}\n+\n // TextX is a skeleton test that can be filled in for debugging one-off cases.\n // Do not remove.\n func TestX(t *testing.T) {\n```

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

### `src/pkg/go/printer/nodes.go` の変更

`signature`関数は、関数のシグネチャ(パラメータと戻り値)を整形して出力する役割を担っています。

変更前:
```go
func (p *printer) signature(params, result *ast.FieldList) {
	p.parameters(params)
	n := result.NumFields()
	if n > 0 {
		p.print(blank)
		if n == 1 && result.List[0].Names == nil {
			// single anonymous result; no ()'s

変更前は、paramsnilであるかどうかのチェックなしに直接p.parameters(params)を呼び出していました。p.parameters内部でparamsnilの場合にパニック(クラッシュ)する可能性がありました。

変更後:

func (p *printer) signature(params, result *ast.FieldList) {
	if params != nil {
		p.parameters(params)
	} else {
		p.print(token.LPAREN, token.RPAREN)
	}
	n := result.NumFields()
	if n > 0 {
		// result != nil
		p.print(blank)
		if n == 1 && result.List[0].Names == nil {
			// single anonymous result; no ()'s

この変更では、paramsnilであるかどうかを最初にチェックするif文が追加されました。

  • if params != nil: paramsnilでない場合(つまり、パラメータリストが存在する場合)は、これまで通りp.parameters(params)を呼び出してパラメータを整形します。
  • else: paramsnilの場合(つまり、パラメータが全くない関数型の場合)は、p.print(token.LPAREN, token.RPAREN)を呼び出して、Goの構文でパラメータなしを示す空の括弧()を出力します。これにより、nilポインタ参照を防ぎつつ、正しい出力を保証します。
  • // result != nilというコメントが追加されていますが、これはn > 0の条件がresultnilでないことを暗に示しているため、その後の処理が安全であることを補足しています。

src/pkg/go/printer/printer_test.go の変更

TestFuncTypeという新しいテスト関数が追加されました。

// TestFuncType tests that an ast.FuncType with a nil Params field
// can be printed (per go/ast specification). Test case for issue 3870.
func TestFuncType(t *testing.T) {
	src := &ast.File{
		Name: &ast.Ident{Name: "p"},
		Decls: []ast.Decl{
			&ast.FuncDecl{
				Name: &ast.Ident{Name: "f"},
				Type: &ast.FuncType{},
			},
		},
	}

	var buf bytes.Buffer
	if err := Fprint(&buf, fset, src); err != nil {
		t.Fatal(err)
	}
	got := buf.String()

	const want = `package p

func f()
`

	if got != want {
		t.Fatalf("got:\n%s\nwant:\n%s\n", got, want)
	}
}

このテストは、ast.FuncTypeParamsフィールドがnilであるケースを明示的にテストします。

  1. srcという*ast.File(Goのソースファイル全体を表すASTノード)を構築します。
  2. その中に、Name"f"で、Type&ast.FuncType{}である*ast.FuncDecl(関数宣言)を追加します。ここで重要なのは、ast.FuncType{}と初期化することで、そのParamsフィールドがGoのデフォルト値であるnilになる点です。
  3. bytes.Bufferを使ってgo/printer.Fprintの出力をキャプチャします。
  4. Fprintがエラーなく実行され、クラッシュしないことを確認します。
  5. 生成された文字列gotが、期待される出力func f()want)と完全に一致するかを検証します。これにより、nilParamsフィールドが正しく空の括弧として整形されることを確認します。

このテストは、Issue #3870で報告されたクラッシュを再現し、修正が正しく機能することを確認するための重要なものです。

関連リンク

参考にした情報源リンク