[インデックス 15392] ファイルの概要
このコミットは、Go言語の静的解析ツールである cmd/vet
における、Error
メソッドの誤検出を修正するものです。具体的には、error
インターフェースの Error()
メソッドと、testing
パッケージの (*T).Error()
のような、Error
という名前を持つが error
インターフェースとは異なるシグネチャを持つメソッドを正確に区別し、適切なチェックを行うように改善されています。これにより、cmd/vet
が誤った警告を発することを防ぎます。
コミット
commit 4434212f1558c124e1823d3d7279ed63a71a31b8
Author: Rob Pike <r@golang.org>
Date: Fri Feb 22 17:16:31 2013 -0800
cmd/vet: use types to test Error methods correctly.
We need go/types to discriminate the Error method from
the error interface and the Error method of the testing package.
Fixes #4753.
R=golang-dev, bradfitz, gri
CC=golang-dev
https://golang.org/cl/7396054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4434212f1558c124e1823d3d7279ed63a71a31b8
元コミット内容
cmd/vet
ツールが Error
メソッドを正しくテストするために、go/types
パッケージを使用するように変更されました。これは、error
インターフェースの Error
メソッドと、testing
パッケージの Error
メソッドを区別する必要があるためです。この変更は、Issue #4753 を修正します。
変更の背景
cmd/vet
は、Go言語のコードベースにおける潜在的なバグや疑わしい構造を検出するための静的解析ツールです。特に、fmt.Printf
のようなフォーマット文字列を使用する関数呼び出しにおいて、フォーマット指定子と引数の型が一致しないなどの問題を検出する機能を持っています。
このコミット以前の cmd/vet
は、Error()
という名前のメソッド呼び出しに対して、そのメソッドが printf
スタイルのフォーマット文字列と引数を取る可能性があると誤って判断し、警告を発することがありました。しかし、Go言語の組み込み error
インターフェースが持つ Error() string
メソッドは、引数を取らず、エラーメッセージを表す文字列を返します。このメソッドは printf
スタイルのチェックの対象となるべきではありません。
一方で、testing
パッケージの (*T).Error()
や (*T).Errorf()
のようなメソッドは、テストの失敗を報告するために使用され、printf
スタイルのフォーマット文字列と引数を取ります。これらのメソッドは cmd/vet
の printf
スタイルのチェックの対象となるべきです。
問題は、cmd/vet
がこれら二種類の Error
メソッドを区別できず、error
インターフェースの Error()
メソッド呼び出しに対しても誤って警告を出していた点にありました (Issue #4753)。このコミットは、go/types
パッケージを利用して、メソッドの実際の型情報に基づいて正確な判別を行うことで、この誤検出を解消することを目的としています。
前提知識の解説
Go言語の error
インターフェース
Go言語において、エラー処理は error
という組み込みインターフェースによって行われます。このインターフェースは非常にシンプルで、以下のように定義されています。
type error interface {
Error() string
}
つまり、任意の型が Error() string
というシグネチャ(引数なし、文字列を返す)を持つメソッドを実装していれば、その型は error
インターフェースを満たします。このメソッドは、エラーの詳細を記述した文字列を返します。
cmd/vet
ツール
cmd/vet
は、Goのソースコードを静的に分析し、疑わしい構造や潜在的なバグを報告するツールです。例えば、以下のような問題を検出します。
printf
フォーマット文字列と引数の不一致- 構造体タグの誤り
- ロックの誤用
- 未使用の変数やインポート(
go vet
はgo build
の一部として実行されるgo tool vet
のラッパーであり、go build
が検出しないようなより深い静的解析を行います)
このツールは、コードの品質と信頼性を向上させるために非常に有用です。
go/types
パッケージ
go/types
パッケージは、Goプログラムの型情報を扱うための標準ライブラリです。このパッケージは、Goのコンパイラが内部的に使用する型システムを公開しており、抽象構文木 (AST) と組み合わせて使用することで、プログラム内の各識別子や式の正確な型情報を取得できます。
go/types
を使用することで、以下のようなことが可能になります。
- 変数、関数、メソッド、インターフェースなどの型を特定する。
- 型が特定のインターフェースを満たすかどうかを判定する。
- 式の評価結果の型を取得する。
- パッケージ間の依存関係を解析する。
cmd/vet
のような静的解析ツールにとって、正確な型情報は不可欠であり、このコミットでは go/types
を利用して Error
メソッドの正確なシグネチャを判別しています。
go/ast
パッケージ
go/ast
パッケージは、Goのソースコードを抽象構文木 (AST) として表現するためのデータ構造を提供します。ソースコードはまず字句解析され、トークン列に変換された後、構文解析されてASTが構築されます。ASTは、プログラムの構造を木構造で表現したもので、各ノードが式、文、宣言などを表します。
cmd/vet
は、このASTを走査することでコードの構造を理解し、問題のあるパターンを検出します。go/types
パッケージは、このASTのノードと型情報を関連付けることで、より高度な解析を可能にします。
技術的詳細
このコミットの核心は、cmd/vet
が go/types
パッケージを利用して、Error
という名前のメソッド呼び出しが、Goの組み込み error
インターフェースの Error() string
メソッドであるかどうかを正確に判別するロジックを導入した点にあります。
-
go/types
の導入と型情報の収集:src/cmd/vet/main.go
にPackage
という新しい構造体が導入されました。この構造体はmap[ast.Expr]types.Type
を持ち、ASTの各式 (ast.Expr
) に対応する型情報 (types.Type
) を格納します。doPackage
関数内で、go/types.Context
が初期化され、Expr
フィールドにコールバック関数exprFn
が設定されます。このexprFn
は、型チェック中に各式の型が解決されるたびに呼び出され、その式と型情報をpkg.types
マップに保存します。- これにより、
cmd/vet
は解析対象のパッケージ全体の型情報を事前に収集できるようになります。
-
File
構造体へのPackage
参照の追加:src/cmd/vet/main.go
のFile
構造体にpkg *Package
フィールドが追加されました。これにより、各ファイル (File
) が属するパッケージの型情報 (Package
) にアクセスできるようになります。
-
isErrorMethodCall
関数の導入:src/cmd/vet/print.go
にisErrorMethodCall(call *ast.CallExpr) bool
という新しいヘルパー関数が追加されました。この関数は、与えられた関数呼び出し (call
) が、error
インターフェースを満たすError() string
メソッドの呼び出しであるかを厳密に判定します。- 判定ロジックは以下のステップで行われます。
- セレクタ式であるか: メソッド呼び出しであるためには、
call.Fun
が*ast.SelectorExpr
である必要があります(例:e.Error()
のe.Error
部分)。関数呼び出し (f()
) の場合はfalse
を返します。 - 引数がないか:
error
インターフェースのError()
メソッドは引数を取りません。呼び出しに引数がある場合はfalse
を返します。 - メソッドの型情報の取得:
f.pkg.types[sel]
を使用して、セレクタ式 (sel
) の型情報を取得します。 - シグネチャの確認: 取得した型が
*types.Signature
であることを確認します。 - レシーバの有無: メソッドであるためには、シグネチャにレシーバ (
sig.Recv
) が存在する必要があります。レシーバがない場合は関数呼び出しとみなしfalse
を返します。 - 引数の数の確認: シグネチャの引数 (
sig.Params
) がゼロであることを確認します。 - 戻り値の数の確認: シグネチャの戻り値 (
sig.Results
) が1つであることを確認します。 - 戻り値の型の確認: 唯一の戻り値の型が、組み込みの
string
型 (types.Typ[types.String]
) と同一であるかをtypes.IsIdentical
を使用して確認します。
- セレクタ式であるか: メソッド呼び出しであるためには、
-
numArgsInSignature
関数の導入:src/cmd/vet/print.go
にnumArgsInSignature(call *ast.CallExpr) int
というヘルパー関数が追加されました。これは、関数呼び出しの対象となる関数の仮引数の数を返します。isErrorMethodCall
と同様に、f.pkg.types
を利用して型情報を取得し、シグネチャから引数の数を抽出します。
-
checkPrint
関数のロジック変更:src/cmd/vet/print.go
のcheckPrint
関数は、printf
スタイルの関数呼び出しをチェックする主要な関数です。この関数内で、引数が存在しない場合の処理 (if len(args) <= skip
) が変更されました。- 変更前は、
name != "Error"
の場合にのみ「引数がない」という警告を出していました。つまり、Error
という名前のメソッド呼び出しは、無条件にprintf
スタイルのチェックから除外されていました。 - 変更後は、まず
name == "Error"
かつf.pkg != nil
(型情報が利用可能) かつf.isErrorMethodCall(call)
がtrue
の場合、つまり**error
インターフェースを満たすError() string
メソッドの呼び出しである場合**は、すぐにreturn
してチェックをスキップします。 - それ以外の場合(
Error
メソッドだがerror
インターフェースを満たさない場合、またはError
以外のメソッドの場合)は、引き続きprintf
スタイルのチェックを行います。特に、testing
パッケージの(*T).Error()
のようなメソッドは、引数がない場合に警告の対象となります。
これらの変更により、cmd/vet
は Error
という名前のメソッド呼び出しに対して、そのメソッドが実際に error
インターフェースの一部であるかどうかを型情報に基づいて正確に判断し、適切な静的解析を行うことができるようになりました。
コアとなるコードの変更箇所
src/cmd/vet/main.go
--- a/src/cmd/vet/main.go
+++ b/src/cmd/vet/main.go
@@ -64,6 +64,7 @@ func Usage() {
// File is a wrapper for the state of a file used in the parser.
// The parse tree walkers are all methods of this type.
type File struct {
+ pkg *Package
fset *token.FileSet
name string
file *ast.File
@@ -158,6 +159,10 @@ func doPackageDir(directory string) {
doPackage(names)
}
+type Package struct {
+ types map[ast.Expr]types.Type
+}
+
// doPackage analyzes the single package constructed from the named files.
func doPackage(names []string) {
var files []*File
@@ -181,16 +186,21 @@ func doPackage(names []string) {
files = append(files, &File{fset: fs, name: name, file: parsedFile})
astFiles = append(astFiles, parsedFile)
}
+ pkg := new(Package)
+ pkg.types = make(map[ast.Expr]types.Type)
+ exprFn := func(x ast.Expr, typ types.Type, val interface{}) {
+ pkg.types[x] = typ
+ }
context := types.Context{
- // TODO: set up Expr, Ident.
+ Expr: exprFn,
}
// Type check the package.
- pkg, err := context.Check(fs, astFiles)
+ _, err := context.Check(fs, astFiles)
if err != nil {
warnf("%s", err)
}
- _ = pkg
for _, file := range files {
+ file.pkg = pkg
file.walkFile(file.name, file.file)
}
}
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"
+ "go/types"
"strconv"
"strings"
"unicode/utf8"
@@ -274,9 +275,18 @@ func (f *File) checkPrint(call *ast.CallExpr, name string, skip int) {
}
}
if len(args) <= skip {
- // TODO: check that the receiver of Error() is of type error.
- if !isLn && name != "Error" {
- f.Badf(call.Pos(), "no args in %s call", name)
+ // If we have a call to a method called Error that satisfies the Error interface,
+ // then it's ok. Otherwise it's something like (*T).Error from the testing package
+ // and we need to check it.
+ if name == "Error" && f.pkg != nil && f.isErrorMethodCall(call) {
+ return
+ }
+ // If it's an Error call now, it's probably for printing errors.
+ if !isLn {
+ // Check the signature to be sure: there are niladic functions called "error".
+ if f.pkg == nil || skip != 0 || f.numArgsInSignature(call) != skip {
+ f.Badf(call.Pos(), "no args in %s call", name)
+ }
}
return
}
@@ -297,6 +307,95 @@ func (f *File) checkPrint(call *ast.CallExpr, name string, skip int) {
}
}
+// numArgsInSignature tells how many formal arguments the function type
+// being called has. Assumes type checking is on (f.pkg != nil).
+func (f *File) numArgsInSignature(call *ast.CallExpr) int {
+ // Check the type of the function or method declaration
+ typ := f.pkg.types[call.Fun]
+ if typ == nil {
+ return 0
+ }
+ // The type must be a signature, but be sure for safety.
+ sig, ok := typ.(*types.Signature)
+ if !ok {
+ return 0
+ }
+ return len(sig.Params)
+}
+
+// isErrorMethodCall reports whether the call is of a method with signature
+// func Error() error
+// where "error" is the universe's error type. We know the method is called "Error"
+// and f.pkg is set.
+func (f *File) isErrorMethodCall(call *ast.CallExpr) bool {
+ // Is it a selector expression? Otherwise it's a function call, not a method call.
+ sel, ok := call.Fun.(*ast.SelectorExpr)
+ if !ok {
+ return false
+ }
+ // The package is type-checked, so if there are no arguments, we're done.
+ if len(call.Args) > 0 {
+ return false
+ }
+ // Check the type of the method declaration
+ typ := f.pkg.types[sel]
+ if typ == nil {
+ return false
+ }
+ // The type must be a signature, but be sure for safety.
+ sig, ok := typ.(*types.Signature)
+ if !ok {
+ return false
+ }
+ // There must be a receiver for it to be a method call. Otherwise it is
+ // a function, not something that satisfies the error interface.
+ if sig.Recv == nil {
+ return false
+ }
+ // There must be no arguments. Already verified by type checking, but be thorough.
+ if len(sig.Params) > 0 {
+ return false
+ }
+ // Finally the real questions.
+ // There must be one result.
+ if len(sig.Results) != 1 {
+ return false
+ }
+ // It must have return type "string" from the universe.
+ result := sig.Results[0].Type
+ if types.IsIdentical(result, types.Typ[types.String]) {
+ return true
+ }
+ return true
+}
+
+// Error methods that do not satisfy the Error interface and should be checked.
+type errorTest1 int
+
+func (errorTest1) Error(...interface{}) string {
+ return "hi"
+}
+
+type errorTest2 int // Analogous to testing's *T type.
+func (errorTest2) Error(...interface{}) {
+}
+
+type errorTest3 int
+
+func (errorTest3) Error() { // No return value.
+}
+
+type errorTest4 int
+
+func (errorTest4) Error() int { // Different return type.
+ return 3
+}
+
+type errorTest5 int
+
+func (errorTest5) error() { // niladic; don't complain if no args (was bug)
+}
+
// This function never executes, but it serves as a simple test for the program.
// Test with make test.
func BadFunctionUsedInTests() {
@@ -322,8 +421,25 @@ func BadFunctionUsedInTests() {
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"
+ // Something that satisfies the error interface.
var e error
- fmt.Println(e.Error()) // correct, used to trigger "no args in Error call"
+ fmt.Println(e.Error()) // ok
+ // Something that looks like an error interface but isn't, such as the (*T).Error method
+ // in the testing package.
+ var et1 errorTest1
+ fmt.Println(et1.Error()) // ERROR "no args in Error call"
+ fmt.Println(et1.Error("hi")) // ok
+ fmt.Println(et1.Error("%d", 3)) // ERROR "possible formatting directive in Error call"
+ var et2 errorTest2
+ et2.Error() // ERROR "no args in Error call"
+ et2.Error("hi") // ok, not an error method.
+ et2.Error("%d", 3) // ERROR "possible formatting directive in Error call"
+ var et3 errorTest3
+ et3.Error() // ok, not an error method.
+ var et4 errorTest4
+ et4.Error() // ok, not an error method.
+ var et5 errorTest5
+ et5.error() // ok, not an error method.
}
// printf is used by the test.
コアとなるコードの解説
このコミットの主要な変更は、cmd/vet
が Go の型システムをより深く理解し、Error
メソッドの呼び出しを正確に分類できるようにした点です。
-
型情報の統合 (
main.go
):- 以前の
cmd/vet
は、ASTを走査する際に、各式の正確な型情報を常に持っているわけではありませんでした。このコミットでは、Package
構造体とFile.pkg
フィールドを導入することで、このギャップを埋めています。 doPackage
関数内でgo/types.Context
を使用してパッケージ全体の型チェックを実行し、その過程でexprFn
コールバックを通じて各ast.Expr
とそれに対応するtypes.Type
をpkg.types
マップに保存します。これにより、cmd/vet
はコードのセマンティックな意味(型)を理解できるようになります。- 各
File
オブジェクトがそのパッケージのPackage
構造体への参照を持つことで、ファイル内の任意のASTノードから関連する型情報にアクセスできるようになります。
- 以前の
-
Error
メソッドの厳密な判別 (print.go
):checkPrint
関数は、printf
スタイルの関数呼び出しの引数を検証する役割を担っています。この関数がError
という名前のメソッド呼び出しに遭遇した際、以前は単純な名前ベースのチェックしか行っていませんでした。- 新しく導入された
isErrorMethodCall
関数は、go/types
から取得した型情報に基づいて、呼び出されたメソッドがerror
インターフェースのError() string
メソッドのシグネチャと完全に一致するかどうかを厳密にチェックします。call.Fun
がセレクタ式であること(obj.Method()
の形式)。- メソッドが引数を取らないこと。
- メソッドがレシーバを持つこと(関数ではなくメソッドであること)。
- メソッドが1つの戻り値を持ち、その型が
string
であること。
- これらの条件をすべて満たす場合のみ、その
Error
メソッドはerror
インターフェースの一部であると判断され、printf
スタイルの引数チェックから除外されます。 - もし
Error
という名前のメソッドであっても、これらの条件を満たさない場合(例:(*testing.T).Error()
は引数を取る可能性がある)、それはerror
インターフェースのメソッドではないと判断され、引き続きprintf
スタイルの引数チェックの対象となります。 numArgsInSignature
関数は、任意の関数呼び出しの仮引数の数を型情報から取得するために使用され、checkPrint
の汎用的な引数チェックロジックを補強します。
-
テストケースの追加:
print.go
の下部に追加されたerrorTest1
からerrorTest5
までの型とメソッドは、様々なシグネチャを持つError
メソッドの例を示しています。BadFunctionUsedInTests
関数内の新しいテスト呼び出しは、これらの異なるError
メソッドがcmd/vet
によってどのように扱われるかを示しています。var e error; fmt.Println(e.Error())
はerror
インターフェースのError()
メソッドなので、ok
となり警告は出ません。errorTest1
はError(...interface{}) string
というシグネチャを持ち、error
インターフェースとは異なるため、引数なしで呼び出すとERROR "no args in Error call"
が出ます。これはtesting.T.Error()
のようなケースを模倣しており、vet
が正しく警告を出すべき対象です。- 他の
errorTest
も、それぞれのシグネチャに基づいてvet
が正しく動作することを確認しています。
これらの変更により、cmd/vet
は Error
メソッドのセマンティックな意味を正確に理解し、開発者が意図しない printf
フォーマットの誤用に対してのみ警告を発するようになり、誤検出が大幅に削減されました。
関連リンク
- GitHub Issue: https://github.com/golang/go/issues/4753
- Gerrit Change-list: https://golang.org/cl/7396054
参考にした情報源リンク
- Go言語の
error
インターフェース: https://go.dev/blog/error-handling-and-go cmd/vet
ドキュメント: https://pkg.go.dev/cmd/vetgo/types
パッケージドキュメント: https://pkg.go.dev/go/typesgo/ast
パッケージドキュメント: https://pkg.go.dev/go/ast