[インデックス 15593] ファイルの概要
このコミットは、Go言語の型チェッカーである go/types
パッケージのテストスイートを拡張し、Go標準ライブラリ全体を型チェックの対象に加えるものです。これにより、go/types
パッケージ自体の堅牢性と正確性を、大規模かつ実用的なコードベース(Go標準ライブラリ)に対して検証できるようになります。また、関連性の低い return.go
ファイルにTODOコメントが追加されています。
コミット
commit d01516796c94f6c4da58bbc2f8c010e3f48ef220
Author: Robert Griesemer <gri@golang.org>
Date: Tue Mar 5 11:42:43 2013 -0800
go/types: add test typechecking std lib
- run time is limited if -short is set
- also added missing TODO to unrelated file return.go
R=r
CC=golang-dev
https://golang.org/cl/7448052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d01516796c94f6c4da58bbc2f8c010e3f48ef220
元コミット内容
go/types: add test typechecking std lib
- run time is limited if -short is set
- also added missing TODO to unrelated file return.go
R=r
CC=golang-dev
https://golang.org/cl/7448052
変更の背景
Go言語の go/types
パッケージは、Goプログラムの静的型チェックを行うための重要なコンポーネントです。これはGoコンパイラの一部として機能し、コードがGoの型規則に準拠していることを保証します。このパッケージの正確性は、Go言語全体の健全性にとって極めて重要です。
このコミット以前は、go/types
パッケージのテストは、おそらく小規模な単体テストや特定のコーナーケースを対象としたものに限られていたと考えられます。しかし、型チェッカーの真の堅牢性を検証するためには、大規模で複雑な実際のコードベースに対してテストを行うことが不可欠です。Go標準ライブラリは、Go言語で書かれた最も広範で信頼性の高いコードベースの一つであり、go/types
パッケージのテスト対象として理想的です。
この変更の主な目的は、Go標準ライブラリ全体を go/types
パッケージで型チェックする新しいテストを追加することで、型チェッカーの網羅的な検証を可能にすることです。これにより、型チェッカーが標準ライブラリ内のあらゆる有効なGoコードを正しく処理できることを保証し、潜在的なバグや不整合を早期に発見できるようになります。
また、テストの実行時間が長くなる可能性があるため、go test -short
フラグが設定されている場合にはテストの実行時間を制限するメカニズムが導入されています。これは、継続的インテグレーション(CI)環境など、テストの迅速な実行が求められる場面での利便性を考慮したものです。
return.go
へのTODOコメントの追加は、直接的な機能変更とは関係ありませんが、コードベースの品質向上と将来的な最適化の可能性を示すものです。
前提知識の解説
このコミットの理解には、以下のGo言語および関連ツールの知識が役立ちます。
Go言語の型システムと go/types
パッケージ
Goは静的型付け言語であり、プログラムの実行前にすべての変数の型が決定されます。go/types
パッケージは、Goプログラムの抽象構文木(AST)を解析し、型情報(変数、関数、構造体などの型)を解決し、型規則に違反がないかをチェックする役割を担います。これはGoコンパイラのフロントエンドにおける重要なフェーズであり、コードの正しさを保証します。
go/parser
と go/token
パッケージ
go/token
: Goソースコードのトークン(キーワード、識別子、演算子など)を管理し、ファイル内の位置情報(行番号、列番号など)を追跡するためのパッケージです。token.FileSet
は、複数のファイルにまたがる位置情報を一元的に管理するために使用されます。go/parser
: Goソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を構築するためのパッケージです。ASTは、プログラムの構造を木構造で表現したもので、型チェックやコード生成などの後続の処理で利用されます。parser.ParseFile
関数は、指定されたGoソースファイルを解析し、ASTを返します。
go/build
パッケージ
go/build
パッケージは、Goのパッケージ構造とビルドコンテキストに関する情報を提供します。これを使用すると、特定のディレクトリにあるGoソースファイルからパッケージ情報を(インポートパス、依存関係、含まれるファイルなど)取得できます。build.Default
は、現在のシステム環境に基づくデフォルトのビルドコンテキストを提供します。
Goのテストフレームワークと testing.Short()
Goには標準で強力なテストフレームワークが組み込まれています。testing
パッケージを使用することで、ユニットテスト、ベンチマークテストなどを記述できます。
testing.T
: 各テスト関数に渡される型で、テストの失敗を報告したり、ログを出力したりするためのメソッドを提供します。testing.Short()
: この関数は、go test
コマンドに-short
フラグが指定されている場合にtrue
を返します。開発者がすべてのテストを実行するのではなく、短時間で完了するテストのみを実行したい場合に利用されます。通常、ネットワークアクセスやファイルI/Oを伴う時間のかかるテストは、testing.Short()
がtrue
の場合にスキップされるように実装されます。
Go標準ライブラリの構造
Go標準ライブラリのソースコードは、Goのインストールディレクトリ(GOROOT
)内の src/pkg
ディレクトリに配置されています。各サブディレクトリがGoのパッケージに対応しており、例えば fmt
パッケージは src/pkg/fmt
にあります。
技術的詳細
このコミットで追加された stdlib_test.go
は、Go標準ライブラリ全体を go/types
パッケージで型チェックするための包括的なテストスイートを提供します。主要な関数とその役割は以下の通りです。
TestStdlib(t *testing.T)
- このテストファイルのエントリポイントとなるテスト関数です。
filepath.Join(runtime.GOROOT(), "src/pkg")
を使用して、Go標準ライブラリのルートディレクトリ(通常は$GOROOT/src/pkg
)のパスを構築します。walkDirs(t, ...)
を呼び出し、標準ライブラリのディレクトリツリーを再帰的に探索し、各Goパッケージを型チェックします。verbose
フラグが設定されている場合、処理されたパッケージ数と合計実行時間を表示します。
walkDirs(t *testing.T, dir string)
- 指定されたディレクトリ
dir
を再帰的に探索する関数です。 - 短時間テストの制限:
testing.Short()
がtrue
の場合、つまりgo test -short
で実行されている場合、テストの実行時間が750ms
を超えると、それ以上の探索を停止し、テストを早期に終了します。これにより、CI環境などでのテスト実行時間を短縮します。 ioutil.ReadDir(dir)
を使用して、現在のディレクトリ内のファイルとサブディレクトリの情報を取得します。pkgfiles(t, dir)
を呼び出して、現在のディレクトリがGoパッケージであるかどうかを判断し、もしそうであればそのパッケージのGoソースファイルリストを取得します。ファイルリストがnil
でなければ、typecheck(t, files)
を呼び出してそのパッケージを型チェックします。- サブディレクトリを探索します。
fi.IsDir()
でディレクトリであることを確認し、fi.Name() != "testdata"
でtestdata
ディレクトリをスキップします。testdata
ディレクトリは通常、テスト用の補助ファイルや不正なコード例などが含まれており、型チェックの対象外とすることが適切です。
pkgfiles(t *testing.T, dir string)
- 指定されたディレクトリ
dir
にあるGoパッケージのソースファイルリストを返します。 go/build.Default
を使用してビルドコンテキストを取得し、CgoEnabled = false
を設定してCgoを無効にします。これは、型チェックの目的でCgoのビルドプロセスを必要としないためです。ctxt.ImportDir(dir, 0)
を使用して、指定されたディレクトリからGoパッケージ情報をインポートします。build.NoGoError
の場合は、Goファイルがないディレクトリなのでnil
を返します。excluded
マップ(builtin
パッケージが含まれる)に含まれるパッケージはスキップします。builtin
パッケージはGo言語の組み込み型や関数を定義しており、通常のGoソースファイルとは異なる特殊な扱いを受けるため、型チェックの対象から除外されます。pkg.GoFiles
とpkg.TestGoFiles
から、パッケージのGoソースファイルとテストGoソースファイルの絶対パスを構築し、それらを結合して返します。
typecheck(t *testing.T, filenames []string)
- 与えられたGoソースファイルのリストを型チェックする関数です。
token.NewFileSet()
で新しいFileSet
を作成します。これは、複数のGoソースファイルにわたる位置情報を管理するために必要です。- 各
filename
に対してparser.ParseFile
を呼び出し、Goソースファイルを解析してASTを構築します。parser.DeclarationErrors|parser.AllErrors
フラグは、宣言エラーを含むすべてのエラーを報告するようにパーサーに指示します。 - パーサーエラーが発生した場合、それが
scanner.ErrorList
であれば個々のエラーをすべてt.Error
で報告し、そうでなければ単一のエラーを報告して処理を終了します。 verbose
フラグが設定されている場合、現在処理中のパッケージ名とファイル名を表示します。go/types.Context
を作成し、Error
フィールドにfunc(err error) { t.Error(err) }
を設定します。これにより、型チェック中に発生したエラーがGoのテストフレームワークに報告され、テストが失敗するようになります。ctxt.Check(fset, files)
を呼び出し、実際に型チェックを実行します。- 型チェックが完了すると、
pkgCount
をインクリメントします。
src/pkg/go/types/return.go
の変更
isTerminating
関数内の不要な改行が削除されました。これは純粋なコードフォーマットの修正です。hasBreak
関数に関するTODOコメントが追加されました。このコメントは、ネストされたbreakable
ステートメント(for
,switch
,select
など)に対してhasBreak
が同じサブツリーを繰り返し走査する可能性があることを指摘し、将来的に単一パスのラベル/ブレークマッチングフェーズに置き換えることでパフォーマンスを改善できる可能性を示唆しています。これは型チェックのロジック自体には直接影響しませんが、コードの保守性と効率性に関する改善点を示しています。
コアとなるコードの変更箇所
src/pkg/go/types/stdlib_test.go
(新規ファイル)
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file tests types.Check by using it to
// typecheck the standard library.
package types
import (
"flag"
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/scanner"
"go/token"
"io/ioutil"
"path/filepath"
"runtime"
"testing"
"time"
)
var verbose = flag.Bool("types.v", false, "verbose mode")
var (
pkgCount int // number of packages processed
start = time.Now()
)
func TestStdlib(t *testing.T) {
walkDirs(t, filepath.Join(runtime.GOROOT(), "src/pkg"))
if *verbose {
fmt.Println(pkgCount, "packages typechecked in", time.Since(start))
}
}
// Package paths of excluded packages.
var excluded = map[string]bool{
"builtin": true,
}
// typecheck typechecks the given package files.
func typecheck(t *testing.T, filenames []string) {
fset := token.NewFileSet()
// parse package files
var files []*ast.File
for _, filename := range filenames {
file, err := parser.ParseFile(fset, filename, nil, parser.DeclarationErrors|parser.AllErrors)
if err != nil {
// the parser error may be a list of individual errors; report them all
if list, ok := err.(scanner.ErrorList); ok {
for _, err := range list {
t.Error(err)
}
return
}
t.Error(err)
return
}
if *verbose {
if len(files) == 0 {
fmt.Println("package", file.Name.Name)
}
fmt.Println("\t", filename)
}
files = append(files, file)
}
// typecheck package files
ctxt := Context{
Error: func(err error) { t.Error(err) },
}
ctxt.Check(fset, files)
pkgCount++
}
// pkgfiles returns the list of package files for the given directory.
func pkgfiles(t *testing.T, dir string) []string {
ctxt := build.Default
ctxt.CgoEnabled = false
pkg, err := ctxt.ImportDir(dir, 0)
if err != nil {
if _, nogo := err.(*build.NoGoError); !nogo {
t.Error(err)
}
return nil
}
if excluded[pkg.ImportPath] {
return nil
}
var filenames []string
for _, name := range pkg.GoFiles {
filenames = append(filenames, filepath.Join(pkg.Dir, name))
}
for _, name := range pkg.TestGoFiles {
filenames = append(filenames, filepath.Join(pkg.Dir, name))
}
return filenames
}
// Note: Could use filepath.Walk instead of walkDirs but that wouldn't
// necessarily be shorter or clearer after adding the code to
// terminate early for -short tests.
func walkDirs(t *testing.T, dir string) {
// limit run time for short tests
if testing.Short() && time.Since(start) >= 750*time.Millisecond {
return
}
fis, err := ioutil.ReadDir(dir)
if err != nil {
t.Error(err)
return
}
// typecheck package in directory
if files := pkgfiles(t, dir); files != nil {
typecheck(t, files)
}
// traverse subdirectories, but don't walk into testdata
for _, fi := range fis {
if fi.IsDir() && fi.Name() != "testdata" {
walkDirs(t, filepath.Join(dir, fi.Name()))
}
}
}
src/pkg/go/types/return.go
--- a/src/pkg/go/types/return.go
+++ b/src/pkg/go/types/return.go
@@ -81,7 +81,6 @@ func (check *checker) isTerminating(s ast.Stmt, label string) bool {
if s.Cond == nil && !hasBreak(s.Body, label, true) {
return true
}
-
}
return false
@@ -106,6 +105,10 @@ func (check *checker) isTerminatingSwitch(body *ast.BlockStmt, label string) boo
return hasDefault
}
+// TODO(gri) For nested breakable statements, the current implementation of hasBreak
+// will traverse the same subtree repeatedly, once for each label. Replace
+// with a single-pass label/break matching phase.
+
// hasBreak reports if s is or contains a break statement
// referring to the label-ed statement or implicit-ly the
// closest outer breakable statement.
コアとなるコードの解説
stdlib_test.go
このファイルは、Go標準ライブラリの型チェックを行うためのテストロジックを完全にカプセル化しています。
- パッケージ宣言とインポート:
package types
と宣言されており、go/types
パッケージの一部としてテストが実行されることを示します。必要な標準ライブラリパッケージ(flag
,fmt
,go/ast
,go/build
,go/parser
,go/scanner
,go/token
,io/ioutil
,path/filepath
,runtime
,testing
,time
)がインポートされています。 verbose
フラグ: コマンドライン引数-types.v
で詳細モードを有効にできるbool
型のフラグです。テストの実行状況をより詳細に確認したい場合に利用されます。pkgCount
とstart
:pkgCount
は型チェックされたパッケージの総数を追跡し、start
はテスト開始時刻を記録して全体の実行時間を計算するために使用されます。TestStdlib
関数:runtime.GOROOT()
を使ってGoのインストールルートパスを取得し、src/pkg
を結合して標準ライブラリのソースディレクトリの絶対パスを生成します。walkDirs
関数を呼び出し、このパスから再帰的にディレクトリを探索し、Goパッケージを型チェックします。- テスト終了後、
verbose
フラグがtrue
であれば、型チェックされたパッケージの総数と、テスト開始からの経過時間を表示します。
excluded
マップ: 型チェックから除外されるパッケージのインポートパスを定義します。現時点ではbuiltin
パッケージのみが除外されています。builtin
パッケージはGo言語の組み込み型や関数を定義しており、通常のGoソースファイルとは異なる特殊な扱いを受けるため、型チェックの対象外とすることが適切です。typecheck
関数:token.NewFileSet()
で新しいファイルセットを作成します。これは、複数のGoソースファイルにわたるコード位置情報を一元的に管理するために不可欠です。- 引数で渡された
filenames
リストの各ファイルについて、parser.ParseFile
を使用してGoソースコードを解析し、AST(抽象構文木)を構築します。parser.DeclarationErrors|parser.AllErrors
は、パーサーが宣言エラーを含むすべての構文エラーを報告するように指示します。 - パーサーエラーが発生した場合、それが
scanner.ErrorList
(複数のエラーを含むリスト)であれば、リスト内の各エラーをt.Error
で報告します。そうでなければ、単一のエラーをt.Error
で報告します。これにより、テストフレームワークがエラーを捕捉し、テストを失敗させることができます。 verbose
モードが有効な場合、処理中のパッケージ名とファイル名を出力します。go/types.Context
のインスタンスを作成し、そのError
フィールドに匿名関数を設定します。この関数は、go/types
パッケージが型チェック中にエラーを検出した際に呼び出されます。t.Error(err)
を呼び出すことで、型チェックエラーがGoのテストフレームワークに報告され、テストが失敗します。ctxt.Check(fset, files)
を呼び出し、実際に型チェックを実行します。このメソッドが、与えられたファイルセットとASTのリストに対して型チェックを行い、エラーがあればContext.Error
に設定された関数を呼び出します。- 型チェックが成功裏に完了すると、
pkgCount
をインクリメントします。
pkgfiles
関数:- 指定されたディレクトリ
dir
内のGoパッケージに関連するソースファイル(通常のGoファイルとテストファイル)のリストを収集します。 go/build.Default
を使用してデフォルトのビルドコンテキストを取得し、CgoEnabled
をfalse
に設定します。これは、Cgo(C言語との連携)が型チェックの目的では不要であるためです。ctxt.ImportDir(dir, 0)
を呼び出して、指定されたディレクトリのGoパッケージ情報をインポートします。これにより、パッケージのインポートパス、含まれるGoファイル、テストファイルなどの情報が得られます。- エラーが発生した場合、それが
build.NoGoError
(Goファイルが見つからないことを示す)でなければt.Error
で報告します。 excluded
マップにパッケージのインポートパスが含まれている場合、そのパッケージはスキップされnil
が返されます。pkg.GoFiles
とpkg.TestGoFiles
から、パッケージのGoソースファイルとテストGoソースファイルの絶対パスを構築し、それらを結合して返します。
- 指定されたディレクトリ
walkDirs
関数:- Go標準ライブラリのディレクトリツリーを再帰的に探索し、各Goパッケージを型チェックするための中心的なロジックを提供します。
- 短時間テストの最適化:
testing.Short()
がtrue
であり、かつテスト開始から750ms
以上経過している場合、関数は即座にリターンします。これにより、go test -short
で実行された場合のテスト時間を制限し、CIなどの環境での迅速なフィードバックを可能にします。 ioutil.ReadDir(dir)
を使用して、現在のディレクトリ内のエントリ(ファイルやサブディレクトリ)を読み取ります。pkgfiles(t, dir)
を呼び出し、現在のディレクトリがGoパッケージであるかどうかを判断します。もしGoパッケージであれば、typecheck(t, files)
を呼び出してそのパッケージを型チェックします。- ディレクトリ内の各エントリをループし、それがディレクトリであり、かつ
testdata
ディレクトリではない場合に、walkDirs
を再帰的に呼び出してサブディレクトリを探索します。testdata
ディレクトリは通常、テスト用の補助ファイルや意図的に不正なコード例などが含まれており、型チェックの対象外とすることが一般的です。
src/pkg/go/types/return.go
このファイルへの変更は非常に小規模であり、主にコードのクリーンアップと将来の最適化に関するメモの追加です。
- 不要な改行の削除:
isTerminating
関数内のif
ステートメントの後にあった不要な改行が削除されました。これは純粋にコードのフォーマットに関する修正であり、機能的な変更はありません。 - TODOコメントの追加:
hasBreak
関数に関するTODOコメントが追加されました。このコメントは、hasBreak
の現在の実装が、ネストされたbreakable
ステートメント(for
,switch
,select
など)に対して、同じサブツリーをラベルごとに繰り返し走査する可能性があることを指摘しています。提案されている改善策は、「単一パスのラベル/ブレークマッチングフェーズ」に置き換えることで、パフォーマンスを向上させるというものです。これは、型チェッカーの効率性に関する将来的な改善の可能性を示唆するものであり、このコミットの主要な目的である標準ライブラリの型チェックテストの追加とは直接関係ありません。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
go/types
パッケージのドキュメント: https://pkg.go.dev/go/typesgo/parser
パッケージのドキュメント: https://pkg.go.dev/go/parsergo/token
パッケージのドキュメント: https://pkg.go.dev/go/tokengo/build
パッケージのドキュメント: https://pkg.go.dev/go/buildtesting
パッケージのドキュメント: https://pkg.go.dev/testing
参考にした情報源リンク
- https://github.com/golang/go/commit/d01516796c94f6c4da58bbc2f8c010e3f48ef220
- Go言語の公式ドキュメントおよびパッケージドキュメント (上記「関連リンク」に記載)
- Go言語のテストに関する一般的な知識
- Go言語のコンパイラとツールに関する一般的な知識
- Go言語の標準ライブラリの構造に関する一般的な知識
go test -short
の動作に関する一般的な知識