[インデックス 13154] ファイルの概要
このコミットは、Go言語のドキュメンテーションツールである godoc
のインデックス生成ロジックに対する重要な改善を含んでいます。主に、インターフェースのメソッドが検索結果で誤って変数宣言として表示されるバグを修正し、同時にインデックス生成のパフォーマンスを向上させています。
コミット
commit 016d0d0900bf9447b5ea4ada697ae64597a85daf
Author: Robert Griesemer <gri@golang.org>
Date: Thu May 24 10:56:35 2012 -0700
godoc: correctly categorize interface methods, performance tuning
- interface methods appeared under VarDecl in search results
(long-standing TODO)
- don't walk parts of AST which contain no indexable material
(minor performance tuning)
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6228047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/016d0d0900bf9447b5ea4ada697ae64597a85daf
元コミット内容
このコミットは、godoc
ツールにおける以下の2つの主要な問題を解決します。
- インターフェースメソッドの誤分類の修正:
godoc
の検索結果において、インターフェースに定義されたメソッドがVarDecl
(変数宣言) として誤って分類されていました。これは長らく未解決の課題 (TODO) でした。 - パフォーマンスチューニング: 抽象構文木 (AST) の中で、インデックス化の対象とならない部分 (例: コメント、インポートパス) を走査しないようにすることで、インデックス生成の効率を向上させました。
変更の背景
godoc
はGo言語のコードからドキュメンテーションを生成し、検索可能な形で提供する非常に重要なツールです。その検索機能の正確性は、開発者がGoの標準ライブラリやサードパーティライブラリのAPIを探索する上で不可欠です。
インターフェースメソッドが変数として誤って分類される問題は、ユーザーが期待する検索結果と異なるため、APIの発見性を損ねる可能性がありました。例えば、io.Reader
インターフェースの Read
メソッドを検索した際に、それがメソッドとしてではなく変数として表示されると、混乱を招きます。このバグは「長らく未解決のTODO」とされており、修正が望まれていました。
また、godoc
が大規模なコードベースをインデックス化する際には、パフォーマンスが重要になります。ASTの全てのノードを無差別に走査することは、不要な処理オーバーヘッドを生じさせます。インデックス化に不要な部分 (例えば、コードの動作には影響しないコメントや、シンボルとして直接検索対象とならないインポートパスなど) をスキップすることで、インデックス生成時間を短縮し、ツールの応答性を向上させることが目的でした。
前提知識の解説
このコミットを理解するためには、以下の概念が重要です。
godoc
: Go言語のソースコードからドキュメンテーションを抽出し、HTML形式で表示したり、コマンドラインから検索したりするためのツールです。Goの標準ライブラリのドキュメントはgodoc
によって生成されています。- 抽象構文木 (Abstract Syntax Tree, AST): プログラムのソースコードの抽象的な構文構造を木構造で表現したものです。Go言語の
go/ast
パッケージは、GoのソースコードをパースしてASTを構築するための機能を提供します。godoc
はこのASTを走査して、型、関数、変数、メソッドなどの情報を抽出し、インデックス化します。 go/ast
パッケージの構造:ast.Node
: ASTの全てのノードが実装するインターフェースです。ast.Decl
: 宣言を表すノードのインターフェースです。ast.GenDecl
:var
,const
,type
キーワードによる一般的な宣言(複数行にわたる宣言も含む)を表します。ast.FuncDecl
: 関数宣言またはメソッド宣言を表します。ast.InterfaceType
: インターフェース型を表します。これにはMethods
という*ast.FieldList
が含まれ、インターフェースのメソッドがこのFieldList
内のast.Field
として表現されます。ast.Field
: 構造体のフィールド、関数のパラメータ/結果、そしてインターフェースのメソッドを表すために使用されます。ast.Ident
: 識別子(変数名、関数名、型名など)を表します。ast.Walk
: ASTを再帰的に走査するためのヘルパー関数です。ast.Visitor
インターフェースを実装したオブジェクトを引数に取り、各ノードを訪問します。
godoc
のインデックス生成:godoc
はコード内の識別子(名前)とその種類(関数、変数、型など)を関連付けてインデックスを作成します。これにより、ユーザーが特定の名前を検索した際に、その名前がコードのどの部分で、どのような種類のエンティティとして使われているかを素早く特定できます。SpotKind
は、このインデックスにおけるエンティティの種類(VarDecl
,FuncDecl
,MethodDecl
,TypeDecl
など)を分類するための内部的な列挙型です。
インターフェースのメソッドが VarDecl
と誤分類されていたのは、go/ast
においてインターフェースのメソッドが ast.Field
として表現され、かつ godoc
のインデックス生成ロジックが ast.Field
を処理する際に、それが構造体のフィールドなのかインターフェースのメソッドなのかを適切に区別せず、一律に VarDecl
として扱っていたためと考えられます。
技術的詳細
このコミットは、src/cmd/godoc/index.go
ファイル内の Indexer
構造体の Visit
メソッドを中心に変更を加えています。Indexer
は ast.Visitor
インターフェースを実装しており、GoのASTを走査しながらインデックスを構築します。
主要な変更点は以下の通りです。
-
visitComment
関数の削除: 以前はvisitComment
というヘルパー関数があり、コメントグループ (*ast.CommentGroup
) を明示的に走査していました。しかし、コメントは通常、インデックス化されるべき識別子を含まないため、この走査は不要なオーバーヘッドでした。この関数が削除され、関連するx.visitComment
の呼び出しも全て削除されました。これにより、コメントのASTノードを走査するコストが削減されます。 -
visitFieldList
関数の導入: 新たにvisitFieldList(kind SpotKind, list *ast.FieldList)
というヘルパー関数が追加されました。この関数は*ast.FieldList
を受け取り、その中の各ast.Field
を処理します。重要なのは、kind
パラメータを受け取ることで、このFieldList
が表す要素(例えば、構造体のフィールドなのか、インターフェースのメソッドなのか)に応じて、適切なSpotKind
を指定して識別子をインデックス化できるようになった点です。 -
visitSpec
関数の変更:visitSpec
関数のシグネチャが(spec ast.Spec, isVarDecl bool)
から(kind SpotKind, spec ast.Spec)
に変更されました。これにより、ValueSpec
(変数や定数の宣言) を処理する際に、呼び出し元から直接ConstDecl
またはVarDecl
のSpotKind
を渡せるようになり、より柔軟かつ正確な分類が可能になりました。 また、ImportSpec
の処理において、ast.Walk(x, n.Path)
が削除され、インポートパスがインデックス化されないようになりました。これはパフォーマンスチューニングの一環です。 -
visitGenDecl
関数の導入:visitGenDecl(decl *ast.GenDecl)
という新しいヘルパー関数が追加されました。この関数はGenDecl
(一般的な宣言、var
,const
,type
など) を処理し、宣言の種類 (token.CONST
かどうか) に応じて適切なSpotKind
を決定し、visitSpec
を呼び出します。これにより、Visit
メソッド内のGenDecl
処理ロジックが簡素化され、再利用性が向上しました。 -
Visit
メソッドの変更 (インターフェースメソッドの修正の核心):- 以前存在した
case *ast.Field:
の処理が削除されました。この古いロジックでは、ast.Field
を一律にVarDecl
として処理しており、これがインターフェースメソッドの誤分類の原因でした。 - 新たに
case *ast.InterfaceType:
の処理が追加されました。
この変更がインターフェースメソッドの誤分類を修正する核心です。case *ast.InterfaceType: x.visitFieldList(MethodDecl, n.Methods)
*ast.InterfaceType
が訪問された際に、そのMethods
フィールド (*ast.FieldList
型) を新しいvisitFieldList
関数に渡し、MethodDecl
というSpotKind
を明示的に指定しています。これにより、インターフェースのメソッドが正しくメソッドとしてインデックス化されるようになりました。 *ast.DeclStmt
と*ast.GenDecl
のケースが、新しく導入されたx.visitGenDecl
を呼び出すように変更され、コードが整理されました。*ast.FuncDecl
と*ast.File
のケースから、不要になったx.visitComment(n.Doc)
の呼び出しが削除されました。
- 以前存在した
これらの変更により、godoc
はASTをより効率的に、かつ正確に走査し、特にインターフェースメソッドの分類において正しいインデックス情報を生成できるようになりました。
コアとなるコードの変更箇所
変更は src/cmd/godoc/index.go
ファイルに集中しています。
--- a/src/cmd/godoc/index.go
+++ b/src/cmd/godoc/index.go
@@ -148,7 +148,7 @@ func init() {
// sanity check: if nKinds is too large, the SpotInfo
// accessor functions may need to be updated
if nKinds > 8 {
- panic("nKinds > 8")
+ panic("internal error: nKinds > 8")
}
}
@@ -457,12 +457,6 @@ func (x *Indexer) addSnippet(s *Snippet) int {
return index
}
-func (x *Indexer) visitComment(c *ast.CommentGroup) {
- if c != nil {
- ast.Walk(x, c)
- }
-}
-
func (x *Indexer) visitIdent(kind SpotKind, id *ast.Ident) {
if id != nil {
lists, found := x.words[id.Name]
@@ -486,20 +480,24 @@ func (x *Indexer) visitIdent(kind SpotKind, id *ast.Ident) {
}
}
-func (x *Indexer) visitSpec(spec ast.Spec, isVarDecl bool) {
+func (x *Indexer) visitFieldList(kind SpotKind, list *ast.FieldList) {
+ for _, f := range list.List {
+ x.decl = nil // no snippets for fields
+ for _, name := range f.Names {
+ x.visitIdent(kind, name)
+ }
+ ast.Walk(x, f.Type)
+ // ignore tag - not indexed at the moment
+ }
+}
+
+func (x *Indexer) visitSpec(kind SpotKind, spec ast.Spec) {
switch n := spec.(type) {
case *ast.ImportSpec:
- x.visitComment(n.Doc)
x.visitIdent(ImportDecl, n.Name)
- ast.Walk(x, n.Path)
- x.visitComment(n.Comment)
+ // ignore path - not indexed at the moment
case *ast.ValueSpec:
- x.visitComment(n.Doc)
- kind := ConstDecl
- if isVarDecl {
- kind = VarDecl
- }
for _, n := range n.Names {
x.visitIdent(kind, n)
}
@@ -507,57 +505,51 @@ func (x *Indexer) visitSpec(spec ast.Spec, isVarDecl bool) {
for _, v := range n.Values {
ast.Walk(x, v)
}
- x.visitComment(n.Comment)
case *ast.TypeSpec:
- x.visitComment(n.Doc)
x.visitIdent(TypeDecl, n.Name)
ast.Walk(x, n.Type)
- x.visitComment(n.Comment)
+ }
+}
+
+func (x *Indexer) visitGenDecl(decl *ast.GenDecl) {
+ kind := VarDecl
+ if decl.Tok == token.CONST {
+ kind = ConstDecl
+ }
+ x.decl = decl
+ for _, s := range decl.Specs {
+ x.visitSpec(kind, s)
}
}
func (x *Indexer) Visit(node ast.Node) ast.Visitor {
- // TODO(gri): methods in interface types are categorized as VarDecl
switch n := node.(type) {
case nil:
- return nil
+ // nothing to do
case *ast.Ident:
x.visitIdent(Use, n)
- case *ast.Field:
- x.decl = nil // no snippets for fields
- x.visitComment(n.Doc)
- for _, m := range n.Names {
- x.visitIdent(VarDecl, m)
- }
- ast.Walk(x, n.Type)
- ast.Walk(x, n.Tag)
- x.visitComment(n.Comment)
+ case *ast.FieldList:
+ x.visitFieldList(VarDecl, n)
+
+ case *ast.InterfaceType:
+ x.visitFieldList(MethodDecl, n.Methods)
case *ast.DeclStmt:
- if decl, ok := n.Decl.(*ast.GenDecl); ok {
- // local declarations can only be *ast.GenDecls
- x.decl = nil // no snippets for local declarations
- x.visitComment(decl.Doc)
- for _, s := range decl.Specs {
- x.visitSpec(s, decl.Tok == token.VAR)
- }
- } else {
- // handle error case gracefully
- ast.Walk(x, n.Decl)
+ // local declarations should only be *ast.GenDecls;
+ // ignore incorrect ASTs
+ if decl, ok := n.Decl.(*ast.GenDecl); ok {
+ x.decl = nil // no snippets for local declarations
+ x.visitGenDecl(decl)
}
case *ast.GenDecl:
x.decl = n
- x.visitComment(n.Doc)
- for _, s := range n.Specs {
- x.visitSpec(s, n.Tok == token.VAR)
- }
+ x.visitGenDecl(n)
case *ast.FuncDecl:
- x.visitComment(n.Doc)
kind := FuncDecl
if n.Recv != nil {
kind = MethodDecl
@@ -571,15 +563,11 @@ func (x *Indexer) Visit(node ast.Node) ast.Visitor {
}
case *ast.File:
- x.visitComment(n.Doc)
x.decl = nil
x.visitIdent(PackageClause, n.Name)
for _, d := range n.Decls {
ast.Walk(x, d)
}
- // don't visit package level comments for now
- // to avoid duplicate visiting from individual
- // nodes
default:
return x
@@ -622,7 +610,7 @@ func (x *Indexer) addFile(filename string, goFile bool) (file *token.File, ast *
// the file set implementation changed or we have another error.
base := x.fset.Base()
if x.sources.Len() != base {
- panic("internal error - file base incorrect")
+ panic("internal error: file base incorrect")
}
// append file contents (src) to x.sources
コアとなるコードの解説
このコミットの核心は、godoc
のAST走査ロジック、特に Indexer
の Visit
メソッドと、それに付随するヘルパー関数の変更にあります。
-
インターフェースメソッドの正しい分類: 最も重要な変更は、
Visit
メソッド内で*ast.InterfaceType
を処理する新しいcase
の追加です。case *ast.InterfaceType: x.visitFieldList(MethodDecl, n.Methods)
以前は、インターフェースのメソッドも
ast.Field
として扱われ、そのast.Field
を処理する汎用的なロジックがVarDecl
と分類していました。この変更により、*ast.InterfaceType
が検出された際に、そのMethods
フィールド(これは*ast.FieldList
型)をvisitFieldList
関数に渡し、MethodDecl
というSpotKind
を明示的に指定することで、インターフェースのメソッドが正しく「メソッド宣言」としてインデックス化されるようになりました。 -
パフォーマンス最適化とコードの整理:
visitComment
関数の削除と、Visit
メソッド内のx.visitComment
呼び出しの削除は、インデックス化に不要なコメントのASTノード走査を完全にスキップすることで、パフォーマンスを向上させます。visitFieldList
とvisitGenDecl
という新しいヘルパー関数の導入は、Visit
メソッドのロジックを簡素化し、コードの可読性と保守性を向上させます。これらの関数は、特定のASTノードタイプ(FieldList
やGenDecl
)の処理をカプセル化し、適切なSpotKind
を決定してvisitIdent
やvisitSpec
を呼び出す役割を担います。visitSpec
のシグネチャ変更 (isVarDecl bool
からkind SpotKind
) は、よりセマンティックな分類を可能にし、ValueSpec
の処理をより正確にします。また、ImportSpec
のパスをインデックス化しないようにしたことも、不要な処理を削減するパフォーマンス改善です。
これらの変更は、godoc
のインデックス生成の正確性と効率性の両方を向上させ、Go開発者にとってより信頼性の高いドキュメンテーションツールを提供することに貢献しています。
関連リンク
- Go言語の
go/ast
パッケージ: https://pkg.go.dev/go/ast - Go言語の
godoc
コマンド: https://pkg.go.dev/cmd/godoc
参考にした情報源リンク
- Go言語の公式ドキュメント
go/ast
パッケージのソースコードgodoc
コマンドのソースコード- Go言語のASTに関する一般的な解説記事
- GitHubのコミットページと関連するコードレビュー (CL)