[インデックス 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.types
と pkg.values
のテストで十分であると判断されています。
変更の背景
go vet
はGoプログラムの潜在的なバグや疑わしい構成を検出するための静的解析ツールです。従来の vet
ツールは、解析中に最初のエラーを検出すると、それ以降の解析を中断してしまう挙動がありました。これにより、一つの問題が解決されても、その後に隠れていた別の問題が発見されないという非効率性がありました。
このコミットの背景には、vet
ツールがより多くの潜在的な問題を一度に報告できるように改善するという目的があります。最初のエラーで停止するのではなく、解析を継続することで、開発者は一度の vet
実行で複数の警告やエラーを確認し、まとめて修正できるようになります。これは開発ワークフローの効率化に寄与します。
また、f.pkg
に関するテストの削除は、コードベースのクリーンアップと最適化の一環です。Goの設計上、すべてのソースファイルは必ず何らかのパッケージに属します。したがって、f.pkg
が nil
であるかどうかをチェックするロジックは常に 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.Context
は go/types
パッケージの中心的な構造体の一つで、型チェックのコンテキストを保持します。これには、パッケージのスコープ、型情報、そしてエラーハンドリングのためのコールバック関数などが含まれます。このコミットでは、types.Context
の Error
フィールドにカスタムのエラーハンドリング関数を設定することで、型チェック中にエラーが発生しても処理を継続させるように変更しています。
f.pkg
とパッケージの概念
Go言語では、すべてのソースファイルは必ず何らかのパッケージに属します。パッケージはGoコードの組織化の基本単位であり、関連する機能や型をグループ化します。vet
ツールが内部的にファイルを処理する際、そのファイルが属するパッケージの情報を保持することがあります。コミットメッセージにある f.pkg
は、おそらく vet
ツール内部の File
構造体などが持つ、現在のファイルが属するパッケージへの参照を指していると考えられます。コミットメッセージが「すべてのファイルにはパッケージが関連付けられている」と述べているのは、Goの基本的な設計原則を指しており、f.pkg
が nil
になることは通常ありえないため、そのチェックが冗長であるという判断に至ったことを示唆しています。
技術的詳細
このコミットは、主に cmd/vet
ツール内の3つのファイル (main.go
, print.go
, taglit.go
) に変更を加えています。
-
エラー継続のメカニズム (
main.go
):doPackage
関数内でtypes.Context
を初期化する際に、Error
フィールドに空の関数func(error) {}
を設定しています。- これにより、
context.Check(fs, astFiles)
が型チェック中にエラーを検出しても、この空の関数が呼び出されるだけで、パニックを起こしたり処理を中断したりすることなく、型チェックプロセスが継続されるようになります。 - 以前は
Error
フィールドが設定されていなかったか、デフォルトの挙動(最初のエラーで停止)が適用されていたと考えられます。
-
冗長な
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.pkg
がnil
になることはないため、これらのチェックは常に同じ結果を返し、コードの複雑性を増すだけの冗長なものでした。削除することで、コードが簡潔になり、意図が明確になります。
-
ファイル名の追加 (
main.go
):doPackageDir
関数内で、pkg.GoFiles
をnames
スライスに追加する行が追加されています。これは、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.Context
は go/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.go
の checkPrintfArg
関数では、以下のようなコードが削除されています。
-\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言語公式ドキュメント: https://go.dev/doc/
go vet
コマンドについて: https://go.dev/blog/go-vetgo/ast
パッケージ: https://pkg.go.dev/go/astgo/types
パッケージ: https://pkg.go.dev/go/types
参考にした情報源リンク
- Go言語の公式ドキュメントおよびパッケージドキュメント
- Go言語のソースコード (特に
cmd/vet
ディレクトリ) - Go言語に関する一般的な知識と静的解析の概念