[インデックス 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がここで使われていない } }このコードでは、
tがint型の場合に宣言されますが、その後のブロックで全く使用されていませんでした。本来であればコンパイラが「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_stmtやcaseなどの文法規則が定義されています。src/cmd/gc/walk.c: Goコンパイラの「ウォーク(walk)」フェーズを担当するファイルです。構文解析によって生成されたASTを走査し、型チェック、最適化、コード生成のための準備など、様々なセマンティック分析を行います。未使用変数の検出もこのフェーズで行われます。Node構造体: コンパイラ内部でASTの各ノードを表すデータ構造です。各ノードは、その種類(op)、関連するシンボル(sym)、型情報、子ノードへのポインタなど、様々な情報を持っています。ONAME:Nodeのopフィールドの値の一つで、名前(変数、関数など)を表すノードを示します。OTYPESW:Nodeのopフィールドの値の一つで、型スイッチ文全体を表すノードを示します。PAUTO: 変数のストレージクラス(記憶域クラス)の一つで、自動変数(ローカル変数)を示します。usedフラグ: コンパイラが変数がコード内で使用されているかどうかを追跡するために、Node構造体内に持つフラグです。このフラグがfalseのままコンパイルが終了すると、未使用変数として警告(またはエラー)が発せられます。yyerror:yacc/bisonによって生成されるパーサー内で使用されるエラー報告関数です。コンパイルエラーや警告メッセージを出力するために使われます。dclcontext: 宣言のコンテキストを管理する変数。変数がどのスコープで宣言されているかを追跡します。newname/dclname: コンパイラ内部で新しい変数を宣言したり、既存の名前を解決したりするための関数。
技術的詳細
このコミットの核心は、Goコンパイラが型スイッチ変数の「使用済み」状態をどのように追跡し、未使用のケースを検出するかという点にあります。
従来のコンパイラでは、型スイッチの各ケース節で宣言される変数は、そのケース節のローカル変数として扱われ、そのケース節内で使用されればusedフラグがセットされました。しかし、型スイッチ全体としてその変数が「宣言されたが使用されていない」という状況を適切に判断するためのメカニズムが不足していました。特に、型スイッチ変数が宣言されるsimple_stmt(v := x.(type) の部分)と、実際にその変数が使用される可能性のある各case節との間の連携が不十分でした。
このコミットでは、以下の主要な変更が導入されています。
-
OTYPESWノードの強化:src/cmd/gc/go.yにおいて、型スイッチ文を表現するOTYPESWノードの構造が変更されました。- 以前は、
OTYPESWノードのleftフィールドに型スイッチ変数のノードが直接格納されていました。 - 変更後、
OTYPESWノードのleftフィールドは、型スイッチ変数の「シンボル」(ONONAMEノード)を指すようになりました。そして、各ケース節で実際に宣言される型スイッチ変数のノード(nn)のdefnフィールドが、このOTYPESWノードを指すように設定されます。これにより、各ケース節の型スイッチ変数と、型スイッチ全体の宣言との間に明確なリンクが確立されます。
-
usedフラグの伝播:src/cmd/gc/walk.cのwalk関数内で、コンパイルの最終段階で未使用変数をチェックするロジックが修正されました。- 新しいロジックでは、まず全てのローカル変数(
PAUTO)についてtypecheckを実行し、その変数が使用されているかどうかを判断します。 - 次に、型スイッチ変数(
l->n->defn && l->n->defn->op == OTYPESWで識別される)の場合、その変数が使用されている(l->n->usedがtrue)であれば、その変数が定義されているOTYPESWノードのleftフィールド(つまり、型スイッチ全体のシンボル)のusedフラグをインクリメントします。これは、型スイッチのいずれかのケースで変数が使用されたことを示すためのカウンターとして機能します。 - 最後に、ローカル変数を再度走査し、もしそれが型スイッチ変数であり、かつその
OTYPESWノードのleftフィールドのusedフラグが0であれば、その型スイッチ変数はどのケースでも使用されていないと判断し、yyerrorを使って「declared and not used」という警告を発します。
-
テストケースの追加:
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節)で型スイッチ変数を宣言する際に、新しく宣言された変数ノードnnのdefnフィールドに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;
この変更は、未使用変数検出のロジックを大きく変えています。
- 全てのローカル変数に対して
typecheckを実行し、usedフラグを更新します。 - 型スイッチ変数(
l->n->defn && l->n->defn->op == OTYPESW)で、かつ使用されている場合、その変数が属する型スイッチ全体のシンボル(l->n->defn->left)のusedフラグをインクリメントします。 - 最後に、未使用のローカル変数をチェックするループで、それが型スイッチ変数であり、かつ型スイッチ全体のシンボルの
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" が期待されます。g と h 関数では 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;の行です。ここで、新しく作成された変数ノードnnのdefn(definitionの略)フィールドに、型スイッチ全体のOTYPESWノードへの参照が設定されます。これにより、各ケース節のローカルな型スイッチ変数が、それが属する親の型スイッチ文と関連付けられます。このdefnフィールドは、コンパイラが変数の定義元を辿るための重要なリンクとなります。
src/cmd/gc/walk.c の変更
walk.c は、構文解析後にASTを走査し、セマンティック分析や最適化を行うコンパイラの「ウォーク」フェーズのコードです。ここでの変更は、主に未使用変数検出のロジックに影響を与えます。
-
未使用変数検出の多段階処理: 変更前は、全てのローカル変数に対して一律に
n->usedフラグをチェックし、falseであれば未使用として報告していました。 変更後、このプロセスはより洗練された多段階のアプローチになりました。-
初期の型チェック:
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フラグを更新します。これにより、通常のローカル変数の使用状況が正確に反映されます。 -
型スイッチ変数の
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フラグは、ここでは単なるブール値ではなく、その型スイッチ変数が「いくつのケース節で実際に使用されたか」を示すカウンターとして機能します。
-
最終的な未使用変数チェック:
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->usedが0であれば、型スイッチ変数がどのケースでも使用されていないと判断し、エラーを報告します。l->n->defn->left->used = 1;: 同じ型スイッチ変数の未使用エラーが複数回報告されるのを防ぐために、一度エラーを報告したらusedフラグを1に設定します。
else { yyerror("%S declared and not used", l->n->sym); }: 型スイッチ変数ではない通常のローカル変数が未使用の場合、ここでエラーを報告します。
-
この一連の変更により、コンパイラは型スイッチの各ケース節で宣言される変数の使用状況を、型スイッチ全体として正確に判断できるようになりました。これにより、Issue #873 と #2162 で報告されたような、型スイッチ変数の未使用に関する誤った挙動が修正されました。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/aac144b1202fc733a206422bed3cc6eafe4ca855
- Go CL (Code Review): https://golang.org/cl/5341043
- Issue 873: https://github.com/golang/go/issues/873
- Issue 2162: https://github.com/golang/go/issues/2162
参考にした情報源リンク
- Go言語の公式ドキュメント(型スイッチに関する記述)
- Goコンパイラのソースコード(
src/cmd/gcディレクトリ内のファイル) - Go言語のIssueトラッカー(Issue #873, #2162 の詳細)
yacc/bisonのドキュメント(go.yの理解のため)- Go言語のコンパイラに関する一般的な情報源(AST、セマンティック分析など)
- Go issue 873: gc: type switch variable not used
- Go issue 2162: gc: type switch default case variable not used
- Go CL 5341043: gc: detect type switch variable not used cases.