Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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/vetgo/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) を使用して条件付きでコンパイルされるようにすることです。

具体的には、以下の新しいファイルが導入されました。

  1. src/cmd/vet/types.go:

    • このファイルは // +build gotypes ビルドタグを持ちます。
    • go/types パッケージに依存する実際の型チェックロジック(Package.check メソッド、matchArgTypenumArgsInSignatureisErrorMethodCall 関数など)が含まれています。
    • Package 構造体の types フィールドが go/types.Type ではなく、新しく定義された Type インターフェース(go/types.Type と同等のシグネチャを持つ)を参照するように変更されています。これにより、go/types が利用できない場合でも、Package 構造体自体の定義は変更せずに済みます。
    • go/types.Context を使用して、ASTから型情報を抽出する処理が行われます。
  2. src/cmd/vet/typestub.go:

    • このファイルは // +build !gotypes ビルドタグを持ちます。
    • これは types.go に対応するスタブ(ダミー)実装を提供します。
    • gotypes タグが有効でない場合(つまり go/types パッケージが利用できない場合)にコンパイルされます。
    • Package.check メソッドは常に nil を返し、型チェックを行いません。
    • matchArgTypenumArgsInSignatureisErrorMethodCall などの関数は、型情報がないために正確なチェックができないため、常に true を返したり、デフォルト値を返したりするような、安全側へのフォールバック実装を提供します。例えば、matchArgType は常に true を返し、型が一致するかどうかのチェックをスキップします。isStruct は常に true と "struct" を返します。

これらの変更により、cmd/vetgo build -tags gotypes でビルドされた場合には完全な型チェック機能を持ち、go build (または go build -tags !gotypes) でビルドされた場合には型チェック機能が限定的または無効になります。

既存のファイルでは、go/types パッケージのインポートが削除され、Package 構造体の types フィールドの型が go/types.Type から新しい Type インターフェースに変更されています。また、型チェックに関連するロジックの呼び出しが、Package 構造体の新しいメソッド(pkg.checkpkg.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 の型チェック機能を条件付きで有効/無効にできるようにした点です。

  1. Type インターフェースの導入: main.goPackage 構造体にあった types map[ast.Expr]types.Typetypes 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 インターフェースを介して型情報にアクセスできるようになります。
  2. 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 を返し、型チェックを行いません。
  3. 型情報に依存する関数の分離: print.gotaglit.go に散らばっていた、f.pkg.typesf.pkg.values といった型情報に直接アクセスするロジック(例: matchArgType, numArgsInSignature, isErrorMethodCall, isStruct の内部ロジック)が、それぞれ types.gotypestub.go に移動されました。

    • types.go に移動されたバージョンは、go/types パッケージの機能を利用して正確な型チェックを行います。
    • typestub.go に移動されたバージョンは、型情報がないことを前提とし、安全なフォールバック動作(例: 常に true を返す、デフォルト値を返す)を提供します。
  4. ビルドタグの活用: 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/vetgo/types パッケージの有無に柔軟に対応できるようになり、go1.1 のディストリビューションにおいて、go/types がまだ安定していない、あるいは特定のビルド環境で利用できないといった状況でも、cmd/vet を提供できるようになりました。これは、ツールのモジュール性と配布の容易性を向上させる重要なリファクタリングです。

関連リンク

参考にした情報源リンク

  • このコミットの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)