[インデックス 15061] ファイルの概要
このコミットは、Go言語の静的解析ツールである cmd/vet において、printf フォーマット文字列のチェック機能が、文字列定数の結合(+演算子による連結)によって生成されたフォーマット文字列を正しく扱えるように改善するものです。これにより、コンパイル時には検出されないが実行時に問題を引き起こす可能性のあるフォーマット文字列の誤用を、vet ツールがより正確に指摘できるようになります。
コミット
commit 87c3c1b0020280fe216933501c96a913eaaafb2b
Author: Russ Cox <rsc@golang.org>
Date: Thu Jan 31 07:53:38 2013 -0800
cmd/vet: handle added string constants in printf format check
Fixes #4599.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7226067
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/87c3c1b0020280fe216933501c96a913eaaafb2b
元コミット内容
cmd/vet: handle added string constants in printf format check
このコミットは、cmd/vet ツールが printf フォーマットチェックを行う際に、結合された文字列定数を適切に処理できるようにするものです。Issue #4599 を修正します。
変更の背景
Go言語には go vet という静的解析ツールがあり、コードの潜在的なバグや疑わしい構成を検出します。その機能の一つに、fmt.Printf などのフォーマット関数に渡されるフォーマット文字列と引数の整合性をチェックする機能があります。例えば、fmt.Printf("%s", 123) のように文字列フォーマット指定子に整数を渡すと、vet は警告を発します。
しかし、このコミット以前の vet ツールは、フォーマット文字列が複数の文字列リテラルを + 演算子で結合して作られている場合に、その結合された文字列を正しく解析できませんでした。例えば、fmt.Printf("%" + "s", "hi") のようなコードがあった場合、vet は %s というフォーマット文字列を認識できず、結果として引数のチェックを適切に行えませんでした。
この問題は、Fixes #4599 と記載されているように、GoのIssueトラッカーで報告された問題(Issue 4599)を解決するために行われました。ただし、現在のGoのIssueトラッカーではIssue 4599は直接見つかりませんでした。これは、非常に古いIssueであるか、番号が変更された可能性があります。しかし、コミットメッセージから、文字列結合によって生成されたフォーマット文字列が vet で正しくチェックされないという具体的な問題があったことがわかります。
この変更の目的は、vet ツールの精度を高め、開発者がより堅牢なコードを書けるように支援することです。特に、文字列結合はGoのコードで頻繁に行われる操作であり、これによって printf フォーマットチェックが迂回されてしまうのは望ましくありませんでした。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とツールに関する知識が必要です。
go vet: Go言語の静的解析ツールです。コンパイルは通るが、実行時に問題を引き起こす可能性のあるコード(例:printfフォーマット文字列と引数の不一致、到達不能なコード、ロックの誤用など)を検出します。開発ワークフローにおいて、早期に潜在的なバグを発見するために非常に有用です。printfフォーマット文字列:fmt.Printf,fmt.Sprintfなどの関数で使用される文字列で、出力の書式を指定します。%s(文字列),%d(整数),%f(浮動小数点数) などのフォーマット指定子が含まれます。go/astパッケージ: Go言語のソースコードを抽象構文木(AST: Abstract Syntax Tree)として表現するためのパッケージです。コンパイラや静的解析ツールがGoのコードを解析する際に使用します。ast.Expr: 抽象構文木における式を表すインターフェースです。ast.BasicLit: 基本リテラル(数値、文字列、文字、浮動小数点数)を表す構造体です。Valueフィールドにリテラルの文字列値が、Kindフィールドにリテラルの種類(token.STRINGなど)が格納されます。ast.ParenExpr: 括弧で囲まれた式(例:(a + b))を表す構造体です。ast.BinaryExpr: 二項演算子(例:a + b,x * y)を含む式を表す構造体です。Opフィールドに演算子(token.ADDなど)が、XとYフィールドに左右のオペランドが格納されます。ast.Ident: 識別子(変数名、関数名など)を表す構造体です。
go/tokenパッケージ: Go言語の字句解析(トークン化)で使われるトークン(キーワード、識別子、演算子など)を定義するパッケージです。token.ADD:+演算子を表すトークンです。token.STRING: 文字列リテラルを表すトークンです。
strconvパッケージ: 文字列と基本的なデータ型(数値、真偽値など)の間で変換を行うためのパッケージです。strconv.Unquote(s string) (string, error): クォートされた文字列リテラル(例:"hello")からクォートを取り除き、その内容を返す関数です。strconv.Quote(s string) string: 文字列をGoの文字列リテラル形式にクォートする関数です。
stringsパッケージ: 文字列操作のためのユーティリティ関数を提供するパッケージです。strings.Contains(s, substr string) bool: 文字列sが部分文字列substrを含むかどうかをチェックします。
技術的詳細
このコミットの核心は、go vet が printf フォーマット文字列を解析する際に、単一の文字列リテラルだけでなく、複数の文字列リテラルが + 演算子で結合された形式も正しく解釈できるようにすることです。
以前の vet は、フォーマット文字列が ast.BasicLit 型の単一の文字列リテラルであることを期待していました。しかし、"foo" + "bar" のような式は、AST上では ast.BinaryExpr として表現されます。この BinaryExpr の Op が token.ADD であり、かつ左右のオペランドが文字列リテラルである場合、それらを結合して一つの論理的な文字列リテラルとして扱う必要があります。
この変更は、src/cmd/vet/print.go ファイル内の literal 関数と checkPrintf 関数に影響を与えます。
literal関数の拡張:literal関数は、与えられたast.Exprから基本リテラル(特に文字列リテラル)を抽出することを目的としています。このコミットでは、ast.ParenExpr(括弧で囲まれた式)とast.BinaryExpr(二項演算子式)のケースが追加されました。ast.ParenExprの場合、括弧の中の式を再帰的にliteral関数で処理します。ast.BinaryExprの場合、演算子が+(token.ADD)であり、かつ左右のオペランドが両方とも文字列リテラルである場合に、それらの文字列をstrconv.Unquoteでアンクォートし、結合し、再度strconv.Quoteでクォートして新しいast.BasicLitを生成します。これにより、"a" + "b"はvetの内部で"ab"として扱われるようになります。
checkPrintf関数の修正:checkPrintf関数は、printfフォーマット文字列の妥当性をチェックします。以前は、フォーマット文字列がast.BasicLitであることを確認した後、そのValueフィールドを直接フォーマット文字列として使用していました。しかし、ast.BasicLit.Valueはクォートされた文字列(例:"\"%s\"")であるため、strconv.Unquoteを使って実際の文字列値(例:"%s")を取得する必要があります。この修正により、vetはクォートされた文字列リテラルから正しいフォーマット文字列を抽出し、その後の解析を正確に行えるようになります。
これらの変更により、vet は fmt.Printf("%" + ("s"), "hi", 3) のような、文字列結合を含む printf 呼び出しに対しても、引数の数が間違っていることを正しく検出できるようになります。
コアとなるコードの変更箇所
変更は src/cmd/vet/print.go ファイルに集中しています。
--- a/src/cmd/vet/print.go
+++ b/src/cmd/vet/print.go
@@ -11,6 +11,7 @@ import (
"fmt"
"go/ast"
"go/token"
+ "strconv"
"strings"
"unicode/utf8"
)
@@ -62,6 +63,23 @@ func (f *File) literal(value ast.Expr) *ast.BasicLit {
switch v := value.(type) {
case *ast.BasicLit:
return v
+ case *ast.ParenExpr:
+ return f.literal(v.X)
+ case *ast.BinaryExpr:
+ if v.Op != token.ADD {
+ break
+ }
+ litX := f.literal(v.X)
+ litY := f.literal(v.Y)
+ if litX != nil && litY != nil {
+ lit := *litX
+ x, errX := strconv.Unquote(litX.Value)
+ y, errY := strconv.Unquote(litY.Value)
+ if errX == nil && errY == nil {
+ lit.Value = strconv.Quote(x + y)
+ return &lit
+ }
+ }
case *ast.Ident:
// See if it's a constant or initial value (we can't tell the difference).
if v.Obj == nil || v.Obj.Decl == nil {
@@ -101,7 +119,10 @@ func (f *File) checkPrintf(call *ast.CallExpr, name string, skip int) {
if lit.Kind != token.STRING {
f.Badf(call.Pos(), "literal %v not a string in call to", lit.Value, name)
}\n- format := lit.Value
+ format, err := strconv.Unquote(lit.Value)
+ if err != nil {
+ f.Badf(call.Pos(), "invalid quoted string literal")
+ }
if !strings.Contains(format, "%") {
if len(call.Args) > skip+1 {
f.Badf(call.Pos(), "no formatting directive in %s call", name)
@@ -282,6 +303,7 @@ func BadFunctionUsedInTests() {
fmt.Println() // not an error
fmt.Println("%s", "hi") // ERROR "possible formatting directive in Println call"
fmt.Printf("%s", "hi", 3) // ERROR "wrong number of args in Printf call"
+ fmt.Printf("%"+("s"), "hi", 3) // ERROR "wrong number of args in Printf call"
fmt.Printf("%s%%%d", "hi", 3) // correct
fmt.Printf("%08s", "woo") // correct
fmt.Printf("% 8s", "woo") // correct
コアとなるコードの解説
literal 関数の変更
func (f *File) literal(value ast.Expr) *ast.BasicLit {
switch v := value.(type) {
case *ast.BasicLit:
return v
case *ast.ParenExpr:
return f.literal(v.X) // 括弧内の式を再帰的に処理
case *ast.BinaryExpr:
if v.Op != token.ADD { // 演算子が '+' でない場合は処理しない
break
}
litX := f.literal(v.X) // 左オペランドをリテラルとして取得
litY := f.literal(v.Y) // 右オペランドをリテラルとして取得
if litX != nil && litY != nil {
lit := *litX // 新しいリテラルを左オペランドのコピーから作成
x, errX := strconv.Unquote(litX.Value) // 左オペランドの文字列をアンクォート
y, errY := strconv.Unquote(litY.Value) // 右オペランドの文字列をアンクォート
if errX == nil && errY == nil { // 両方とも正常にアンクォートできたら
lit.Value = strconv.Quote(x + y) // 結合して再度クォートし、新しいリテラル値とする
return &lit
}
}
// ... (ast.Ident などの既存のケース)
}
return nil
}
この変更により、literal 関数は以下の種類の式から文字列リテラルを抽出できるようになりました。
*ast.BasicLit: 既存の動作。単一の文字列リテラルをそのまま返します。*ast.ParenExpr:("foo")のように括弧で囲まれた式の場合、括弧の中の式v.Xを再帰的にliteral関数に渡して処理します。*ast.BinaryExpr:+演算子による文字列結合を処理します。v.Op != token.ADDで、演算子が+でない場合は処理をスキップします。litX := f.literal(v.X)とlitY := f.literal(v.Y)で、左右のオペランドを再帰的にliteral関数で処理し、それぞれが文字列リテラルであることを確認します。strconv.Unquoteを使用して、クォートされた文字列リテラル(例:"\"foo\"")から実際の文字列値(例:"foo")を抽出します。- 抽出した文字列
xとyを結合し、strconv.Quoteで再度クォートして、新しいast.BasicLitのValueフィールドに設定します。これにより、"a" + "b"はvetの内部で"ab"という単一の文字列リテラルとして扱われるようになります。
checkPrintf 関数の変更
func (f *File) checkPrintf(call *ast.CallExpr, name string, skip int) {
// ... (既存のコード)
if lit.Kind != token.STRING {
f.Badf(call.Pos(), "literal %v not a string in call to", lit.Value, name)
}
// 変更点: lit.Value はクォートされた文字列なので、Unquote する必要がある
format, err := strconv.Unquote(lit.Value)
if err != nil {
f.Badf(call.Pos(), "invalid quoted string literal")
}
if !strings.Contains(format, "%") {
// ... (既存のコード)
}
// ... (既存のコード)
}
この変更は、checkPrintf 関数が printf フォーマット文字列を扱う方法を修正します。
- 以前は
format := lit.Valueと直接代入していましたが、lit.Valueは"で囲まれたクォート済みの文字列リテラル(例:"\"%s\"")でした。 - 新しいコードでは
format, err := strconv.Unquote(lit.Value)を使用して、lit.Valueから実際の文字列値(例:"%s")を抽出します。 strconv.Unquoteがエラーを返した場合(無効なクォート文字列の場合)、f.Badfを呼び出してエラーを報告します。- これにより、
strings.Contains(format, "%")などの後続のフォーマット文字列解析が、正しい非クォート文字列に対して行われるようになり、vetのprintfフォーマットチェックの精度が向上します。
テストケースの追加
コミットの最後には、新しいテストケースが追加されています。
fmt.Printf("%"+("s"), "hi", 3) // ERROR "wrong number of args in Printf call"
この行は、% と ("s") という文字列結合によってフォーマット文字列が生成されるケースを示しています。このコミット以前は、vet はこの行をエラーとして検出できませんでしたが、変更後は wrong number of args in Printf call というエラーを正しく報告するようになります。これは、%s というフォーマット指定子に対して引数が2つ("hi" と 3)渡されており、%s は1つの引数しか期待しないためです。
関連リンク
- Go Change-Id:
7226067(Gerrit Code Review): https://golang.org/cl/7226067
参考にした情報源リンク
- Go言語の
go/astパッケージのドキュメント: https://pkg.go.dev/go/ast - Go言語の
go/tokenパッケージのドキュメント: https://pkg.go.dev/go/token - Go言語の
strconvパッケージのドキュメント: https://pkg.go.dev/strconv - Go言語の
stringsパッケージのドキュメント: https://pkg.go.dev/strings go vetコマンドのドキュメント (Go公式ブログなど): https://go.dev/blog/go-vet (一般的なgo vetの情報源)- Go言語の
fmtパッケージのドキュメント: https://pkg.go.dev/fmt