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

[インデックス 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/parsergo/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.GoFilespkg.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 型のフラグです。テストの実行状況をより詳細に確認したい場合に利用されます。
  • pkgCountstart: 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 を使用してデフォルトのビルドコンテキストを取得し、CgoEnabledfalse に設定します。これは、Cgo(C言語との連携)が型チェックの目的では不要であるためです。
    • ctxt.ImportDir(dir, 0) を呼び出して、指定されたディレクトリのGoパッケージ情報をインポートします。これにより、パッケージのインポートパス、含まれるGoファイル、テストファイルなどの情報が得られます。
    • エラーが発生した場合、それが build.NoGoError(Goファイルが見つからないことを示す)でなければ t.Error で報告します。
    • excluded マップにパッケージのインポートパスが含まれている場合、そのパッケージはスキップされ nil が返されます。
    • pkg.GoFilespkg.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 など)に対して、同じサブツリーをラベルごとに繰り返し走査する可能性があることを指摘しています。提案されている改善策は、「単一パスのラベル/ブレークマッチングフェーズ」に置き換えることで、パフォーマンスを向上させるというものです。これは、型チェッカーの効率性に関する将来的な改善の可能性を示唆するものであり、このコミットの主要な目的である標準ライブラリの型チェックテストの追加とは直接関係ありません。

関連リンク

参考にした情報源リンク

  • https://github.com/golang/go/commit/d01516796c94f6c4da58bbc2f8c010e3f48ef220
  • Go言語の公式ドキュメントおよびパッケージドキュメント (上記「関連リンク」に記載)
  • Go言語のテストに関する一般的な知識
  • Go言語のコンパイラとツールに関する一般的な知識
  • Go言語の標準ライブラリの構造に関する一般的な知識
  • go test -short の動作に関する一般的な知識