[インデックス 15067] ファイルの概要
このコミットは、Go言語のcmd/vet
ツールに新しい機能を追加するものです。具体的には、Goソースコード内のビルドタグ(+build
コメント)が正しい位置に配置され、かつ正しい形式であるかを検証する機能が導入されました。これにより、開発者が誤ったビルドタグを使用することによる潜在的な問題を未然に防ぐことができます。
コミット
commit 739aa6b1ba6f643a37370d569c5f67827f5c370c
Author: Russ Cox <rsc@golang.org>
Date: Thu Jan 31 13:52:27 2013 -0800
cmd/vet: check for misplaced and malformed build tags
Fixes #4184.
R=golang-dev, bradfitz, minux.ma, cookieo9
CC=golang-dev
https://golang.org/cl/7251044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/739aa6b1ba6f643a37370d569c5f67827f5c370c
元コミット内容
cmd/vet
: 配置が誤っている、または形式が不正なビルドタグをチェックする。
Issue #4184 を修正。
変更の背景
Go言語のビルドタグは、特定の環境や条件に基づいてファイルのコンパイルを制御するための強力なメカニズムです。しかし、その構文や配置ルールには厳密な制約があります。例えば、ビルドタグはファイルの先頭にあるコメントブロック内に記述する必要があり、また特定の形式に従う必要があります。これらのルールが守られない場合、Goツールチェーンはファイルを正しく認識できず、意図しないコンパイルエラーや動作を引き起こす可能性があります。
このコミットが導入される以前は、cmd/vet
ツールはビルドタグの配置や形式に関するチェックを行っていませんでした。そのため、開発者が誤ったビルドタグを記述しても、コンパイル時までその問題が検出されないことがありました。これは、開発プロセスにおいてデバッグの手間を増やし、生産性を低下させる要因となっていました。
このコミットの目的は、cmd/vet
にビルドタグの検証機能を追加することで、これらの問題を早期に発見し、開発者がより堅牢で保守しやすいGoコードを書けるように支援することです。コミットメッセージにある「Fixes #4184」は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。ただし、現在の公開されているGoのIssueトラッカーでは、この番号のIssueはBazel関連のものであり、直接的な関連は見られません。これは、GoのIssueトラッカーの履歴的な変更や、内部的なIssue番号の可能性を示唆しています。
前提知識の解説
cmd/vet
とは
cmd/vet
は、Go言語のソースコードを静的に解析し、潜在的なバグや疑わしい構造を報告するツールです。Goの標準ツールチェーンの一部として提供されており、Goプログラムの品質と信頼性を向上させるために広く利用されています。vet
は、例えばprintf
フォーマット文字列の不一致、構造体タグの誤り、アトミック操作の誤用など、様々な種類の問題を検出できます。これは、コンパイラが検出できないが、実行時に問題を引き起こす可能性のあるコードパターンを特定するのに役立ちます。
Goのビルドタグ(Build Tags)
Goのビルドタグは、特定のビルド条件に基づいてソースファイルをコンパイルに含めるか除外するかを制御するための特別なコメントです。これらは、異なるオペレーティングシステム、アーキテクチャ、またはカスタムビルド設定に対応するコードを記述する際に非常に有用です。
ビルドタグの構文とルール:
- 形式: ビルドタグは、
// +build tag1 tag2
の形式で記述されます。+build
の後にスペース区切りでタグが続きます。 - 位置: ビルドタグは、ファイルの先頭にあるパッケージ宣言の前に、空行を挟まずに連続するコメントブロック内に記述する必要があります。コメントブロックの後に空行が1つ以上続くことで、ビルドタグの有効範囲が終了します。
- 論理演算:
- スペース区切りは論理AND (
&&
) を意味します。例:// +build linux amd64
は、linux
とamd64
の両方のタグが有効な場合にファイルがコンパイルされます。 - カンマ区切りは論理OR (
||
) を意味します。例:// +build go1.12,go1.13
は、go1.12
またはgo1.13
のいずれかのタグが有効な場合にファイルがコンパイルされます。 !
プレフィックスは論理NOT (!
) を意味します。例:// +build !windows
は、windows
以外の環境でファイルがコンパイルされます。
- スペース区切りは論理AND (
- 有効なタグ名: タグ名は英数字とアンダースコア(
_
)のみで構成される必要があります。
例:
// +build linux,darwin
// +build amd64
package main
// このファイルはLinuxまたはmacOSで、かつamd64アーキテクチャの場合にのみコンパイルされます。
このコミットは、これらのルール、特に「位置」と「有効なタグ名」に関する違反を検出することを目的としています。
技術的詳細
このコミットは、cmd/vet
に新しいチェック項目「ビルドタグの検証」を追加します。この機能は、主に以下の点を検証します。
- 配置の正当性: ビルドタグがファイルの先頭の適切な位置に存在するかどうか。パッケージ宣言の後や、ファイルの先頭のコメントブロックではない場所にビルドタグが記述されている場合、エラーとして報告されます。
- 形式の正当性: ビルドタグの構文が正しいかどうか。
+build
の後に余分な文字がないか(例:+buildasdf
)。- ビルド制約に無効な文字が含まれていないか(英数字とアンダースコア以外)。
- 二重否定(
!!
)のような無効な論理演算子が使用されていないか。
この検証は、ファイルのバイト列を直接読み込み、行ごとに解析することで行われます。これにより、Goパーサーがファイルを解析する前に、ビルドタグに関する問題を検出することが可能になります。
新しいbuildtag.go
ファイルには、checkBuildTag
関数が実装されています。この関数は、ファイル名とファイルの内容(バイト列)を受け取り、以下のロジックでビルドタグを検証します。
- カットオフポイントの決定: ビルドタグが有効なのは、ファイルの先頭にあるコメントブロックとそれに続く空行までです。
checkBuildTag
関数は、この「カットオフポイント」を特定します。これは、ファイルの先頭からコメント行と空行を読み進め、コメントではない行や、コメントブロックの後に続く空行の後の行が見つかった時点で決定されます。 - 行ごとの解析: ファイルの各行を走査し、
//
で始まるコメント行をチェックします。 +build
コメントの検出: コメント行が+build
で始まる場合、それがビルドタグであると判断します。- 配置の検証: 検出されたビルドタグがカットオフポイントより後に現れた場合、「
+build
コメントがファイル内で遅すぎる位置に現れています」というエラーを報告します。 - 形式の検証:
+build
の直後にスペースがないなど、+build
キーワード自体の形式が不正な場合、「形式が不正な+build
コメントの可能性があります」というエラーを報告します。- ビルド制約の引数(例:
linux
,!windows
)を解析し、無効な文字(英数字とアンダースコア以外)が含まれていないか、または二重否定(!!
)が使用されていないかをチェックします。これらの違反が見つかった場合、それぞれ適切なエラーメッセージを報告します。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
-
src/cmd/vet/Makefile
:- テストコマンドが変更され、
vet
がすべてのGoファイル(*.go
)に対して実行されるようになりました。これにより、新しいビルドタグチェックが既存のテストスイートに統合されます。
- テストコマンドが変更され、
-
src/cmd/vet/buildtag.go
(新規追加):- ビルドタグの検証ロジックを実装する
checkBuildTag
関数が含まれています。このファイルは、ビルドタグの配置と形式に関する詳細なチェックを行います。 - ファイルの先頭に、このファイル自体がテスト目的で不正なビルドタグを含んでいることを示すコメントと、
vet
が検出するはずのエラーを示す// ERROR
コメントが含まれています。
- ビルドタグの検証ロジックを実装する
-
src/cmd/vet/buildtag_bad.go
(新規追加):- 意図的に不正なビルドタグを含むテストファイルです。このファイルは、
vet
の新しいビルドタグチェック機能が正しく動作するかを検証するために使用されます。 - 様々な種類の不正なビルドタグ(配置が遅い、二重否定、無効な文字など)が含まれており、それぞれに対応する
// ERROR
コメントが付与されています。
- 意図的に不正なビルドタグを含むテストファイルです。このファイルは、
-
src/cmd/vet/main.go
:vetBuildTags
という新しいフラグが追加され、ビルドタグのチェックを有効にするかどうかを制御できるようになりました。doFile
関数が変更され、ファイルの解析前にcheckBuildTag
関数を呼び出すようになりました。これにより、Goパーサーがファイルを読み込む前にビルドタグの検証が行われます。- ファイルの読み込み方法が変更され、
io.Reader
からioutil.ReadAll
を使用してファイル全体をバイト列として読み込むようになりました。これは、checkBuildTag
関数がバイト列を必要とするためです。
-
src/cmd/vet/taglit.go
:- 既存のテストケースに、構造体タグの検証に関する
// ERROR
コメントが追加されました。これは、このコミットの直接的な変更とは関係ありませんが、vet
のテストフレームワークの改善の一環として行われた可能性があります。
- 既存のテストケースに、構造体タグの検証に関する
コアとなるコードの解説
src/cmd/vet/buildtag.go
このファイルは、ビルドタグの検証ロジックの核心です。
// checkBuildTag checks that build tags are in the correct location and well-formed.
func checkBuildTag(name string, data []byte) {
if !*vetBuildTags && !*vetAll {
return
}
lines := bytes.SplitAfter(data, nl)
// Determine cutpoint where +build comments are no longer valid.
// They are valid in leading // comments in the file followed by
// a blank line.
var cutoff int
for i, line := range lines {
line = bytes.TrimSpace(line)
if len(line) == 0 {
cutoff = i
continue
}
if bytes.HasPrefix(line, slashSlash) {
continue
}
break
}
for i, line := range lines {
line = bytes.TrimSpace(line)
if !bytes.HasPrefix(line, slashSlash) {
continue
}
text := bytes.TrimSpace(line[2:])
if bytes.HasPrefix(text, plusBuild) {
fields := bytes.Fields(text)
if !bytes.Equal(fields[0], plusBuild) {
// Comment is something like +buildasdf not +build.
fmt.Fprintf(os.Stderr, "%s:%d: possible malformed +build comment\\n", name, i+1)
continue
}
if i >= cutoff {
fmt.Fprintf(os.Stderr, "%s:%d: +build comment appears too late in file\\n", name, i+1)
setExit(1)
continue
}
// Check arguments.
Args:
for _, arg := range fields[1:] {
for _, elem := strings.Split(string(arg), ",") {
if strings.HasPrefix(elem, "!!") {
fmt.Fprintf(os.Stderr, "%s:%d: invalid double negative in build constraint: %s\\n", name, i+1, arg)
setExit(1)
break Args
}
if strings.HasPrefix(elem, "!") {
elem = elem[1:]
}
for _, c := range elem {
if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' {
fmt.Fprintf(os.Stderr, "%s:%d: invalid non-alphanumeric build constraint: %s\\n", name, i+1, arg)
setExit(1)
break Args
}
}
}
}
continue
}
// Comment with +build but not at beginning.
if bytes.Contains(line, plusBuild) && i < cutoff {
fmt.Fprintf(os.Stderr, "%s:%d: possible malformed +build comment\\n", name, i+1)
continue
}
}
}
checkBuildTag
関数は、ファイルの内容をバイト列として受け取り、改行で分割して行ごとに処理します。cutoff
変数は、ビルドタグが有効である最終行のインデックスを決定します。これは、ファイルの先頭からコメント行と空行を読み進め、コメントではない行が見つかるか、コメントブロックの後に空行が続く場所で設定されます。- 各行を走査し、
//
で始まるコメント行をチェックします。 - コメント行が
+build
で始まる場合、その行がビルドタグであると判断し、その配置(cutoff
より前か)と形式(+build
キーワードの直後の文字、引数の有効性、二重否定の有無)を検証します。 bytes.Contains(line, plusBuild) && i < cutoff
の条件は、+build
という文字列がコメント行に含まれているが、それが+build
で始まっていない(つまり、// Some text +build
のような形式)場合に、形式が不正なビルドタグとして警告を発します。
src/cmd/vet/main.go
main.go
の変更は、新しいビルドタグチェックをvet
ツールに統合する部分です。
var (
vetAll = flag.Bool("all", true, "check everything; disabled if any explicit check is requested")
vetAtomic = flag.Bool("atomic", false, "check for common mistaken usages of the sync/atomic package")
vetBuildTags = flag.Bool("buildtags", false, "check that +build tags are valid") // 新しいフラグ
vetMethods = flag.Bool("methods", false, "check that canonically named methods are canonically defined")
vetPrintf = flag.Bool("printf", false, "check printf-like invocations")
vetStructTags = flag.Bool("structtags", false, "check that struct field tags have canonical format")
vetUntaggedLiteral = flag.Bool("composites", false, "check that composite literals used type-tagged elements")
vetRangeLoops = flag.Bool("rangeloops", false, "check that range loop variables are used correctly")
)
// ...
func doFile(name string, reader io.Reader) {
if reader == nil {
f, err := os.Open(name)
if err != nil {
errorf("%s: %s", name, err)
return
}
defer f.Close()
reader = f
}
data, err := ioutil.ReadAll(reader) // ファイル全体をバイト列として読み込む
if err != nil {
errorf("%s: %s", name, err)
return
}
checkBuildTag(name, data) // 新しいビルドタグチェックを呼び出す
fs := token.NewFileSet()
parsedFile, err := parser.ParseFile(fs, name, bytes.NewReader(data), 0) // バイト列から再度リーダーを作成してパース
if err != nil {
errorf("%s: %s", name, err)
return
}
// ... 既存のvetチェックロジック ...
}
vetBuildTags
フラグの追加により、ユーザーはvet -buildtags
のようにコマンドラインからビルドタグチェックを明示的に有効にできるようになりました。doFile
関数内で、ファイルのコンテンツをioutil.ReadAll
で一度バイト列として読み込み、そのバイト列をcheckBuildTag
関数に渡しています。その後、同じバイト列から新しいbytes.NewReader
を作成し、parser.ParseFile
に渡してGoのAST(抽象構文木)を構築しています。これにより、ビルドタグのチェックがファイルのパース前に行われることが保証されます。
関連リンク
- Go言語のビルド制約(Build Constraints)に関する公式ドキュメント: https://pkg.go.dev/cmd/go#hdr-Build_constraints
cmd/vet
ツールの概要: https://pkg.go.dev/cmd/vet
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
cmd/vet
ディレクトリ) - Go言語のIssueトラッカー(ただし、Issue #4184は直接関連する情報が見つからなかった)
- Go言語のビルドタグに関する一般的な解説記事