[インデックス 10830] ファイルの概要
このコミットは、Go言語の静的解析ツールである govet
に、printf
系関数のフォーマット文字列における動詞(verb)とフラグ(flag)のチェック機能を追加するものです。これにより、開発者が fmt.Printf
や log.Printf
などの関数で誤ったフォーマット指定子を使用した場合に、コンパイル時ではなく静的解析の段階で警告またはエラーを検出できるようになります。また、この新しいチェック機能によって発見された既存の標準ライブラリのテストコード内の誤りも修正しています。
コミット
commit 197eb8f7c3703e46d3fc351d277e03cd3b413fbc
Author: Rob Pike <r@golang.org>
Date: Thu Dec 15 15:17:52 2011 -0800
govet: add checking for printf verbs
Also fix the errors it catches.
Fixes #1654.
R=rsc
CC=golang-dev
https://golang.org/cl/5489060
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/197eb8f7c3703e46d3fc351d277e03cd3b413fbc
元コミット内容
govet: add checking for printf verbs
Also fix the errors it catches.
Fixes #1654.
R=rsc
CC=golang-dev
https://golang.org/cl/5489060
変更の背景
Go言語の fmt
パッケージやその他のログ出力関数など、printf
スタイルのフォーマット文字列を使用する関数は、特定のフォーマット動詞(例: %s
for string, %d
for integer, %v
for default)と、それらを修飾するフラグ(例: #
for alternate format, +
for always-sign,
for space-padding)をサポートしています。しかし、これらの動詞やフラグの組み合わせには有効なものと無効なものがあります。例えば、文字列を意味する %s
に #
フラグ(通常は数値の代替フォーマットに使用)を組み合わせる %#s
は、Goの fmt
パッケージではサポートされておらず、実行時に予期せぬ動作やパニックを引き起こす可能性があります。
govet
はGoプログラムの一般的なエラーを検出するための静的解析ツールですが、このコミット以前は、printf
系関数の引数の数とフォーマット文字列の動詞の数が一致するかどうかはチェックしていましたが、個々の動詞やフラグの組み合わせの妥当性まではチェックしていませんでした。このため、開発者が誤ったフォーマット指定子を使用しても、コンパイルエラーにはならず、実行時まで問題が顕在化しないという課題がありました。
このコミットは、このギャップを埋め、govet
が printf
系関数のフォーマット動詞とフラグの有効性を静的に検証できるようにすることで、より堅牢なコードの記述を支援することを目的としています。また、この新機能によって標準ライブラリ内の既存の誤りも発見・修正されており、その有効性が示されています。
前提知識の解説
- Go言語の
fmt
パッケージとprintf
系関数:fmt.Printf
,fmt.Sprintf
,fmt.Errorf
など、C言語のprintf
に似た書式指定文字列を使って値を出力する関数群です。書式指定文字列は%
で始まる「フォーマット動詞」と、その動詞を修飾する「フラグ」から構成されます。- フォーマット動詞 (Verbs): 値の型や表示形式を指定します。例:
%s
(文字列),%d
(整数),%v
(デフォルト形式),%T
(型名)。 - フラグ (Flags): 動詞の振る舞いを変更します。例:
#
(代替フォーマット),+
(常に符号を表示),-
(左寄せ),0
(ゼロ埋め),
- フォーマット動詞 (Verbs): 値の型や表示形式を指定します。例:
govet
: Go言語の公式ツールの一つで、ソースコードを静的に解析し、疑わしい構造や潜在的なエラー(例:printf
フォーマット文字列と引数の不一致、到達不能なコード、未使用の変数など)を報告します。- 静的解析 (Static Analysis): プログラムを実行せずにソースコードを分析し、潜在的なバグ、セキュリティ脆弱性、コーディング規約違反などを検出する手法です。
- 抽象構文木 (Abstract Syntax Tree, AST): ソースコードの構造を木構造で表現したものです。
govet
のような静的解析ツールは、通常、ソースコードをASTに変換し、そのASTを走査して分析を行います。このコミットではast.CallExpr
(関数呼び出しのASTノード) やcall.Pos()
(ASTノードのソースコード上の位置) が使用されています。 rune
: Go言語におけるUnicodeコードポイントを表す型です。文字列の文字を扱う際に使用されます。strings.ContainsRune
:strings
パッケージの関数で、ある文字列が特定のrune
を含んでいるかどうかをチェックします。
技術的詳細
このコミットの主要な技術的変更は、src/cmd/govet/print.go
ファイルに集約されています。
-
parsePrintfVerb
メソッドの変更:- 以前は独立した関数でしたが、
*File
型のメソッド(f *File) parsePrintfVerb
に変更されました。これにより、解析中のファイル (*File
) のコンテキスト(エラー報告など)にアクセスできるようになりました。 - フォーマット文字列から動詞だけでなく、それに付随するフラグも抽出するように拡張されました。抽出されたフラグは
flags
という[]byte
スライスに格納されます。 - 小数点
.
もフラグとして扱われるように変更されました(例:%.2f
の.2
の.
)。
- 以前は独立した関数でしたが、
-
printVerb
構造体とprintVerbs
変数の導入:printVerb
構造体は、特定のprintf
動詞 (verb
フィールド) と、その動詞に対して有効なフラグの集合 (flags
フィールド、文字列として保持) を定義します。printVerbs
は[]printVerb
型のスライスで、Goのfmt
パッケージがサポートする主要な動詞(b
,c
,d
,e
,E
,f
,F
,g
,G
,o
,p
,q
,s
,t
,T
,v
,x
,X
)と、それぞれに対応する有効なフラグのセットがハードコードされています。例えば、's'
(文字列) には"-"
(左寄せ) と"."
(精度) のみが有効なフラグとして定義されています。
-
checkPrintfVerb
メソッドの追加:- この新しいメソッドが、動詞と抽出されたフラグの妥当性を検証する中心的なロジックを担います。
- 引数として、関数呼び出しのASTノード (
call *ast.CallExpr
)、解析対象の動詞 (verb rune
)、および抽出されたフラグ (flags []byte
) を受け取ります。 printVerbs
スライスを線形探索し、与えられたverb
に一致するprintVerb
エントリを見つけます。- 見つかったエントリの
v.flags
文字列に対して、抽出された各flag
が含まれているか (strings.ContainsRune
) をチェックします。 - もし、動詞が見つからない場合、または動詞に対して無効なフラグが使用されている場合、
f.Badf
を呼び出してエラーを報告します。f.Badf
はgovet
のエラー報告メカニズムであり、ソースコード上の位置情報と共に詳細なエラーメッセージを出力します。
-
既存のテストコードの修正:
src/pkg/encoding/xml/marshal_test.go
:t.Errorf
のフォーマット文字列で%#s
が%q
に変更されました。%#s
は文字列に対して無効なフラグの組み合わせであり、新しいgovet
チェックによって検出されます。%q
はGoの構文で引用符付きの文字列を出力する動詞です。src/pkg/net/http/readrequest_test.go
およびsrc/pkg/net/textproto/reader_test.go
:t.Errorf
のフォーマット文字列で%#d
が#%d
に変更されました。これは、#
が動詞の一部ではなく、単なるリテラル文字として扱われるようにするための修正です。src/pkg/os/os_test.go
:t.Fatalf
のフォーマット文字列で%r
が%v
に変更されました。%r
はGoのfmt
パッケージには存在しない動詞であり、新しいgovet
チェックによって検出されます。%v
は任意の値をデフォルトのフォーマットで出力する動詞です。
これらの変更により、govet
は printf
系関数のフォーマット文字列の誤用をより早期に、かつ正確に検出できるようになり、Goコードの品質と堅牢性が向上します。
コアとなるコードの変更箇所
src/cmd/govet/print.go
--- a/src/cmd/govet/print.go
+++ b/src/cmd/govet/print.go
@@ -67,7 +67,7 @@ func (f *File) checkPrintf(call *ast.CallExpr, name string, skip int) {
if !ok {
// Too hard to check.
if *verbose {
- f.Warn(call.Pos(), "can't check args for call to", name)
+ f.Warn(call.Pos(), "can't check non-literal format in call to", name)
}
return
}
@@ -85,7 +85,7 @@ func (f *File) checkPrintf(call *ast.CallExpr, name string, skip int) {
for i, w := 0, 0; i < len(lit.Value); i += w {
w = 1
if lit.Value[i] == '%' {
- nbytes, nargs := parsePrintfVerb(lit.Value[i:])
+ nbytes, nargs := f.parsePrintfVerb(call, lit.Value[i:])
w = nbytes
numArgs += nargs
}
@@ -99,8 +99,9 @@ func (f *File) checkPrintf(call *ast.CallExpr, name string, skip int) {
// parsePrintfVerb returns the number of bytes and number of arguments
// consumed by the Printf directive that begins s, including its percent sign
// and verb.
-func parsePrintfVerb(s string) (nbytes, nargs int) {
+func (f *File) parsePrintfVerb(call *ast.CallExpr, s string) (nbytes, nargs int) {
// There's guaranteed a percent sign.
+ flags := make([]byte, 0, 5)
nbytes = 1
end := len(s)
// There may be flags.
@@ -108,6 +109,7 @@ FlagLoop:
for nbytes < end {
switch s[nbytes] {
case '#', '0', '+', '-', ' ':
+ flags = append(flags, s[nbytes])
nbytes++
default:
break FlagLoop
@@ -127,6 +129,7 @@ FlagLoop:
getNum()
// If there's a period, there may be a precision.
if nbytes < end && s[nbytes] == '.' {
+ flags = append(flags, '.') // Treat precision as a flag.
nbytes++
getNum()
}
@@ -135,10 +138,70 @@ FlagLoop:
nbytes += w
if c != '%' {
nargs++
+ f.checkPrintfVerb(call, c, flags)
}
return
}
+type printVerb struct {
+ verb rune
+ flags string // known flags are all ASCII
+}
+
+// Common flag sets for printf verbs.
+const (
+ numFlag = " -+.0"
+ sharpNumFlag = " -+.0#"
+ allFlags = " -+.0#"
+)
+
+// printVerbs identifies which flags are known to printf for each verb.
+// TODO: A type that implements Formatter may do what it wants, and govet
+// will complain incorrectly.
+var printVerbs = []printVerb{
+ // '-' is a width modifier, always valid.
+ // '.' is a precision for float, max width for strings.
+ // '+' is required sign for numbers, Go format for %v.
+ // '#' is alternate format for several verbs.
+ // ' ' is spacer for numbers
+ {'b', numFlag},
+ {'c', "-"},
+ {'d', numFlag},
+ {'e', "-."},
+ {'E', numFlag},
+ {'f', numFlag},
+ {'F', numFlag},
+ {'g', numFlag},
+ {'G', numFlag},
+ {'o', sharpNumFlag},
+ {'p', "-#"},
+ {'q', "-+#."},
+ {'s', "-."},
+ {'t', "-"},
+ {'T', "-"},
+ {'U', "-#"},
+ {'v', allFlags},
+ {'x', sharpNumFlag},
+ {'X', sharpNumFlag},
+}
+
+const printfVerbs = "bcdeEfFgGopqstTvxUX"
+
+func (f *File) checkPrintfVerb(call *ast.CallExpr, verb rune, flags []byte) {
+ // Linear scan is fast enough for a small list.
+ for _, v := range printVerbs {
+ if v.verb == verb {
+ for _, flag := range flags {
+ if !strings.ContainsRune(v.flags, rune(flag)) {
+ f.Badf(call.Pos(), "unrecognized printf flag for verb %q: %q", verb, flag)
+ }
+ }
+ return
+ }
+ }
+ f.Badf(call.Pos(), "unrecognized printf verb %q", verb)
+}
+
// checkPrint checks a call to an unformatted print routine such as Println.
// The skip argument records how many arguments to ignore; that is,
// call.Args[skip] is the first argument to be printed.
@@ -183,6 +246,8 @@ func BadFunctionUsedInTests() {
f := new(File)
f.Warn(0, "%s", "hello", 3) // ERROR "possible formatting directive in Warn call"
f.Warnf(0, "%s", "hello", 3) // ERROR "wrong number of args in Warnf call"
+ f.Warnf(0, "%r", "hello") // ERROR "unrecognized printf verb"
+ f.Warnf(0, "%#s", "hello") // ERROR "unrecognized printf flag"
}
type BadTypeUsedInTests struct {
src/pkg/encoding/xml/marshal_test.go
--- a/src/pkg/encoding/xml/marshal_test.go
+++ b/src/pkg/encoding/xml/marshal_test.go
@@ -394,7 +394,7 @@ func TestUnmarshal(t *testing.T) {
if err != nil {
t.Errorf("#%d: unexpected error: %#v", i, err)
} else if got, want := dest, test.Value; !reflect.DeepEqual(got, want) {
- t.Errorf("#%d: unmarshal(%#s) = %#v, want %#v", i, test.ExpectXML, got, want)
+ t.Errorf("#%d: unmarshal(%q) = %#v, want %#v", i, test.ExpectXML, got, want)
}
}
}
src/pkg/net/http/readrequest_test.go
--- a/src/pkg/net/http/readrequest_test.go
+++ b/src/pkg/net/http/readrequest_test.go
@@ -219,7 +219,7 @@ func TestReadRequest(t *testing.T) {
if body != tt.Body {
t.Errorf("#%d: Body = %q want %q", i, body, tt.Body)
}
if !reflect.DeepEqual(tt.Trailer, req.Trailer) {
- t.Errorf("%#d. Trailers differ.\n got: %v\nwant: %v", i, req.Trailer, tt.Trailer)
+ t.Errorf("#%d. Trailers differ.\n got: %v\nwant: %v", i, req.Trailer, tt.Trailer)
}
}
}
src/pkg/net/textproto/reader_test.go
--- a/src/pkg/net/textproto/reader_test.go
+++ b/src/pkg/net/textproto/reader_test.go
@@ -203,7 +203,7 @@ func TestRFC959Lines(t *testing.T) {
if code != tt.wantCode {
t.Errorf("#%d: code=%d, want %d", i, code, tt.wantCode)
}
if msg != tt.wantMsg {
- t.Errorf("%#d: msg=%q, want %q", i, msg, tt.wantMsg)
+ t.Errorf("#%d: msg=%q, want %q", i, msg, tt.wantMsg)
}
}
}
src/pkg/os/os_test.go
--- a/src/pkg/os/os_test.go
+++ b/src/pkg/os/os_test.go
@@ -919,7 +919,7 @@ func TestReadAt(t *testing.T) {
b := make([]byte, 5)
n, err := f.ReadAt(b, 7)
if err != nil || n != len(b) {
- t.Fatalf("ReadAt 7: %d, %r", n, err)
+ t.Fatalf("ReadAt 7: %d, %v", n, err)
}
if string(b) != "world" {
t.Fatalf("ReadAt 7: have %q want %q", string(b), "world")
コアとなるコードの解説
このコミットの核となる変更は、src/cmd/govet/print.go
ファイル内の checkPrintfVerb
メソッドと、それをサポートするデータ構造 printVerb
および printVerbs
の導入です。
-
printVerb
構造体とprintVerbs
変数:printVerb
は、printf
の各動詞(verb
)と、その動詞に適用可能なフラグの集合(flags
)を定義します。flags
は文字列として表現され、例えばnumFlag = " -+.0"
は、数値関連の動詞(%d
,%f
など)に対して、スペース、ハイフン、プラス、ピリオド、ゼロのフラグが有効であることを示します。printVerbs
は、Goのfmt
パッケージがサポートする全ての標準的なprintf
動詞と、それらに対応する有効なフラグの組み合わせを網羅したテーブルです。このテーブルは、govet
がフォーマット文字列の妥当性を検証するための参照点となります。
-
parsePrintfVerb
メソッドの変更:- このメソッドは、フォーマット文字列から
%
で始まる部分を解析し、動詞とフラグを分離する役割を担います。 - 変更点として、解析中に見つかったフラグ(例:
#
,0
,+
,-
,.
)をflags
という[]byte
スライスに逐次追加するようになりました。これにより、動詞だけでなく、その動詞に付随する全てのフラグを正確に抽出できるようになります。 - メソッドのシグネチャが
func (f *File) parsePrintfVerb(call *ast.CallExpr, s string) (nbytes, nargs int)
に変更され、*File
のメソッドとなり、call *ast.CallExpr
を引数に取ることで、エラー報告時に正確なソースコード位置を提供できるようになりました。
- このメソッドは、フォーマット文字列から
-
checkPrintfVerb
メソッドの追加:- このメソッドは、
parsePrintfVerb
によって抽出されたverb
とflags
を受け取り、printVerbs
テーブルと照合して妥当性を検証します。 - まず、与えられた
verb
がprintVerbs
テーブルに存在するかどうかを確認します。存在しない場合(例:%r
のような未知の動詞)、f.Badf
を呼び出して「unrecognized printf verb」エラーを報告します。 - 次に、
verb
がテーブルに見つかった場合、抽出された各flag
が、そのverb
に対応するv.flags
文字列に含まれているか (strings.ContainsRune
) をチェックします。含まれていない場合(例:%#s
のような無効なフラグ)、f.Badf
を呼び出して「unrecognized printf flag for verb %q: %q」エラーを報告します。
- このメソッドは、
これらの変更により、govet
は printf
系関数のフォーマット文字列の構文エラーを、実行時ではなく静的に検出できるようになり、開発者はより早期に問題を特定し、修正することが可能になります。これは、Go言語の堅牢性と開発効率の向上に大きく貢献する機能追加です。
関連リンク
- Go言語の
fmt
パッケージのドキュメント: https://pkg.go.dev/fmt govet
のドキュメント(go doc cmd/vet
またはgo tool vet -help
で確認可能)- Go言語のIssue #1654: https://github.com/golang/go/issues/1654 (このコミットが修正したIssue)
- Gerrit Change-ID 5489060: https://golang.org/cl/5489060 (このコミットの元の変更リスト)
参考にした情報源リンク
- 上記の関連リンクに記載されたGo言語の公式ドキュメント、Issueトラッカー、Gerritの変更リスト。
- Go言語のソースコード(特に
src/fmt/print.go
はprintf
の実装詳細を理解する上で参考になります)。 - 静的解析に関する一般的な知識。