[インデックス 15611] ファイルの概要
このコミットは、Go言語の静的解析ツールである cmd/vet において、型チェックに関連するコードを go/types パッケージへの依存から分離し、ビルドタグを用いてその機能を有効/無効にできるようにするためのものです。これにより、go1.1 のリリースにおいて go/types パッケージへの依存を簡素化することが目的とされています。
コミット
commit 0ed517e5e68813581de8f6a7e94211d82ff36dd2
Author: Rob Pike <r@golang.org>
Date: Wed Mar 6 12:49:56 2013 -0800
cmd/vet: isolate the type checking code into a separate file
We can enable/disable type checking with a build tag.
Should simplify cutting the go1.1 distribution free of go/types.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7482045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0ed517e5e68813581de8f6a7e94211d82ff36dd2
元コミット内容
cmd/vet: isolate the type checking code into a separate file
We can enable/disable type checking with a build tag.
Should simplify cutting the go1.1 distribution free of go/types.
このコミットは、cmd/vet ツール内の型チェックコードを独立したファイルに分離し、ビルドタグを使用して型チェックの有効/無効を切り替えられるようにします。これにより、go1.1 ディストリビューションから go/types パッケージへの依存を切り離すことを簡素化するはずです。
変更の背景
Go言語の cmd/vet は、Goプログラムの潜在的なバグや疑わしい構成を検出するための静的解析ツールです。このツールは、コードの正確な解析のために型情報にアクセスする必要があります。Go言語の標準ライブラリには、Goのソースコードを解析し、型情報を構築するための go/types パッケージが存在します。
しかし、go1.1 のリリースに向けて、cmd/vet が go/types パッケージに直接依存していることが、ディストリビューションの管理やビルドプロセスにおいて複雑さを増す可能性がありました。特に、go/types パッケージ自体がまだ開発途上であったり、特定の環境での利用に制約があったりする場合、cmd/vet が常に go/types に依存していると、柔軟性が損なわれます。
このコミットの背景には、cmd/vet の型チェック機能を、go/types パッケージが利用可能な場合にのみ有効にし、そうでない場合には無効にする、という選択肢を提供することで、ツールの配布と利用の柔軟性を高めるという意図があります。これにより、go1.1 のリリースプロセスが簡素化され、go/types パッケージの進化に cmd/vet が過度に結合されることを避けることができます。
前提知識の解説
cmd/vet: Go言語の標準ツールの一つで、Goソースコードを静的に解析し、一般的なプログラミングエラー(例:printfフォーマット文字列と引数の不一致、構造体リテラルのタグなしフィールドなど)を検出します。go/typesパッケージ: Go言語の標準ライブラリの一部で、Goプログラムの型情報を解析し、表現するためのパッケージです。コンパイラや静的解析ツールが、コードの型チェックを行う際に利用します。このパッケージは、GoのAST(抽象構文木)を基に、変数、関数、型などのシンボル情報を解決し、それらの型を決定する機能を提供します。- ビルドタグ (Build Tags): Go言語のビルドシステムが提供する機能で、ソースファイルに特定のコメント行(例:
// +build tagname)を記述することで、そのファイルが特定のビルド条件(例: OS、アーキテクチャ、カスタムタグ)が満たされた場合にのみコンパイルされるように制御できます。これにより、異なる環境や目的に応じてコードのサブセットをビルドすることが可能になります。このコミットでは、gotypesというカスタムビルドタグが導入され、go/typesパッケージに依存するコードとそうでないコードを切り替えるために使用されます。 go1.1ディストリビューション: Go言語のバージョン1.1のリリースを指します。この時期はGo言語が成熟し、より広範な利用を目指していた段階であり、ツールの依存関係の管理が重要視されていました。
技術的詳細
このコミットの主要な技術的変更は、cmd/vet の型チェックロジックを go/types パッケージへの直接的な依存から切り離し、ビルドタグ (gotypes) を使用して条件付きでコンパイルされるようにすることです。
具体的には、以下の新しいファイルが導入されました。
-
src/cmd/vet/types.go:- このファイルは
// +build gotypesビルドタグを持ちます。 go/typesパッケージに依存する実際の型チェックロジック(Package.checkメソッド、matchArgType、numArgsInSignature、isErrorMethodCall関数など)が含まれています。Package構造体のtypesフィールドがgo/types.Typeではなく、新しく定義されたTypeインターフェース(go/types.Typeと同等のシグネチャを持つ)を参照するように変更されています。これにより、go/typesが利用できない場合でも、Package構造体自体の定義は変更せずに済みます。go/types.Contextを使用して、ASTから型情報を抽出する処理が行われます。
- このファイルは
-
src/cmd/vet/typestub.go:- このファイルは
// +build !gotypesビルドタグを持ちます。 - これは
types.goに対応するスタブ(ダミー)実装を提供します。 gotypesタグが有効でない場合(つまりgo/typesパッケージが利用できない場合)にコンパイルされます。Package.checkメソッドは常にnilを返し、型チェックを行いません。matchArgType、numArgsInSignature、isErrorMethodCallなどの関数は、型情報がないために正確なチェックができないため、常にtrueを返したり、デフォルト値を返したりするような、安全側へのフォールバック実装を提供します。例えば、matchArgTypeは常にtrueを返し、型が一致するかどうかのチェックをスキップします。isStructは常にtrueと "struct" を返します。
- このファイルは
これらの変更により、cmd/vet は go build -tags gotypes でビルドされた場合には完全な型チェック機能を持ち、go build (または go build -tags !gotypes) でビルドされた場合には型チェック機能が限定的または無効になります。
既存のファイルでは、go/types パッケージのインポートが削除され、Package 構造体の types フィールドの型が go/types.Type から新しい Type インターフェースに変更されています。また、型チェックに関連するロジックの呼び出しが、Package 構造体の新しいメソッド(pkg.check、pkg.isStruct など)に集約されています。
Makefile も更新され、test コマンドで go build -tags 'vet_test gotypes' を使用するように変更されており、テスト時には go/types を有効にした状態で vet がビルドされることを示しています。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルと、その変更の概要は以下の通りです。
-
src/cmd/vet/Makefile:testコマンドのビルドタグにgotypesが追加されました。--- a/src/cmd/vet/Makefile +++ b/src/cmd/vet/Makefile @@ -2,7 +2,8 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. +# Assumes go/types is installed test testshort: - go build -tags vet_test + go build -tags 'vet_test gotypes' ../../../test/errchk ./vet -compositewhitelist=false -printfuncs='Warn:1,Warnf:1' *.go
-
src/cmd/vet/main.go:go/typesのインポートが削除されました。Package構造体のtypesフィールドの型がtypes.TypeからTypeに変更されました。- 型チェックのロジックが
context.Checkからpkg.checkメソッドの呼び出しに置き換えられました。--- a/src/cmd/vet/main.go +++ b/src/cmd/vet/main.go @@ -15,7 +15,6 @@ import ( "go/parser" "go/printer" "go/token" - "go/types" "io/ioutil" "os" "path/filepath" @@ -175,7 +174,7 @@ func doPackageDir(directory string) { } type Package struct { - types map[ast.Expr]types.Type + types map[ast.Expr]Type values map[ast.Expr]interface{} } @@ -207,22 +206,8 @@ func doPackage(names []string) { astFiles = append(astFiles, parsedFile) } pkg := new(Package) - pkg.types = make(map[ast.Expr]types.Type) - pkg.values = make(map[ast.Expr]interface{}) - exprFn := func(x ast.Expr, typ types.Type, val interface{}) { - pkg.types[x] = typ - if val != nil { - pkg.values[x] = val - } - } - // By providing the Context with our own error function, it will continue - // past the first error. There is no need for that function to do anything. - context := types.Context{ - Expr: exprFn, - Error: func(error) {}, - } // Type check the package. - _, err := context.Check(fs, astFiles) + err := pkg.check(fs, astFiles) if err != nil && *verbose { warnf("%s", err) }
-
src/cmd/vet/print.go:go/typesのインポートが削除されました。matchArgType,numArgsInSignature,isErrorMethodCallといった型情報に依存する関数が削除されました。これらの関数は新しく作成されたtypes.goに移動しました。--- a/src/cmd/vet/print.go +++ b/src/cmd/vet/print.go @@ -10,7 +10,6 @@ import ( "flag" "go/ast" "go/token" - "go/types" "strconv" "strings" "unicode/utf8" @@ -302,59 +301,6 @@ func (f *File) checkPrintfArg(call *ast.CallExpr, verb rune, flags []byte, argNu f.Badf(call.Pos(), "unrecognized printf verb %q", verb) } -func (f *File) matchArgType(t printfArgType, arg ast.Expr) bool { - // TODO: for now, we can only test builtin types and untyped constants. - typ := f.pkg.types[arg] - if typ == nil { - return true - } - basic, ok := typ.(*types.Basic) - if !ok { - return true - } - switch basic.Kind { - case types.Bool: - return t&argBool != 0 - case types.Int, types.Int8, types.Int16, types.Int32, types.Int64: - fallthrough - case types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64, types.Uintptr: - return t&argInt != 0 - case types.Float32, types.Float64, types.Complex64, types.Complex128: - return t&argFloat != 0 - case types.String: - return t&argString != 0 - case types.UnsafePointer: - return t&(argPointer|argInt) != 0 - case types.UntypedBool: - return t&argBool != 0 - case types.UntypedComplex: - return t&argFloat != 0 - case types.UntypedFloat: - // If it's integral, we can use an int format. - switch f.pkg.values[arg].(type) { - case int, int8, int16, int32, int64: - return t&(argInt|argFloat) != 0 - case uint, uint8, uint16, uint32, uint64: - return t&(argInt|argFloat) != 0 - } - return t&argFloat != 0 - case types.UntypedInt: - return t&argInt != 0 - case types.UntypedRune: - return t&(argInt|argRune) != 0 - case types.UntypedString: - return t&argString != 0 - case types.UntypedNil: - return t&argPointer != 0 // TODO? - case types.Invalid: - if *verbose { - f.Warnf(arg.Pos(), "printf argument %v has invalid or unknown type", arg) - } - return true // Probably a type check problem. - } - return false -} - // checkPrint checks a call to an unformatted print routine such as Println. // call.Args[firstArg] is the first argument to be printed.\n func (f *File) checkPrint(call *ast.CallExpr, name string, firstArg int) { @@ -403,64 +349,3 @@ func (f *File) checkPrint(call *ast.CallExpr, name string, firstArg int) { } } } - -// numArgsInSignature tells how many formal arguments the function type -// being called has. -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() string -// where "string" is the universe's string type. We know the method is called "Error". -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 false -}
-
src/cmd/vet/taglit.go:go/typesのインポートが削除されました。checkUntaggedLiteral関数内で、型チェックによる構造体判定ロジックがf.pkg.isStruct(c)の呼び出しに置き換えられました。exp/types.ObjListがホワイトリストから削除されました。--- a/src/cmd/vet/taglit.go +++ b/src/cmd/vet/taglit.go @@ -9,7 +9,6 @@ package main import ( "flag" "go/ast" - "go/types" "strings" ) @@ -22,19 +21,9 @@ func (f *File) checkUntaggedLiteral(c *ast.CompositeLit) { return } - // Check that the CompositeLit's type is a slice or array (which needs no tag), if possible. - typ := f.pkg.types[c] - if typ != nil { - // If it's a named type, pull out the underlying type. - if namedType, ok := typ.(*types.NamedType); ok { - typ = namedType.Underlying - } - switch typ.(type) { - case *types.Slice: - return - case *types.Array: - return - } + isStruct, typeString := f.pkg.isStruct(c) + if !isStruct { + return } // It's a struct, or we can't tell it's not a struct because we don't have types. @@ -72,11 +61,7 @@ func (f *File) checkUntaggedLiteral(c *ast.CompositeLit) { return } - pre := "" - if typ != nil { - pre = typ.String() + " " - } - f.Warn(c.Pos(), pre+"composite literal uses untagged fields") + f.Warn(c.Pos(), typeString+" composite literal uses untagged fields") } // pkgPath returns the import path "image/png" for the package name "png". @@ -124,7 +109,6 @@ var untaggedLiteralWhitelist = map[string]bool{ "encoding/xml.Comment": true, "encoding/xml.Directive": true, "exp/norm.Decomposition": true, - "exp/types.ObjList": true, "go/scanner.ErrorList": true, "image/color.Palette": true, "net.HardwareAddr": true,
-
src/cmd/vet/types.go(新規ファイル):// +build gotypesビルドタグを持つ。go/typesパッケージをインポート。Typeインターフェースを定義(go/types.Typeの代わり)。Package.checkメソッドの実装(go/types.Contextを使用した実際の型チェック)。Package.isStructメソッドの実装(go/types.Structを使用した構造体判定)。matchArgType,numArgsInSignature,isErrorMethodCallの実際の型チェックロジックを実装。
-
src/cmd/vet/typestub.go(新規ファイル):// +build !gotypesビルドタグを持つ。go/typesパッケージをインポートしない。Typeインターフェースを定義(types.goと同じ)。Package.checkメソッドのスタブ実装(常にnilを返す)。Package.isStructメソッドのスタブ実装(常にtrueと "struct" を返す)。matchArgType,numArgsInSignature,isErrorMethodCallのスタブ実装(型情報なしで安全なフォールバック)。
コアとなるコードの解説
このコミットの核心は、Goのビルドタグを利用して、cmd/vet の型チェック機能を条件付きで有効/無効にできるようにした点です。
-
Typeインターフェースの導入:main.goのPackage構造体にあったtypes map[ast.Expr]types.Typeがtypes map[ast.Expr]Typeに変更されました。ここでTypeは、go/types.Typeと同じString() stringメソッドを持つ独自のインターフェースとして定義されています。src/cmd/vet/types.goでは、このTypeインターフェースがgo/types.Typeのエイリアスとして機能し、実際のgo/typesの型が使用されます。src/cmd/vet/typestub.goでは、Typeインターフェースはダミーとして存在し、go/typesに依存しないコードがコンパイルされるようにします。 このアプローチにより、main.goや他のファイルはgo/typesパッケージに直接依存することなく、Typeインターフェースを介して型情報にアクセスできるようになります。
-
Package.checkメソッドの分離: 以前はmain.go内で直接行われていたgo/types.Contextを使用した型チェック処理が、Package構造体の新しいメソッドpkg.check(fs *token.FileSet, astFiles []*ast.File) errorとして分離されました。types.goでは、このcheckメソッドがgo/types.Contextを初期化し、context.Checkを呼び出して実際の型チェックを実行します。typestub.goでは、このcheckメソッドは単にnilを返し、型チェックを行いません。
-
型情報に依存する関数の分離:
print.goやtaglit.goに散らばっていた、f.pkg.typesやf.pkg.valuesといった型情報に直接アクセスするロジック(例:matchArgType,numArgsInSignature,isErrorMethodCall,isStructの内部ロジック)が、それぞれtypes.goとtypestub.goに移動されました。types.goに移動されたバージョンは、go/typesパッケージの機能を利用して正確な型チェックを行います。typestub.goに移動されたバージョンは、型情報がないことを前提とし、安全なフォールバック動作(例: 常にtrueを返す、デフォルト値を返す)を提供します。
-
ビルドタグの活用:
types.goには// +build gotypesが、typestub.goには// +build !gotypesが記述されています。go build -tags gotypesでビルドすると、types.goがコンパイルされ、typestub.goは無視されます。これにより、cmd/vetは完全な型チェック機能を持つことになります。go build(またはgo build -tags !gotypes) でビルドすると、typestub.goがコンパイルされ、types.goは無視されます。この場合、cmd/vetは型チェック機能が限定的または無効になります。
この変更により、cmd/vet は go/types パッケージの有無に柔軟に対応できるようになり、go1.1 のディストリビューションにおいて、go/types がまだ安定していない、あるいは特定のビルド環境で利用できないといった状況でも、cmd/vet を提供できるようになりました。これは、ツールのモジュール性と配布の容易性を向上させる重要なリファクタリングです。
関連リンク
- Go言語の
cmd/vetツールに関する公式ドキュメント(現在のバージョン): https://pkg.go.dev/cmd/vet - Go言語の
go/typesパッケージに関する公式ドキュメント(現在のバージョン): https://pkg.go.dev/go/types - Go言語のビルドタグに関する公式ドキュメント: https://pkg.go.dev/cmd/go#hdr-Build_constraints
参考にした情報源リンク
- このコミットのChange-Id:
https://golang.org/cl/7482045(GoのGerritコードレビューシステムへのリンク) - Go言語の公式リポジトリ: https://github.com/golang/go
- Go 1.1 Release Notes (当時のリリース情報): https://go.dev/doc/go1.1 (当時の背景を理解するために参照)
- Go言語のビルドタグに関する一般的な情報源(例: Go Modules and Build Tags)
- Go言語の静的解析ツールに関する一般的な情報源(例: Go Static Analysis Tools)