[インデックス 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言語の概念とツールに関する知識が必要です。
-
go vet
:go vet
は、Go言語のソースコードを静的に解析し、潜在的なバグや疑わしいコード構成を検出するツールです。コンパイル時には検出されないが、実行時に問題を引き起こす可能性のあるパターン(例:printf
フォーマット文字列と引数の不一致、到達不能なコード、ロックの誤用など)を特定します。開発プロセスにおいて、早期に問題を検出することで、デバッグの手間を削減し、コード品質を向上させる役割を担っています。 -
fmt.Printf
とフォーマット文字列:fmt.Printf
は、C言語のprintf
に似たGo言語の関数で、指定されたフォーマット文字列に基づいて引数を整形して出力します。フォーマット文字列には、%d
(整数)、%s
(文字列)、%f
(浮動小数点数)などの「動詞(verb)」が含まれ、対応する引数の型と一致する必要があります。go vet
は、このフォーマット文字列と引数の型の不一致を検出する主要な機能の一つです。 -
go/ast
パッケージ (Abstract Syntax Tree):go/ast
パッケージは、Go言語のソースコードを抽象構文木(AST: Abstract Syntax Tree)として表現するためのデータ構造を提供します。コンパイラや静的解析ツールは、ソースコードを直接扱うのではなく、まずASTに変換してから解析を行います。ASTは、プログラムの構造を木構造で表現したもので、各ノードが式、文、宣言などの言語要素に対応します。vet
はこのASTを走査してコードのパターンを分析します。 -
go/types
パッケージ:go/types
パッケージは、Goプログラムの型情報を扱うための機能を提供します。ASTがプログラムの構文構造を表すのに対し、go/types
は各式の型、変数の型、関数のシグネチャなど、セマンティックな情報を提供します。vet
は、printf
の引数の型がフォーマット動詞と一致するかどうかを判断するために、このパッケージの型情報を使用します。 -
go/printer
パッケージ:go/printer
パッケージは、Go言語のASTノードをGoのソースコードとして整形して出力する機能を提供します。これは、ASTを人間が読める形式のコードに戻す「pretty-printing」を行うために使用されます。このコミットでは、vet
がエラーメッセージ内で問題のあるASTノード(式)を、このgo/printer
を使って整形し、より分かりやすい形で表示するために利用されています。
技術的詳細
このコミットの技術的な核心は、go vet
がエラーメッセージを生成する際に、単に引数の「型」や「値」を文字列として出力するのではなく、go/ast
パッケージで表現される「式」そのものを go/printer
を用いてGoのコードとして整形し、エラーメッセージに埋め込む点にあります。
変更前は、vet
が printf
の引数型不一致を検出した場合、エラーメッセージは f.Badf(call.Pos(), "arg %s for printf verb %%%c of wrong type: %s", arg, verb, typeString)
のような形式で生成されていました。ここで arg
は ast.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.Expr
を bytes.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.go
の matchArgType
関数において、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のコードとして整形するようになった点です。
-
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
オブジェクトです。
-
エラーメッセージの改善:
src/cmd/vet/print.go
のcheckPrintfArg
関数は、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
など)が埋め込まれるようになり、開発者はどの式が問題を引き起こしているのかを一目で理解できるようになりました。 -
型なし整数リテラルの扱い:
src/cmd/vet/print.go
のmatchArgType
関数における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コードの品質向上に貢献しています。
関連リンク
- Go Issue #4945:
cmd/vet
:printf
の引数型チェックのエラーメッセージが不明瞭 - Gerrit Change-Id:
https://golang.org/cl/7448046
参考にした情報源リンク
- Go言語公式ドキュメント:
go vet
- Go言語公式ドキュメント:
fmt
パッケージ - Go言語公式ドキュメント:
go/ast
パッケージ - Go言語公式ドキュメント:
go/types
パッケージ - Go言語公式ドキュメント:
go/printer
パッケージ - Go言語のASTと型チェックに関するブログ記事やチュートリアル (一般的な情報源として)
- (具体的なURLは省略しますが、
Go AST tutorial
,Go type checking
などで検索すると多数見つかります。)
- (具体的なURLは省略しますが、
- Go言語のコミット履歴とGerritレビューシステム (一般的な情報源として)