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

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

このコミットは、Goコンパイラ(gc)における型スイッチ(type switch)の変数使用状況検出に関するバグ修正と改善を目的としています。具体的には、型スイッチ内で宣言された変数が使用されていない場合に、コンパイラが正しく警告を発するように変更が加えられています。

コミット

commit aac144b1202fc733a206422bed3cc6eafe4ca855
Author: Luuk van Dijk <lvd@golang.org>
Date:   Fri Nov 4 17:03:50 2011 +0100

    gc: detect type switch variable not used cases.
    
    Fixes #873
    Fixes #2162
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5341043
---
 src/cmd/gc/go.y                   | 32 ++++++++++++++++----------------
 src/cmd/gc/walk.c                 | 33 +++++++++++++++++++++++++--------
 src/pkg/encoding/xml/read.go      |  2 +-\n src/pkg/exp/types/const.go        |  2 +-\n src/pkg/go/parser/parser.go       |  2 +-\n test/fixedbugs/bug141.go          |  2 +-\n test/fixedbugs/bug200.go          |  2 +-\n test/fixedbugs/bug213.go          |  2 +-\n test/fixedbugs/bug248.dir/bug2.go |  2 +-\n test/fixedbugs/bug309.go          |  2 ++\n test/fixedbugs/bug373.go          | 32 ++++++++++++++++++++++++++++++++\n 11 files changed, 82 insertions(+), 31 deletions(-)

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

https://github.com/golang/go/commit/aac144b1202fc733a206422bed3cc6eafe4ca855

元コミット内容

このコミットは、Goコンパイラ(gc)が型スイッチ(type switch)内で宣言された変数が使用されていないケースを正しく検出するように修正します。これにより、コンパイラは未使用変数に関する適切な警告を発するようになります。

この変更は、以下の2つのバグを修正します。

  • Issue #873: 型スイッチのケース節で宣言された変数が使用されていない場合に、コンパイラが警告を発しない問題。
  • Issue #2162: 同様に、型スイッチのdefault節で宣言された変数が使用されていない場合に、コンパイラが警告を発しない問題。

変更の背景

Go言語では、宣言されたローカル変数が使用されていない場合、コンパイル時にエラー(または警告)を出すことが言語仕様で定められています。これは、プログラマが意図しない変数宣言や、デッドコードの存在に気づくのを助けるための重要な機能です。

しかし、Goコンパイラの初期の実装では、型スイッチのケース節やdefault節で宣言される「型スイッチ変数」(例: switch t := x.(type)t)について、この未使用変数検出が正しく機能していませんでした。

具体的には、以下のシナリオで問題が発生していました。

  • Issue #873:

    func f(x interface{}) {
        switch t := x.(type) { // ここでtが宣言される
        case int:
            // tがここで使われていない
        }
    }
    

    このコードでは、tint型の場合に宣言されますが、その後のブロックで全く使用されていませんでした。本来であればコンパイラが「tが宣言されたが使用されていない」という警告を出すべきですが、それがされていませんでした。

  • Issue #2162:

    func h(x interface{}) {
        switch t := x.(type) { // ここでtが宣言される
        case int:
        case float32:
        default:
            // tがここで使われていない
        }
    }
    

    同様に、default節でtが宣言されるものの、使用されていないケースでも警告が出ませんでした。

これらのバグは、コードの品質低下や潜在的なバグの見落としにつながるため、修正が必要とされました。このコミットは、コンパイラの内部ロジックを調整し、型スイッチ変数の使用状況を正確に追跡することで、これらの問題を解決しています。

前提知識の解説

このコミットの理解には、Goコンパイラの内部構造とGo言語のいくつかの概念に関する知識が必要です。

  • Goコンパイラ (gc): Go言語の公式コンパイラです。ソースコードを解析し、中間表現に変換し、最終的に実行可能なバイナリを生成します。src/cmd/gc ディレクトリにそのソースコードがあります。
  • 型スイッチ (type switch): Go言語の制御構造の一つで、インターフェース型の変数の動的な型に基づいて異なる処理を実行するために使用されます。
    switch v := i.(type) {
    case int:
        // iがint型の場合、vはint型
    case string:
        // iがstring型の場合、vはstring型
    default:
        // その他の場合、vはiと同じインターフェース型
    }
    
    ここで v は「型スイッチ変数」と呼ばれ、各ケース節内でそのケースに対応する具体的な型を持ちます。
  • src/cmd/gc/go.y: Goコンパイラの字句解析器と構文解析器の定義ファイルです。yacc(またはbison)形式で記述されており、Go言語の文法規則が定義されています。このファイルは、ソースコードがどのように解析され、抽象構文木(AST)が構築されるかを決定します。simple_stmtcase などの文法規則が定義されています。
  • src/cmd/gc/walk.c: Goコンパイラの「ウォーク(walk)」フェーズを担当するファイルです。構文解析によって生成されたASTを走査し、型チェック、最適化、コード生成のための準備など、様々なセマンティック分析を行います。未使用変数の検出もこのフェーズで行われます。
  • Node 構造体: コンパイラ内部でASTの各ノードを表すデータ構造です。各ノードは、その種類(op)、関連するシンボル(sym)、型情報、子ノードへのポインタなど、様々な情報を持っています。
  • ONAME: Nodeop フィールドの値の一つで、名前(変数、関数など)を表すノードを示します。
  • OTYPESW: Nodeop フィールドの値の一つで、型スイッチ文全体を表すノードを示します。
  • PAUTO: 変数のストレージクラス(記憶域クラス)の一つで、自動変数(ローカル変数)を示します。
  • used フラグ: コンパイラが変数がコード内で使用されているかどうかを追跡するために、Node 構造体内に持つフラグです。このフラグがfalseのままコンパイルが終了すると、未使用変数として警告(またはエラー)が発せられます。
  • yyerror: yacc/bisonによって生成されるパーサー内で使用されるエラー報告関数です。コンパイルエラーや警告メッセージを出力するために使われます。
  • dclcontext: 宣言のコンテキストを管理する変数。変数がどのスコープで宣言されているかを追跡します。
  • newname / dclname: コンパイラ内部で新しい変数を宣言したり、既存の名前を解決したりするための関数。

技術的詳細

このコミットの核心は、Goコンパイラが型スイッチ変数の「使用済み」状態をどのように追跡し、未使用のケースを検出するかという点にあります。

従来のコンパイラでは、型スイッチの各ケース節で宣言される変数は、そのケース節のローカル変数として扱われ、そのケース節内で使用されればusedフラグがセットされました。しかし、型スイッチ全体としてその変数が「宣言されたが使用されていない」という状況を適切に判断するためのメカニズムが不足していました。特に、型スイッチ変数が宣言されるsimple_stmtv := x.(type) の部分)と、実際にその変数が使用される可能性のある各case節との間の連携が不十分でした。

このコミットでは、以下の主要な変更が導入されています。

  1. OTYPESW ノードの強化:

    • src/cmd/gc/go.y において、型スイッチ文を表現する OTYPESW ノードの構造が変更されました。
    • 以前は、OTYPESW ノードの left フィールドに型スイッチ変数のノードが直接格納されていました。
    • 変更後、OTYPESW ノードの left フィールドは、型スイッチ変数の「シンボル」(ONONAME ノード)を指すようになりました。そして、各ケース節で実際に宣言される型スイッチ変数のノード(nn)の defn フィールドが、この OTYPESW ノードを指すように設定されます。これにより、各ケース節の型スイッチ変数と、型スイッチ全体の宣言との間に明確なリンクが確立されます。
  2. used フラグの伝播:

    • src/cmd/gc/walk.cwalk 関数内で、コンパイルの最終段階で未使用変数をチェックするロジックが修正されました。
    • 新しいロジックでは、まず全てのローカル変数(PAUTO)について typecheck を実行し、その変数が使用されているかどうかを判断します。
    • 次に、型スイッチ変数(l->n->defn && l->n->defn->op == OTYPESW で識別される)の場合、その変数が使用されている(l->n->usedtrue)であれば、その変数が定義されている OTYPESW ノードの left フィールド(つまり、型スイッチ全体のシンボル)の used フラグをインクリメントします。これは、型スイッチのいずれかのケースで変数が使用されたことを示すためのカウンターとして機能します。
    • 最後に、ローカル変数を再度走査し、もしそれが型スイッチ変数であり、かつその OTYPESW ノードの left フィールドの used フラグが 0 であれば、その型スイッチ変数はどのケースでも使用されていないと判断し、yyerror を使って「declared and not used」という警告を発します。
  3. テストケースの追加:

    • test/fixedbugs/bug373.go という新しいテストファイルが追加されました。このファイルには、Issue #873 と #2162 で報告されたシナリオを再現するコードが含まれており、コンパイラが正しく警告を発するかどうかを検証します。

これらの変更により、コンパイラは型スイッチ変数の使用状況をより正確に追跡できるようになり、未使用変数に関する警告が適切に生成されるようになりました。

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

src/cmd/gc/go.y

simple_stmt ルールと case ルールが変更されています。

  • simple_stmt (型スイッチの宣言部分):

    --- a/src/cmd/gc/go.y
    +++ b/src/cmd/gc/go.y
    @@ -418,9 +418,7 @@ simple_stmt:
     |\texpr_list LCOLAS expr_list
     \t{\
     \t\tif($3->n->op == OTYPESW) {\
    -\t\t\tNode *n;\
    -\t\t\t\
    -\t\t\tn = N;\
    +\t\t\t$$ = nod(OTYPESW, N, $3->n->right);\
     \t\t\tif($3->next != nil)\
     \t\t\t\tyyerror(\"expr.(type) must be alone in list\");\
     \t\t\tif($1->next != nil)\
    @@ -428,8 +426,7 @@ simple_stmt:
     \t\t\telse if($1->n->op != ONAME && $1->n->op != OTYPE && $1->n->op != ONONAME)\
     \t\t\t\tyyerror(\"invalid variable name %N in type switch\", $1->n);\
     \t\t\telse\
    -\t\t\t\tn = $1->n;\
    -\t\t\t$$ = nod(OTYPESW, n, $3->n->right);\
    +\t\t\t\t$$->left = dclname($1->n->sym);  // it\'s a colas, so must not re-use an oldname.\
     \t\t\tbreak;\
     \t\t}\
     \t\t$$ = colas($1, $3);\
    

    型スイッチの宣言部分 (v := x.(type)) で、OTYPESW ノードの left フィールドに直接変数ノードを格納するのではなく、dclname($1->n->sym) を使ってシンボルを格納するように変更されています。これにより、型スイッチ全体のシンボルと各ケース節の変数との関連付けがより柔軟になります。

  • case ルール (型スイッチのケース節):

    --- a/src/cmd/gc/go.y
    +++ b/src/cmd/gc/go.y
    @@ -458,12 +455,13 @@ case:
     \t\t$$->list = $2;\
     \t\tif(typesw != N && typesw->right != N && (n=typesw->right->left) != N) {\
     \t\t\t// type switch - declare variable\
    -\t\t\tn = newname(n->sym);\
    -\t\t\tn->used = 1;\t// TODO(rsc): better job here\
    -\t\t\tdeclare(n, dclcontext);\
    -\t\t\t$$->nname = n;\
    +\t\t\tnn = newname(n->sym);\
    +\t\t\tdeclare(nn, dclcontext);\
    +\t\t\t$$->nname = nn;\
    +\
    +\t\t\t// keep track of the instances for reporting unused\
    +\t\t\tnn->defn = typesw->right;\
     \t\t}\
    -\t\tbreak;\
     \t}\
     |\tLCASE expr_or_type_list \'=\' expr \':\'
     \t{\
    @@ -494,16 +492,18 @@ case:
     \t}\
     |\tLDEFAULT \':\'
     \t{\
    -\t\tNode *n;\
    +\t\tNode *n, *nn;\
     \
     \t\tmarkdcl();\
     \t\t$$ = nod(OXCASE, N, N);\
     \t\tif(typesw != N && typesw->right != N && (n=typesw->right->left) != N) {\
     \t\t\t// type switch - declare variable\
    -\t\t\tn = newname(n->sym);\
    -\t\t\tn->used = 1;\t// TODO(rsc): better job here\
    -\t\t\tdeclare(n, dclcontext);\
    -\t\t\t$$->nname = n;\
    +\t\t\tnn = newname(n->sym);\
    +\t\t\tdeclare(nn, dclcontext);\
    +\t\t\t$$->nname = nn;\
    +\
    +\t\t\t// keep track of the instances for reporting unused\
    +\t\t\tnn->defn = typesw->right;\
     \t\t}\
     \t}\
    

    case 節(および default 節)で型スイッチ変数を宣言する際に、新しく宣言された変数ノード nndefn フィールドに typesw->right を設定しています。typesw->right は、型スイッチ全体の OTYPESW ノードを指します。これにより、各ケース節の変数と、その変数が属する型スイッチ全体との間に逆参照が確立されます。

src/cmd/gc/walk.c

walk 関数内の未使用変数検出ロジックが大幅に変更されています。

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -63,7 +63,6 @@ walk(Node *fn)
 {
 	char s[50];
 	NodeList *l;
-\tNode *n;\
 	int lno;
 
 	curfn = fn;
@@ -77,15 +76,33 @@ walk(Node *fn)
 		yyerror("function ends without a return statement");
 
 	lno = lineno;
+\
+\t// Final typecheck for any unused variables.\
+\t// It's hard to be on the heap when not-used, but best to be consistent about &~PHEAP here and below.\
+\tfor(l=fn->dcl; l; l=l->next)\
+\t\tif(l->n->op == ONAME && (l->n->class&~PHEAP) == PAUTO)\
+\t\t\ttypecheck(&l->n, Erv | Easgn);\
+\
+\t// Propagate the used flag for typeswitch variables up to the NONAME in it's definition.\
+\tfor(l=fn->dcl; l; l=l->next)\
+\t\tif(l->n->op == ONAME && (l->n->class&~PHEAP) == PAUTO && l->n->defn && l->n->defn->op == OTYPESW && l->n->used)\
+\t\t\tl->n->defn->left->used++;\
+\t\
 	for(l=fn->dcl; l; l=l->next) {
-\t\tn = l->n;\
-\t\tif(n->op != ONAME || n->class != PAUTO)\
+\t\tif(l->n->op != ONAME || (l->n->class&~PHEAP) != PAUTO || l->n->sym->name[0] == '&' || l->n->used)\
 	\t\tcontinue;\
-\t\tlineno = n->lineno;\
-\t\ttypecheck(&n, Erv | Easgn);\t// only needed for unused variables
-\t\tif(!n->used && n->sym->name[0] != '&' && !nsyntaxerrors)\
-\t\t\tyyerror("%S declared and not used\", n->sym);\
-\t}\
+\t\tif(l->n->defn && l->n->defn->op == OTYPESW) {\
+\t\t\tif(l->n->defn->defn->left->used)\
+\t\t\t\tcontinue;\
+\t\t\tlineno = l->n->defn->left->lineno;\
+\t\t\tyyerror("%S declared and not used\", l->n->sym);\
+\t\t\tl->n->defn->left->used = 1; // suppress repeats\
+\t\t} else {\
+\t\t\tlineno = l->n->lineno;\
+\t\t\tyyerror("%S declared and not used\", l->n->sym);\
+\t\t}\
+\t}\t
+\
 	lineno = lno;
 	if(nerrors != 0)
 		return;

この変更は、未使用変数検出のロジックを大きく変えています。

  1. 全てのローカル変数に対してtypecheckを実行し、usedフラグを更新します。
  2. 型スイッチ変数(l->n->defn && l->n->defn->op == OTYPESW)で、かつ使用されている場合、その変数が属する型スイッチ全体のシンボル(l->n->defn->left)の used フラグをインクリメントします。
  3. 最後に、未使用のローカル変数をチェックするループで、それが型スイッチ変数であり、かつ型スイッチ全体のシンボルの used フラグが 0 であれば、未使用として報告します。l->n->defn->left->used = 1; は、同じエラーが複数回報告されるのを防ぐためのものです。

test/fixedbugs/bug373.go

新しいテストファイルで、型スイッチ変数の未使用ケースをテストしています。

// errchk $G $D/$F.go

// Copyright 2011 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.

// Issue 873, 2162

package foo

func f(x interface{}) {
	switch t := x.(type) {  // ERROR "declared and not used"
	case int:
	}
}

func g(x interface{}) {
	switch t := x.(type) {
	case int:
	case float32:
		println(t) // t is used here
	}
}

func h(x interface{}) {
	switch t := x.(type) {
	case int:
	case float32:
	default:
		println(t) // t is used here
	}
}

f 関数では t が使用されていないため、ERROR "declared and not used" が期待されます。gh 関数では t が使用されているため、エラーは期待されません。

コアとなるコードの解説

このコミットの主要な変更は、Goコンパイラが型スイッチ変数の「使用済み」状態を追跡する方法を改善することにあります。

src/cmd/gc/go.y の変更

go.y はGo言語の構文を定義するファイルです。ここでの変更は、型スイッチの構文解析時に、コンパイラが内部で構築する抽象構文木(AST)の構造を調整することに焦点を当てています。

  • 型スイッチ宣言のAST構築: 以前は、switch t := x.(type)t のような型スイッチ変数が宣言されると、その変数ノードが直接 OTYPESW(型スイッチ全体を表すノード)の left フィールドに格納されていました。 変更後、$$->left = dclname($1->n->sym); の行が追加されました。これは、OTYPESW ノードの left フィールドに、型スイッチ変数の「シンボル」(名前)を格納するようにします。このシンボルは、型スイッチ全体で共通の「この型スイッチで宣言される変数」という概念を表します。

  • 各ケース節での変数宣言とリンク: 各 case 節(例: case int:)や default 節では、newname(n->sym) を使って新しい変数ノード nn が作成され、そのケース節内で使用される型スイッチ変数を表します。 重要な変更は nn->defn = typesw->right; の行です。ここで、新しく作成された変数ノード nndefn(definitionの略)フィールドに、型スイッチ全体の OTYPESW ノードへの参照が設定されます。これにより、各ケース節のローカルな型スイッチ変数が、それが属する親の型スイッチ文と関連付けられます。この defn フィールドは、コンパイラが変数の定義元を辿るための重要なリンクとなります。

src/cmd/gc/walk.c の変更

walk.c は、構文解析後にASTを走査し、セマンティック分析や最適化を行うコンパイラの「ウォーク」フェーズのコードです。ここでの変更は、主に未使用変数検出のロジックに影響を与えます。

  • 未使用変数検出の多段階処理: 変更前は、全てのローカル変数に対して一律に n->used フラグをチェックし、false であれば未使用として報告していました。 変更後、このプロセスはより洗練された多段階のアプローチになりました。

    1. 初期の型チェック: for(l=fn->dcl; l; l=l->next) if(l->n->op == ONAME && (l->n->class&~PHEAP) == PAUTO) typecheck(&l->n, Erv | Easgn); このループは、関数内の全ての自動変数(ローカル変数)に対して typecheck を実行します。typecheck 関数は、変数が実際にコード内で使用されているかどうかを分析し、その結果に基づいて変数ノードの used フラグを更新します。これにより、通常のローカル変数の使用状況が正確に反映されます。

    2. 型スイッチ変数の used フラグ伝播: for(l=fn->dcl; l; l=l->next) if(l->n->op == ONAME && (l->n->class&~PHEAP) == PAUTO && l->n->defn && l->n->defn->op == OTYPESW && l->n->used) l->n->defn->left->used++; このループが、型スイッチ変数の未使用検出の鍵となります。

      • l->n->op == ONAME && (l->n->class&~PHEAP) == PAUTO: 現在のノードがローカル変数であることを確認します。
      • l->n->defn && l->n->defn->op == OTYPESW: この変数が型スイッチ内で宣言された変数であることを確認します(defn フィールドが OTYPESW ノードを指しているため)。
      • l->n->used: この特定のケース節内で、型スイッチ変数が実際に使用されているかどうかをチェックします。
      • l->n->defn->left->used++: もし上記の条件が全て真であれば、その型スイッチ変数が属する型スイッチ全体のシンボル(OTYPESW ノードの left フィールドが指すもの)の used フラグをインクリメントします。この used フラグは、ここでは単なるブール値ではなく、その型スイッチ変数が「いくつのケース節で実際に使用されたか」を示すカウンターとして機能します。
    3. 最終的な未使用変数チェック: for(l=fn->dcl; l; l=l->next) { ... } この最後のループで、全てのローカル変数を再度走査し、未使用変数を報告します。

      • if(l->n->op != ONAME || (l->n->class&~PHEAP) != PAUTO || l->n->sym->name[0] == '&' || l->n->used) continue;: 通常のローカル変数で、既にusedフラグがtrueの場合や、特殊な変数(&で始まるシンボルなど)はスキップします。
      • if(l->n->defn && l->n->defn->op == OTYPESW): 現在の変数が型スイッチ変数である場合。
        • if(l->n->defn->left->used) continue;: ここが重要です。もし型スイッチ全体のシンボル(l->n->defn->left)の used フラグが 0 でなければ(つまり、型スイッチのいずれかのケースで変数が使用されていれば)、この型スイッチ変数は未使用ではないと判断し、スキップします。
        • yyerror("%S declared and not used", l->n->sym);: もし l->n->defn->left->used0 であれば、型スイッチ変数がどのケースでも使用されていないと判断し、エラーを報告します。
        • l->n->defn->left->used = 1;: 同じ型スイッチ変数の未使用エラーが複数回報告されるのを防ぐために、一度エラーを報告したら used フラグを 1 に設定します。
      • else { yyerror("%S declared and not used", l->n->sym); }: 型スイッチ変数ではない通常のローカル変数が未使用の場合、ここでエラーを報告します。

この一連の変更により、コンパイラは型スイッチの各ケース節で宣言される変数の使用状況を、型スイッチ全体として正確に判断できるようになりました。これにより、Issue #873 と #2162 で報告されたような、型スイッチ変数の未使用に関する誤った挙動が修正されました。

関連リンク

参考にした情報源リンク