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

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

このコミットは、Go言語の公式フォーマッタである gofmt ツールに、空の宣言グループ(例: var (), const (), type ())を自動的に削除する機能を追加するものです。これにより、gofmt が生成するコードの整形がさらに洗練され、不要な構文要素が取り除かれることで、コードの可読性と簡潔性が向上します。

コミット

commit 138099ae96332d2a5a63888c96001286d7273907
Author: Simon Whitehead <chemnova@gmail.com>
Date:   Tue Jul 1 09:32:03 2014 -0700

    gofmt/main: Added removal of empty declaration groups.
    
    Fixes #7631.
    
    LGTM=gri
    R=golang-codereviews, bradfitz, gri
    CC=golang-codereviews
    https://golang.org/cl/101410046
---
 src/cmd/gofmt/gofmt_test.go             |  5 +++--
 src/cmd/gofmt/simplify.go               | 29 +++++++++++++++++++++++++++++
 src/cmd/gofmt/testdata/emptydecl.golden | 10 ++++++++++\
 src/cmd/gofmt/testdata/emptydecl.input  | 12 ++++++++++++\
 4 files changed, 54 insertions(+), 2 deletions(-)

diff --git a/src/cmd/gofmt/gofmt_test.go b/src/cmd/gofmt/gofmt_test.go
index b9335b8f3d..b767a6bf55 100644
--- a/src/cmd/gofmt/gofmt_test.go
+++ b/cmd/gofmt/gofmt_test.go
@@ -87,8 +87,9 @@ var tests = []struct {
 	{"testdata/stdin*.input", ""},
 	{"testdata/comments.input", ""},
 	{"testdata/import.input", ""},
-	{"testdata/crlf.input", ""},       // test case for issue 3961; see also TestCRLF
-	{"testdata/typeswitch.input", ""}, // test case for issue 4470
+	{"testdata/crlf.input", ""},        // test case for issue 3961; see also TestCRLF
+	{"testdata/typeswitch.input", ""},  // test case for issue 4470
+	{"testdata/emptydecl.input", "-s"}, // test case for issue 7631
 }
 
 func TestRewrite(t *testing.T) {
diff --git a/src/cmd/gofmt/simplify.go b/src/cmd/gofmt/simplify.go
index 45d000d675..b1556be74e 100644
--- a/src/cmd/gofmt/simplify.go
+++ b/src/cmd/gofmt/simplify.go
@@ -117,5 +117,34 @@ func simplify(f *ast.File) {
 		}
 	}
 
+	// remove empty declarations such as "const ()", etc
+	removeEmptyDeclGroups(f)
+
 	ast.Walk(&s, f)
 }
+
+func removeEmptyDeclGroups(f *ast.File) {
+	i := 0
+	for _, d := range f.Decls {
+		if g, ok := d.(*ast.GenDecl); !ok || !isEmpty(f, g) {
+			f.Decls[i] = d
+			i++
+		}
+	}
+	f.Decls = f.Decls[:i]
+}
+
+func isEmpty(f *ast.File, g *ast.GenDecl) bool {
+	if g.Doc != nil || g.Specs != nil {
+		return false
+	}
+
+	for _, c := range f.Comments {
+		// if there is a comment in the declaration, it is not considered empty
+		if g.Pos() <= c.Pos() && c.End() <= g.End() {
+			return false
+		}
+	}
+
+	return true
+}
diff --git a/src/cmd/gofmt/testdata/emptydecl.golden b/src/cmd/gofmt/testdata/emptydecl.golden
new file mode 100644
index 0000000000..9fe62c9738
--- /dev/null
+++ b/src/cmd/gofmt/testdata/emptydecl.golden
@@ -0,0 +1,10 @@
+package main
+
+// Keep this declaration
+var ()
+
+const (
+// Keep this declaration
+)
+
+func main() {}
diff --git a/src/cmd/gofmt/testdata/emptydecl.input b/src/cmd/gofmt/testdata/emptydecl.input
new file mode 100644
index 0000000000..d1cab00ef7
--- /dev/null
+++ b/src/cmd/gofmt/testdata/emptydecl.input
@@ -0,0 +1,12 @@
+package main
+
+// Keep this declaration
+var ()
+
+const (
+// Keep this declaration
+)
+
+type ()
+
+func main() {}
\ No newline at end of file

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

https://github.com/golang/go/commit/138099ae96332d2a5a63888c96001286d7273907

元コミット内容

このコミットは、gofmt ツールが空の宣言グループ(例: var (), const (), type ())を処理する方法を変更します。以前の gofmt は、これらの空の宣言グループをそのまま残していました。しかし、このような空の宣言グループはコードの意図を明確にせず、冗長であると見なされる場合があります。この変更により、gofmt はこれらの空の宣言グループを自動的に削除し、よりクリーンで簡潔なGoコードを生成するようになります。この機能追加は、Goの内部課題追跡システムで報告されたIssue #7631を解決することを目的としています。

変更の背景

Go言語のコードフォーマッタである gofmt は、Goコミュニティにおいてコードスタイルの一貫性を保つ上で非常に重要なツールです。gofmt は、コードを解析し、Goの標準的なスタイルガイドラインに沿って整形することで、開発者がコードのスタイルについて議論する時間を減らし、より本質的な開発作業に集中できるようにします。

しかし、このコミットが導入される以前の gofmt には、var (), const (), type () のような、中身が空の宣言グループをそのまま残してしまうという挙動がありました。これらの空の宣言グループは、構文的には有効ですが、実際のコードには何の宣言も含まれていないため、多くの場合、単なるノイズとなり、コードの可読性を損なう可能性がありました。

例えば、以下のようなコードがあったとします。

package main

var ()

const ()

type ()

func main() {
}

このコードは有効ですが、var (), const (), type () は何も宣言していないため、冗長です。開発者が誤ってこれらを残してしまったり、リファクタリングの過程で中身が空になったりした場合でも、gofmt はこれらを削除せず、そのまま出力していました。

この問題は、Goの内部課題追跡システムでIssue #7631として報告されました。このコミットは、この課題に対応し、gofmt がこのような空の宣言グループを自動的に認識し、削除するように改善することで、よりクリーンで意図が明確なコードを生成できるようにすることを目的としています。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語および gofmt に関する基本的な知識が必要です。

  • gofmt: gofmt は、Go言語のソースコードを自動的に整形するツールです。Go言語の標準ライブラリの一部として提供されており、Goのコードベース全体で一貫したコーディングスタイルを強制するために広く使用されています。gofmt は、Goのソースコードを解析して抽象構文木(AST)を構築し、そのASTを操作して整形されたコードを再生成します。これにより、インデント、スペース、改行、括弧の位置など、多くのスタイルに関する問題を自動的に修正します。

  • Goの抽象構文木(AST)と go/ast パッケージ: Goコンパイラや gofmt のようなツールは、Goのソースコードを直接テキストとして扱うのではなく、その構造を表現する抽象構文木(Abstract Syntax Tree, AST)に変換して処理します。ASTは、プログラムの構造を木構造で表現したもので、各ノードがプログラムの特定の構文要素(変数宣言、関数定義、式など)に対応します。 Go言語では、標準ライブラリの go/ast パッケージがASTのデータ構造と操作を提供します。gofmt はこのパッケージを利用して、ソースコードをASTにパースし、ASTを走査・変更することで、整形されたコードを生成します。

  • ast.File: go/ast パッケージにおける ast.File は、Goの単一のソースファイル全体を表すASTのルートノードです。この構造体には、パッケージ名、インポート宣言、そしてファイル内のすべてのトップレベル宣言(変数、定数、型、関数など)のリストが含まれています。このコミットでは、ast.FileDecls フィールド(宣言のリスト)を操作して、空の宣言グループを削除します。

  • ast.GenDecl: go/ast パッケージにおける ast.GenDecl は、Go言語の一般的な宣言(General Declaration)を表すASTノードです。これには、var (変数宣言), const (定数宣言), type (型宣言) などが含まれます。これらの宣言は、単一の要素を宣言することもできますし、括弧 () を使って複数の要素をグループ化して宣言することもできます。このコミットの対象となる「空の宣言グループ」は、この ast.GenDecl の一種で、Specs フィールド(宣言される要素のリスト)が空であるものを指します。

  • 宣言グループ: Go言語では、var, const, type キーワードの後に括弧 () を使用して、複数の変数、定数、または型をまとめて宣言することができます。これを宣言グループと呼びます。 例:

    var (
        x int
        y string
    )
    
    const (
        Pi = 3.14
        E  = 2.71
    )
    
    type (
        MyInt int
        MyString string
    )
    

    このコミットで問題となるのは、これらのグループの中身が空である場合、つまり var (), const (), type () のような形式です。

技術的詳細

このコミットの技術的な核心は、gofmtsrc/cmd/gofmt/simplify.go ファイルに新しい関数 removeEmptyDeclGroups とそのヘルパー関数 isEmpty を追加し、既存の simplify 関数からこれらを呼び出すことで、ASTから空の宣言グループを効率的に削除する点にあります。

  1. simplify 関数への組み込み: simplify.go 内の simplify 関数は、gofmt がASTを整形する際に呼び出される主要な関数の一つです。このコミットでは、simplify 関数の既存の処理の後に、新しく追加された removeEmptyDeclGroups(f) が呼び出されるように変更されました。これにより、ASTの他の簡素化処理が完了した後で、空の宣言グループの削除が行われます。

  2. removeEmptyDeclGroups 関数の実装: この関数は *ast.File 型の引数 f を受け取ります。これは、処理対象のGoソースファイルのAST全体を表します。 関数内部では、f.Decls (ファイル内のトップレベル宣言のリスト) をイテレートします。 各宣言 d について、以下のチェックを行います。

    • g, ok := d.(*ast.GenDecl): 宣言 dast.GenDecl 型(一般的な宣言、つまり var, const, type 宣言)であるかをチェックします。okfalse の場合、それは ast.FuncDecl (関数宣言) など他の種類の宣言であるため、処理の対象外としてそのまま保持します。
    • !isEmpty(f, g): ast.GenDecl であった場合、新しく追加された isEmpty ヘルパー関数を呼び出し、その宣言グループが「空」であるかどうかを判定します。isEmptyfalse(つまり空ではない)を返した場合、その宣言グループは保持されます。 空ではない宣言のみを新しい宣言リストにコピーしていくことで、結果的に空の宣言グループが削除された f.Decls が再構築されます。
  3. isEmpty ヘルパー関数の実装: この関数は *ast.File 型の f*ast.GenDecl 型の g を受け取り、与えられた ast.GenDecl が空であると見なせるかどうかをブール値で返します。 空であると判定するための条件は以下の通りです。

    • g.Doc != nil || g.Specs != nil: 宣言グループにドキュメントコメント (g.Doc) が付いている場合、または宣言される要素 (g.Specs) が存在する場合、それは空ではないと判断されます。Specs が存在しないことが、空の宣言グループの基本的な条件です。
    • コメントの扱い: 宣言グループの内部にコメントが存在する場合、その宣言グループは空とは見なされません。これは、開発者が意図的にコメントを残して、将来の宣言のためのプレースホルダーとして使用している可能性があるためです。isEmpty 関数は、f.Comments (ファイル全体のコメントリスト) を走査し、現在の ast.GenDecl の開始位置 (g.Pos()) と終了位置 (g.End()) の間にコメント (c) が存在するかどうかをチェックします。もし存在すれば、その宣言グループは空ではないと判断されます。

これらの変更により、gofmt は、単に中身がないだけでなく、ドキュメントコメントや内部コメントも持たない真に「空」の宣言グループのみを識別し、削除するようになりました。これにより、開発者の意図を尊重しつつ、コードの冗長性を排除することが可能になります。

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

このコミットにおける主要なコード変更は、src/cmd/gofmt/simplify.go ファイルに集中しています。

src/cmd/gofmt/simplify.go の変更点:

--- a/src/cmd/gofmt/simplify.go
+++ b/src/cmd/gofmt/simplify.go
@@ -117,5 +117,34 @@ func simplify(f *ast.File) {
 		}
 	}
 
+	// remove empty declarations such as "const ()", etc
+	removeEmptyDeclGroups(f)
+
 	ast.Walk(&s, f)
 }
+
+func removeEmptyDeclGroups(f *ast.File) {
+	i := 0
+	for _, d := range f.Decls {
+		if g, ok := d.(*ast.GenDecl); !ok || !isEmpty(f, g) {
+			f.Decls[i] = d
+			i++
+		}
+	}
+	f.Decls = f.Decls[:i]
+}
+
+func isEmpty(f *ast.File, g *ast.GenDecl) bool {
+	if g.Doc != nil || g.Specs != nil {
+		return false
+	}
+
+	for _, c := range f.Comments {
+		// if there is a comment in the declaration, it is not considered empty
+		if g.Pos() <= c.Pos() && c.End() <= g.End() {
+			return false
+		}
+	}
+
+	return true
+}

src/cmd/gofmt/gofmt_test.go の変更点:

新しいテストケース testdata/emptydecl.inputtestdata/emptydecl.golden が追加され、gofmt_test.gotests 変数にそのテストケースが追加されました。

--- a/src/cmd/gofmt/gofmt_test.go
+++ b/src/cmd/gofmt/gofmt_test.go
@@ -87,8 +87,9 @@ var tests = []struct {
 	{"testdata/stdin*.input", ""},
 	{"testdata/comments.input", ""},
 	{"testdata/import.input", ""},
-	{"testdata/crlf.input", ""},       // test case for issue 3961; see also TestCRLF
-	{"testdata/typeswitch.input", ""}, // test case for issue 4470
+	{"testdata/crlf.input", ""},        // test case for issue 3961; see also TestCRLF
+	{"testdata/typeswitch.input", ""},  // test case for issue 4470
+	{"testdata/emptydecl.input", "-s"}, // test case for issue 7631
 }
 
 func TestRewrite(t *testing.T) {

新しいテストデータファイル:

  • src/cmd/gofmt/testdata/emptydecl.input:

    package main
    
    // Keep this declaration
    var ()
    
    const (
    // Keep this declaration
    )
    
    type ()
    
    func main() {}
    
  • src/cmd/gofmt/testdata/emptydecl.golden:

    package main
    
    // Keep this declaration
    var ()
    
    const (
    // Keep this declaration
    )
    
    func main() {}
    

コアとなるコードの解説

このコミットの核となるのは、simplify.go に追加された removeEmptyDeclGroups 関数と isEmpty 関数です。

removeEmptyDeclGroups(f *ast.File) 関数

この関数は、Goソースファイルの抽象構文木(AST)を表現する *ast.File を受け取り、そのファイル内のトップレベル宣言から空の宣言グループを削除します。

  1. 宣言の走査: f.Decls は、ファイル内のすべてのトップレベル宣言(変数、定数、型、関数など)のリストです。この関数は、このリストをループで走査します。
  2. ast.GenDecl の識別: 各宣言 d について、d.(*ast.GenDecl) を使用して、それが var, const, type のいずれかの一般的な宣言グループであるかどうかをチェックします。
    • もし ast.GenDecl でない場合(例: 関数宣言 ast.FuncDecl)、その宣言は空の宣言グループではないため、そのまま保持されます。
    • もし ast.GenDecl であった場合、次に isEmpty 関数を呼び出して、その宣言グループが本当に空であるかどうかを判定します。
  3. 空でない宣言の保持: !isEmpty(f, g) の条件が true(つまり、宣言が ast.GenDecl でないか、または ast.GenDecl であっても空ではない)の場合、その宣言は f.Decls の先頭から順に詰め直されます。変数 i は、保持される宣言の次の書き込み位置を追跡します。
  4. リストのトリミング: ループが終了した後、f.Decls = f.Decls[:i] を実行することで、元の f.Decls リストを、空の宣言グループが削除された新しい(より短い)リストにトリミングします。これにより、ASTから不要なノードが効果的に削除されます。

isEmpty(f *ast.File, g *ast.GenDecl) 関数

このヘルパー関数は、与えられた ast.GenDeclgofmt によって削除されるべき「空」の宣言グループであるかどうかを判定します。

  1. 基本的な空のチェック:

    • g.Doc != nil: 宣言グループにドキュメントコメント(///* */ で始まる宣言直前のコメント)が付いている場合、それは開発者によって意図的に残されたものである可能性が高いため、空とは見なされません。
    • g.Specs != nil: 宣言グループ内に実際の宣言要素(例: var x int)が存在する場合、それは明らかに空ではないため、false を返します。 これらの条件のいずれかが真であれば、関数は直ちに false を返し、その宣言グループは削除されません。
  2. 内部コメントのチェック: g.Docg.Specs もない場合でも、宣言グループの内部にコメントが存在する可能性があります(例: const ( // TODO: Add constants here ))。このようなコメントは、開発者が将来の追加のためにプレースホルダーとして残している可能性があるため、gofmt はこれを削除すべきではありません。

    • f.Comments は、ファイル全体のすべてのコメントのリストです。
    • この関数は f.Comments を走査し、各コメント c の位置が現在の ast.GenDecl g の範囲内(g.Pos() <= c.Pos() && c.End() <= g.End())にあるかどうかをチェックします。
    • もし範囲内にコメントが見つかった場合、その宣言グループは空ではないと判断され、false を返します。
  3. 真に空の判定: 上記のすべてのチェックを通過した場合(つまり、ドキュメントコメントも、宣言要素も、内部コメントも存在しない場合)、その ast.GenDecl は真に空であると判断され、true を返します。これにより、removeEmptyDeclGroups 関数はその宣言を削除します。

これらの関数が連携することで、gofmt はGoコードから冗長な空の宣言グループをインテリジェントに削除し、コードの整形品質を向上させます。

関連リンク

参考にした情報源リンク

  • https://golang.org/cl/101410046 (Go Gerrit Change-IDページ)
  • Go言語公式ドキュメント (gofmt, go/astパッケージに関する一般的な情報)```

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

このコミットは、Go言語の公式フォーマッタである gofmt ツールに、空の宣言グループ(例: var (), const (), type ())を自動的に削除する機能を追加するものです。これにより、gofmt が生成するコードの整形がさらに洗練され、不要な構文要素が取り除かれることで、コードの可読性と簡潔性が向上します。

コミット

commit 138099ae96332d2a5a63888c96001286d7273907
Author: Simon Whitehead <chemnova@gmail.com>
Date:   Tue Jul 1 09:32:03 2014 -0700

    gofmt/main: Added removal of empty declaration groups.
    
    Fixes #7631.
    
    LGTM=gri
    R=golang-codereviews, bradfitz, gri
    CC=golang-codereviews
    https://golang.org/cl/101410046
---
 src/cmd/gofmt/gofmt_test.go             |  5 +++--
 src/cmd/gofmt/simplify.go               | 29 +++++++++++++++++++++++++++++
 src/cmd/gofmt/testdata/emptydecl.golden | 10 ++++++++++\
 src/cmd/gofmt/testdata/emptydecl.input  | 12 ++++++++++++\
 4 files changed, 54 insertions(+), 2 deletions(-)

diff --git a/src/cmd/gofmt/gofmt_test.go b/src/cmd/gofmt/gofmt_test.go
index b9335b8f3d..b767a6bf55 100644
--- a/src/cmd/gofmt/gofmt_test.go
+++ b/src/cmd/gofmt/gofmt_test.go
@@ -87,8 +87,9 @@ var tests = []struct {
 	{"testdata/stdin*.input", ""},
 	{"testdata/comments.input", ""},
 	{"testdata/import.input", ""},
-	{"testdata/crlf.input", ""},       // test case for issue 3961; see also TestCRLF
-	{"testdata/typeswitch.input", ""}, // test case for issue 4470
+	{"testdata/crlf.input", ""},        // test case for issue 3961; see also TestCRLF
+	{"testdata/typeswitch.input", ""},  // test case for issue 4470
+	{"testdata/emptydecl.input", "-s"}, // test case for issue 7631
 }
 
 func TestRewrite(t *testing.T) {
diff --git a/src/cmd/gofmt/simplify.go b/src/cmd/gofmt/simplify.go
index 45d000d675..b1556be74e 100644
--- a/src/cmd/gofmt/simplify.go
+++ b/src/cmd/gofmt/simplify.go
@@ -117,5 +117,34 @@ func simplify(f *ast.File) {
 		}
 	}
 
+	// remove empty declarations such as "const ()", etc
+	removeEmptyDeclGroups(f)
+
 	ast.Walk(&s, f)
 }
+
+func removeEmptyDeclGroups(f *ast.File) {
+	i := 0
+	for _, d := range f.Decls {
+		if g, ok := d.(*ast.GenDecl); !ok || !isEmpty(f, g) {
+			f.Decls[i] = d
+			i++
+		}
+	}
+	f.Decls = f.Decls[:i]
+}
+
+func isEmpty(f *ast.File, g *ast.GenDecl) bool {
+	if g.Doc != nil || g.Specs != nil {
+		return false
+	}
+
+	for _, c := range f.Comments {
+		// if there is a comment in the declaration, it is not considered empty
+		if g.Pos() <= c.Pos() && c.End() <= g.End() {
+			return false
+		}
+	}
+
+	return true
+}
diff --git a/src/cmd/gofmt/testdata/emptydecl.golden b/src/cmd/gofmt/testdata/emptydecl.golden
new file mode 100644
index 0000000000..9fe62c9738
--- /dev/null
+++ b/src/cmd/gofmt/testdata/emptydecl.golden
@@ -0,0 +1,10 @@
+package main
+
+// Keep this declaration
+var ()
+
+const (
+// Keep this declaration
+)
+
+func main() {}
diff --git a/src/cmd/gofmt/testdata/emptydecl.input b/src/cmd/gofmt/testdata/emptydecl.input
new file mode 100644
index 0000000000..d1cab00ef7
--- /dev/null
+++ b/src/cmd/gofmt/testdata/emptydecl.input
@@ -0,0 +1,12 @@
+package main
+
+// Keep this declaration
+var ()
+
+const (
+// Keep this declaration
+)
+
+type ()
+
+func main() {}
\ No newline at end of file

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

https://github.com/golang/go/commit/138099ae96332d2a5a63888c96001286d7273907

元コミット内容

このコミットは、gofmt ツールが空の宣言グループ(例: var (), const (), type ())を処理する方法を変更します。以前の gofmt は、これらの空の宣言グループをそのまま残していました。しかし、このような空の宣言グループはコードの意図を明確にせず、冗長であると見なされる場合があります。この変更により、gofmt はこれらの空の宣言グループを自動的に削除し、よりクリーンで簡潔なGoコードを生成するようになります。この機能追加は、Goの内部課題追跡システムで報告されたIssue #7631を解決することを目的としています。

変更の背景

Go言語のコードフォーマッタである gofmt は、Goコミュニティにおいてコードスタイルの一貫性を保つ上で非常に重要なツールです。gofmt は、コードを解析し、Goの標準的なスタイルガイドラインに沿って整形することで、開発者がコードのスタイルについて議論する時間を減らし、より本質的な開発作業に集中できるようにします。

しかし、このコミットが導入される以前の gofmt には、var (), const (), type () のような、中身が空の宣言グループをそのまま残してしまうという挙動がありました。これらの空の宣言グループは、構文的には有効ですが、実際のコードには何の宣言も含まれていないため、多くの場合、単なるノイズとなり、コードの可読性を損なう可能性がありました。

例えば、以下のようなコードがあったとします。

package main

var ()

const ()

type ()

func main() {
}

このコードは有効ですが、var (), const (), type () は何も宣言していないため、冗長です。開発者が誤ってこれらを残してしまったり、リファクタリングの過程で中身が空になったりした場合でも、gofmt はこれらを削除せず、そのまま出力していました。

この問題は、Goの内部課題追跡システムでIssue #7631として報告されました。このコミットは、この課題に対応し、gofmt がこのような空の宣言グループを自動的に認識し、削除するように改善することで、よりクリーンで意図が明確なコードを生成できるようにすることを目的としています。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語および gofmt に関する基本的な知識が必要です。

  • gofmt: gofmt は、Go言語のソースコードを自動的に整形するツールです。Go言語の標準ライブラリの一部として提供されており、Goのコードベース全体で一貫したコーディングスタイルを強制するために広く使用されています。gofmt は、Goのソースコードを解析して抽象構文木(AST)を構築し、そのASTを操作して整形されたコードを再生成します。これにより、インデント、スペース、改行、括弧の位置など、多くのスタイルに関する問題を自動的に修正します。

  • Goの抽象構文木(AST)と go/ast パッケージ: Goコンパイラや gofmt のようなツールは、Goのソースコードを直接テキストとして扱うのではなく、その構造を表現する抽象構文木(Abstract Syntax Tree, AST)に変換して処理します。ASTは、プログラムの構造を木構造で表現したもので、各ノードがプログラムの特定の構文要素(変数宣言、関数定義、式など)に対応します。 Go言語では、標準ライブラリの go/ast パッケージがASTのデータ構造と操作を提供します。gofmt はこのパッケージを利用して、ソースコードをASTにパースし、ASTを走査・変更することで、整形されたコードを生成します。

  • ast.File: go/ast パッケージにおける ast.File は、Goの単一のソースファイル全体を表すASTのルートノードです。この構造体には、パッケージ名、インポート宣言、そしてファイル内のすべてのトップレベル宣言(変数、定数、型、関数など)のリストが含まれています。このコミットでは、ast.FileDecls フィールド(宣言のリスト)を操作して、空の宣言グループを削除します。

  • ast.GenDecl: go/ast パッケージにおける ast.GenDecl は、Go言語の一般的な宣言(General Declaration)を表すASTノードです。これには、var (変数宣言), const (定数宣言), type (型宣言) などが含まれます。これらの宣言は、単一の要素を宣言することもできますし、括弧 () を使って複数の要素をグループ化して宣言することもできます。このコミットの対象となる「空の宣言グループ」は、この ast.GenDecl の一種で、Specs フィールド(宣言される要素のリスト)が空であるものを指します。

  • 宣言グループ: Go言語では、var, const, type キーワードの後に括弧 () を使用して、複数の変数、定数、または型をまとめて宣言することができます。これを宣言グループと呼びます。 例:

    var (
        x int
        y string
    )
    
    const (
        Pi = 3.14
        E  = 2.71
    )
    
    type (
        MyInt int
        MyString string
    )
    

    このコミットで問題となるのは、これらのグループの中身が空である場合、つまり var (), const (), type () のような形式です。

技術的詳細

このコミットの技術的な核心は、gofmtsrc/cmd/gofmt/simplify.go ファイルに新しい関数 removeEmptyDeclGroups とそのヘルパー関数 isEmpty を追加し、既存の simplify 関数からこれらを呼び出すことで、ASTから空の宣言グループを効率的に削除する点にあります。

  1. simplify 関数への組み込み: simplify.go 内の simplify 関数は、gofmt がASTを整形する際に呼び出される主要な関数の一つです。このコミットでは、simplify 関数の既存の処理の後に、新しく追加された removeEmptyDeclGroups(f) が呼び出されるように変更されました。これにより、ASTの他の簡素化処理が完了した後で、空の宣言グループの削除が行われます。

  2. removeEmptyDeclGroups 関数の実装: この関数は *ast.File 型の引数 f を受け取ります。これは、処理対象のGoソースファイルのAST全体を表します。 関数内部では、f.Decls (ファイル内のトップレベル宣言のリスト) をイテレートします。 各宣言 d について、以下のチェックを行います。

    • g, ok := d.(*ast.GenDecl): 宣言 dast.GenDecl 型(一般的な宣言、つまり var, const, type 宣言)であるかをチェックします。okfalse の場合、それは ast.FuncDecl (関数宣言) など他の種類の宣言であるため、処理の対象外としてそのまま保持します。
    • !isEmpty(f, g): ast.GenDecl であった場合、新しく追加された isEmpty ヘルパー関数を呼び出し、その宣言グループが「空」であるかどうかを判定します。isEmptyfalse(つまり空ではない)を返した場合、その宣言グループは保持されます。 空ではない宣言のみを新しい宣言リストにコピーしていくことで、結果的に空の宣言グループが削除された f.Decls が再構築されます。
  3. isEmpty ヘルパー関数の実装: この関数は *ast.File 型の f*ast.GenDecl 型の g を受け取り、与えられた ast.GenDecl が空であると見なせるかどうかをブール値で返します。 空であると判定するための条件は以下の通りです。

    • g.Doc != nil || g.Specs != nil: 宣言グループにドキュメントコメント (g.Doc) が付いている場合、または宣言される要素 (g.Specs) が存在する場合、それは空ではないと判断されます。Specs が存在しないことが、空の宣言グループの基本的な条件です。
    • コメントの扱い: 宣言グループの内部にコメントが存在する場合、その宣言グループは空とは見なされません。これは、開発者が意図的にコメントを残して、将来の宣言のためのプレースホルダーとして使用している可能性があるためです。isEmpty 関数は、f.Comments (ファイル全体のコメントリスト) を走査し、現在の ast.GenDecl の開始位置 (g.Pos()) と終了位置 (g.End()) の間にコメント (c) が存在するかどうかをチェックします。もし存在すれば、その宣言グループは空ではないと判断されます。

これらの変更により、gofmt は、単に中身がないだけでなく、ドキュメントコメントや内部コメントも持たない真に「空」の宣言グループのみを識別し、削除するようになりました。これにより、開発者の意図を尊重しつつ、コードの冗長性を排除することが可能になります。

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

このコミットにおける主要なコード変更は、src/cmd/gofmt/simplify.go ファイルに集中しています。

src/cmd/gofmt/simplify.go の変更点:

--- a/src/cmd/gofmt/simplify.go
+++ b/src/cmd/gofmt/simplify.go
@@ -117,5 +117,34 @@ func simplify(f *ast.File) {
 		}
 	}
 
+	// remove empty declarations such as "const ()", etc
+	removeEmptyDeclGroups(f)
+
 	ast.Walk(&s, f)
 }
+
+func removeEmptyDeclGroups(f *ast.File) {
+	i := 0
+	for _, d := range f.Decls {
+		if g, ok := d.(*ast.GenDecl); !ok || !isEmpty(f, g) {
+			f.Decls[i] = d
+			i++
+		}
+	}
+	f.Decls = f.Decls[:i]
+}
+
+func isEmpty(f *ast.File, g *ast.GenDecl) bool {
+	if g.Doc != nil || g.Specs != nil {
+		return false
+	}
+
+	for _, c := range f.Comments {
+		// if there is a comment in the declaration, it is not considered empty
+		if g.Pos() <= c.Pos() && c.End() <= g.End() {
+			return false
+		}
+	}
+
+	return true
+}

src/cmd/gofmt/gofmt_test.go の変更点:

新しいテストケース testdata/emptydecl.inputtestdata/emptydecl.golden が追加され、gofmt_test.gotests 変数にそのテストケースが追加されました。

--- a/src/cmd/gofmt/gofmt_test.go
+++ b/src/cmd/gofmt/gofmt_test.go
@@ -87,8 +87,9 @@ var tests = []struct {
 	{"testdata/stdin*.input", ""},
 	{"testdata/comments.input", ""},
 	{"testdata/import.input", ""},
-	{"testdata/crlf.input", ""},       // test case for issue 3961; see also TestCRLF
-	{"testdata/typeswitch.input", ""}, // test case for issue 4470
+	{"testdata/crlf.input", ""},        // test case for issue 3961; see also TestCRLF
+	{"testdata/typeswitch.input", ""},  // test case for issue 4470
+	{"testdata/emptydecl.input", "-s"}, // test case for issue 7631
 }
 
 func TestRewrite(t *testing.T) {

新しいテストデータファイル:

  • src/cmd/gofmt/testdata/emptydecl.input:

    package main
    
    // Keep this declaration
    var ()
    
    const (
    // Keep this declaration
    )
    
    type ()
    
    func main() {}
    
  • src/cmd/gofmt/testdata/emptydecl.golden:

    package main
    
    // Keep this declaration
    var ()
    
    const (
    // Keep this declaration
    )
    
    func main() {}
    

コアとなるコードの解説

このコミットの核となるのは、simplify.go に追加された removeEmptyDeclGroups 関数と isEmpty 関数です。

removeEmptyDeclGroups(f *ast.File) 関数

この関数は、Goソースファイルの抽象構文木(AST)を表現する *ast.File を受け取り、そのファイル内のトップレベル宣言から空の宣言グループを削除します。

  1. 宣言の走査: f.Decls は、ファイル内のすべてのトップレベル宣言(変数、定数、型、関数など)のリストです。この関数は、このリストをループで走査します。
  2. ast.GenDecl の識別: 各宣言 d について、d.(*ast.GenDecl) を使用して、それが var, const, type のいずれかの一般的な宣言グループであるかどうかをチェックします。
    • もし ast.GenDecl でない場合(例: 関数宣言 ast.FuncDecl)、その宣言は空の宣言グループではないため、そのまま保持されます。
    • もし ast.GenDecl であった場合、次に isEmpty 関数を呼び出して、その宣言グループが本当に空であるかどうかを判定します。
  3. 空でない宣言の保持: !isEmpty(f, g) の条件が true(つまり、宣言が ast.GenDecl でないか、または ast.GenDecl であっても空ではない)の場合、その宣言は f.Decls の先頭から順に詰め直されます。変数 i は、保持される宣言の次の書き込み位置を追跡します。
  4. リストのトリミング: ループが終了した後、f.Decls = f.Decls[:i] を実行することで、元の f.Decls リストを、空の宣言グループが削除された新しい(より短い)リストにトリミングします。これにより、ASTから不要なノードが効果的に削除されます。

isEmpty(f *ast.File, g *ast.GenDecl) 関数

このヘルパー関数は、与えられた ast.GenDeclgofmt によって削除されるべき「空」の宣言グループであるかどうかを判定します。

  1. 基本的な空のチェック:

    • g.Doc != nil: 宣言グループにドキュメントコメント(///* */ で始まる宣言直前のコメント)が付いている場合、それは開発者によって意図的に残されたものである可能性が高いため、空とは見なされません。
    • g.Specs != nil: 宣言グループ内に実際の宣言要素(例: var x int)が存在する場合、それは明らかに空ではないため、false を返します。 これらの条件のいずれかが真であれば、関数は直ちに false を返し、その宣言グループは削除されません。
  2. 内部コメントのチェック: g.Docg.Specs もない場合でも、宣言グループの内部にコメントが存在する可能性があります(例: const ( // TODO: Add constants here ))。このようなコメントは、開発者が将来の追加のためにプレースホルダーとして残している可能性があるため、gofmt はこれを削除すべきではありません。

    • f.Comments は、ファイル全体のすべてのコメントのリストです。
    • この関数は f.Comments を走査し、各コメント c の位置が現在の ast.GenDecl g の範囲内(g.Pos() <= c.Pos() && c.End() <= g.End())にあるかどうかをチェックします。
    • もし範囲内にコメントが見つかった場合、その宣言グループは空ではないと判断され、false を返します。
  3. 真に空の判定: 上記のすべてのチェックを通過した場合(つまり、ドキュメントコメントも、宣言要素も、内部コメントも存在しない場合)、その ast.GenDecl は真に空であると判断され、true を返します。これにより、removeEmptyDeclGroups 関数はその宣言を削除します。

これらの関数が連携することで、gofmt はGoコードから冗長な空の宣言グループをインテリジェントに削除し、コードの整形品質を向上させます。

関連リンク

参考にした情報源リンク

  • https://golang.org/cl/101410046 (Go Gerrit Change-IDページ)
  • Go言語公式ドキュメント (gofmt, go/astパッケージに関する一般的な情報)