[インデックス 10061] govet: canonical dynamic method signatures チェック機能の追加
コミット
- コミットハッシュ: b0ec32db117a750d6259a601b37d5bd89ff44a2b
- 作成者: Russ Cox rsc@golang.org
- 日付: 2011年10月19日 16:06:16 -0400
- メッセージ: govet: check canonical dynamic method signatures
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b0ec32db117a750d6259a601b37d5bd89ff44a2b
元コミット内容
このコミットは、Go言語のgovetツールに「canonical dynamic method signatures」(正規の動的メソッドシグネチャ)チェック機能を追加したものです。主な内容は以下の通りです:
fmt.Scanner
インターフェースのScan
メソッドが正しい引数型(fmt.ScanState
)を持つことを確認io.ByteReader
インターフェースのReadByte
メソッドが正しい戻り値型(byte, os.Error
)を持つことを確認- 様々な標準ライブラリ(fmt、gob、json、flate)が動的にチェックするメソッドのシグネチャを検証
- rune型が導入される際により重要になる機能の先行実装
変更の背景
2011年当時、Go言語では多くの標準ライブラリが動的インターフェースチェックを使用してメソッドの存在を確認していました。これは、コンパイル時の型チェックとは異なり、実行時に特定のメソッドが存在するかどうかを確認する仕組みです。
問題は、これらのメソッドが間違ったシグネチャを持っていても、コンパイル時にエラーが発生しないことでした。代わりに実行時の動的チェックが失敗し、時として原因が分からないエラーとなって現れていました。
特に以下のような場面で問題が発生していました:
fmt
パッケージのScanner
インターフェースの不正な実装io
パッケージのByteReader
インターフェースの不正な実装gob
、json
、flate
パッケージでの動的メソッドチェック
また、コミットメッセージで言及されている通り、rune型が導入される際にこれらのチェックがより重要になることが予想されていました。
前提知識の解説
動的インターフェースチェック
Go言語では、インターフェースの実装を静的(コンパイル時)と動的(実行時)の両方で行うことができます。
静的チェックの例:
var w io.Writer = os.Stdout // コンパイル時にチェック
動的チェックの例:
if scanner, ok := v.(fmt.Scanner); ok {
// 実行時にScannerインターフェースを実装しているかチェック
scanner.Scan(state, verb)
}
AST(抽象構文木)解析
govetツールは、Go言語の**AST(Abstract Syntax Tree)**を解析してコードの問題を検出します。ASTは、ソースコードの構造を木構造で表現したものです。
このコミットでは、以下のASTノードを処理しています:
*ast.FuncDecl
:関数/メソッド宣言*ast.InterfaceType
:インターフェース定義*ast.Field
:フィールド定義
正規のメソッドシグネチャ
Go言語では、特定のインターフェースを実装するメソッドに正規のシグネチャが定義されています。例:
fmt.Scanner.Scan
:Scan(fmt.ScanState, rune) error
io.ByteReader.ReadByte
:ReadByte() (byte, error)
io.Writer.Write
:Write([]byte) (int, error)
技術的詳細
1. データ構造の拡張
File
構造体が拡張され、新しい機能をサポートするようになりました:
type File struct {
fset *token.FileSet // ファイルセット(位置情報管理)
file *ast.File // ASTファイル
b bytes.Buffer // メソッドで使用するバッファ
}
2. 正規メソッドの定義
canonicalMethods
マップで、チェック対象のメソッドとその正規シグネチャを定義:
var canonicalMethods = map[string]MethodSig{
"Format": {[]string{"=fmt.State", "int"}, []string{}},
"GobDecode": {[]string{"[]byte"}, []string{"os.Error"}},
"GobEncode": {[]string{}, []string{"[]byte", "os.Error"}},
"MarshalJSON": {[]string{}, []string{"[]byte", "os.Error"}},
"ReadByte": {[]string{}, []string{"byte", "os.Error"}},
"Scan": {[]string{"=fmt.ScanState", "int"}, []string{"os.Error"}},
// ...
}
3. 「=」プレフィックスの仕組み
引数に=
プレフィックスが付いている場合、これはシグナル引数として機能します:
=fmt.ScanState
:この型の引数があれば、正規のfmt.Scanner
実装を意図している=io.Reader
:この型の引数があれば、正規のio.ReaderFrom
実装を意図している
これにより、同名だが異なる目的のメソッドを区別できます。
4. 型マッチング機能
matchParams
関数とmatchParamType
関数で、期待する型と実際の型を比較:
func (f *File) matchParams(expect []string, actual []ast.Expr, prefix string) bool
func (f *File) matchParamType(expect string, actual ast.Expr) bool
コアとなるコードの変更箇所
1. ASTビジターの拡張(src/cmd/govet/govet.go:181-192)
case *ast.FuncDecl:
f.checkMethodDecl(n)
case *ast.InterfaceType:
f.checkInterfaceType(n)
2. メソッド宣言チェック(src/cmd/govet/govet.go:96-103)
func (f *File) checkMethodDecl(d *ast.FuncDecl) {
if d.Recv == nil {
return // メソッドではない
}
f.checkMethod(d.Name, d.Type)
}
3. インターフェースタイプチェック(src/cmd/govet/govet.go:107-113)
func (f *File) checkInterfaceType(t *ast.InterfaceType) {
for _, field := range t.Methods.List {
for _, id := range field.Names {
f.checkMethod(id, field.Type.(*ast.FuncType))
}
}
}
コアとなるコードの解説
checkMethod関数の動作フロー
- メソッド名の確認:
canonicalMethods
マップにメソッド名が存在するかチェック - 引数・戻り値の抽出:
typeFlatten
関数でAST構造から型情報を抽出 - シグナル引数のマッチング:
=
プレフィックス付きの引数がマッチするかチェック - 完全マッチング:すべての引数・戻り値がマッチするかチェック
- エラー報告:不一致の場合、期待するシグネチャと実際のシグネチャを出力
typeFlatten関数の重要性
Go言語では、以下のような宣言が可能です:
func f(int, bool) // 引数名なし
func f(x, y, z int) // 複数の引数が同じ型
typeFlatten
関数は、これらの異なる宣言形式を統一的に処理し、型の配列として返します。
エラーメッセージの生成
エラーメッセージは以下の形式で生成されます:
method ReadByte() byte should have signature ReadByte() (byte, os.Error)
これにより、開発者は正しいシグネチャを一目で理解できます。