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

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

このコミットは、Goコンパイラ(cmd/gc)における「imported and not used」(インポートされたが使用されていない)エラーメッセージの改善に関するものです。具体的には、パッケージがリネームインポートされた場合や、インポートパスの最後の要素と実際のパッケージ名が異なる場合に、エラーメッセージにそのパッケージ名が明示的に表示されるように変更されました。これにより、開発者はどのパッケージが未使用であるかをより正確に把握できるようになります。

コミット

commit 8d530f2472f1c4ef5cf0513a6735869fb606fa96
Author: Russ Cox <rsc@golang.org>
Date:   Mon Sep 9 12:21:09 2013 -0400

    cmd/gc: show package name in 'imported and not used' error
    
    Fixes #5957.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/13250046

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

https://github.com/golang/go/commit/8d530f2472f1c4ef5cf0513a6735869fb606fa96

元コミット内容

cmd/gc: show package name in 'imported and not used' error

このコミットの目的は、Goコンパイラが「imported and not used」エラーを報告する際に、インポートされたパッケージの実際の名前をエラーメッセージに含めるようにすることです。これは、Go言語のIssue #5957を修正するものです。

変更の背景

Go言語では、インポートされたパッケージがコード内で使用されない場合、コンパイル時に「imported and not used」というエラーが発生します。これは、不要な依存関係を排除し、コードの可読性を高めるためのGoの設計思想の一部です。

しかし、従来のこのエラーメッセージには、インポートパスのみが表示され、リネームインポート(例: import foo "fmt")や、インポートパスの最後の要素と実際のパッケージ名が異なる場合(例: import "github.com/user/repo/mypackage"package mypkg と宣言されている場合)に、どの名前でインポートされたパッケージが未使用なのかが不明瞭になるという問題がありました。

例えば、import foo "fmt" と記述し、fmtパッケージをfooという名前でインポートしたが使用しなかった場合、従来のエラーメッセージは単に「imported and not used: "fmt"」と表示され、fooというエイリアスが使われていないことが直接的に示されませんでした。これにより、特に大規模なプロジェクトや複雑なインポート構造を持つコードベースでは、開発者がエラーの原因を特定し、修正するのに時間がかかる可能性がありました。

このコミットは、この問題を解決し、エラーメッセージの明確性を向上させることを目的としています。

前提知識の解説

Go言語のパッケージとインポート

Go言語では、コードは「パッケージ」という単位で整理されます。各Goファイルはpackage宣言を持ち、そのファイルがどのパッケージに属するかを示します。他のパッケージの機能を利用するには、importキーワードを使ってそのパッケージをインポートする必要があります。

インポートの形式にはいくつか種類があります。

  • 標準的なインポート: import "path/to/package"
    • この場合、パッケージ内のエクスポートされた要素は、インポートパスの最後の要素(例: package)をプレフィックスとして使用してアクセスされます(例: package.Function())。
  • リネームインポート: import alias "path/to/package"
    • パッケージを別の名前(alias)でインポートします。この場合、エクスポートされた要素はalias.Function()のようにアクセスされます。
  • ドットインポート: import . "path/to/package"
    • パッケージ内のエクスポートされた要素を、パッケージ名をプレフィックスなしで直接アクセスできるようにします。これは通常、非推奨とされていますが、テストコードなどで使用されることがあります。
  • ブランクインポート: import _ "path/to/package"
    • パッケージをインポートしますが、そのパッケージ内のエクスポートされた要素を直接使用することはありません。主に、パッケージのinit関数を実行するためや、サイドエフェクトのために使用されます。

Goコンパイラ (cmd/gc)

cmd/gcは、Go言語の公式コンパイラです。Goのソースコードをコンパイルして実行可能なバイナリを生成する役割を担っています。コンパイルプロセス中に、構文エラー、型エラー、未使用の変数やインポートなど、様々な種類の問題を検出して報告します。

imported and not used エラー

Goコンパイラが報告するエラーの一つで、ソースコード内でインポートされたパッケージが実際には使用されていない場合に発生します。このエラーは、コードの品質を保ち、不要な依存関係を排除するためのGoの厳格なルールの一部です。

yyerrorl 関数

yyerrorlは、Goコンパイラのフロントエンド(字句解析器/構文解析器)で使用されるエラー報告関数です。指定された行番号でエラーメッセージを出力するために使用されます。

Strlit 構造体

Goコンパイラの内部で使用される文字列リテラルを表す構造体です。path->sのようにアクセスされる場合、これは通常、インポートパスの文字列を指します。

技術的詳細

このコミットの主要な変更は、src/cmd/gc/lex.cファイルに新しい静的関数pkgnotusedが導入され、既存のyyerrorl呼び出しがこの新しい関数に置き換えられた点です。

pkgnotused 関数の導入

新しい関数pkgnotusedは、以下の引数を取ります。

  • lineno: エラーが発生した行番号。
  • path: インポートされたパッケージの完全なインポートパス(Strlit型)。
  • name: ソースコード内でそのパッケージがインポートされた際に使用された名前(リネームインポートの場合のエイリアス、または標準インポートの場合のパッケージ名)。

この関数は、インポートパスの最後の要素(例: "path/to/package""package")を抽出し、それがname引数と一致するかどうかを比較します。

  1. インポートパスの最後の要素の抽出: strrchr(path->s, '/') を使用して、インポートパス文字列 (path->s) の中で最後のスラッシュ (/) の位置を探します。

    • もしスラッシュが見つかれば、その次の文字から文字列の最後までがパッケージの要素名とみなされます(elem++)。
    • スラッシュが見つからなければ、インポートパス全体が要素名とみなされます(elem = path->s)。
    • GoのインポートパスはWindows上でも常にフォワードスラッシュを使用するため、この処理はOSに依存しません。
  2. エラーメッセージの生成:

    • 抽出された要素名(elem)と、ソースコードで使われた名前(name)がstrcmpで比較されます。
    • もし両者が一致する場合(例: import "fmt"fmt が使われていない場合)、従来通りのエラーメッセージ「imported and not used: "%Z"」が出力されます。ここで%Zはインポートパスに置き換えられます。
    • もし両者が一致しない場合(例: import foo "fmt"foo が使われていない場合、または import "./a"package surprise が宣言されている場合)、より詳細なエラーメッセージ「imported and not used: "%Z" as %s」が出力されます。ここで%Zはインポートパスに、%sname(エイリアスまたは実際のパッケージ名)に置き換えられます。

既存のyyerrorl呼び出しの置き換え

mkpackage関数内で、未使用のインポートを検出した際にyyerrorlを直接呼び出していた箇所が、新しく定義されたpkgnotused関数への呼び出しに置き換えられました。これにより、エラーメッセージの生成ロジックが一元化され、より詳細な情報が提供されるようになりました。

具体的には、以下の2つの箇所で変更が行われています。

  • s->def->usedfalse の場合(通常の未使用インポート)
  • s->def->pack->usedfalse の場合(ドットインポートなど、トップレベルの名前が未使用の場合)

これらの変更により、コンパイラはインポートパスだけでなく、そのパッケージがコード内でどのような名前で参照されようとしていたか(または参照されなかったか)をエラーメッセージに含めることができるようになりました。

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

src/cmd/gc/lex.c

--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -2279,6 +2279,28 @@ yytinit(void)\
 	}\t\t
 }\
 \
+static void
+pkgnotused(int lineno, Strlit *path, char *name)
+{\
+	char *elem;
+	
+	// If the package was imported with a name other than the final
+	// import path element, show it explicitly in the error message.
+	// Note that this handles both renamed imports and imports of
+	// packages containing unconventional package declarations.
+	// Note that this uses / always, even on Windows, because Go import
+	// paths always use forward slashes.
+	elem = strrchr(path->s, '/');
+	if(elem != nil)
+		elem++;
+	else
+		elem = path->s;
+	if(strcmp(elem, name) == 0)
+		yyerrorl(lineno, "imported and not used: \"%Z\"", path);
+	else
+		yyerrorl(lineno, "imported and not used: \"%Z\" as %s", path, name);
+}
+\
 void
 mkpackage(char* pkgname)
 {
@@ -2304,7 +2326,7 @@ mkpackage(char* pkgname)\
 					// errors if a conflicting top-level name is
 					// introduced by a different file.
 					if(!s->def->used && !nsyntaxerrors)
-						yyerrorl(s->def->lineno, "imported and not used: \"%Z\"", s->def->pkg->path);\
+						pkgnotused(s->def->lineno, s->def->pkg->path, s->name);\
 					s->def = N;
 					continue;
 				}
@@ -2312,7 +2334,7 @@ mkpackage(char* pkgname)\
 					// throw away top-level name left over
 					// from previous import . "x"
 					if(s->def->pack != N && !s->def->pack->used && !nsyntaxerrors) {
-						yyerrorl(s->def->pack->lineno, "imported and not used: \"%Z\"", s->def->pack->pkg->path);\
+						pkgnotused(s->def->pack->lineno, s->def->pack->pkg->path, s->name);\
 						s->def->pack->used = 1;\
 					}\
 					s->def = N;

コアとなるコードの解説

pkgnotused 関数

static void
pkgnotused(int lineno, Strlit *path, char *name)
{
	char *elem;
	
	// If the package was imported with a name other than the final
	// import path element, show it explicitly in the error message.
	// Note that this handles both renamed imports and imports of
	// packages containing unconventional package declarations.
	// Note that this uses / always, even on Windows, because Go import
	// paths always use forward slashes.
	elem = strrchr(path->s, '/'); // インポートパスの最後の '/' を検索
	if(elem != nil)
		elem++; // '/' の次の文字からが要素名
	else
		elem = path->s; // '/' がなければパス全体が要素名
	
	// インポートパスの最後の要素と、コードで使われた名前を比較
	if(strcmp(elem, name) == 0)
		// 一致する場合(通常のインポートで未使用)
		yyerrorl(lineno, "imported and not used: \"%Z\"", path);
	else
		// 一致しない場合(リネームインポートや、パッケージ名とパスの最後の要素が異なる場合)
		yyerrorl(lineno, "imported and not used: \"%Z\" as %s", path, name);
}

この関数は、エラーメッセージを生成する際の中心的なロジックを担っています。path(インポートパス)とname(コード内で使用された、または使用されるべきだった名前)を比較し、それに応じて異なるエラーメッセージを生成します。これにより、エラーメッセージがより文脈に即したものになります。

mkpackage 関数内の変更

mkpackage関数は、Goコンパイラがパッケージのインポートと定義を処理する際に使用される重要な関数です。この関数内で、未使用のインポートを検出する既存のロジックが変更されました。

変更前:

yyerrorl(s->def->lineno, "imported and not used: \"%Z\"", s->def->pkg->path);

変更後:

pkgnotused(s->def->lineno, s->def->pkg->path, s->name);

この変更により、直接yyerrorlを呼び出す代わりに、新しく定義されたpkgnotused関数が呼び出されるようになりました。pkgnotused関数には、インポートパス(s->def->pkg->path)だけでなく、そのパッケージがコード内でどのような名前で参照されようとしていたかを示すs->nameも渡されます。これにより、pkgnotused関数がより詳細なエラーメッセージを生成できるようになります。

同様の変更が、ドットインポートに関連する未使用のトップレベル名を処理する箇所でも行われています。

これらの変更により、Goコンパイラは、単に「どのパスのパッケージが未使用か」だけでなく、「どの名前でインポートされたパッケージが未使用か」という情報もエラーメッセージに含めることができるようになり、開発者にとってのデバッグ体験が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (パッケージとインポートに関する情報)
  • Goコンパイラのソースコード (src/cmd/gc/lex.c の周辺コード)
  • strrchr 関数のC言語リファレンス
  • strcmp 関数のC言語リファレンス
  • Go言語のIssueトラッカー (Issue #5957の詳細)
  • Go Code Review (CL 13250046の詳細)