[インデックス 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言語に関する一般的な知識と静的解析の概念