[インデックス 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