[インデックス 14567] ファイルの概要
このコミットは、Go言語の公式ツールである go vet
の print.go
ファイルに対する変更です。print.go
は go vet
の一部として、Printf
のようなフォーマット関数呼び出しにおける引数の数や型の誤りを検出する役割を担っています。具体的には、フォーマット文字列とそれに続く引数の整合性をチェックし、不一致があれば警告を発します。
コミット
commit 82f2b36e74a3ae7c5f7ca1cd762214e8ccb555b8
Author: David Symonds <dsymonds@golang.org>
Date: Thu Dec 6 15:17:31 2012 +1100
vet: be less strict about number of arguments when a ... is present.
R=golang-dev
CC=golang-dev
https://golang.org/cl/6883046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/82f2b36e74a3ae7c5f7ca1cd762214e8ccb555b8
元コミット内容
このコミットは、go vet
ツールが Printf
のような可変引数(variadic arguments、Go言語では ...
で表現される)を持つ関数呼び出しをチェックする際の厳格さを緩和することを目的としています。具体的には、可変引数を持つ関数に対して、引数の数が厳密に一致しない場合でも、可変引数スライスが展開されている(...
が使われている)場合はエラーとしないように変更されています。
変更の背景
go vet
は、Goプログラムの潜在的なバグや疑わしい構成を検出するための静的解析ツールです。特に Printf
系の関数呼び出しにおいては、フォーマット文字列と引数の数が一致しない場合に警告を出す機能があります。しかし、Go言語の可変引数機能は、スライスを ...
を使って展開することで、個々の要素を引数として渡すことができます。
このコミット以前の go vet
は、このような可変引数の展開が行われた場合でも、期待される引数の数と実際に渡された引数の数を厳密に比較していました。これにより、fmt.Printf("%q %q", multi()...)
のような正当なコードであっても、「引数の数が間違っている」という誤った警告(false positive)を生成してしまう問題がありました。
この変更は、このような誤検出を解消し、go vet
がより正確な診断を行うようにするために導入されました。可変引数が展開されている場合は、引数の数が期待値以上であれば許容することで、ツールの利便性と正確性を向上させています。
前提知識の解説
go vet
go vet
は、Go言語のソースコードを静的に解析し、潜在的なバグや疑わしいコード構成を報告するツールです。コンパイルは通るものの、実行時に問題を引き起こす可能性のあるコードパターン(例: Printf
のフォーマット文字列と引数の不一致、到達不能なコード、ロックの誤用など)を検出します。開発者がコードの品質を維持し、早期に問題を特定するのに役立ちます。
可変引数(Variadic Functions)
Go言語では、関数の最後のパラメータに ...
を付けることで、任意の数の引数を受け取ることができます。これを可変引数関数と呼びます。関数内では、可変引数はその型のスライスとして扱われます。
例:
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
この sum
関数は sum(1, 2, 3)
のように呼び出すこともできますし、nums := []int{1, 2, 3}; sum(nums...)
のようにスライスを展開して渡すこともできます。後者の場合、スライスの各要素が個別の引数として関数に渡されます。
ast
パッケージ
Go言語の go/ast
パッケージは、Goプログラムの抽象構文木(Abstract Syntax Tree, AST)を表現するための型と関数を提供します。go vet
のような静的解析ツールは、このASTを解析することで、コードの構造や意味を理解し、特定のパターンを検出します。
このコミットで参照されている ast.CallExpr
は関数呼び出しを表すASTノードです。また、call.Ellipsis
は、関数呼び出しが可変引数を展開しているかどうか(つまり ...
が使われているか)を示す情報を含んでいます。IsValid()
メソッドは、その情報が有効であるか、つまり ...
が存在するかどうかをチェックします。
Printf
フォーマット
fmt.Printf
や log.Printf
など、Go言語にはC言語の printf
に似たフォーマット関数が多数存在します。これらの関数は、最初の引数にフォーマット文字列を取り、その後にフォーマット指定子(例: %d
, %s
, %v
など)に対応する引数を取ります。go vet
は、このフォーマット文字列と後続の引数の数や型が一致しているかをチェックします。
技術的詳細
この変更は、src/cmd/vet/print.go
内の checkPrintf
関数に焦点を当てています。この関数は、Printf
系の関数呼び出しを解析し、引数の数を検証するロジックを含んでいます。
変更前のロジックでは、expect := len(call.Args) - (skip + 1)
で期待される引数の数を計算し、if numArgs != expect
で厳密に比較していました。ここで numArgs
は実際に渡された引数の数です。
変更後のコードでは、この厳密な比較の前に、可変引数の展開が行われているかどうかをチェックする条件が追加されました。
if call.Ellipsis.IsValid() && numArgs >= expect
call.Ellipsis.IsValid()
: これは、現在の関数呼び出し (call
) が可変引数を展開しているかどうか(つまり、multi()...
のように...
演算子を使用しているか)をチェックします。ast.CallExpr
構造体のEllipsis
フィールドは、...
演算子の位置情報(token.Pos
)を保持しており、IsValid()
はその位置情報が有効であるか(つまり...
が存在するか)を返します。numArgs >= expect
:numArgs
は実際に渡された引数の数、expect
はフォーマット文字列から計算された期待される引数の数です。可変引数が展開されている場合、実際に渡される引数の数は期待される数以上であれば問題ないというロジックです。例えば、fmt.Printf("%q %q", multi()...)
の場合、multi()
が返すスライスの要素数が2つ以上であれば、フォーマット文字列と一致すると見なされます。
この条件が true
の場合、つまり可変引数が展開されており、かつ実際に渡された引数の数が期待値以上であれば、return
ステートメントが実行され、それ以降の厳密な引数数チェックはスキップされます。これにより、go vet
は正当な可変引数展開のケースで誤った警告を出さなくなります。
また、テストケースとして fmt.Printf("%q %q", multi()...)
が追加されており、これが ok
となるように動作が変更されたことが示されています。
コアとなるコードの変更箇所
--- a/src/cmd/vet/print.go
+++ b/src/cmd/vet/print.go
@@ -120,6 +120,10 @@ func (f *File) checkPrintf(call *ast.CallExpr, name string, skip int) {
}
}
expect := len(call.Args) - (skip + 1)
+ // Don't be too strict on dotdotdot.
+ if call.Ellipsis.IsValid() && numArgs >= expect {
+ return
+ }
if numArgs != expect {
f.Badf(call.Pos(), "wrong number of args in %s call: %d needed but %d args", name, numArgs, expect)
}
@@ -280,6 +284,7 @@ func BadFunctionUsedInTests() {\n fmt.Printf(\"%s%%%d\", \"hi\", 3) // correct\n fmt.Printf(\"%.*d\", 3, 3) // correct\n fmt.Printf(\"%.*d\", 3, 3, 3) // ERROR \"wrong number of args in Printf call\"\n+\tfmt.Printf(\"%q %q\", multi()...) // ok\n \tprintf(\"now is the time\", \"buddy\") // ERROR \"no formatting directive\"\n \tPrintf(\"now is the time\", \"buddy\") // ERROR \"no formatting directive\"\n \tPrintf(\"hi\") // ok\n@@ -297,3 +302,8 @@ func BadFunctionUsedInTests() {\n func printf(format string, args ...interface{}) {\n \tpanic(\"don\'t call - testing only\")\n }\n+\n+// multi is used by the test.\n+func multi() []interface{} {\n+\tpanic(\"don\'t call - testing only\")\n+}\n```
## コアとなるコードの解説
変更は主に `checkPrintf` 関数内で行われています。
1. **既存の引数数計算**:
`expect := len(call.Args) - (skip + 1)`
この行は、フォーマット文字列とそれに続く引数のうち、フォーマット指定子に対応する引数の期待数を計算しています。`call.Args` は関数呼び出しの全引数を表すASTノードのリストです。`skip` は、`Printf` のような関数でフォーマット文字列自体が引数リストの先頭にある場合など、チェック対象外の引数をスキップするためのオフセットです。
2. **可変引数展開のチェックと緩和ロジックの追加**:
```go
// Don't be too strict on dotdotdot.
if call.Ellipsis.IsValid() && numArgs >= expect {
return
}
```
このブロックが新たに追加された部分です。
- `call.Ellipsis.IsValid()`: 関数呼び出しが `...` 演算子を使って可変引数を展開しているかどうかを判定します。
- `numArgs >= expect`: 実際に渡された引数の数 (`numArgs`) が、フォーマット文字列から期待される引数の数 (`expect`) 以上であるかをチェックします。
この2つの条件が両方とも真である場合、つまり可変引数が展開されており、かつ引数の数が不足していない場合は、`checkPrintf` 関数はそこで処理を終了し、引数数の不一致に関する警告は発生しません。これにより、`fmt.Printf("%q %q", multi()...)` のような有効なコードが誤って警告されるのを防ぎます。
3. **厳密な引数数チェック(変更なし)**:
```go
if numArgs != expect {
f.Badf(call.Pos(), "wrong number of args in %s call: %d needed but %d args", name, numArgs, expect)
}
```
この部分は変更されていません。上記の緩和ロジックが適用されない場合(つまり、可変引数展開がない場合や、展開されていても引数が不足している場合)に、引き続き厳密な引数数のチェックが行われ、不一致があればエラーが報告されます。
4. **テストケースの追加**:
```go
+ fmt.Printf("%q %q", multi()...) // ok
```
`BadFunctionUsedInTests` 関数内に、`multi()` 関数が返すスライスを `...` で展開して `Printf` に渡す新しいテストケースが追加されました。この行には `// ok` とコメントされており、このコードが `go vet` によって警告されないことが期待されていることを示しています。
5. **`multi` 関数の追加**:
```go
+// multi is used by the test.
+func multi() []interface{} {
+ panic("don\'t call - testing only")
+}
```
上記のテストケースで使用されるダミーの `multi` 関数が追加されました。これは `[]interface{}` 型のスライスを返す関数で、テスト目的のみに使用されます。
これらの変更により、`go vet` は可変引数展開のケースをより適切に処理できるようになり、誤検出が減少しました。
## 関連リンク
* Go言語の `go vet` ドキュメント: [https://pkg.go.dev/cmd/vet](https://pkg.go.dev/cmd/vet)
* Go言語の可変引数に関する公式ドキュメント(Effective Goより): [https://go.dev/doc/effective_go#variadic](https://go.dev/doc/effective_go#variadic)
* Go言語の `go/ast` パッケージ: [https://pkg.go.dev/go/ast](https://pkg.go.dev/go/ast)
## 参考にした情報源リンク
* Go言語のコミット履歴 (GitHub): [https://github.com/golang/go/commits/master](https://github.com/golang/go/commits/master)
* Go Code Review Comments (golang.org/cl/6883046 に関連する可能性のあるコードレビューシステム): [https://go.dev/wiki/CodeReviewComments](https://go.dev/wiki/CodeReviewComments)
* Go言語の `fmt` パッケージ: [https://pkg.go.dev/fmt](https://pkg.go.dev/fmt)
* Go言語の `token` パッケージ: [https://pkg.go.dev/go/token](https://pkg.go.dev/go/token)
* Go言語の `ast.CallExpr` 構造体に関する情報: [https://pkg.go.dev/go/ast#CallExpr](https://pkg.go.dev/go/ast#CallExpr)
* Go言語の `ast.Field` 構造体に関する情報 (Ellipsisフィールドの型): [https://pkg.go.dev/go/ast#Field](https://pkg.go.dev/go/ast#Field)
* Go言語の `token.Pos` 型に関する情報: [https://pkg.go.dev/go/token#Pos](https://pkg.go.dev/go/token#Pos)
* Go言語の `token.Pos.IsValid()` メソッドに関する情報: [https://pkg.go.dev/go/token#Pos.IsValid](https://pkg.go.dev/go/token#Pos.IsValid)