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

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

このコミットは、Go言語の静的解析ツールである cmd/vet において、printf 系の関数の引数に関するエラーメッセージの表示方法を改善するものです。具体的には、エラーメッセージ内で問題のある式(ast.Expr)を整形して表示するために、go/printer パッケージを利用するように変更されています。これにより、vet が報告するエラーメッセージがより分かりやすくなり、開発者が問題を特定しやすくなります。

コミット

commit 749082e2a48719101606e7416b70fbdeea93a523
Author: Rob Pike <r@golang.org>
Date:   Fri Mar 1 12:30:09 2013 -0800

    cmd/vet: use go/printer to pretty-print expressions in printf messages
    Fixes #4945.
    Most examples in this issue now better, but #10 is incomplete and I'm not
    certain how to reproduce it. It actually looks like a go/types problem, since
    the type being reported is coming directly from that package.
    Please reopen the issue if you disagree.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/7448046

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

https://github.com/golang/go/commit/749082e2a48719101606e7416b70fbdeea93a523

元コミット内容

cmd/vet: use go/printer to pretty-print expressions in printf messages Fixes #4945. Most examples in this issue now better, but #10 is incomplete and I'm not certain how to reproduce it. It actually looks like a go/types problem, since the type being reported is coming directly from that package. Please reopen the issue if you disagree.

変更の背景

この変更は、Go言語のIssue #4945「cmd/vet: printf の引数型チェックのエラーメッセージが不明瞭」を修正するために行われました。

go vet は、Goプログラムの潜在的なバグや疑わしい構成を検出するためのツールです。特に fmt.Printf などの printf 系の関数において、フォーマット文字列と引数の型が一致しない場合に警告を発する機能があります。しかし、この警告メッセージが、どの引数が問題なのか、その引数がどのような式であるのかを正確に示せていないという問題がありました。

例えば、fmt.Printf("%d", "hello") のようなコードがあった場合、vet は「%d に文字列が渡されている」という警告を出すべきですが、その「文字列」が具体的にどのソースコード上の式なのかが分かりにくい場合がありました。特に、複雑な式や関数呼び出しの結果が引数として渡されている場合、単に「引数」と表示されるだけでは、開発者がコードのどこを修正すべきか判断しづらかったのです。

このコミットの目的は、vet が生成するエラーメッセージにおいて、問題のある引数(式)をGoのコードとして整形して表示することで、メッセージの可読性と有用性を大幅に向上させることにありました。これにより、開発者は vet の警告をより迅速に理解し、修正できるようになります。

前提知識の解説

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

  1. go vet: go vet は、Go言語のソースコードを静的に解析し、潜在的なバグや疑わしいコード構成を検出するツールです。コンパイル時には検出されないが、実行時に問題を引き起こす可能性のあるパターン(例: printf フォーマット文字列と引数の不一致、到達不能なコード、ロックの誤用など)を特定します。開発プロセスにおいて、早期に問題を検出することで、デバッグの手間を削減し、コード品質を向上させる役割を担っています。

  2. fmt.Printf とフォーマット文字列: fmt.Printf は、C言語の printf に似たGo言語の関数で、指定されたフォーマット文字列に基づいて引数を整形して出力します。フォーマット文字列には、%d(整数)、%s(文字列)、%f(浮動小数点数)などの「動詞(verb)」が含まれ、対応する引数の型と一致する必要があります。go vet は、このフォーマット文字列と引数の型の不一致を検出する主要な機能の一つです。

  3. go/ast パッケージ (Abstract Syntax Tree): go/ast パッケージは、Go言語のソースコードを抽象構文木(AST: Abstract Syntax Tree)として表現するためのデータ構造を提供します。コンパイラや静的解析ツールは、ソースコードを直接扱うのではなく、まずASTに変換してから解析を行います。ASTは、プログラムの構造を木構造で表現したもので、各ノードが式、文、宣言などの言語要素に対応します。vet はこのASTを走査してコードのパターンを分析します。

  4. go/types パッケージ: go/types パッケージは、Goプログラムの型情報を扱うための機能を提供します。ASTがプログラムの構文構造を表すのに対し、go/types は各式の型、変数の型、関数のシグネチャなど、セマンティックな情報を提供します。vet は、printf の引数の型がフォーマット動詞と一致するかどうかを判断するために、このパッケージの型情報を使用します。

  5. go/printer パッケージ: go/printer パッケージは、Go言語のASTノードをGoのソースコードとして整形して出力する機能を提供します。これは、ASTを人間が読める形式のコードに戻す「pretty-printing」を行うために使用されます。このコミットでは、vet がエラーメッセージ内で問題のあるASTノード(式)を、この go/printer を使って整形し、より分かりやすい形で表示するために利用されています。

技術的詳細

このコミットの技術的な核心は、go vet がエラーメッセージを生成する際に、単に引数の「型」や「値」を文字列として出力するのではなく、go/ast パッケージで表現される「式」そのものを go/printer を用いてGoのコードとして整形し、エラーメッセージに埋め込む点にあります。

変更前は、vetprintf の引数型不一致を検出した場合、エラーメッセージは f.Badf(call.Pos(), "arg %s for printf verb %%%c of wrong type: %s", arg, verb, typeString) のような形式で生成されていました。ここで argast.Expr 型のオブジェクトですが、そのまま %s で文字列化されると、その式のGoコードとしての表現ではなく、内部的なオブジェクトの文字列表現(例えば、&{...} のようなもの)が出力される可能性がありました。これは、開発者にとってどのコードが問題なのかを特定しづらくしていました。

このコミットでは、f.gofmt(arg) という新しいヘルパー関数が導入され、この関数が go/printer を内部的に使用して ast.Expr をGoのコード文字列に変換します。そして、この整形された文字列がエラーメッセージに組み込まれるようになりました。

具体的には、src/cmd/vet/print.go 内の checkPrintfArg 関数において、f.Badf の呼び出し箇所で、引数 arg を直接渡す代わりに f.gofmt(arg) の結果を渡すように変更されています。これにより、エラーメッセージは例えば「arg "hi" for printf verb %b of wrong type」のように、問題の引数である "hi" という文字列リテラルがそのまま表示されるようになります。

また、src/cmd/vet/main.go にあった goFmt 関数(コメントが // goFmt returns a string representation of the expression となっていたもの)が、gofmt にリネームされ、その役割がより明確化されています。この関数が go/printer.Fprint を利用して ast.Exprbytes.Buffer に書き込み、その内容を文字列として返すという実装になっています。

src/cmd/vet/test_print.go では、この変更によって vet の出力メッセージがどのように変わるかを示すために、テストケースの期待されるエラーメッセージが更新されています。特に、fmt.Printf("%b", "hi") のようなケースで、以前は「arg for printf verb %b of wrong type」のようなメッセージだったものが、「arg .hi. for printf verb %b of wrong type」のように、具体的な引数の値がメッセージに含まれるようになっています。

さらに、src/cmd/vet/print.gomatchArgType 関数において、types.UntypedInt の型チェックロジックが微修正されています。以前は t&(argInt|argFloat) != 0 となっていましたが、t&argInt != 0 に変更されています。これは、printf のフォーマット動詞が %g のような浮動小数点数を期待する場合でも、型なし整数リテラル(例: 1234)が渡された場合に vet が誤って警告を出さないようにするための調整と考えられます。ただし、このコミットの主な目的は go/printer の導入によるメッセージ改善であり、この型チェックの変更は副次的なものか、関連する別のバグ修正の一部である可能性があります。

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

src/cmd/vet/main.go

--- a/src/cmd/vet/main.go
+++ b/src/cmd/vet/main.go
@@ -392,7 +392,7 @@ func (f *File) walkRangeStmt(n *ast.RangeStmt) {
 	checkRangeLoop(f, n)
 }
 
-// goFmt returns a string representation of the expression
+// gofmt returns a string representation of the expression.
 func (f *File) gofmt(x ast.Expr) string {
 	f.b.Reset()
 	printer.Fprint(&f.b, f.fset, x)
  • goFmt 関数が gofmt にリネームされ、コメントも修正されています。この関数が go/printer を使って式を整形する役割を担います。

src/cmd/vet/print.go

--- a/src/cmd/vet/print.go
+++ b/src/cmd/vet/print.go
@@ -280,7 +280,7 @@ func (f *File) checkPrintfArg(call *ast.CallExpr, verb rune, flags []byte, argNu
 			// arg must be integer.
 			for i := 0; i < nargs-1; i++ {
 				if !f.matchArgType(argInt, call.Args[argNum+i]) {
-\t\t\t\t\tf.Badf(call.Pos(), "arg %s for * in printf format not of type int", call.Args[argNum+i])
+\t\t\t\t\tf.Badf(call.Pos(), "arg %s for * in printf format not of type int", f.gofmt(call.Args[argNum+i]))
 				}
 			}
 			for _, v := range printVerbs {
@@ -291,7 +291,7 @@ func (f *File) checkPrintfArg(call *ast.CallExpr, verb rune, flags []byte, argNu
 					if typ := f.pkg.types[arg]; typ != nil {
 						typeString = typ.String()
 					}
-\t\t\t\t\tf.Badf(call.Pos(), "arg %s for printf verb %%%c of wrong type: %s", arg, verb, typeString)
+\t\t\t\t\t\tf.Badf(call.Pos(), "arg %s for printf verb %%%c of wrong type: %s", f.gofmt(arg), verb, typeString)
 					}
 					break
 				}
@@ -339,7 +339,7 @@ func (f *File) matchArgType(t printfArgType, arg ast.Expr) bool {
 		}
 		return t&argFloat != 0
 	case types.UntypedInt:
-\t\treturn t&(argInt|argFloat) != 0 // You might say Printf("%g", 1234)
+\t\treturn t&argInt != 0
 	case types.UntypedRune:
 		return t&(argInt|argRune) != 0
 	case types.UntypedString:
  • checkPrintfArg 関数内で、f.Badf を呼び出す際に、call.Args[argNum+i]arg といった ast.Expr を直接 %s で出力する代わりに、f.gofmt() 関数を通して整形された文字列を渡すように変更されています。
  • matchArgType 関数内の types.UntypedInt のケースで、型チェックの条件が t&(argInt|argFloat) != 0 から t&argInt != 0 に変更されています。

src/cmd/vet/test_print.go

--- a/src/cmd/vet/test_print.go
+++ b/src/cmd/vet/test_print.go
@@ -59,12 +59,12 @@ func PrintfTests() {
 	fmt.Printf("%b %b", 3, i)
 	fmt.Printf("%c %c %c %c", 3, i, 'x', r)
 	fmt.Printf("%d %d", 3, i)
-\tfmt.Printf("%e %e %e", 3, 3e9, x)
-\tfmt.Printf("%E %E %E", 3, 3e9, x)
-\tfmt.Printf("%f %f %f", 3, 3e9, x)
-\tfmt.Printf("%F %F %F", 3, 3e9, x)
-\tfmt.Printf("%g %g %g", 3, 3e9, x)
-\tfmt.Printf("%G %G %G", 3, 3e9, x)
+\tfmt.Printf("%e %e", 3e9, x)
+\tfmt.Printf("%E %E", 3e9, x)
+\tfmt.Printf("%f %f", 3e9, x)
+\tfmt.Printf("%F %F", 3e9, x)
+\tfmt.Printf("%g %g", 3e9, x)
+\tfmt.Printf("%G %G", 3e9, x)
 	fmt.Printf("%o %o", 3, i)
 	fmt.Printf("%p %p", p, nil)
 	fmt.Printf("%q %q %q %q", 3, i, 'x', r)
@@ -77,24 +77,24 @@ func PrintfTests() {
 	fmt.Printf("%X %X %X %X", 3, i, "hi", s)
 	fmt.Printf("%.*s %d %g", 3, "hi", 23, 2.3)
 	// Some bad format/argTypes
-\tfmt.Printf("%b", 2.3)                      // ERROR "arg for printf verb %b of wrong type"
-\tfmt.Printf("%c", 2.3)                      // ERROR "arg for printf verb %c of wrong type"
-\tfmt.Printf("%d", 2.3)                      // ERROR "arg for printf verb %d of wrong type"
-\tfmt.Printf("%e", "hi")                     // ERROR "arg for printf verb %e of wrong type"
-\tfmt.Printf("%E", true)                     // ERROR "arg for printf verb %E of wrong type"
-\tfmt.Printf("%f", "hi")                     // ERROR "arg for printf verb %f of wrong type"
-\tfmt.Printf("%F", 'x')                      // ERROR "arg for printf verb %F of wrong type"
-\tfmt.Printf("%g", "hi")                     // ERROR "arg for printf verb %g of wrong type"
-\tfmt.Printf("%G", i)                        // ERROR "arg for printf verb %G of wrong type"
-\tfmt.Printf("%o", x)                        // ERROR "arg for printf verb %o of wrong type"
-\tfmt.Printf("%p", 23)                       // ERROR "arg for printf verb %p of wrong type"
-\tfmt.Printf("%q", x)                        // ERROR "arg for printf verb %q of wrong type"
-\tfmt.Printf("%s", b)                        // ERROR "arg for printf verb %s of wrong type"
-\tfmt.Printf("%t", 23)                       // ERROR "arg for printf verb %t of wrong type"
-\tfmt.Printf("%U", x)                        // ERROR "arg for printf verb %U of wrong type"
-\tfmt.Printf("%x", nil)                      // ERROR "arg for printf verb %x of wrong type"
-\tfmt.Printf("%X", 2.3)                      // ERROR "arg for printf verb %X of wrong type"
-\tfmt.Printf("%.*s %d %g", 3, "hi", 23, 'x') // ERROR "arg for printf verb %g of wrong type"
+\tfmt.Printf("%b", "hi")                     // ERROR "arg .hi. for printf verb %b of wrong type"
+\tfmt.Printf("%c", 2.3)                      // ERROR "arg 2.3 for printf verb %c of wrong type"
+\tfmt.Printf("%d", 2.3)                      // ERROR "arg 2.3 for printf verb %d of wrong type"
+\tfmt.Printf("%e", "hi")                     // ERROR "arg .hi. for printf verb %e of wrong type"
+\tfmt.Printf("%E", true)                     // ERROR "arg true for printf verb %E of wrong type"
+\tfmt.Printf("%f", "hi")                     // ERROR "arg .hi. for printf verb %f of wrong type"
+\tfmt.Printf("%F", 'x')                      // ERROR "arg 'x' for printf verb %F of wrong type"
+\tfmt.Printf("%g", "hi")                     // ERROR "arg .hi. for printf verb %g of wrong type"
+\tfmt.Printf("%G", i)                        // ERROR "arg i for printf verb %G of wrong type"
+\tfmt.Printf("%o", x)                        // ERROR "arg x for printf verb %o of wrong type"
+\tfmt.Printf("%p", 23)                       // ERROR "arg 23 for printf verb %p of wrong type"
+\tfmt.Printf("%q", x)                        // ERROR "arg x for printf verb %q of wrong type"
+\tfmt.Printf("%s", b)                        // ERROR "arg b for printf verb %s of wrong type"
+\tfmt.Printf("%t", 23)                       // ERROR "arg 23 for printf verb %t of wrong type"
+\tfmt.Printf("%U", x)                        // ERROR "arg x for printf verb %U of wrong type"
+\tfmt.Printf("%x", nil)                      // ERROR "arg nil for printf verb %x of wrong type"
+\tfmt.Printf("%X", 2.3)                      // ERROR "arg 2.3 for printf verb %X of wrong type"
+\tfmt.Printf("%.*s %d %g", 3, "hi", 23, 'x') // ERROR "arg 'x' for printf verb %g of wrong type"
 \t// TODO
 \tfmt.Println()                      // not an error
 \tfmt.Println("%s", "hi")            // ERROR "possible formatting directive in Println call"
@@ -105,9 +105,9 @@ func PrintfTests() {\n \tfmt.Printf("% 8s", "woo")          // correct
 \tfmt.Printf("%.*d", 3, 3)           // correct
 \tfmt.Printf("%.*d", 3, 3, 3)        // ERROR "wrong number of args for format in Printf call"
-\tfmt.Printf("%.*d", "hi", 3)        // ERROR "arg for * in printf format not of type int"
+\tfmt.Printf("%.*d", "hi", 3)        // ERROR "arg .hi. for * in printf format not of type int"
 \tfmt.Printf("%.*d", i, 3)           // correct
-\tfmt.Printf("%.*d", s, 3)           // ERROR "arg for * in printf format not of type int"
+\tfmt.Printf("%.*d", s, 3)           // ERROR "arg s for * in printf format not of type int"
 \tfmt.Printf("%q %q", multi()...)    // ok
 \tfmt.Printf("%#q", `blah`)          // ok
 \tprintf("now is the time", "buddy") // ERROR "no formatting directive"
  • PrintfTests 関数内の fmt.Printf のテストケースにおいて、期待されるエラーメッセージの文字列が、go/printer による整形後の出力に合わせて更新されています。特に、問題の引数が具体的なリテラル値や変数名として表示されるようになっています。
  • fmt.Printf("%e %e %e", 3, 3e9, x) のような行が fmt.Printf("%e %e", 3e9, x) のように引数が減らされている箇所がありますが、これはテストケースの簡略化または、以前のテストが過剰な引数を含んでいたためと考えられます。

コアとなるコードの解説

このコミットのコアとなる変更は、go vet がエラーメッセージを生成する際に、go/printer パッケージを利用して ast.Expr をGoのコードとして整形するようになった点です。

  1. gofmt 関数の役割: src/cmd/vet/main.go にある gofmt 関数(以前は goFmt)は、ast.Expr 型の引数 x を受け取り、それをGoのソースコードとして整形した文字列を返します。この関数は内部で printer.Fprint(&f.b, f.fset, x) を呼び出しています。

    • f.b: bytes.Buffer のインスタンスで、整形されたコードが一時的に書き込まれるバッファです。
    • f.fset: token.FileSet のインスタンスで、ソースコードの位置情報(行番号、列番号など)を管理します。go/printer が正確な整形を行うために必要です。
    • x: 整形したい ast.Expr オブジェクトです。
  2. エラーメッセージの改善: src/cmd/vet/print.gocheckPrintfArg 関数は、printf 系の関数の引数がフォーマット動詞と一致するかどうかをチェックする主要なロジックを含んでいます。この関数内で、引数の型が不一致であると判断された場合に f.Badf を呼び出してエラーを報告します。 変更前は、f.Badf%s フォーマット指定子に ast.Expr オブジェクトが直接渡されていました。Goのデフォルトの文字列変換では、オブジェクトの内部表現が出力されることが多く、例えば &{...} のような読みにくい形式になることがありました。 変更後は、f.gofmt(call.Args[argNum+i])f.gofmt(arg) のように、gofmt 関数を通して整形された文字列が渡されるようになりました。これにより、エラーメッセージには ast.Expr が表す実際のGoのコード(例: "hi", 2.3, i, x など)が埋め込まれるようになり、開発者はどの式が問題を引き起こしているのかを一目で理解できるようになりました。

  3. 型なし整数リテラルの扱い: src/cmd/vet/print.gomatchArgType 関数における types.UntypedInt の変更は、printf の引数型チェックの精度を向上させるためのものです。

    • 変更前: return t&(argInt|argFloat) != 0 これは、「型なし整数リテラルは、整数または浮動小数点数のどちらの引数型にもマッチしうる」という解釈でした。例えば、fmt.Printf("%g", 1234) のように、浮動小数点数を期待する %g に整数リテラル 1234 が渡された場合でも、vet は警告を出さないことを意図していました。
    • 変更後: return t&argInt != 0 この変更により、「型なし整数リテラルは、整数引数型にのみマッチする」という厳密な解釈になりました。これは、printf の文脈において、型なし整数リテラルが浮動小数点数として扱われるケースをより厳密にチェックするため、または特定のフォーマット動詞に対してより正確な型チェックを行うための調整と考えられます。コミットメッセージの「#10 is incomplete and I'm not certain how to reproduce it. It actually looks like a go/types problem, since the type being reported is coming directly from that package.」という記述から、go/types パッケージからの型情報に基づいて vet が誤った判断を下すケースがあった可能性があり、その修正の一環であると推測されます。

これらの変更により、go vet はより正確で、かつ開発者にとって理解しやすいエラーメッセージを提供するようになり、Goコードの品質向上に貢献しています。

関連リンク

参考にした情報源リンク