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

[インデックス 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.CompositeLitast.ParenExpr などのASTノードを直接検査することで、型情報に依存せずにリテラルの種類を判断しようとしています。

技術的詳細

このコミットの主要な変更点は、cmd/vettaglit.go ファイルにある checkUntaggedLiteral 関数に、go/types パッケージからの型情報がない場合でも、複合リテラルが構造体リテラルであり、かつフィールドタグが不要であると判断できるロジックを追加したことです。

以前のバージョンでは、isStruct 関数が go/types の情報に大きく依存しており、型情報がない場合は isStructfalse を返し、結果として checkUntaggedLiteral が早期リターンしていました。しかし、これは型情報がない場合に、本来警告すべきでないケースでも警告を出す原因となっていました。

新しいロジックでは、ast.CompositeLitType フィールドを直接検査することで、型情報なしでリテラルの種類を推測します。

  1. 括弧の除去: ast.ParenExpr を介して型が括弧で囲まれている場合、内側の型にアクセスします。
  2. リテラル型の直接判定:
    • *ast.ArrayType (配列リテラル) の場合、タグは不要なので return
    • *ast.MapType (マップリテラル) の場合、タグは不要なので return
    • *ast.StructType (匿名構造体リテラル) の場合、その場で定義された構造体なのでタグは不要であり return
    • *ast.Ident (単純な型名、例: MyStruct{...}) の場合、その型が現在のパッケージで宣言されている可能性が高く、タグは不要と判断して return。これは、import . "pkg" のような特殊なケースを除けば、ほとんどの場合に当てはまります。
  3. pkg.Name のようなセレクタ型: 上記のいずれにも当てはまらない場合、型は pkg.Name のようなセレクタ型であると推測されます。この場合のみ、isStruct 関数を呼び出して、それが構造体であるかどうかを go/types の情報を使って確認します。

これにより、go/types の情報がなくても、配列、マップ、匿名構造体、または単純な型名のリテラルであれば、誤って「タグなしフィールド」の警告を出すことがなくなりました。

また、src/cmd/vet/types.goisStruct 関数も変更され、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.gocheckUntaggedLiteral 関数

--- 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.goisStruct 関数

--- 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.goisStruct 関数

--- 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.gocheckUntaggedLiteral 関数

この関数は、複合リテラルがタグなしフィールドを持つ構造体リテラルであるかどうかをチェックします。

 	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.goisStruct 関数

 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.goisStruct 関数

 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 は型情報が不足している状況でも、より賢明に「タグなしフィールド」の警告を抑制できるようになり、誤検知によるノイズが大幅に削減されました。

関連リンク

参考にした情報源リンク