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

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

このコミットは、Go言語の静的解析ツールである cmd/vet に、到達不能なコード(unreachable code)を診断する機能を追加するものです。具体的には、returnpanicgoto、無限ループなどの制御フローによって、その後に続くコードが実行されることがない場合に警告を発するようになります。

コミット

commit 228fe9d057400bb9fcad484dc500b0bb54981d91
Author: Russ Cox <rsc@golang.org>
Date:   Fri Mar 22 17:46:45 2013 -0400

    cmd/vet: diagnose unreachable code
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/7493048

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

https://github.com/golang/go/commit/228fe9d057400bb9fcad484dc500b0bb54981d91

元コミット内容

cmd/vet: diagnose unreachable code

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/7493048

変更の背景

Go言語の vet ツールは、Goプログラムにおける疑わしい構造や潜在的なエラーを検出するための静的解析ツールです。コードの品質向上、バグの早期発見、そしてGoのイディオムに沿ったコード記述を促進することを目的としています。

このコミットが導入された2013年3月時点では、vet ツールは既に様々なチェック(例: printf フォーマット文字列の誤り、構造体タグの誤り、sync/atomic パッケージの誤用など)を提供していました。しかし、プログラムの実行フロー上、決して到達しないコード(到達不能コード)を検出する機能はまだありませんでした。

到達不能コードは、以下のような問題を引き起こす可能性があります。

  • デッドコード: 実行されないため、プログラムのサイズを不必要に増やし、保守性を低下させます。
  • 論理エラーの兆候: 開発者が意図しない制御フローの誤りを示している場合があります。例えば、return 文の後に誤ってコードを記述してしまった場合などです。
  • 混乱: コードを読んだ際に、なぜそのコードが存在するのか、どのような意図があるのかが不明瞭になり、理解を妨げます。

このような背景から、コードの健全性をさらに高めるために、vet ツールに到達不能コードの診断機能を追加する必要性が認識されました。これにより、開発者はより堅牢でクリーンなコードを記述できるようになります。

前提知識の解説

このコミットの理解には、以下の概念に関する知識が役立ちます。

  • Go言語の制御フロー: if/elsefor ループ、switchselectgotobreakcontinuereturnpanic といったGo言語の基本的な制御フロー文の動作を理解している必要があります。特に、returnpanic が関数の実行を終了させること、goto が無条件ジャンプを行うこと、breakcontinue がループの制御フローに影響を与えることを把握しておくことが重要です。
  • 抽象構文木 (AST): Goコンパイラやツールは、ソースコードを解析して抽象構文木(AST: Abstract Syntax Tree)と呼ばれるツリー構造に変換します。ASTはプログラムの構造を表現し、静的解析ツールはこのASTを走査してコードの特性を分析します。go/ast パッケージはGoのASTを操作するための標準ライブラリです。
  • 静的解析: プログラムを実行せずに、ソースコードを分析して潜在的な問題や特性を特定する手法です。go vet は静的解析ツールの一例です。
  • 到達可能性解析 (Reachability Analysis): プログラムの特定の部分が実行可能であるかどうかを判断する解析手法です。このコミットでは、コードブロック内の各ステートメントが、その前のステートメントの実行結果によって到達可能であるかどうかを判断しています。
  • go/token パッケージ: Goのソースコードを構成するトークン(キーワード、識別子、演算子など)を定義するパッケージです。token.GOTOtoken.BREAK など、制御フローに関連するトークンが使用されています。
  • go/ast パッケージ: Goの抽象構文木(AST)を表現するためのデータ構造と関数を提供するパッケージです。ast.Stmt はステートメントを表すインターフェースであり、ast.BlockStmt はブロックステートメント({ ... })を表します。ast.BranchStmtbreak, continue, goto, fallthrough などの分岐ステートメントを表します。

技術的詳細

このコミットで導入された到達不能コードの診断機能は、deadcode.go ファイルに実装されています。主要なロジックは deadState 構造体と、そのメソッドである findLabels および findDead に集約されています。

deadState 構造体

deadState 構造体は、到達可能性解析の状態を保持します。

  • f *File: 現在解析中のファイルへのポインタ。警告を報告するために使用されます。
  • hasBreak map[ast.Stmt]bool: 各ステートメントが break 文のターゲットになり得るかどうかを追跡します。
  • hasGoto map[string]bool: 各ラベルが goto 文のターゲットとして使用されているかどうかを追跡します。
  • labels map[string]ast.Stmt: ラベル名とそのラベルが付けられたステートメントのマッピングを保持します。
  • breakTarget ast.Stmt: 現在の break 文のデフォルトのターゲット(最も内側のループまたは switch/select 文)。
  • reachable bool: 現在処理中のステートメントが到達可能であるかどうかを示すフラグ。

checkUnreachable メソッド

File 型の checkUnreachable メソッドが、関数のボディ(*ast.BlockStmt)を受け取り、到達不能コードのチェックを開始します。

  1. deadState のインスタンスを初期化します。
  2. d.findLabels(body) を呼び出し、関数ボディ内のすべてのラベル、goto の使用、および break のターゲットに関する情報を収集します。
  3. d.reachable = true を設定し、初期状態では関数の開始点が到達可能であることを示します。
  4. d.findDead(body) を呼び出し、実際の到達不能コードの検出を開始します。

findLabels メソッド

このメソッドは、ASTを再帰的に走査し、以下の情報を収集します。

  • ラベルの定義: *ast.LabeledStmt を見つけると、そのラベル名と対応するステートメントを d.labels マップに記録します。
  • goto の使用: token.GOTO を持つ *ast.BranchStmt を見つけると、そのターゲットラベル名を d.hasGoto マップに記録します。
  • break のターゲット: token.BREAK を持つ *ast.BranchStmt を見つけると、その break がどのステートメントをターゲットとしているかを判断し、d.hasBreak マップに記録します。ラベル付き break の場合は d.labels を参照し、ラベルなし break の場合は d.breakTarget を使用します。
  • breakTarget の更新: forrangeselectswitchtype switch といったループ/選択ステートメントに入るときは d.breakTarget を更新し、抜けるときに元に戻すことで、break のスコープを正しく処理します。

findDead メソッド

このメソッドは、ASTを再帰的に走査し、各ステートメントの到達可能性を判断します。

  1. ラベル付き goto ターゲットの処理: もし現在のステートメントがラベル付きステートメントであり、そのラベルが goto 文のターゲットとして使用されている場合、そのステートメントは到達可能であるとみなされます(d.reachable = true)。これは、goto 文自体が到達可能であるかどうかのチェックは行わないため、やや保守的なアプローチです。
  2. 到達不能コードの警告: d.reachablefalse の場合、現在のステートメントは到達不能であると判断され、警告が報告されます。ただし、*ast.EmptyStmt(空のステートメント)は警告の対象外です。警告後、d.reachabletrue に設定され、次のステートメントに関するエラーが抑制されます。
  3. 制御フローの分析: 各ステートメントのタイプに応じて、d.reachable の状態を更新します。
    • 終了ステートメント: returnpanic(組み込みの panic 関数のみ)、gotobreakcontinuefallthroughswitch 文内)は、その後のコードを到達不能にします。これらのステートメントを処理した後、d.reachablefalse に設定されます。
    • ブロックステートメント: *ast.BlockStmt の場合、その中の各ステートメントを順に findDead で処理します。
    • if ステートメント: if のボディと else のボディをそれぞれ findDead で処理します。else ブロックがない場合、if 文の後に続くコードは常に到達可能とみなされます。else ブロックがある場合、ifelse の両方のパスが終了しない限り、if 文の後に続くコードは到達可能とみなされます。
    • for / range ループ: ループのボディを findDead で処理します。ループが条件を持つ場合(for x == nil など)や、ループから break で抜け出す可能性がある場合、ループの後に続くコードは到達可能とみなされます。無限ループ(for {})の場合、ループの後に続くコードは到達不能とみなされます。
    • select ステートメント: 各 CommClause のボディを findDead で処理します。いずれかのケースが終了しないか、breakselect 文から抜け出す可能性がある場合、select 文の後に続くコードは到達可能とみなされます。default ケースがない select は、いずれかのケースが実行されるまでブロックするため、その後に続くコードは到達不能とみなされます。
    • switch / type switch ステートメント: 各 CaseClause のボディを findDead で処理します。default ケースが存在しない switch 文は、どのケースにもマッチしない場合に何もしないため、その後に続くコードは到達可能とみなされます。default ケースが存在する場合、または breakswitch 文から抜け出す可能性がある場合、switch 文の後に続くコードは到達可能とみなされます。

この解析は、構文的な到達可能性のみを診断し、実行時の条件によって到達不能になるコード(例: if false { ... })は検出しないことに注意が必要です。

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

src/cmd/vet/deadcode.go (新規ファイル)

このファイルは、到達不能コードの診断ロジックを完全に新規で実装しています。

// Copyright 2013 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Check for syntactically unreachable code.

package main

import (
	"go/ast"
	"go/token"
)

type deadState struct {
	f           *File
	hasBreak    map[ast.Stmt]bool
	hasGoto     map[string]bool
	labels      map[string]ast.Stmt
	breakTarget ast.Stmt

	reachable bool
}

// checkUnreachable checks a function body for dead code.
func (f *File) checkUnreachable(body *ast.BlockStmt) {
	if !vet("unreachable") || body == nil {
		return
	}

	d := &deadState{
		f:        f,
		hasBreak: make(map[ast.Stmt]bool),
		hasGoto:  make(map[string]bool),
		labels:   make(map[string]ast.Stmt),
	}

	d.findLabels(body)

	d.reachable = true
	d.findDead(body)
}

// findLabels gathers information about the labels defined and used by stmt
// and about which statements break, whether a label is involved or not.
func (d *deadState) findLabels(stmt ast.Stmt) {
	// ... (省略: 各ASTノードタイプに応じたラベル、goto、breakターゲットの収集ロジック)
}

// findDead walks the statement looking for dead code.
// If d.reachable is false on entry, stmt itself is dead.
// When findDead returns, d.reachable tells whether the
// statement following stmt is reachable.
func (d *deadState) findDead(stmt ast.Stmt) {
	// ... (省略: 各ASTノードタイプに応じた到達可能性の判断ロジック)
}

src/cmd/vet/main.go (変更箇所)

main.go では、unreachable チェックを vet ツールのオプションとして追加し、関数の走査時に checkUnreachable メソッドを呼び出すように変更されています。

--- a/src/cmd/vet/main.go
+++ b/src/cmd/vet/main.go
@@ -28,16 +28,17 @@ var exitCode = 0
 // Flags to control which checks to perform. "all" is set to true here, and disabled later if
 // a flag is set explicitly.
 var report = map[string]*bool{
-"	all":        flag.Bool("all", true, "check everything; disabled if any explicit check is requested"),
-"	asmdecl":    flag.Bool("asmdecl", false, "check assembly against Go declarations"),
-"	assign":     flag.Bool("assign", false, "check for useless assignments"),
-"	atomic":     flag.Bool("atomic", false, "check for common mistaken usages of the sync/atomic package"),
-"	buildtags":  flag.Bool("buildtags", false, "check that +build tags are valid"),
-"	composites": flag.Bool("composites", false, "check that composite literals used type-tagged elements"),
-"	methods":    flag.Bool("methods", false, "check that canonically named methods are canonically defined"),
-"	printf":     flag.Bool("printf", false, "check printf-like invocations"),
-"	structtags": flag.Bool("structtags", false, "check that struct field tags have canonical format"),
-"	rangeloops": flag.Bool("rangeloops", false, "check that range loop variables are used correctly"),
+"	all":         flag.Bool("all", true, "check everything; disabled if any explicit check is requested"),
+"	asmdecl":     flag.Bool("asmdecl", false, "check assembly against Go declarations"),
+"	assign":      flag.Bool("assign", false, "check for useless assignments"),
+"	atomic":      flag.Bool("atomic", false, "check for common mistaken usages of the sync/atomic package"),
+"	buildtags":   flag.Bool("buildtags", false, "check that +build tags are valid"),
+"	composites":  flag.Bool("composites", false, "check that composite literals used type-tagged elements"),
+"	methods":     flag.Bool("methods", false, "check that canonically named methods are canonically defined"),
+"	printf":      flag.Bool("printf", false, "check printf-like invocations"),
+"	rangeloops":  flag.Bool("rangeloops", false, "check that range loop variables are used correctly"),
+"	structtags":  flag.Bool("structtags", false, "check that struct field tags have canonical format"),
+"	unreachable": flag.Bool("unreachable", false, "check for unreachable code"),
  }\n \n  // vet tells whether to report errors for the named check, a flag name.\n@@ -336,7 +337,9 @@ func (f *File) Visit(node ast.Node) ast.Visitor {\n  	case *ast.Field:\n  	\tf.walkFieldTag(n)\n  	case *ast.FuncDecl:\n-"	\tf.walkMethodDecl(n)\n+"	\tf.walkFuncDecl(n)\n+"	case *ast.FuncLit:\n+"	\tf.walkFuncLit(n)\n  	case *ast.InterfaceType:\n  	\tf.walkInterfaceType(n)\n  	case *ast.RangeStmt:\
@@ -379,18 +382,22 @@ func (f *File) walkFieldTag(field *ast.Field) {\n  	f.checkCanonicalFieldTag(field)\n  }\n  \n-// walkMethodDecl walks the method\'s signature.\n+// walkMethod walks the method\'s signature.\n  func (f *File) walkMethod(id *ast.Ident, t *ast.FuncType) {\n  	f.checkCanonicalMethod(id, t)\n  }\n  \n-// walkMethodDecl walks the method signature in the declaration.\n-func (f *File) walkMethodDecl(d *ast.FuncDecl) {\n-\tif d.Recv == nil {\n-\t\t// not a method\n-\t\treturn\n+// walkFuncDecl walks a function declaration.\n+func (f *File) walkFuncDecl(d *ast.FuncDecl) {\n+\tf.checkUnreachable(d.Body)\n+\tif d.Recv != nil {\n+\t\tf.walkMethod(d.Name, d.Type)\n  	}\n-\tf.walkMethod(d.Name, d.Type)\n+}\n+\n+// walkFuncLit walks a function literal.\n+func (f *File) walkFuncLit(x *ast.FuncLit) {\n+\tf.checkUnreachable(x.Body)\n  }\n  \n  // walkInterfaceType walks the method signatures of an interface.\n```

### `src/cmd/vet/test_deadcode.go` (新規ファイル)

このファイルは、`deadcode.go` で実装された到達不能コード診断機能のテストケースを大量に含んでいます。様々な制御フローパターン(`return`, `panic`, `goto`, `for` ループ, `switch`, `select`, ラベル付きステートメントなど)における到達不能コードの検出が正しく行われることを検証しています。

```go
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build vet_test
// +build ignore

// This file contains tests for the dead code checker.

package main

// ... (省略: 多数のテスト関数)

コアとなるコードの解説

このコミットの核心は、deadcode.go に実装された deadState 構造体と、findLabels および findDead メソッドによるASTの走査と到達可能性の判断ロジックです。

deadState の役割

deadState は、ASTの走査中に現在のコンテキスト(どのラベルが定義されているか、どの break がどのループ/スイッチをターゲットにしているか、現在のコードが到達可能か)を保持するための重要なデータ構造です。これにより、複雑な制御フローを持つコードでも正確な到達可能性解析が可能になります。

findLabels の役割

findLabels は、findDead が到達可能性を判断する前に、プログラム全体のラベルと分岐ターゲットに関するメタデータを収集する「前処理」の役割を担います。特に goto 文やラベル付き break 文のターゲットを事前に把握しておくことで、findDead がこれらの特殊な制御フローを正しく処理できるようになります。

findDead の役割とアルゴリズム

findDead は、深さ優先探索(DFS)のような形でASTを再帰的に走査します。各ステートメントを処理する際、d.reachable フラグを基にそのステートメントが到達可能かどうかを判断します。

  • 初期状態: checkUnreachable から findDead が呼び出される際、関数のエントリポイントは常に到達可能であるため、d.reachabletrue に設定されます。
  • 到達不能の検出: findDead があるステートメントに入ったときに d.reachablefalse であれば、そのステートメントは到達不能であると判断され、vet ツールによって警告が報告されます。警告後、d.reachabletrue にリセットされます。これは、一度到達不能と判断されたブロック内でも、その後のステートメントがさらに到達不能であるという重複した警告を避けるためです。
  • 制御フローによる d.reachable の更新:
    • 終了ステートメント: returnpanicgotobreakcontinuefallthrough など、現在の実行パスを終了させるステートメントを処理した後、d.reachablefalse に設定されます。これにより、これらのステートメントの後に続くコードが到達不能としてマークされます。
    • 条件分岐 (if): if 文のボディと else 文のボディをそれぞれ再帰的に findDead で処理します。if 文の後に続くコードの到達可能性は、ifelse の両方のパスが終了しない場合にのみ true になります。else がない場合は、if の条件が false のパスが存在するため、常に true です。
    • ループ (for, range): ループのボディを処理します。ループが無限ループ(for {})である場合、ループの後に続くコードは到達不能になります。それ以外の場合(条件付きループや break で抜け出す可能性があるループ)、ループの後に続くコードは到達可能とみなされます。
    • select / switch: 各ケースのボディを処理します。selectswitch の後に続くコードの到達可能性は、すべてのケースが終了しないか、break で抜け出す可能性がある場合に true になります。default ケースがない select はブロックするため、その後に続くコードは到達不能とみなされます。default ケースがない switch は、どのケースにもマッチしない場合に何もしないため、その後に続くコードは到達可能とみなされます。

このロジックにより、Goの複雑な制御フロー構造を正確に解析し、開発者が気づきにくい到達不能コードを特定できるようになっています。

関連リンク

参考にした情報源リンク