[インデックス 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.File
のDecls
フィールド(宣言のリスト)を操作して、空の宣言グループを削除します。 -
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 ()
のような形式です。
技術的詳細
このコミットの技術的な核心は、gofmt
の src/cmd/gofmt/simplify.go
ファイルに新しい関数 removeEmptyDeclGroups
とそのヘルパー関数 isEmpty
を追加し、既存の simplify
関数からこれらを呼び出すことで、ASTから空の宣言グループを効率的に削除する点にあります。
-
simplify
関数への組み込み:simplify.go
内のsimplify
関数は、gofmt
がASTを整形する際に呼び出される主要な関数の一つです。このコミットでは、simplify
関数の既存の処理の後に、新しく追加されたremoveEmptyDeclGroups(f)
が呼び出されるように変更されました。これにより、ASTの他の簡素化処理が完了した後で、空の宣言グループの削除が行われます。 -
removeEmptyDeclGroups
関数の実装: この関数は*ast.File
型の引数f
を受け取ります。これは、処理対象のGoソースファイルのAST全体を表します。 関数内部では、f.Decls
(ファイル内のトップレベル宣言のリスト) をイテレートします。 各宣言d
について、以下のチェックを行います。g, ok := d.(*ast.GenDecl)
: 宣言d
がast.GenDecl
型(一般的な宣言、つまりvar
,const
,type
宣言)であるかをチェックします。ok
がfalse
の場合、それはast.FuncDecl
(関数宣言) など他の種類の宣言であるため、処理の対象外としてそのまま保持します。!isEmpty(f, g)
:ast.GenDecl
であった場合、新しく追加されたisEmpty
ヘルパー関数を呼び出し、その宣言グループが「空」であるかどうかを判定します。isEmpty
がfalse
(つまり空ではない)を返した場合、その宣言グループは保持されます。 空ではない宣言のみを新しい宣言リストにコピーしていくことで、結果的に空の宣言グループが削除されたf.Decls
が再構築されます。
-
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.input
と testdata/emptydecl.golden
が追加され、gofmt_test.go
の tests
変数にそのテストケースが追加されました。
--- 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
を受け取り、そのファイル内のトップレベル宣言から空の宣言グループを削除します。
- 宣言の走査:
f.Decls
は、ファイル内のすべてのトップレベル宣言(変数、定数、型、関数など)のリストです。この関数は、このリストをループで走査します。 ast.GenDecl
の識別: 各宣言d
について、d.(*ast.GenDecl)
を使用して、それがvar
,const
,type
のいずれかの一般的な宣言グループであるかどうかをチェックします。- もし
ast.GenDecl
でない場合(例: 関数宣言ast.FuncDecl
)、その宣言は空の宣言グループではないため、そのまま保持されます。 - もし
ast.GenDecl
であった場合、次にisEmpty
関数を呼び出して、その宣言グループが本当に空であるかどうかを判定します。
- もし
- 空でない宣言の保持:
!isEmpty(f, g)
の条件がtrue
(つまり、宣言がast.GenDecl
でないか、またはast.GenDecl
であっても空ではない)の場合、その宣言はf.Decls
の先頭から順に詰め直されます。変数i
は、保持される宣言の次の書き込み位置を追跡します。 - リストのトリミング: ループが終了した後、
f.Decls = f.Decls[:i]
を実行することで、元のf.Decls
リストを、空の宣言グループが削除された新しい(より短い)リストにトリミングします。これにより、ASTから不要なノードが効果的に削除されます。
isEmpty(f *ast.File, g *ast.GenDecl)
関数
このヘルパー関数は、与えられた ast.GenDecl
が gofmt
によって削除されるべき「空」の宣言グループであるかどうかを判定します。
-
基本的な空のチェック:
g.Doc != nil
: 宣言グループにドキュメントコメント(//
や/* */
で始まる宣言直前のコメント)が付いている場合、それは開発者によって意図的に残されたものである可能性が高いため、空とは見なされません。g.Specs != nil
: 宣言グループ内に実際の宣言要素(例:var x int
)が存在する場合、それは明らかに空ではないため、false
を返します。 これらの条件のいずれかが真であれば、関数は直ちにfalse
を返し、その宣言グループは削除されません。
-
内部コメントのチェック:
g.Doc
もg.Specs
もない場合でも、宣言グループの内部にコメントが存在する可能性があります(例:const ( // TODO: Add constants here )
)。このようなコメントは、開発者が将来の追加のためにプレースホルダーとして残している可能性があるため、gofmt
はこれを削除すべきではありません。f.Comments
は、ファイル全体のすべてのコメントのリストです。- この関数は
f.Comments
を走査し、各コメントc
の位置が現在のast.GenDecl
g
の範囲内(g.Pos() <= c.Pos() && c.End() <= g.End()
)にあるかどうかをチェックします。 - もし範囲内にコメントが見つかった場合、その宣言グループは空ではないと判断され、
false
を返します。
-
真に空の判定: 上記のすべてのチェックを通過した場合(つまり、ドキュメントコメントも、宣言要素も、内部コメントも存在しない場合)、その
ast.GenDecl
は真に空であると判断され、true
を返します。これにより、removeEmptyDeclGroups
関数はその宣言を削除します。
これらの関数が連携することで、gofmt
はGoコードから冗長な空の宣言グループをインテリジェントに削除し、コードの整形品質を向上させます。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/138099ae96332d2a5a63888c96001286d7273907
- Go Gerrit Change-ID: https://golang.org/cl/101410046
- 関連するGo Issue: Issue #7631 (この課題は、GoプロジェクトがGitHubに移行する前の内部課題追跡システムで管理されていたものです。そのため、GitHub上では直接参照できませんが、このコミットによって解決されました。)
参考にした情報源リンク
- 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.File
のDecls
フィールド(宣言のリスト)を操作して、空の宣言グループを削除します。 -
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 ()
のような形式です。
技術的詳細
このコミットの技術的な核心は、gofmt
の src/cmd/gofmt/simplify.go
ファイルに新しい関数 removeEmptyDeclGroups
とそのヘルパー関数 isEmpty
を追加し、既存の simplify
関数からこれらを呼び出すことで、ASTから空の宣言グループを効率的に削除する点にあります。
-
simplify
関数への組み込み:simplify.go
内のsimplify
関数は、gofmt
がASTを整形する際に呼び出される主要な関数の一つです。このコミットでは、simplify
関数の既存の処理の後に、新しく追加されたremoveEmptyDeclGroups(f)
が呼び出されるように変更されました。これにより、ASTの他の簡素化処理が完了した後で、空の宣言グループの削除が行われます。 -
removeEmptyDeclGroups
関数の実装: この関数は*ast.File
型の引数f
を受け取ります。これは、処理対象のGoソースファイルのAST全体を表します。 関数内部では、f.Decls
(ファイル内のトップレベル宣言のリスト) をイテレートします。 各宣言d
について、以下のチェックを行います。g, ok := d.(*ast.GenDecl)
: 宣言d
がast.GenDecl
型(一般的な宣言、つまりvar
,const
,type
宣言)であるかをチェックします。ok
がfalse
の場合、それはast.FuncDecl
(関数宣言) など他の種類の宣言であるため、処理の対象外としてそのまま保持します。!isEmpty(f, g)
:ast.GenDecl
であった場合、新しく追加されたisEmpty
ヘルパー関数を呼び出し、その宣言グループが「空」であるかどうかを判定します。isEmpty
がfalse
(つまり空ではない)を返した場合、その宣言グループは保持されます。 空ではない宣言のみを新しい宣言リストにコピーしていくことで、結果的に空の宣言グループが削除されたf.Decls
が再構築されます。
-
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.input
と testdata/emptydecl.golden
が追加され、gofmt_test.go
の tests
変数にそのテストケースが追加されました。
--- 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
を受け取り、そのファイル内のトップレベル宣言から空の宣言グループを削除します。
- 宣言の走査:
f.Decls
は、ファイル内のすべてのトップレベル宣言(変数、定数、型、関数など)のリストです。この関数は、このリストをループで走査します。 ast.GenDecl
の識別: 各宣言d
について、d.(*ast.GenDecl)
を使用して、それがvar
,const
,type
のいずれかの一般的な宣言グループであるかどうかをチェックします。- もし
ast.GenDecl
でない場合(例: 関数宣言ast.FuncDecl
)、その宣言は空の宣言グループではないため、そのまま保持されます。 - もし
ast.GenDecl
であった場合、次にisEmpty
関数を呼び出して、その宣言グループが本当に空であるかどうかを判定します。
- もし
- 空でない宣言の保持:
!isEmpty(f, g)
の条件がtrue
(つまり、宣言がast.GenDecl
でないか、またはast.GenDecl
であっても空ではない)の場合、その宣言はf.Decls
の先頭から順に詰め直されます。変数i
は、保持される宣言の次の書き込み位置を追跡します。 - リストのトリミング: ループが終了した後、
f.Decls = f.Decls[:i]
を実行することで、元のf.Decls
リストを、空の宣言グループが削除された新しい(より短い)リストにトリミングします。これにより、ASTから不要なノードが効果的に削除されます。
isEmpty(f *ast.File, g *ast.GenDecl)
関数
このヘルパー関数は、与えられた ast.GenDecl
が gofmt
によって削除されるべき「空」の宣言グループであるかどうかを判定します。
-
基本的な空のチェック:
g.Doc != nil
: 宣言グループにドキュメントコメント(//
や/* */
で始まる宣言直前のコメント)が付いている場合、それは開発者によって意図的に残されたものである可能性が高いため、空とは見なされません。g.Specs != nil
: 宣言グループ内に実際の宣言要素(例:var x int
)が存在する場合、それは明らかに空ではないため、false
を返します。 これらの条件のいずれかが真であれば、関数は直ちにfalse
を返し、その宣言グループは削除されません。
-
内部コメントのチェック:
g.Doc
もg.Specs
もない場合でも、宣言グループの内部にコメントが存在する可能性があります(例:const ( // TODO: Add constants here )
)。このようなコメントは、開発者が将来の追加のためにプレースホルダーとして残している可能性があるため、gofmt
はこれを削除すべきではありません。f.Comments
は、ファイル全体のすべてのコメントのリストです。- この関数は
f.Comments
を走査し、各コメントc
の位置が現在のast.GenDecl
g
の範囲内(g.Pos() <= c.Pos() && c.End() <= g.End()
)にあるかどうかをチェックします。 - もし範囲内にコメントが見つかった場合、その宣言グループは空ではないと判断され、
false
を返します。
-
真に空の判定: 上記のすべてのチェックを通過した場合(つまり、ドキュメントコメントも、宣言要素も、内部コメントも存在しない場合)、その
ast.GenDecl
は真に空であると判断され、true
を返します。これにより、removeEmptyDeclGroups
関数はその宣言を削除します。
これらの関数が連携することで、gofmt
はGoコードから冗長な空の宣言グループをインテリジェントに削除し、コードの整形品質を向上させます。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/138099ae96332d2a5a63888c96001286d7273907
- Go Gerrit Change-ID: https://golang.org/cl/101410046
- 関連するGo Issue: Issue #7631 (この課題は、GoプロジェクトがGitHubに移行する前の内部課題追跡システムで管理されていたものです。そのため、GitHub上では直接参照できませんが、このコミットによって解決されました。)
参考にした情報源リンク
- https://golang.org/cl/101410046 (Go Gerrit Change-IDページ)
- Go言語公式ドキュメント (gofmt, go/astパッケージに関する一般的な情報)