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