[インデックス 15385] ファイルの概要
このコミットは、Go言語の静的解析ツールである cmd/vet
の内部構造を、ファイル単位の処理からパッケージ単位の処理へと再構築するものです。この変更の主な目的は、go vet
がGoの型チェッカー (go/types
パッケージ) を利用できるようにすることにあります。これにより、go vet
はより高度な、型情報に基づいた静的解析を実行できるようになります。
コミット
commit 48f79a95d08c3baff87f4fbc06d3f2cc9762ec5a
Author: Rob Pike <r@golang.org>
Date: Fri Feb 22 13:32:43 2013 -0800
cmd/vet: restructure to be package-driven
This is a simple refactoring of main.go that will enable the type checker
to be used during vetting.
The change has an unimportant effect on the arguments: it now assumes
that all files named explicitly on the command line belong to the same
package. When run by the go command, this was true already.
Also restore a missing parenthesis from an error message.
R=golang-dev, gri, bradfitz
CC=golang-dev
https://golang.org/cl/7393052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/48f79a95d08c3baff87f4fbc06d3f2cc9762ec5a
元コミット内容
cmd/vet
: パッケージ駆動型に再構築
これは、main.go
の単純なリファクタリングであり、型チェッカーをベッティング中に使用できるようにします。
この変更は引数に重要でない影響を与えます。コマンドラインで明示的に指定されたすべてのファイルが同じパッケージに属すると仮定するようになりました。go
コマンドによって実行される場合、これはすでに真でした。
また、エラーメッセージから欠落していた括弧を復元します。
R=golang-dev, gri, bradfitz CC=golang-dev https://golang.org/cl/7393052
変更の背景
go vet
ツールは、Goプログラムの潜在的なバグや疑わしい構造を検出するための静的解析ツールです。しかし、このコミット以前の go vet
は、主に構文木 (go/ast
) の情報に基づいて解析を行っており、型情報 (go/types
) を直接利用していませんでした。型情報は、より深い意味論的な解析、例えば型アサーションの誤り、インターフェースの実装ミス、nilポインタのデリファレンス可能性など、より複雑な問題を検出するために不可欠です。
このコミットの背景には、go vet
の解析能力を向上させるという明確な意図があります。型チェッカーを統合することで、go vet
はより多くの種類の問題を検出し、開発者により正確なフィードバックを提供できるようになります。この統合を実現するためには、go vet
が個々のファイルではなく、Goのパッケージとしてコードを認識し、処理する必要がありました。Goの型チェッカーはパッケージ全体を対象として型チェックを行うため、go vet
もそれに合わせて「パッケージ駆動型」のアーキテクチャに移行する必要があったのです。
また、コミットメッセージにあるように、go
コマンドが go vet
を実行する際には、すでにパッケージ単位でファイルが渡されていました。この変更は、go vet
を直接コマンドラインから実行する場合の動作を、go
コマンド経由で実行する場合の動作に合わせる側面も持っています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と標準ライブラリのパッケージに関する知識が不可欠です。
-
go vet
:go vet
は、Go言語のソースコードを静的に解析し、潜在的なエラーや疑わしい構造を報告するツールです。例えば、Printf
のフォーマット文字列と引数の不一致、到達不能なコード、ロックの誤用などを検出します。これはコンパイルエラーにはならないが、実行時に問題を引き起こす可能性のあるコードパターンを見つけるのに役立ちます。 -
Goのパッケージ (Packages): Go言語のコードは「パッケージ」という単位で組織されます。パッケージは関連するGoソースファイルの集合であり、名前空間を提供します。Goのプログラムは、
main
パッケージとその中のmain
関数から実行が開始されます。他のパッケージはimport
文を使って利用されます。Goのビルドシステムやツールは、通常、パッケージ単位で動作します。 -
静的解析 (Static Analysis): プログラムを実行せずに、ソースコードを分析して潜在的なエラーや品質の問題を特定する手法です。
go vet
は静的解析ツールの一例です。静的解析は、コンパイル時や実行時では見つけにくい問題を早期に発見するのに役立ちます。 -
go/ast
パッケージ (Abstract Syntax Tree):go/ast
パッケージは、Goのソースコードの抽象構文木 (AST) を表現するためのデータ構造を提供します。ASTは、ソースコードの構文構造を木構造で表現したもので、プログラムの構造を解析するために使用されます。go/parser
パッケージによってソースコードが解析されると、このASTが生成されます。 -
go/parser
パッケージ:go/parser
パッケージは、Goのソースコードを解析し、go/ast
パッケージのASTを生成するための機能を提供します。このパッケージは、ソースコードをトークンに分割し、そのトークン列から構文木を構築します。 -
go/token
パッケージ:go/token
パッケージは、Goのソースコード内の位置(ファイル名、行番号、列番号など)やトークン(キーワード、識別子、演算子など)を表現するための定数と型を提供します。go/parser
やgo/ast
と組み合わせて、コードの特定の部分がソースファイルのどこにあるかを特定するのに使われます。 -
go/build
パッケージ:go/build
パッケージは、Goのパッケージのビルドプロセスをシミュレートするための機能を提供します。これには、ソースファイルの検索、パッケージの依存関係の解決、ビルドタグの処理などが含まれます。このパッケージは、特定のディレクトリからGoのパッケージ情報を読み込むために使用されます。 -
go/types
パッケージ (Type Checker):go/types
パッケージは、Goの型システムを実装し、Goのソースコードの型チェックを行うための機能を提供します。これは、Goのコンパイラの重要な部分であり、変数や式の型が正しく、Goの型規則に準拠していることを検証します。型チェッカーは、ASTだけでなく、インポートされたパッケージの情報も利用して、プログラム全体の意味論的な整合性を確認します。go vet
がこのパッケージを利用できるようになることで、より深い意味論的な解析が可能になります。 -
パッケージ駆動型 (Package-driven): この文脈では、ツールが個々のソースファイルを独立して処理するのではなく、それらのファイルが属するGoのパッケージ全体を一つの単位として処理することを意味します。Goの型システムはパッケージ全体にわたるため、型チェックを行うためにはパッケージ全体を考慮する必要があります。
技術的詳細
このコミットの技術的な核心は、go vet
のコード解析フローを、ファイル単位からパッケージ単位へと根本的に変更した点にあります。
-
go/types
パッケージの導入: 最も重要な変更は、go/types
パッケージがsrc/cmd/vet/main.go
にインポートされたことです。これにより、go vet
はGoの公式な型チェッカーの機能を利用できるようになり、型情報に基づいたより高度な静的解析が可能になります。 -
go/build
パッケージの導入:go/build
パッケージもインポートされ、doPackageDir
関数内でbuild.Default.ImportDir
が使用されています。これは、指定されたディレクトリからGoのパッケージ情報を読み込み、そのパッケージに属するすべてのGoソースファイルを特定するために使われます。これにより、go vet
はディレクトリを走査する際に、単にGoファイルを見つけるだけでなく、それらが構成するパッケージのコンテキストを理解できるようになります。 -
doFile
からdoPackage
/doPackageDir
への移行: 以前はdoFile
関数が個々のGoファイルを解析していましたが、このコミットではdoFile
が削除され、代わりにdoPackage
とdoPackageDir
という新しい関数が導入されました。doPackageDir(directory string)
: 指定されたディレクトリ内の単一のGoパッケージを解析します。go/build
を使用してパッケージ内のすべてのGoファイルを特定し、それらをdoPackage
に渡します。doPackage(names []string)
: 指定されたファイル名のリストから単一のGoパッケージを構築し、解析します。この関数内でgo/parser.ParseFile
を使ってASTを構築し、その後go/types.Context.Check
を呼び出してパッケージ全体の型チェックを実行します。
-
コマンドライン引数の処理の変更:
main
関数内のコマンドライン引数の処理が変更されました。- 以前は、引数が指定されない場合は標準入力から読み込み、引数が指定された場合はファイルまたはディレクトリとして処理していました。
- 変更後、引数が指定されない場合は
Usage()
を表示して終了するようになりました。 - 引数がディレクトリとファイルの混在である場合はエラー (
Usage()
) となりました。これは、go vet
が単一のパッケージを処理するという新しいモデルに起因します。 - 引数がディレクトリのみの場合は
walkDir
を呼び出し、各ディレクトリに対してdoPackageDir
を実行します。 - 引数がファイルのみの場合は、それらのファイルが単一のパッケージに属すると仮定し、
doPackage
を直接呼び出します。
-
エラーハンドリングの改善:
errorf
関数がos.Exit(2)
を呼び出してプログラムを終了するようになり、新たにwarnf
関数が導入されました。warnf
はエラーメッセージを出力しますが、プログラムは終了しません (setExit(1)
を呼び出すことで終了コードを設定するのみ)。これにより、致命的ではないエラー(例:ディレクトリの処理中に発生したエラーで、他のディレクトリの処理は続行したい場合)を適切に報告できるようになりました。 -
src/cmd/vet/method.go
の修正:checkCanonicalMethod
関数内のエラーメッセージの整形ロジックが修正されました。strings.TrimPrefix(actual, "func(")
がstrings.TrimPrefix(actual, "func")
に変更されました。これは、func(
という文字列が常にプレフィックスとして存在するとは限らないケースに対応するための、小さなバグ修正です。
これらの変更により、go vet
はGoのパッケージ構造をより深く理解し、型情報に基づいたより強力な静的解析を実行するための基盤が確立されました。
コアとなるコードの変更箇所
主に src/cmd/vet/main.go
に大きな変更が集中しています。
src/cmd/vet/main.go
-
インポートの変更:
--- a/src/cmd/vet/main.go +++ b/src/cmd/vet/main.go @@ -11,10 +11,11 @@ import ( "flag" "fmt" "go/ast" + "go/build" // 追加 "go/parser" "go/printer" "go/token" - "io" // 削除 + "go/types" // 追加 "io/ioutil" "os" "path/filepath"
go/build
とgo/types
が追加され、io
が削除されました。 -
Usage
関数の変更:--- a/src/cmd/vet/main.go +++ b/src/cmd/vet/main.go @@ -54,6 +55,8 @@ func setExit(err int) { // Usage is a replacement usage function for the flags package. func Usage() { fmt.Fprintf(os.Stderr, "Usage of %s:\\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "\tvet [flags] directory...\\n") // 追加 + fmt.Fprintf(os.Stderr, "\tvet [flags] files... # Must be a single package\\n") // 追加 flag.PrintDefaults() os.Exit(2) }
vet
コマンドの新しい使用方法が追加されました。 -
File
struct へのname
フィールド追加:--- a/src/cmd/vet/main.go +++ b/src/cmd/vet/main.go @@ -62,6 +65,7 @@ func Usage() { // The parse tree walkers are all methods of this type. type File struct {\n \tfset *token.FileSet\n+\tname string // 追加\n \tfile *ast.File\n \tb bytes.Buffer // for use by methods\n }\n ``` `File` 構造体に `name` フィールドが追加され、ファイル名が保持されるようになりました。
-
main
関数の大幅な変更:--- a/src/cmd/vet/main.go +++ b/src/cmd/vet/main.go @@ -102,56 +106,104 @@ func main() { }\n \n \tif flag.NArg() == 0 {\n-\t\tdoFile("stdin", os.Stdin)\n-\t} else {\n+\t\tUsage()\n+\t}\n+\tdirs := false\n+\tfiles := false\n+\tfor _, name := range flag.Args() {\n+\t\t// Is it a directory?\n+\t\tfi, err := os.Stat(name)\n+\t\tif err != nil {\n+\t\t\twarnf("error walking tree: %s", err)\n+\t\t\tcontinue\n+\t\t}\n+\t\tif fi.IsDir() {\n+\t\t\tdirs = true\n+\t\t} else {\n+\t\t\tfiles = true\n+\t\t}\n+\t}\n+\tif dirs && files {\n+\t\tUsage()\n+\t}\n+\tif dirs {\n for _, name := range flag.Args() {\n-\t\t\t// Is it a directory?\n-\t\t\tif fi, err := os.Stat(name); err == nil && fi.IsDir() {\n-\t\t\t\twalkDir(name)\n-\t\t\t} else {\n-\t\t\t\tdoFile(name, nil)\n-\t\t\t}\n+\t\t\twalkDir(name)\n }\n+\t\treturn\n }\n+\tdoPackage(flag.Args())\n os.Exit(exitCode)\n }
引数の解析ロジックが完全に変更され、ディレクトリとファイルの混在を禁止し、それぞれ
walkDir
またはdoPackage
を呼び出すようになりました。 -
doFile
の削除とdoPackageDir
,doPackage
の追加:--- a/src/cmd/vet/main.go +++ b/src/cmd/vet/main.go @@ -160,11 +212,18 @@ func walkDir(root string) { // error formats the error to standard error, adding program // identification and a newline func errorf(format string, args ...interface{}) { fmt.Fprintf(os.Stderr, "vet: "+format+"\\n", args...)\n-\tsetExit(2)\n+\tos.Exit(2) // 変更: 常に終了する\n+}\n+\n+// warnf formats the error to standard error, adding program\n+// identification and a newline, but does not exit.\n+func warnf(format string, args ...interface{}) {\n+ fmt.Fprintf(os.Stderr, "vet: "+format+"\\n", args...)\n+ setExit(1) // 追加: 終了しない警告\n }
doFile
関数が削除され、代わりにパッケージ単位で処理を行うdoPackageDir
とdoPackage
が追加されました。doPackage
内でgo/types.Context.Check
が呼び出されています。 -
visit
関数の変更:--- a/src/cmd/vet/main.go +++ b/src/cmd/vet/main.go @@ -160,11 +212,18 @@ func walkDir(root string) { // error formats the error to standard error, adding program // identification and a newline func errorf(format string, args ...interface{}) { fmt.Fprintf(os.Stderr, "vet: "+format+"\\n", args...)\n-\tsetExit(2)\n+\tos.Exit(2)\n+}\n+\n+// warnf formats the error to standard error, adding program\n+// identification and a newline, but does not exit.\n+func warnf(format string, args ...interface{}) {\n+ fmt.Fprintf(os.Stderr, "vet: "+format+"\\n", args...)\n+ setExit(1)\n }
filepath.Walk
のコールバック関数であるvisit
が、ファイルではなくディレクトリを処理するように変更され、doPackageDir
を呼び出すようになりました。 -
エラー報告関数の変更:
errorf
が常にos.Exit(2)
を呼び出すようになり、新たにwarnf
が追加されました。
src/cmd/vet/method.go
checkCanonicalMethod
関数の修正:--- a/src/cmd/vet/method.go +++ b/src/cmd/vet/method.go @@ -90,7 +90,7 @@ func (f *File) checkCanonicalMethod(id *ast.Ident, t *ast.FuncType) { fmt.Fprintf(&f.b, "<%s>", err) } actual := f.b.String() -\t\tactual = strings.TrimPrefix(actual, "func(") // 変更前 +\t\tactual = strings.TrimPrefix(actual, "func") // 変更後 actual = id.Name + actual f.Warnf(id.Pos(), "method %s should have signature %s", actual, expectFmt)
strings.TrimPrefix
の引数がfunc(
からfunc
に変更されました。
コアとなるコードの解説
このコミットのコアとなる変更は、go vet
がGoのソースコードを解析する際の基本的な単位を、個々のファイルからGoの「パッケージ」へと移行させた点にあります。
-
パッケージのロードと型チェック (
doPackage
とdoPackageDir
):doPackageDir(directory string)
: この関数は、指定されたdirectory
内のGoパッケージをロードする責任を負います。go/build.Default.ImportDir(directory, 0)
を使用して、そのディレクトリに存在するGoソースファイル(.go
ファイル、Cgoファイルなど)をすべて特定します。これにより、単一のディレクトリが単一のGoパッケージを構成するというGoの慣習に従って、関連するすべてのファイルがグループ化されます。エラーが発生した場合(例えば、Goソースファイルがない場合や、ビルドエラーがある場合)、warnf
を使って警告を出力し、処理を続行します。doPackage(names []string)
: この関数は、doPackageDir
によって収集されたファイル名のリスト (names
) を受け取り、それらのファイルからGoパッケージを構築し、型チェックを実行します。- まず、各ファイルを
go/parser.ParseFile
を使って解析し、go/ast.File
構造体(AST)のリストを生成します。 - 次に、
go/types.Context{}.Check(fs, astFiles)
を呼び出します。これがこのコミットの最も重要な部分です。go/types.Context.Check
は、与えられたファイルセット (fs
) とASTのリスト (astFiles
) を基に、Goの型チェッカーを実行します。これにより、変数、関数、型の定義と使用がGoの型規則に準拠しているかどうかが検証され、型情報が構築されます。この型情報が、その後のgo vet
の解析で利用可能になります。 - 型チェック中にエラーが発生した場合、
warnf
を使って警告を出力します。これは、型チェックエラーがあってもgo vet
の他のチェックは続行できることを意味します。 - 最後に、
for _, file := range files { file.walkFile(file.name, file.file) }
ループを通じて、各ファイルのASTをウォークし、go vet
の具体的なチェック(例えば、Printf
のフォーマットチェックなど)を実行します。この際、型チェッカーによって構築された型情報が利用可能になっているため、より高度なチェックが可能になります。
- まず、各ファイルを
-
コマンドライン引数の処理の厳格化 (
main
関数):main
関数では、コマンドライン引数がディレクトリとファイルの混在を許容しないように変更されました。これは、go vet
が単一のパッケージを処理するという新しいモデルを反映しています。もしユーザーがgo vet dir1 file1.go
のように実行しようとすると、エラーメッセージが表示され、プログラムは終了します。これは、dir1
とfile1.go
が異なるパッケージに属する可能性があるため、単一の型チェックコンテキストで処理できないためです。 -
エラーメッセージの修正 (
src/cmd/vet/method.go
):src/cmd/vet/method.go
のcheckCanonicalMethod
関数におけるstrings.TrimPrefix(actual, "func(")
からstrings.TrimPrefix(actual, "func")
への変更は、生成されるメソッドシグネチャの文字列からfunc
キーワードを正しく削除するためのものです。Goの関数シグネチャはfunc
で始まるため、その後に続く括弧の有無にかかわらずfunc
を削除することで、より汎用的にシグネチャを整形できるようになります。これは、エラーメッセージの表示品質を向上させるための小さな修正ですが、ツールの堅牢性を示しています。
これらの変更により、go vet
はGoのコードベースをより深く、より正確に理解できるようになり、その結果、より有用な静的解析結果を提供できるようになりました。
関連リンク
- Go Change-Id:
I2222222222222222222222222222222222222222
(これはコミットメッセージに記載されているhttps://golang.org/cl/7393052
の Change-Id に対応します) - Go CL (Code Review) 7393052: https://golang.org/cl/7393052
参考にした情報源リンク
- Go言語公式ドキュメント:
go vet
コマンド: https://pkg.go.dev/cmd/vetgo/ast
パッケージ: https://pkg.go.dev/go/astgo/parser
パッケージ: https://pkg.go.dev/go/parsergo/token
パッケージ: https://pkg.go.dev/go/tokengo/build
パッケージ: https://pkg.go.dev/go/buildgo/types
パッケージ: https://pkg.go.dev/go/types
- Go言語のパッケージとモジュールに関する一般的な情報源
- 静的解析に関する一般的な情報源
- 抽象構文木 (AST) と型チェックに関する一般的な情報源
- Go言語のソースコード解析に関するブログ記事やチュートリアル (一般的な知識の補強のため)
- Go言語のコミット履歴とコードレビューシステム (Gerrit) の利用方法に関する情報 (CLリンクの理解のため)
- Rob Pike氏のGo言語に関する講演や記事 (Go言語の設計思想を理解するため)
- Brad Fitzpatrick氏のGo言語に関する貢献 (レビュー担当者として)
- Ian Lance Taylor氏 (gri) のGo言語に関する貢献 (レビュー担当者として)