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

[インデックス 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/parsergo/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 のコード解析フローを、ファイル単位からパッケージ単位へと根本的に変更した点にあります。

  1. go/types パッケージの導入: 最も重要な変更は、go/types パッケージが src/cmd/vet/main.go にインポートされたことです。これにより、go vet はGoの公式な型チェッカーの機能を利用できるようになり、型情報に基づいたより高度な静的解析が可能になります。

  2. go/build パッケージの導入: go/build パッケージもインポートされ、doPackageDir 関数内で build.Default.ImportDir が使用されています。これは、指定されたディレクトリからGoのパッケージ情報を読み込み、そのパッケージに属するすべてのGoソースファイルを特定するために使われます。これにより、go vet はディレクトリを走査する際に、単にGoファイルを見つけるだけでなく、それらが構成するパッケージのコンテキストを理解できるようになります。

  3. doFile から doPackage / doPackageDir への移行: 以前は doFile 関数が個々のGoファイルを解析していましたが、このコミットでは doFile が削除され、代わりに doPackagedoPackageDir という新しい関数が導入されました。

    • doPackageDir(directory string): 指定されたディレクトリ内の単一のGoパッケージを解析します。go/build を使用してパッケージ内のすべてのGoファイルを特定し、それらを doPackage に渡します。
    • doPackage(names []string): 指定されたファイル名のリストから単一のGoパッケージを構築し、解析します。この関数内で go/parser.ParseFile を使ってASTを構築し、その後 go/types.Context.Check を呼び出してパッケージ全体の型チェックを実行します。
  4. コマンドライン引数の処理の変更: main 関数内のコマンドライン引数の処理が変更されました。

    • 以前は、引数が指定されない場合は標準入力から読み込み、引数が指定された場合はファイルまたはディレクトリとして処理していました。
    • 変更後、引数が指定されない場合は Usage() を表示して終了するようになりました。
    • 引数がディレクトリとファイルの混在である場合はエラー (Usage()) となりました。これは、go vet が単一のパッケージを処理するという新しいモデルに起因します。
    • 引数がディレクトリのみの場合は walkDir を呼び出し、各ディレクトリに対して doPackageDir を実行します。
    • 引数がファイルのみの場合は、それらのファイルが単一のパッケージに属すると仮定し、doPackage を直接呼び出します。
  5. エラーハンドリングの改善: errorf 関数が os.Exit(2) を呼び出してプログラムを終了するようになり、新たに warnf 関数が導入されました。warnf はエラーメッセージを出力しますが、プログラムは終了しません (setExit(1) を呼び出すことで終了コードを設定するのみ)。これにより、致命的ではないエラー(例:ディレクトリの処理中に発生したエラーで、他のディレクトリの処理は続行したい場合)を適切に報告できるようになりました。

  6. 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/buildgo/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 関数が削除され、代わりにパッケージ単位で処理を行う doPackageDirdoPackage が追加されました。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の「パッケージ」へと移行させた点にあります。

  1. パッケージのロードと型チェック (doPackagedoPackageDir):

    • 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 のフォーマットチェックなど)を実行します。この際、型チェッカーによって構築された型情報が利用可能になっているため、より高度なチェックが可能になります。
  2. コマンドライン引数の処理の厳格化 (main 関数): main 関数では、コマンドライン引数がディレクトリとファイルの混在を許容しないように変更されました。これは、go vet が単一のパッケージを処理するという新しいモデルを反映しています。もしユーザーが go vet dir1 file1.go のように実行しようとすると、エラーメッセージが表示され、プログラムは終了します。これは、dir1file1.go が異なるパッケージに属する可能性があるため、単一の型チェックコンテキストで処理できないためです。

  3. エラーメッセージの修正 (src/cmd/vet/method.go): src/cmd/vet/method.gocheckCanonicalMethod 関数における 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言語のパッケージとモジュールに関する一般的な情報源
  • 静的解析に関する一般的な情報源
  • 抽象構文木 (AST) と型チェックに関する一般的な情報源
  • Go言語のソースコード解析に関するブログ記事やチュートリアル (一般的な知識の補強のため)
  • Go言語のコミット履歴とコードレビューシステム (Gerrit) の利用方法に関する情報 (CLリンクの理解のため)
  • Rob Pike氏のGo言語に関する講演や記事 (Go言語の設計思想を理解するため)
  • Brad Fitzpatrick氏のGo言語に関する貢献 (レビュー担当者として)
  • Ian Lance Taylor氏 (gri) のGo言語に関する貢献 (レビュー担当者として)