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

[インデックス 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つの主要な問題を解決します。

  1. インターフェースメソッドの誤分類の修正: godoc の検索結果において、インターフェースに定義されたメソッドが VarDecl (変数宣言) として誤って分類されていました。これは長らく未解決の課題 (TODO) でした。
  2. パフォーマンスチューニング: 抽象構文木 (AST) の中で、インデックス化の対象とならない部分 (例: コメント、インポートパス) を走査しないようにすることで、インデックス生成の効率を向上させました。

変更の背景

godoc はGo言語のコードからドキュメンテーションを生成し、検索可能な形で提供する非常に重要なツールです。その検索機能の正確性は、開発者がGoの標準ライブラリやサードパーティライブラリのAPIを探索する上で不可欠です。

インターフェースメソッドが変数として誤って分類される問題は、ユーザーが期待する検索結果と異なるため、APIの発見性を損ねる可能性がありました。例えば、io.Reader インターフェースの Read メソッドを検索した際に、それがメソッドとしてではなく変数として表示されると、混乱を招きます。このバグは「長らく未解決のTODO」とされており、修正が望まれていました。

また、godoc が大規模なコードベースをインデックス化する際には、パフォーマンスが重要になります。ASTの全てのノードを無差別に走査することは、不要な処理オーバーヘッドを生じさせます。インデックス化に不要な部分 (例えば、コードの動作には影響しないコメントや、シンボルとして直接検索対象とならないインポートパスなど) をスキップすることで、インデックス生成時間を短縮し、ツールの応答性を向上させることが目的でした。

前提知識の解説

このコミットを理解するためには、以下の概念が重要です。

  1. godoc: Go言語のソースコードからドキュメンテーションを抽出し、HTML形式で表示したり、コマンドラインから検索したりするためのツールです。Goの標準ライブラリのドキュメントは godoc によって生成されています。
  2. 抽象構文木 (Abstract Syntax Tree, AST): プログラムのソースコードの抽象的な構文構造を木構造で表現したものです。Go言語の go/ast パッケージは、GoのソースコードをパースしてASTを構築するための機能を提供します。godoc はこのASTを走査して、型、関数、変数、メソッドなどの情報を抽出し、インデックス化します。
  3. 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 インターフェースを実装したオブジェクトを引数に取り、各ノードを訪問します。
  4. godoc のインデックス生成: godoc はコード内の識別子(名前)とその種類(関数、変数、型など)を関連付けてインデックスを作成します。これにより、ユーザーが特定の名前を検索した際に、その名前がコードのどの部分で、どのような種類のエンティティとして使われているかを素早く特定できます。SpotKind は、このインデックスにおけるエンティティの種類(VarDecl, FuncDecl, MethodDecl, TypeDecl など)を分類するための内部的な列挙型です。

インターフェースのメソッドが VarDecl と誤分類されていたのは、go/ast においてインターフェースのメソッドが ast.Field として表現され、かつ godoc のインデックス生成ロジックが ast.Field を処理する際に、それが構造体のフィールドなのかインターフェースのメソッドなのかを適切に区別せず、一律に VarDecl として扱っていたためと考えられます。

技術的詳細

このコミットは、src/cmd/godoc/index.go ファイル内の Indexer 構造体の Visit メソッドを中心に変更を加えています。Indexerast.Visitor インターフェースを実装しており、GoのASTを走査しながらインデックスを構築します。

主要な変更点は以下の通りです。

  1. visitComment 関数の削除: 以前は visitComment というヘルパー関数があり、コメントグループ (*ast.CommentGroup) を明示的に走査していました。しかし、コメントは通常、インデックス化されるべき識別子を含まないため、この走査は不要なオーバーヘッドでした。この関数が削除され、関連する x.visitComment の呼び出しも全て削除されました。これにより、コメントのASTノードを走査するコストが削減されます。

  2. visitFieldList 関数の導入: 新たに visitFieldList(kind SpotKind, list *ast.FieldList) というヘルパー関数が追加されました。この関数は *ast.FieldList を受け取り、その中の各 ast.Field を処理します。重要なのは、kind パラメータを受け取ることで、この FieldList が表す要素(例えば、構造体のフィールドなのか、インターフェースのメソッドなのか)に応じて、適切な SpotKind を指定して識別子をインデックス化できるようになった点です。

  3. visitSpec 関数の変更: visitSpec 関数のシグネチャが (spec ast.Spec, isVarDecl bool) から (kind SpotKind, spec ast.Spec) に変更されました。これにより、ValueSpec (変数や定数の宣言) を処理する際に、呼び出し元から直接 ConstDecl または VarDeclSpotKind を渡せるようになり、より柔軟かつ正確な分類が可能になりました。 また、ImportSpec の処理において、ast.Walk(x, n.Path) が削除され、インポートパスがインデックス化されないようになりました。これはパフォーマンスチューニングの一環です。

  4. visitGenDecl 関数の導入: visitGenDecl(decl *ast.GenDecl) という新しいヘルパー関数が追加されました。この関数は GenDecl (一般的な宣言、var, const, type など) を処理し、宣言の種類 (token.CONST かどうか) に応じて適切な SpotKind を決定し、visitSpec を呼び出します。これにより、Visit メソッド内の GenDecl 処理ロジックが簡素化され、再利用性が向上しました。

  5. 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走査ロジック、特に IndexerVisit メソッドと、それに付随するヘルパー関数の変更にあります。

  1. インターフェースメソッドの正しい分類: 最も重要な変更は、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 を明示的に指定することで、インターフェースのメソッドが正しく「メソッド宣言」としてインデックス化されるようになりました。

  2. パフォーマンス最適化とコードの整理:

    • visitComment 関数の削除と、Visit メソッド内の x.visitComment 呼び出しの削除は、インデックス化に不要なコメントのASTノード走査を完全にスキップすることで、パフォーマンスを向上させます。
    • visitFieldListvisitGenDecl という新しいヘルパー関数の導入は、Visit メソッドのロジックを簡素化し、コードの可読性と保守性を向上させます。これらの関数は、特定のASTノードタイプ(FieldListGenDecl)の処理をカプセル化し、適切な SpotKind を決定して visitIdentvisitSpec を呼び出す役割を担います。
    • visitSpec のシグネチャ変更 (isVarDecl bool から kind SpotKind) は、よりセマンティックな分類を可能にし、ValueSpec の処理をより正確にします。また、ImportSpec のパスをインデックス化しないようにしたことも、不要な処理を削減するパフォーマンス改善です。

これらの変更は、godoc のインデックス生成の正確性と効率性の両方を向上させ、Go開発者にとってより信頼性の高いドキュメンテーションツールを提供することに貢献しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • go/ast パッケージのソースコード
  • godoc コマンドのソースコード
  • Go言語のASTに関する一般的な解説記事
  • GitHubのコミットページと関連するコードレビュー (CL)