[インデックス 15755] ファイルの概要
このコミットは、Go言語の静的解析ツールである cmd/vet
における struct tag literal
の検査ロジックを改善し、go/types
パッケージによる型情報がない場合でも誤検知(false positives)を減らすことを目的としています。特に、型情報がなくてもリテラルがフィールドタグを必要としないことが明らかな場合に、不必要な警告を抑制することで、ツールのノイズを大幅に削減しています。
コミット
commit 3048a4c7b35cd8af0d8d0fe97a4a970e7ffe6478
Author: Russ Cox <rsc@golang.org>
Date: Wed Mar 13 17:37:37 2013 -0400
cmd/vet: make struct tag literal test work better with no go/types
Eliminate false positives when you can tell even without
type information that the literal does not need field tags.
Far too noisy otherwise.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/7797043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3048a4c7b35cd8af0d8d0fe97a4a970e7ffe6478
元コミット内容
cmd/vet: make struct tag literal test work better with no go/types
Eliminate false positives when you can tell even without
type information that the literal does not need field tags.
Far too noisy otherwise.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/7797043
変更の背景
cmd/vet
はGo言語のコードを静的に解析し、潜在的なバグや疑わしい構造を検出するツールです。このコミット以前の cmd/vet
には、struct tag literal
の検査において、go/types
パッケージからの型情報が利用できない場合に、誤った警告(false positives)を多発するという問題がありました。
具体的には、構造体リテラルがフィールドタグを必要としないことがコードの構文から明らかであるにもかかわらず、型情報がないために vet
がそれを判断できず、不必要な警告を出していました。このような誤検知は、開発者にとってノイズとなり、本当に修正すべき問題を見落とす原因となるため、ツールの有用性を損ねていました。このコミットは、この「ノイズが多すぎる」状態を改善し、vet
の実用性を高めることを目的としています。
前提知識の解説
cmd/vet
cmd/vet
は、Go言語のソースコードを静的に解析し、疑わしい構造や潜在的なエラーを報告するツールです。例えば、printf
フォーマット文字列と引数の不一致、到達不能なコード、ロックの誤用などを検出します。開発者がコードレビューやテストの前に問題を特定するのに役立ちます。
複合リテラル (Composite Literals)
Go言語における複合リテラルは、構造体、配列、スライス、マップなどの複合型の値を直接初期化するための構文です。 例:
- 構造体リテラル:
MyStruct{Field1: value1, Field2: value2}
またはMyStruct{value1, value2}
(フィールド名を省略した場合) - 配列リテラル:
[3]int{1, 2, 3}
- スライスリテラル:
[]string{"a", "b"}
- マップリテラル:
map[string]int{"key": 1}
構造体タグ (Struct Tags)
Go言語の構造体のフィールドには、json:"name"
や xml:"id"
のような文字列リテラルを付加することができます。これを構造体タグと呼びます。構造体タグは、リフレクションAPIを通じて実行時にアクセスでき、主にJSONエンコーディング/デコーディング、データベースマッピング、コマンドライン引数解析など、外部システムとの連携においてフィールドの振る舞いを制御するために使用されます。
go/types
パッケージ
go/types
パッケージは、Go言語のプログラムの型情報を扱うための標準ライブラリです。コンパイラや静的解析ツールが、変数や式の型、インターフェースの実装関係、メソッドセットなどを正確に理解するために使用します。このパッケージは、コードのセマンティックな解析に不可欠ですが、cmd/vet
が実行される環境によっては、この型情報が常に利用できるとは限りません(例えば、クロスコンパイル環境や、go/types
がインストールされていない環境など)。
ast
パッケージ (Abstract Syntax Tree)
go/ast
パッケージは、Go言語のソースコードを抽象構文木(AST)として表現するためのデータ構造を提供します。cmd/vet
のような静的解析ツールは、ソースコードをASTにパースし、そのASTを走査することでコードの構造やパターンを分析します。このコミットでは、ast.CompositeLit
や ast.ParenExpr
などのASTノードを直接検査することで、型情報に依存せずにリテラルの種類を判断しようとしています。
技術的詳細
このコミットの主要な変更点は、cmd/vet
の taglit.go
ファイルにある checkUntaggedLiteral
関数に、go/types
パッケージからの型情報がない場合でも、複合リテラルが構造体リテラルであり、かつフィールドタグが不要であると判断できるロジックを追加したことです。
以前のバージョンでは、isStruct
関数が go/types
の情報に大きく依存しており、型情報がない場合は isStruct
が false
を返し、結果として checkUntaggedLiteral
が早期リターンしていました。しかし、これは型情報がない場合に、本来警告すべきでないケースでも警告を出す原因となっていました。
新しいロジックでは、ast.CompositeLit
の Type
フィールドを直接検査することで、型情報なしでリテラルの種類を推測します。
- 括弧の除去:
ast.ParenExpr
を介して型が括弧で囲まれている場合、内側の型にアクセスします。 - リテラル型の直接判定:
*ast.ArrayType
(配列リテラル) の場合、タグは不要なのでreturn
。*ast.MapType
(マップリテラル) の場合、タグは不要なのでreturn
。*ast.StructType
(匿名構造体リテラル) の場合、その場で定義された構造体なのでタグは不要でありreturn
。*ast.Ident
(単純な型名、例:MyStruct{...}
) の場合、その型が現在のパッケージで宣言されている可能性が高く、タグは不要と判断してreturn
。これは、import . "pkg"
のような特殊なケースを除けば、ほとんどの場合に当てはまります。
pkg.Name
のようなセレクタ型: 上記のいずれにも当てはまらない場合、型はpkg.Name
のようなセレクタ型であると推測されます。この場合のみ、isStruct
関数を呼び出して、それが構造体であるかどうかをgo/types
の情報を使って確認します。
これにより、go/types
の情報がなくても、配列、マップ、匿名構造体、または単純な型名のリテラルであれば、誤って「タグなしフィールド」の警告を出すことがなくなりました。
また、src/cmd/vet/types.go
の isStruct
関数も変更され、typ == nil
の場合に true
を返すように修正されています。これは、型情報がない場合でも、checkUntaggedLiteral
が検査を続行できるようにするためです。これにより、型情報がない状況でも、ASTベースのヒューリスティックが適用されるようになります。
src/cmd/vet/typestub.go
は、go/types
が利用できない場合のスタブ実装であり、isStruct
が常に true
を返すように変更されています。これは、型情報がない場合に、checkUntaggedLiteral
が常に実行されるようにするための変更です。
src/cmd/vet/Makefile
には、go/types
なしでテストを実行するための test_notypes
ターゲットが追加され、この変更が意図通りに機能するかを確認できるようになっています。
コアとなるコードの変更箇所
src/cmd/vet/taglit.go
の checkUntaggedLiteral
関数
--- a/src/cmd/vet/taglit.go
+++ b/src/cmd/vet/taglit.go
@@ -14,18 +14,49 @@ import (
var compositeWhiteList = flag.Bool("compositewhitelist", true, "use composite white list; for testing only")
-// checkUntaggedLiteral checks if a composite literal is an struct literal with
+// checkUntaggedLiteral checks if a composite literal is a struct literal with
// untagged fields.
func (f *File) checkUntaggedLiteral(c *ast.CompositeLit) {
if !vet("composites") {
return
}
+ typ := c.Type
+ for {
+ if typ1, ok := c.Type.(*ast.ParenExpr); ok {
+ typ = typ1
+ continue
+ }
+ break
+ }
+
+ switch typ.(type) {
+ case *ast.ArrayType:
+ return
+ case *ast.MapType:
+ return
+ case *ast.StructType:
+ return // a literal struct type does not need to use tags
+ case *ast.Ident:
+ // A simple type name like t or T does not need tags either,
+ // since it is almost certainly declared in the current package.
+ // (The exception is names being used via import . "pkg", but
+ // those are already breaking the Go 1 compatibility promise,
+ // so not reporting potential additional breakage seems okay.)
+ return
+ }
+
+ // Otherwise the type is a selector like pkg.Name.
+ // We only care if pkg.Name is a struct, not if it's a map, array, or slice.
isStruct, typeString := f.pkg.isStruct(c)
if !isStruct {
return
}
+ if typeString == "" { // isStruct doesn't know
+ typeString = f.gofmt(typ)
+ }
+
// It's a struct, or we can't tell it's not a struct because we don't have types.
// Check if the CompositeLit contains an untagged field.
src/cmd/vet/types.go
の isStruct
関数
--- a/src/cmd/vet/types.go
+++ b/src/cmd/vet/types.go
@@ -47,23 +47,21 @@ func (pkg *Package) check(fs *token.FileSet, astFiles []*ast.File) error {
func (pkg *Package) isStruct(c *ast.CompositeLit) (bool, string) {
// Check that the CompositeLit's type is a slice or array (which needs no tag), if possible.
typ := pkg.types[c]
- if typ == nil {
- return false, ""
- }
// If it's a named type, pull out the underlying type.
+ actual := typ
if namedType, ok := typ.(*types.NamedType); ok {
- typ = namedType.Underlying
+ actual = namedType.Underlying
}
- switch typ.(type) {
+ if actual == nil {
+ // No type information available. Assume true, so we do the check.
+ return true, ""
+ }
+ switch actual.(type) {
case *types.Struct:
+ return true, typ.String()
default:
return false, ""
}
- typeString := ""
- if typ != nil {
- typeString = typ.String() + " "
- }
- return true, typeString
}
func (f *File) matchArgType(t printfArgType, arg ast.Expr) bool {
src/cmd/vet/typestub.go
の isStruct
関数
--- a/src/cmd/vet/typestub.go
+++ b/src/cmd/vet/typestub.go
@@ -25,5 +25,5 @@ func (pkg *Package) check(fs *token.FileSet, astFiles []*ast.File) error {
}
func (pkg *Package) isStruct(c *ast.CompositeLit) (bool, string) {
- return true, "struct" // Assume true, so we do the check.
+ return true, "" // Assume true, so we do the check.
}
src/cmd/vet/Makefile
--- a/src/cmd/vet/Makefile
+++ b/src/cmd/vet/Makefile
@@ -5,5 +5,8 @@
# Assumes go/types is installed
test testshort:
go build -tags 'vet_test gotypes'
- ../../../test/errchk ./vet -compositewhitelist=false -printfuncs='Warn:1,Warnf:1' *.go
+ ../../../test/errchk ./vet -printfuncs='Warn:1,Warnf:1' *.go
+ test_notypes:
+ go build -tags 'vet_test'
+ ../../../test/errchk ./vet -printfuncs='Warn:1,Warnf:1' *.go
コアとなるコードの解説
src/cmd/vet/taglit.go
の checkUntaggedLiteral
関数
この関数は、複合リテラルがタグなしフィールドを持つ構造体リテラルであるかどうかをチェックします。
typ := c.Type
for {
if typ1, ok := c.Type.(*ast.ParenExpr); ok {
typ = typ1
continue
}
break
}
この部分では、複合リテラルの型が (Type)
のように括弧で囲まれている場合に、内側の実際の型 (typ1
) を取得しています。これは、ASTを正確に解析するために必要です。
switch typ.(type) {
case *ast.ArrayType:
return
case *ast.MapType:
return
case *ast.StructType:
return // a literal struct type does not need to use tags
case *ast.Ident:
// A simple type name like t or T does not need tags either,
// since it is almost certainly declared in the current package.
// (The exception is names being used via import . "pkg", but
// those are already breaking the Go 1 compatibility promise,
// so not reporting potential additional breakage seems okay.)
return
}
ここがこのコミットの核心部分です。go/types
の情報に頼らずに、ASTの構造からリテラルの種類を判断しています。
*ast.ArrayType
や*ast.MapType
であれば、配列やマップのリテラルなので、構造体タグは関係ありません。*ast.StructType
であれば、それは匿名構造体のリテラルです(例:struct { X int }{1}
)。この場合も、タグは不要です。*ast.Ident
であれば、MyStruct{...}
のように、現在のパッケージで定義された型名のリテラルである可能性が高いです。このような場合も、通常はタグは不要と判断されます。import . "pkg"
のような特殊なケースは、Go 1の互換性保証を破る可能性があるため、ここでは無視しても問題ないと判断されています。
これらのケースでは、vet
は警告を出す必要がないため、関数を早期に終了します。
// Otherwise the type is a selector like pkg.Name.
// We only care if pkg.Name is a struct, not if it's a map, array, or slice.
isStruct, typeString := f.pkg.isStruct(c)
if !isStruct {
return
}
上記の switch
文で処理されなかった場合、型は pkg.Name
のようなセレクタ型であると推測されます。この場合のみ、f.pkg.isStruct(c)
を呼び出して、それが実際に構造体であるかどうかを go/types
の情報(利用可能であれば)を使って確認します。もし構造体でなければ、ここでも早期に終了します。
src/cmd/vet/types.go
の isStruct
関数
func (pkg *Package) isStruct(c *ast.CompositeLit) (bool, string) {
// Check that the CompositeLit's type is a slice or array (which needs no tag), if possible.
typ := pkg.types[c]
// If it's a named type, pull out the underlying type.
actual := typ
if namedType, ok := typ.(*types.NamedType); ok {
actual = namedType.Underlying
}
if actual == nil {
// No type information available. Assume true, so we do the check.
return true, ""
}
switch actual.(type) {
case *types.Struct:
return true, typ.String()
default:
return false, ""
}
}
この関数は、複合リテラルの型が構造体であるかどうかを go/types
の情報に基づいて判断します。
重要な変更は if actual == nil
のブロックです。以前は typ == nil
の場合に false
を返していましたが、この変更により、型情報が利用できない場合 (actual == nil
) でも true
を返すようになりました。これは、checkUntaggedLiteral
がASTベースのヒューリスティックを適用できるように、検査を続行させるための変更です。型情報がない場合でも、vet
が可能な限り多くのチェックを実行できるようにするためのものです。
src/cmd/vet/typestub.go
の isStruct
関数
func (pkg *Package) isStruct(c *ast.CompositeLit) (bool, string) {
return true, "" // Assume true, so we do the check.
}
このファイルは、go/types
が利用できないビルド環境(例えば、vet_test
タグのみでビルドする場合)のためのスタブ実装を提供します。以前は return true, "struct"
でしたが、typeString
が不要になったため ""
に変更されています。ここでの true
は、型情報がない場合に isStruct
が常に「構造体である可能性がある」と報告し、checkUntaggedLiteral
がASTベースのチェックに進むことを保証します。
src/cmd/vet/Makefile
test_notypes:
go build -tags 'vet_test'
../../../test/errchk ./vet -printfuncs='Warn:1,Warnf:1' *.go
test_notypes
ターゲットの追加は、このコミットの変更が go/types
パッケージなしで正しく機能するかを検証するためのものです。-tags 'vet_test'
を使用することで、go/types
のスタブ実装が使用され、型情報がない状況での vet
の振る舞いをテストできます。
これらの変更により、cmd/vet
は型情報が不足している状況でも、より賢明に「タグなしフィールド」の警告を抑制できるようになり、誤検知によるノイズが大幅に削減されました。
関連リンク
- Go CL (Code Review) 7797043: https://golang.org/cl/7797043
参考にした情報源リンク
- Go言語の公式ドキュメント (cmd/vet, go/ast, go/types, struct tagsなど): https://golang.org/pkg/
- Go言語の複合リテラルに関する情報: https://go.dev/ref/spec#Composite_literals
- Go言語の構造体タグに関する情報: https://go.dev/ref/spec#Struct_types
- Go言語の静的解析に関する一般的な情報 (例:
go vet
の使い方): https://go.dev/blog/go-vet - Go言語のASTに関する情報: https://go.dev/blog/go-ast-package