[インデックス 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)