[インデックス 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はエラーを検出できない
この場合、vet
は format
が定数であることを認識しても、その format
の「値」が何であるかを追跡して解析する機能が不足していました。そのため、実行時エラーにつながる可能性のある潜在的なバグを見逃してしまう可能性がありました。
このコミットは、このような vet
の限界を克服し、より堅牢な静的解析を提供するために行われました。名前付き定数に対してもフォーマット文字列のチェックを行うことで、開発者がより早期に、より多くの書式エラーを発見できるようになります。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の概念とツールに関する知識が必要です。
-
go vet
:go vet
はGo言語のソースコードを静的に解析し、疑わしい構成要素や潜在的なエラーを報告するツールです。コンパイルは通るものの、実行時に問題を引き起こす可能性のあるコードパターン(例:Printf
のフォーマット文字列と引数の不一致、到達不能なコード、ロックの誤用など)を検出するのに役立ちます。開発ワークフローにおいて、バグの早期発見とコード品質の向上に貢献します。 -
go/ast
パッケージ (Abstract Syntax Tree):go/ast
パッケージは、Go言語のソースコードを抽象構文木(AST)として表現するためのデータ構造を提供します。コンパイラや静的解析ツールは、ソースコードをASTに変換し、このツリー構造を走査することでコードの意味を理解し、分析を行います。ast.Expr
: Go言語の式を表すインターフェースです。リテラル、識別子、関数呼び出しなど、あらゆる種類の式がこのインターフェースを実装します。ast.BasicLit
: 数値、文字列、真偽値などの基本的なリテラル(直接記述された値)を表すASTノードです。例えば、"hello"
や123
などがこれに該当します。ast.Ident
: 識別子(変数名、定数名、関数名など)を表すASTノードです。例えば、format
やfmt.Printf
のPrintf
などがこれに該当します。ast.ValueSpec
:const
、var
、type
宣言における個々の仕様(例:const x = 1, y = 2
のx = 1
やy = 2
)を表すASTノードです。定数宣言の場合、ValueSpec
はその定数の名前と初期値(式)を含みます。
-
go/token
パッケージ:go/token
パッケージは、Go言語の字句解析(トークン化)で使われるトークン(キーワード、識別子、演算子、リテラルなど)の定義を提供します。token.STRING
: 文字列リテラルを表すトークンタイプです。
-
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")
のように、フォーマット文字列が数値リテラルであるといった不正なケースを検出するためのものです。
これらの変更により、vet
は Printf
系の関数の使用において、より広範なエラーパターンを検出できるようになり、ツールの堅牢性が向上しました。
コアとなるコードの変更箇所
変更は src/cmd/vet/print.go
ファイルに集中しています。
-
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 ```
-
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 ```
-
テストケースの追加:
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.CallExpr
の Args
フィールドが直接 *ast.BasicLit
を参照している場合に限られていました。
しかし、const format = "%s"
のようにフォーマット文字列が名前付き定数として定義されている場合、ast.CallExpr
の Args
フィールドは *ast.Ident
(識別子 format
)を参照します。従来の checkPrintf
はこの *ast.Ident
を *ast.BasicLit
に型アサーションしようとして失敗し、解析をスキップしていました。
新しい literal
関数は、この問題を解決します。
literal
関数は、まず与えられたast.Expr
が直接*ast.BasicLit
であるかをチェックします。もしそうであれば、それが直接のリテラルなので、その値を返します。- 次に、
ast.Expr
が*ast.Ident
であるかをチェックします。もし識別子であれば、その識別子が指す宣言 (v.Obj.Decl
) を調べます。 - 特に、その宣言が
*ast.ValueSpec
(定数、変数、型の宣言)であり、かつその識別子が定数として定義されている場合、literal
関数はその定数の初期値(valueSpec.Values
)を調べます。 - もしその初期値が
*ast.BasicLit
であれば、literal
関数はそのリテラル値を返します。これにより、format
という識別子から、それが参照する文字列リテラル"%s"
を取得できるようになります。
checkPrintf
関数は、この literal
関数を使ってフォーマット文字列の引数からリテラル値を取得するようになりました。これにより、vet
は名前付き定数として定義されたフォーマット文字列に対しても、その実際の文字列値を解析し、引数との整合性をチェックできるようになりました。
また、取得したリテラルが token.STRING
型(文字列リテラル)であるかどうかのチェックも追加されました。これは、fmt.Printf(123, "arg")
のような、フォーマット文字列が文字列ではない不正なケースを検出するために重要です。
これらの変更により、vet
はより多くのコードパターンで Printf
の誤用を検出できるようになり、Goプログラムの信頼性と品質が向上しました。
関連リンク
- Go言語の
vet
コマンドについて: - Go言語の
go/ast
パッケージ: - Go言語の
go/token
パッケージ: - Go言語の
fmt
パッケージ: - このコミットのGo Gerritレビューページ:
参考にした情報源リンク
- 上記の「関連リンク」セクションに記載されている公式ドキュメントとGo Gerritのレビューページ。
- Go言語のASTに関する一般的な知識と、
go/ast
パッケージの構造。 go vet
ツールの機能と目的に関する一般的な理解。