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

[インデックス 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のビルドタグは、特定のビルド条件に基づいてソースファイルをコンパイルに含めるか除外するかを制御するための特別なコメントです。これらは、異なるオペレーティングシステム、アーキテクチャ、またはカスタムビルド設定に対応するコードを記述する際に非常に有用です。

ビルドタグの構文とルール:

  1. 形式: ビルドタグは、// +build tag1 tag2 の形式で記述されます。+buildの後にスペース区切りでタグが続きます。
  2. 位置: ビルドタグは、ファイルの先頭にあるパッケージ宣言の前に、空行を挟まずに連続するコメントブロック内に記述する必要があります。コメントブロックの後に空行が1つ以上続くことで、ビルドタグの有効範囲が終了します。
  3. 論理演算:
    • スペース区切りは論理AND (&&) を意味します。例: // +build linux amd64 は、linuxamd64の両方のタグが有効な場合にファイルがコンパイルされます。
    • カンマ区切りは論理OR (||) を意味します。例: // +build go1.12,go1.13 は、go1.12またはgo1.13のいずれかのタグが有効な場合にファイルがコンパイルされます。
    • ! プレフィックスは論理NOT (!) を意味します。例: // +build !windows は、windows以外の環境でファイルがコンパイルされます。
  4. 有効なタグ名: タグ名は英数字とアンダースコア(_)のみで構成される必要があります。

例:

// +build linux,darwin
// +build amd64

package main

// このファイルはLinuxまたはmacOSで、かつamd64アーキテクチャの場合にのみコンパイルされます。

このコミットは、これらのルール、特に「位置」と「有効なタグ名」に関する違反を検出することを目的としています。

技術的詳細

このコミットは、cmd/vetに新しいチェック項目「ビルドタグの検証」を追加します。この機能は、主に以下の点を検証します。

  1. 配置の正当性: ビルドタグがファイルの先頭の適切な位置に存在するかどうか。パッケージ宣言の後や、ファイルの先頭のコメントブロックではない場所にビルドタグが記述されている場合、エラーとして報告されます。
  2. 形式の正当性: ビルドタグの構文が正しいかどうか。
    • +buildの後に余分な文字がないか(例: +buildasdf)。
    • ビルド制約に無効な文字が含まれていないか(英数字とアンダースコア以外)。
    • 二重否定(!!)のような無効な論理演算子が使用されていないか。

この検証は、ファイルのバイト列を直接読み込み、行ごとに解析することで行われます。これにより、Goパーサーがファイルを解析する前に、ビルドタグに関する問題を検出することが可能になります。

新しいbuildtag.goファイルには、checkBuildTag関数が実装されています。この関数は、ファイル名とファイルの内容(バイト列)を受け取り、以下のロジックでビルドタグを検証します。

  • カットオフポイントの決定: ビルドタグが有効なのは、ファイルの先頭にあるコメントブロックとそれに続く空行までです。checkBuildTag関数は、この「カットオフポイント」を特定します。これは、ファイルの先頭からコメント行と空行を読み進め、コメントではない行や、コメントブロックの後に続く空行の後の行が見つかった時点で決定されます。
  • 行ごとの解析: ファイルの各行を走査し、//で始まるコメント行をチェックします。
  • +buildコメントの検出: コメント行が+buildで始まる場合、それがビルドタグであると判断します。
  • 配置の検証: 検出されたビルドタグがカットオフポイントより後に現れた場合、「+buildコメントがファイル内で遅すぎる位置に現れています」というエラーを報告します。
  • 形式の検証:
    • +buildの直後にスペースがないなど、+buildキーワード自体の形式が不正な場合、「形式が不正な+buildコメントの可能性があります」というエラーを報告します。
    • ビルド制約の引数(例: linux, !windows)を解析し、無効な文字(英数字とアンダースコア以外)が含まれていないか、または二重否定(!!)が使用されていないかをチェックします。これらの違反が見つかった場合、それぞれ適切なエラーメッセージを報告します。

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

このコミットでは、主に以下のファイルが変更されています。

  1. src/cmd/vet/Makefile:

    • テストコマンドが変更され、vetがすべてのGoファイル(*.go)に対して実行されるようになりました。これにより、新しいビルドタグチェックが既存のテストスイートに統合されます。
  2. src/cmd/vet/buildtag.go (新規追加):

    • ビルドタグの検証ロジックを実装するcheckBuildTag関数が含まれています。このファイルは、ビルドタグの配置と形式に関する詳細なチェックを行います。
    • ファイルの先頭に、このファイル自体がテスト目的で不正なビルドタグを含んでいることを示すコメントと、vetが検出するはずのエラーを示す// ERRORコメントが含まれています。
  3. src/cmd/vet/buildtag_bad.go (新規追加):

    • 意図的に不正なビルドタグを含むテストファイルです。このファイルは、vetの新しいビルドタグチェック機能が正しく動作するかを検証するために使用されます。
    • 様々な種類の不正なビルドタグ(配置が遅い、二重否定、無効な文字など)が含まれており、それぞれに対応する// ERRORコメントが付与されています。
  4. src/cmd/vet/main.go:

    • vetBuildTagsという新しいフラグが追加され、ビルドタグのチェックを有効にするかどうかを制御できるようになりました。
    • doFile関数が変更され、ファイルの解析前にcheckBuildTag関数を呼び出すようになりました。これにより、Goパーサーがファイルを読み込む前にビルドタグの検証が行われます。
    • ファイルの読み込み方法が変更され、io.Readerからioutil.ReadAllを使用してファイル全体をバイト列として読み込むようになりました。これは、checkBuildTag関数がバイト列を必要とするためです。
  5. 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言語の公式ドキュメント
  • Go言語のソースコード(特にcmd/vetディレクトリ)
  • Go言語のIssueトラッカー(ただし、Issue #4184は直接関連する情報が見つからなかった)
  • Go言語のビルドタグに関する一般的な解説記事