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

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

このコミットは、Go言語の静的解析ツールである vet コマンドの print サブコマンドにおける改善とバグ修正に関するものです。具体的には、fmt.Printf のような書式付き出力関数のフォーマット文字列のチェックにおいて、リテラル値だけでなく、名前付き定数として定義されたフォーマット文字列も適切にチェックできるように拡張されました。これにより、より多くの潜在的な書式エラーをコンパイル前に検出できるようになります。

コミット

commit 97a7defed437ce80534424cd8584eb97aff0e829
Author: Rob Pike <r@golang.org>
Date:   Wed Apr 25 12:14:38 2012 +1000

    vet: check values for named constants as well as literals.
    As in:
            const format = "%s"
            fmt.Printf(format, "hi")
    Also fix a couple of bugs by rewriting the routine.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6099057

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

https://github.com/golang/go/commit/97a7defed437ce80534424cd8584eb97aff0e829

元コミット内容

vet: check values for named constants as well as literals. As in: const format = "%s" fmt.Printf(format, "hi") Also fix a couple of bugs by rewriting the routine.

このコミットは、vet ツールが fmt.Printf などの書式付き出力関数の呼び出しをチェックする際に、フォーマット文字列がリテラル(直接記述された文字列)である場合だけでなく、名前付き定数として定義されている場合もその値をチェックするように拡張するものです。また、ルーチンを書き直すことで、いくつかの既存のバグも修正しています。

変更の背景

Go言語の fmt パッケージには、Printf のような書式付き出力関数があり、これらはC言語の printf と同様に、フォーマット文字列とそれに続く引数の型や数が一致しているかを厳密にチェックする必要があります。しかし、従来の go vet ツールは、フォーマット文字列が直接コードに記述されたリテラルである場合にのみ、その内容を解析して引数との整合性をチェックしていました。

例えば、以下のようなコードがあった場合、

fmt.Printf("%d", "hello") // vetはエラーを検出できる

これは vet によって「%d は整数を期待しているのに文字列が渡されている」というエラーが検出されます。しかし、フォーマット文字列が定数として定義されている場合、例えば:

const format = "%d"
fmt.Printf(format, "hello") // 従来のvetはエラーを検出できない

この場合、vetformat が定数であることを認識しても、その format の「値」が何であるかを追跡して解析する機能が不足していました。そのため、実行時エラーにつながる可能性のある潜在的なバグを見逃してしまう可能性がありました。

このコミットは、このような vet の限界を克服し、より堅牢な静的解析を提供するために行われました。名前付き定数に対してもフォーマット文字列のチェックを行うことで、開発者がより早期に、より多くの書式エラーを発見できるようになります。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の概念とツールに関する知識が必要です。

  1. go vet: go vet はGo言語のソースコードを静的に解析し、疑わしい構成要素や潜在的なエラーを報告するツールです。コンパイルは通るものの、実行時に問題を引き起こす可能性のあるコードパターン(例: Printf のフォーマット文字列と引数の不一致、到達不能なコード、ロックの誤用など)を検出するのに役立ちます。開発ワークフローにおいて、バグの早期発見とコード品質の向上に貢献します。

  2. go/ast パッケージ (Abstract Syntax Tree): go/ast パッケージは、Go言語のソースコードを抽象構文木(AST)として表現するためのデータ構造を提供します。コンパイラや静的解析ツールは、ソースコードをASTに変換し、このツリー構造を走査することでコードの意味を理解し、分析を行います。

    • ast.Expr: Go言語の式を表すインターフェースです。リテラル、識別子、関数呼び出しなど、あらゆる種類の式がこのインターフェースを実装します。
    • ast.BasicLit: 数値、文字列、真偽値などの基本的なリテラル(直接記述された値)を表すASTノードです。例えば、"hello"123 などがこれに該当します。
    • ast.Ident: 識別子(変数名、定数名、関数名など)を表すASTノードです。例えば、formatfmt.PrintfPrintf などがこれに該当します。
    • ast.ValueSpec: constvartype 宣言における個々の仕様(例: const x = 1, y = 2x = 1y = 2)を表すASTノードです。定数宣言の場合、ValueSpec はその定数の名前と初期値(式)を含みます。
  3. go/token パッケージ: go/token パッケージは、Go言語の字句解析(トークン化)で使われるトークン(キーワード、識別子、演算子、リテラルなど)の定義を提供します。

    • token.STRING: 文字列リテラルを表すトークンタイプです。
  4. fmt.Printf とフォーマット文字列: fmt.Printf は、指定されたフォーマット文字列に従って値を整形し、標準出力に出力する関数です。フォーマット文字列には、%s(文字列)、%d(整数)、%f(浮動小数点数)などの「フォーマット動詞」が含まれ、これらが続く引数に対応します。Printf の正しい使用には、フォーマット動詞と引数の型および数の厳密な一致が必要です。

これらの概念を理解することで、vet がどのようにASTを解析し、コードの構造を理解して潜在的な問題を検出しているのか、そしてこのコミットがその解析能力をどのように拡張したのかが明確になります。

技術的詳細

このコミットの主要な技術的変更点は、src/cmd/vet/print.go ファイル内の checkPrintf 関数が、フォーマット文字列の取得方法を変更したことです。

変更前は、checkPrintf 関数は call.Args[skip](フォーマット文字列が位置する引数)が直接 *ast.BasicLit(基本的なリテラル)であるかどうかをチェックし、そうでない場合は解析をスキップしていました。これは、const format = "%s" のように定数として定義されたフォーマット文字列の場合、call.Args[skip]*ast.Ident(識別子)となるため、vet がその値を追跡できなかったことを意味します。

このコミットでは、以下の新しい関数 literal が導入されました。

// literal returns the literal value represented by the expression, or nil if it is not a literal.
func (f *File) literal(value ast.Expr) *ast.BasicLit {
	switch v := value.(type) {
	case *ast.BasicLit:
		return v
	case *ast.Ident:
		// See if it's a constant or initial value (we can't tell the difference).
		if v.Obj == nil || v.Obj.Decl == nil {
			return nil
		}
		valueSpec, ok := v.Obj.Decl.(*ast.ValueSpec)
		if ok && len(valueSpec.Names) == len(valueSpec.Values) {
			// Find the index in the list of names
			var i int
			for i = 0; i < len(valueSpec.Names); i++ {
				if valueSpec.Names[i].Name == v.Name {
					if lit, ok := valueSpec.Values[i].(*ast.BasicLit); ok {
						return lit
					}
					return nil
				}
			}
		}
	}
	return nil
}

この literal 関数は、与えられた ast.Expr がリテラル値である場合に、その *ast.BasicLit を返します。

  • もし value が直接 *ast.BasicLit であれば、それをそのまま返します。
  • もし value*ast.Ident(識別子)であれば、その識別子が定数宣言 (*ast.ValueSpec) に関連付けられているかどうかをチェックします。関連付けられている場合、その定数の初期値が *ast.BasicLit であれば、そのリテラル値を返します。これにより、const format = "%s" のようなケースで、format という識別子から " %s" という文字列リテラルを抽出できるようになります。

checkPrintf 関数内では、フォーマット文字列の取得にこの新しい literal 関数が使用されるようになりました。

-	lit, ok := arg.(*ast.BasicLit)
-	if !ok {
-		// Too hard to check.
+	lit := f.literal(call.Args[skip])
+	if lit == nil {
 		if *verbose {
 			f.Warn(call.Pos(), "can't check non-literal format in call to", name)
 		}
 		return
 	}

これにより、checkPrintf はリテラルだけでなく、名前付き定数として定義されたフォーマット文字列も解析できるようになりました。

さらに、フォーマット文字列が文字列リテラルであることを確認するチェックも追加されました。

+	if lit.Kind != token.STRING {
+		f.Badf(call.Pos(), "literal %v not a string in call to", lit.Value, name)
+	}
+	format := lit.Value

これは、fmt.Printf(123, "arg") のように、フォーマット文字列が数値リテラルであるといった不正なケースを検出するためのものです。

これらの変更により、vetPrintf 系の関数の使用において、より広範なエラーパターンを検出できるようになり、ツールの堅牢性が向上しました。

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

変更は src/cmd/vet/print.go ファイルに集中しています。

  1. literal 関数の追加: print.go の54行目と55行目の間に、literal という新しいヘルパー関数が追加されました。この関数は ast.Expr を受け取り、それがリテラル(またはリテラルに解決される定数)であれば *ast.BasicLit を返します。

    --- a/src/cmd/vet/print.go
    +++ b/src/cmd/vet/print.go
    @@ -54,6 +54,33 @@ func (f *File) checkFmtPrintfCall(call *ast.CallExpr, Name string) {
     	}\n }\n \n+// literal returns the literal value represented by the expression, or nil if it is not a literal.\n+func (f *File) literal(value ast.Expr) *ast.BasicLit {\n+\tswitch v := value.(type) {\n+\tcase *ast.BasicLit:\n+\t\treturn v\n+\tcase *ast.Ident:\n+\t\t// See if it\'s a constant or initial value (we can\'t tell the difference).\n+\t\tif v.Obj == nil || v.Obj.Decl == nil {\n+\t\t\treturn nil\n+\t\t}\n+\t\tvalueSpec, ok := v.Obj.Decl.(*ast.ValueSpec)\n+\t\tif ok && len(valueSpec.Names) == len(valueSpec.Values) {\n+\t\t\t// Find the index in the list of names\n+\t\t\tvar i int\n+\t\t\tfor i = 0; i < len(valueSpec.Names); i++ {\n+\t\t\t\tif valueSpec.Names[i].Name == v.Name {\n+\t\t\t\t\tif lit, ok := valueSpec.Values[i].(*ast.BasicLit); ok {\n+\t\t\t\t\t\treturn lit\n+\t\t\t\t\t}\n+\t\t\t\t\treturn nil\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\treturn nil\n+}\n+\n     // checkPrintf checks a call to a formatted print routine such as Printf.\n     // The skip argument records how many arguments to ignore; that is,\n     // call.Args[skip] is (well, should be) the format argument.\n    ```
    
    
  2. checkPrintf 関数の変更: checkPrintf 関数内で、フォーマット文字列の引数 (call.Args[skip]) を処理するロジックが変更されました。

    • 以前は arg.(*ast.BasicLit) で直接型アサーションを行っていましたが、これを新しく追加された f.literal() 関数の呼び出しに置き換えました。
    • フォーマット文字列が token.STRING 型であることを確認する新しいチェックが追加されました。
    • フォーマット文字列の値を保持するために format 変数が導入され、以降の処理で lit.Value の代わりに format が使用されるようになりました。
    --- a/src/cmd/vet/print.go
    +++ b/src/cmd/vet/print.go
    @@ -61,31 +88,30 @@ func (f *File) checkPrintf(call *ast.CallExpr, name string, skip int) {\n     	if len(call.Args) <= skip {\n     		return\n     	}\n-    	// Common case: literal is first argument.\n-    	arg := call.Args[skip]\n-    	lit, ok := arg.(*ast.BasicLit)\n-    	if !ok {\n-    		// Too hard to check.\n+    	lit := f.literal(call.Args[skip])
    
  •   if lit == nil {
      	if *verbose {\n     			f.Warn(call.Pos(), "can't check non-literal format in call to", name)\n     		}\n     		return\n     	}\n-    	if lit.Kind == token.STRING {\n-    		if !strings.Contains(lit.Value, "%") {\n-    			if len(call.Args) > skip+1 {\n-    				f.Badf(call.Pos(), "no formatting directive in %s call", name)\n-    			}\n-    			return\n+    	if lit.Kind != token.STRING {
    
  •   	f.Badf(call.Pos(), "literal %v not a string in call to", lit.Value, name)
      	}\n+    	format := lit.Value
    
  •   if !strings.Contains(format, "%") {
    
  •   	if len(call.Args) > skip+1 {
    
  •   		f.Badf(call.Pos(), "no formatting directive in %s call", name)
    
  •   	}
    
  •   	return
      }\n     	// Hard part: check formats against args.\n     	// Trivial but useful test: count.\n     	numArgs := 0\n-    	for i, w := 0, 0; i < len(lit.Value); i += w {\n+    	for i, w := 0, 0; i < len(format); i += w {
      	w = 1\n-    		if lit.Value[i] == '%' {\n-    			nbytes, nargs := f.parsePrintfVerb(call, lit.Value[i:])\n+    		if format[i] == '%' {
    
  •   		nbytes, nargs := f.parsePrintfVerb(call, format[i:])
      		w = nbytes\n     			numArgs += nargs\n     		}\n    ```
    
  1. テストケースの追加: BadFunctionUsedInTests 関数内に、新しい機能のテストケースが追加されました。const で定義されたフォーマット文字列を使用し、引数の数が間違っている場合に vet がエラーを検出することを確認しています。

    --- a/src/cmd/vet/print.go
    +++ b/src/cmd/vet/print.go
    @@ -254,6 +280,9 @@ func BadFunctionUsedInTests() {\n     	printf("now is the time", "buddy") // ERROR "no formatting directive"\n     	Printf("now is the time", "buddy") // ERROR "no formatting directive"\n     	Printf("hi")                       // ok\n+    	const format = "%s %s\\n"\n+    	Printf(format, "hi", "there")
    
  •   Printf(format, "hi") // ERROR "wrong number of args in Printf call"
      f := new(File)\n     	f.Warn(0, "%s", "hello", 3)  // ERROR "possible formatting directive in Warn call"\n     	f.Warnf(0, "%s", "hello", 3) // ERROR "wrong number of args in Warnf call"\n    ```
    

コアとなるコードの解説

このコミットの核心は、vet ツールが fmt.Printf のような書式付き出力関数のフォーマット文字列を解析する能力を向上させた点にあります。

以前の checkPrintf 関数は、フォーマット文字列が *ast.BasicLit(つまり、コードに直接記述された "..." のような文字列リテラル)である場合にのみ、その内容を解析していました。これは、AST上では ast.CallExprArgs フィールドが直接 *ast.BasicLit を参照している場合に限られていました。

しかし、const format = "%s" のようにフォーマット文字列が名前付き定数として定義されている場合、ast.CallExprArgs フィールドは *ast.Ident(識別子 format)を参照します。従来の checkPrintf はこの *ast.Ident*ast.BasicLit に型アサーションしようとして失敗し、解析をスキップしていました。

新しい literal 関数は、この問題を解決します。

  1. literal 関数は、まず与えられた ast.Expr が直接 *ast.BasicLit であるかをチェックします。もしそうであれば、それが直接のリテラルなので、その値を返します。
  2. 次に、ast.Expr*ast.Ident であるかをチェックします。もし識別子であれば、その識別子が指す宣言 (v.Obj.Decl) を調べます。
  3. 特に、その宣言が *ast.ValueSpec(定数、変数、型の宣言)であり、かつその識別子が定数として定義されている場合、literal 関数はその定数の初期値(valueSpec.Values)を調べます。
  4. もしその初期値が *ast.BasicLit であれば、literal 関数はそのリテラル値を返します。これにより、format という識別子から、それが参照する文字列リテラル "%s" を取得できるようになります。

checkPrintf 関数は、この literal 関数を使ってフォーマット文字列の引数からリテラル値を取得するようになりました。これにより、vet は名前付き定数として定義されたフォーマット文字列に対しても、その実際の文字列値を解析し、引数との整合性をチェックできるようになりました。

また、取得したリテラルが token.STRING 型(文字列リテラル)であるかどうかのチェックも追加されました。これは、fmt.Printf(123, "arg") のような、フォーマット文字列が文字列ではない不正なケースを検出するために重要です。

これらの変更により、vet はより多くのコードパターンで Printf の誤用を検出できるようになり、Goプログラムの信頼性と品質が向上しました。

関連リンク

参考にした情報源リンク

  • 上記の「関連リンク」セクションに記載されている公式ドキュメントとGo Gerritのレビューページ。
  • Go言語のASTに関する一般的な知識と、go/ast パッケージの構造。
  • go vet ツールの機能と目的に関する一般的な理解。