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

[インデックス 15480] ファイルの概要

このコミットは、Go言語の静的解析ツールである cmd/vet の内部的な改善に関するものです。具体的には、vet ツールが最初のエラーで処理を中断するのではなく、エラーが発生しても解析を継続するように変更し、また、ファイルがパッケージを持つかどうかをチェックする冗長なテストコードを削除しています。

コミット

commit 9e19337de9ab0344bcbd056064c70249e65d52ed
Author: Rob Pike <r@golang.org>
Date:   Wed Feb 27 15:43:33 2013 -0800

    cmd/vet: continue past first error
    Also delete bogus tests for f.pkg (does the file have a package) since all
    files have a package attached. The tests for pkg.types and pkg.values
    suffice.
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/7418043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/9e19337de9ab0344bcbd056064c70249e65d52ed

元コミット内容

cmd/vet: continue past first error Also delete bogus tests for f.pkg (does the file have a package) since all files have a package attached. The tests for pkg.types and pkg.values suffice.

このコミットメッセージは、cmd/vet ツールが最初のエラーで停止する動作を改め、エラーが発生しても処理を継続するように変更したことを示しています。さらに、f.pkg (ファイルがパッケージを持っているか) に関する不要なテストを削除したことも述べています。これは、すべてのGoファイルには必ずパッケージが関連付けられているため、このチェックが冗長であったためです。代わりに、pkg.typespkg.values のテストで十分であると判断されています。

変更の背景

go vet はGoプログラムの潜在的なバグや疑わしい構成を検出するための静的解析ツールです。従来の vet ツールは、解析中に最初のエラーを検出すると、それ以降の解析を中断してしまう挙動がありました。これにより、一つの問題が解決されても、その後に隠れていた別の問題が発見されないという非効率性がありました。

このコミットの背景には、vet ツールがより多くの潜在的な問題を一度に報告できるように改善するという目的があります。最初のエラーで停止するのではなく、解析を継続することで、開発者は一度の vet 実行で複数の警告やエラーを確認し、まとめて修正できるようになります。これは開発ワークフローの効率化に寄与します。

また、f.pkg に関するテストの削除は、コードベースのクリーンアップと最適化の一環です。Goの設計上、すべてのソースファイルは必ず何らかのパッケージに属します。したがって、f.pkgnil であるかどうかをチェックするロジックは常に false となり、無意味なコードパスとなっていました。このような冗長なチェックを削除することで、コードの可読性が向上し、ツールのパフォーマンスがわずかながら改善される可能性があります。

前提知識の解説

Go言語の静的解析と go vet

Go言語には、コードの品質と信頼性を向上させるための強力なツール群が標準で提供されています。その一つが go vet コマンドです。go vet は、Goのソースコードを解析し、コンパイルエラーにはならないものの、潜在的なバグや疑わしいコードパターン(例: Printf フォーマット文字列と引数の不一致、構造体タグの誤り、ロックの誤用など)を検出します。これは、コンパイラが検出できないような論理的な誤りや慣用的なGoコードからの逸脱を見つけるのに役立ちます。

GoのAST (Abstract Syntax Tree)

go vet のような静的解析ツールは、Goのソースコードを直接テキストとして扱うのではなく、抽象構文木 (AST: Abstract Syntax Tree) として解析します。ASTは、プログラムの構造を木構造で表現したもので、各ノードがコードの要素(変数宣言、関数呼び出し、式など)に対応します。go/ast パッケージはGoのソースコードをASTにパースするための機能を提供し、go/types パッケージはAST上の各ノードの型情報を解決する機能を提供します。vet ツールはこれらのパッケージを利用してコードのセマンティックな解析を行います。

go/types パッケージと types.Context

go/types パッケージは、Goプログラムの型チェックを行うための低レベルなAPIを提供します。コンパイラや静的解析ツールは、このパッケージを使用して、変数や式の型を解決し、型の一貫性を検証します。 types.Contextgo/types パッケージの中心的な構造体の一つで、型チェックのコンテキストを保持します。これには、パッケージのスコープ、型情報、そしてエラーハンドリングのためのコールバック関数などが含まれます。このコミットでは、types.ContextError フィールドにカスタムのエラーハンドリング関数を設定することで、型チェック中にエラーが発生しても処理を継続させるように変更しています。

f.pkg とパッケージの概念

Go言語では、すべてのソースファイルは必ず何らかのパッケージに属します。パッケージはGoコードの組織化の基本単位であり、関連する機能や型をグループ化します。vet ツールが内部的にファイルを処理する際、そのファイルが属するパッケージの情報を保持することがあります。コミットメッセージにある f.pkg は、おそらく vet ツール内部の File 構造体などが持つ、現在のファイルが属するパッケージへの参照を指していると考えられます。コミットメッセージが「すべてのファイルにはパッケージが関連付けられている」と述べているのは、Goの基本的な設計原則を指しており、f.pkgnil になることは通常ありえないため、そのチェックが冗長であるという判断に至ったことを示唆しています。

技術的詳細

このコミットは、主に cmd/vet ツール内の3つのファイル (main.go, print.go, taglit.go) に変更を加えています。

  1. エラー継続のメカニズム (main.go):

    • doPackage 関数内で types.Context を初期化する際に、Error フィールドに空の関数 func(error) {} を設定しています。
    • これにより、context.Check(fs, astFiles) が型チェック中にエラーを検出しても、この空の関数が呼び出されるだけで、パニックを起こしたり処理を中断したりすることなく、型チェックプロセスが継続されるようになります。
    • 以前は Error フィールドが設定されていなかったか、デフォルトの挙動(最初のエラーで停止)が適用されていたと考えられます。
  2. 冗長な f.pkg チェックの削除 (print.go, taglit.go):

    • src/cmd/vet/print.go 内の checkPrintfArg, checkPrint, numArgsInSignature, isErrorMethodCall 関数から、f.pkg == nil または f.pkg != nil といった条件分岐が削除されています。
    • 同様に、src/cmd/vet/taglit.go 内の checkUntaggedLiteral 関数からも if f.pkg != nil という条件が削除され、f.pkg が常に有効であるという前提で処理が進むようになっています。
    • これらの変更は、Goのファイルが常にパッケージに属するという事実に基づいています。f.pkgnil になることはないため、これらのチェックは常に同じ結果を返し、コードの複雑性を増すだけの冗長なものでした。削除することで、コードが簡潔になり、意図が明確になります。
  3. ファイル名の追加 (main.go):

    • doPackageDir 関数内で、pkg.GoFilesnames スライスに追加する行が追加されています。これは、vet が解析対象とするファイルリストを正しく構築するために必要な変更であり、以前は pkg.GoFiles が含まれていなかった可能性があります。

コアとなるコードの変更箇所

src/cmd/vet/main.go

--- a/src/cmd/vet/main.go
+++ b/src/cmd/vet/main.go
@@ -160,6 +160,7 @@ func doPackageDir(directory string) {
 		return
 	}
 	var names []string
+	names = append(names, pkg.GoFiles...)
 	names = append(names, pkg.CgoFiles...)
 	names = append(names, pkg.TestGoFiles...) // These are also in the "foo" package.
 	prefixDirectory(directory, names)
@@ -209,8 +210,11 @@ func doPackage(names []string) {
 			pkg.values[x] = val
 		}
 	}\n+\t// By providing the Context with our own error function, it will continue\n+\t// past the first error. There is no need for that function to do anything.\n \tcontext := types.Context{\n-\t\tExpr: exprFn,\n+\t\tExpr:  exprFn,\n+\t\tError: func(error) {},\n \t}\n \t// Type check the package.\n \t_, err := context.Check(fs, astFiles)

src/cmd/vet/print.go

--- a/src/cmd/vet/print.go
+++ b/src/cmd/vet/print.go
@@ -276,9 +276,6 @@ func (f *File) checkPrintfArg(call *ast.CallExpr, verb rune, flags []byte, argNu
 					return
 				}
 			}
-\t\t\tif f.pkg == nil { // Nothing more to do.\n-\t\t\t\treturn\n-\t\t\t}\n \t\t\t// Verb is good. If nargs>1, we have something like %.*s and all but the final
 \t\t\t// arg must be integer.
 \t\t\tfor i := 0; i < nargs-1; i++ {
@@ -373,13 +370,13 @@ func (f *File) checkPrint(call *ast.CallExpr, name string, firstArg int) {
 		// If we have a call to a method called Error that satisfies the Error interface,
 		// then it's ok. Otherwise it's something like (*T).Error from the testing package
 		// and we need to check it.
-\t\tif name == "Error" && f.pkg != nil && f.isErrorMethodCall(call) {\n+\t\tif name == "Error" && f.isErrorMethodCall(call) {\n \t\t\treturn
 \t\t}
 \t\t// If it's an Error call now, it's probably for printing errors.
 \t\tif !isLn {
 \t\t\t// Check the signature to be sure: there are niladic functions called "error".
-\t\t\tif f.pkg == nil || firstArg != 0 || f.numArgsInSignature(call) != firstArg {\n+\t\t\tif firstArg != 0 || f.numArgsInSignature(call) != firstArg {\n \t\t\t\tf.Badf(call.Pos(), "no args in %s call", name)
 \t\t\t}
 \t\t}
@@ -403,7 +400,7 @@ func (f *File) checkPrint(call *ast.CallExpr, name string, firstArg int) {
 }
 
 // numArgsInSignature tells how many formal arguments the function type
-// being called has. Assumes type checking is on (f.pkg != nil).
+// 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]
@@ -420,8 +417,7 @@ func (f *File) numArgsInSignature(call *ast.CallExpr) bool {
 
 // 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"
-// and f.pkg is set.
+// 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)

src/cmd/vet/taglit.go

--- a/src/cmd/vet/taglit.go
+++ b/src/cmd/vet/taglit.go
@@ -22,20 +22,18 @@ func (f *File) checkUntaggedLiteral(c *ast.CompositeLit) {
 		return
 	}
 
-\t// Check that the CompositeLit's type is a slice or array (which need no tag), if possible.\n-\tif f.pkg != nil {\n-\t\ttyp := f.pkg.types[c]\n-\t\tif typ != nil {\n-\t\t\t// If it's a named type, pull out the underlying type.\n-\t\t\tif namedType, ok := typ.(*types.NamedType); ok {\n-\t\t\t\ttyp = namedType.Underlying\n-\t\t\t}\n-\t\t\tswitch typ.(type) {\n-\t\t\tcase *types.Slice:\n-\t\t\t\treturn\n-\t\t\tcase *types.Array:\n-\t\t\t\treturn\n-\t\t\t}\n+\t// Check that the CompositeLit's type is a slice or array (which needs no tag), if possible.\n+\ttyp := f.pkg.types[c]\n+\tif typ != nil {\n+\t\t// If it's a named type, pull out the underlying type.\n+\t\tif namedType, ok := typ.(*types.NamedType); ok {\n+\t\t\ttyp = namedType.Underlying\n+\t\t}\n+\t\tswitch typ.(type) {\n+\t\tcase *types.Slice:\n+\t\t\treturn\n+\t\tcase *types.Array:\n+\t\t\treturn\n \t\t}\n \t}\
 
@@ -69,8 +67,8 @@ func (f *File) checkUntaggedLiteral(c *ast.CompositeLit) {
 		f.Warnf(c.Pos(), "unresolvable package for %s.%s literal", pkg.Name, s.Sel.Name)
 		return
 	}\n-\ttyp := path + "." + s.Sel.Name\n-\tif *compositeWhiteList && untaggedLiteralWhitelist[typ] {\n+\ttypeName := path + "." + s.Sel.Name\n+\tif *compositeWhiteList && untaggedLiteralWhitelist[typeName] {\n \t\treturn
 \t}
 

コアとなるコードの解説

main.go の変更点

main.go の変更は、go vet のエラーハンドリングポリシーの根本的な変更を示しています。

	// 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)

types.Contextgo/types パッケージが型チェックを行う際に使用するコンテキストです。この構造体には Error フィールドがあり、型チェック中にエラーが発生した際に呼び出される関数を定義できます。以前は、この Error フィールドが設定されていなかったか、デフォルトの挙動(最初のエラーで処理を中断)が適用されていたと考えられます。

このコミットでは、Error: func(error) {} という空の関数を Error フィールドに割り当てています。これにより、context.Check メソッドが型チェックエラーを検出しても、この空の関数が呼び出されるだけで、vet ツールはエラーをログに記録しつつも、その後の解析を継続できるようになります。これは、vet が一度の実行でより多くの潜在的な問題を報告できるようにするための重要な変更です。

また、names = append(names, pkg.GoFiles...) の追加は、vet が解析対象とするGoソースファイルのリストに、通常のGoファイルを確実に含めるための修正です。これにより、vet が意図したすべてのファイルを正しく処理できるようになります。

print.go および taglit.go の変更点

これらのファイルにおける変更は、主に冗長な f.pkg のチェックを削除することに焦点を当てています。

例えば、print.gocheckPrintfArg 関数では、以下のようなコードが削除されています。

-\t\t\tif f.pkg == nil { // Nothing more to do.\n-\t\t\t\treturn\n-\t\t\t}\n```

このコードは、「もしファイルがパッケージを持っていなければ、これ以上何もしない」というチェックを行っていました。しかし、Goの設計上、すべてのGoソースファイルは必ずパッケージに属するため、`f.pkg` が `nil` になることはありません。したがって、この条件は常に `false` となり、この `if` ブロック内の `return` 文が実行されることはありませんでした。このようなチェックは無意味であり、コードの複雑性を不必要に高めていました。

同様に、`taglit.go` の `checkUntaggedLiteral` 関数でも、`if f.pkg != nil` という条件が削除されています。

```diff
-\tif f.pkg != nil {\n-\t\ttyp := f.pkg.types[c]\n-\t\tif typ != nil {\n-\t\t\t// If it's a named type, pull out the underlying type.\n-\t\t\tif namedType, ok := typ.(*types.NamedType); ok {\n-\t\t\t\ttyp = namedType.Underlying\n-\t\t\t}\n-\t\t\tswitch typ.(type) {\n-\t\t\tcase *types.Slice:\n-\t\t\t\treturn\n-\t\t\tcase *types.Array:\n-\t\t\t\treturn\n-\t\t\t}\n+\ttyp := f.pkg.types[c]\n+\tif typ != nil {\n+\t\t// If it's a named type, pull out the underlying type.\n+\t\tif namedType, ok := typ.(*types.NamedType); ok {\n+\t\t\ttyp = namedType.Underlying\n+\t\t}\n+\t\tswitch typ.(type) {\n+\t\tcase *types.Slice:\n+\t\t\treturn\n+\t\tcase *types.Array:\n+\t\t\treturn\n \t\t}\n \t}

この変更により、f.pkg が常に有効であるという前提で、f.pkg.types[c] から型情報を直接取得するようになっています。これにより、コードがより直接的で理解しやすくなっています。

これらの変更は、vet ツールのコードベースをよりクリーンで効率的に保つためのリファクタリングの一環です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントおよびパッケージドキュメント
  • Go言語のソースコード (特に cmd/vet ディレクトリ)
  • Go言語に関する一般的な知識と静的解析の概念