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

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

このコミットは、Go言語の公式ドキュメントツールであるgodocにおいて、定数(constants)と変数(variables)に対してもHTMLのid属性を付与するように変更を加えるものです。これにより、生成されたドキュメント内で定数や変数への直接リンクが可能になり、ドキュメントのナビゲーションとユーザビリティが向上します。

コミット

commit 611e8dbf52a10cd8a4340545e1f6296b671f1fcc
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Mar 27 15:14:28 2013 -0700

    cmd/godoc: emit id's for constants and variables
    
    Fixes #5077.
    
    R=r
    CC=golang-dev
    https://golang.org/cl/8021044

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

https://github.com/golang/go/commit/611e8dbf52a10cd8a4340545e1f6296b671f1fcc

元コミット内容

cmd/godoc: 定数と変数にIDを出力する

Issue #5077 を修正。

変更の背景

Go言語の公式ドキュメント生成ツールであるgodocは、Goのソースコードから自動的にドキュメントを生成し、ウェブブラウザで閲覧可能な形式で提供します。これまでのgodocでは、パッケージ、型、関数などには一意のHTML idが割り当てられ、それらへの直接リンク(アンカーリンク)が可能でした。しかし、定数や変数にはidが割り当てられていなかったため、特定の定数や変数の定義箇所へ直接ジャンプすることができませんでした。

この不便さは、特に大規模なコードベースや、多くの定数・変数が定義されているパッケージのドキュメントを参照する際に顕著でした。ユーザーは目的の定数や変数を探すために、ドキュメント全体をスクロールする必要がありました。

このコミットは、GitHub Issue #5077 で報告されたこの問題を解決するために行われました。目的は、定数や変数にも適切なHTML idを付与し、それらへの直接リンクを可能にすることで、godocが生成するドキュメントのナビゲーション性と利便性を向上させることです。

前提知識の解説

  • godoc: Go言語のソースコードからドキュメントを生成し、HTTPサーバーとして提供するツールです。コード内のコメントや宣言を解析し、自動的にAPIドキュメントを生成します。開発者がコードとドキュメントを同時に管理できる「ドキュメント・ドリブン開発」を促進します。
  • go/astパッケージ: Go言語の抽象構文木(Abstract Syntax Tree, AST)を扱うための標準ライブラリです。Goのソースコードを解析し、その構造をプログラムで操作可能なデータ構造として表現します。godocはこのパッケージを利用してソースコードの構造を理解し、ドキュメントを生成します。
  • ast.Node: go/astパッケージにおけるASTの各要素を表すインターフェースです。関数宣言、型宣言、変数宣言、識別子など、Goコードのあらゆる構成要素がast.Nodeとして表現されます。
  • ast.Ident: 識別子(identifier)を表すASTノードです。変数名、関数名、型名などがこれに該当します。
  • HTML id属性とアンカーリンク: HTML要素に一意の識別子を付与するための属性です。id属性が設定された要素には、#に続けてid名を指定することで、URLから直接その要素にジャンプするアンカーリンクを作成できます。例えば、<h2 id="section1">Section 1</h2>という要素があれば、yourpage.html#section1で直接この見出しに移動できます。
  • 定数と変数: Go言語における定数(const)と変数(var)は、プログラム内で値を保持するための基本的な要素です。定数はコンパイル時に値が確定し変更できないのに対し、変数は実行時に値を変更できます。

技術的詳細

この変更の核心は、godocがASTを走査してドキュメントを生成する際に、定数や変数の宣言を識別し、それらに対応するHTML要素に一意のidを付与するロジックを追加することです。

具体的には、以下の点が変更されています。

  1. identModeの導入: 識別子(ast.Ident)がコード内でどのように使用されているか(使用されているだけか、宣言されているか、特に定数や変数として宣言されているか)を区別するための新しい列挙型identModeが導入されました。

    • identUse: 識別子が単に使用されている場合。
    • identDef: 識別子が宣言されている場合(関数、型、フィールドなど、定数や変数以外)。
    • identVal: 識別子が定数または変数として宣言されている場合。 このidentModeは、godocがどの識別子にidを付与すべきかを正確に判断するために使用されます。
  2. link構造体の変更: 既存のlink構造体が変更され、identMode型のmodeフィールドと、*ast.Identの代わりにstring型のnameフィールドを持つようになりました。これにより、リンク情報がより汎用的に、かつidentModeに基づいて処理できるようになります。

  3. identModesFor関数の導入: 以前のdefs関数(識別子が「定義されている」かどうかを判断する)がidentModesForに置き換えられました。この新しい関数は、ASTを走査し、各ast.IdentがどのidentModeに該当するかをマッピングとして返します。特に、ast.ValueSpec(定数や変数の宣言)やast.AssignStmt(短い変数宣言:=)の場合にidentValモードを割り当てるロロジックが追加されました。

  4. LinkifyText関数の変更: この関数は、テキスト内の識別子をHTMLリンクに変換する主要なロジックを含んでいます。変更点として、identModeに基づいて異なるHTMLタグを生成するようになりました。

    • identValモードの識別子(定数や変数)に対しては、<span id="%s">という形式の<span>タグを生成し、そのid属性に識別子の名前を設定します。これにより、CSSでスタイルを適用したり、JavaScriptで操作したりするだけでなく、アンカーリンクのターゲットとして機能するようになります。
    • それ以外の宣言された識別子(identDefモード)や使用されているだけの識別子(identUseモード)は、引き続き既存の<a>タグ(アンカーリンク)として処理されます。

これらの変更により、godocは定数や変数の宣言箇所を正確に特定し、それらに対応するHTML要素に一意のidを付与できるようになりました。

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

主に src/cmd/godoc/linkify.go ファイルが変更されています。

diff --git a/src/cmd/godoc/linkify.go b/src/cmd/godoc/linkify.go
index 1f976951b5..5b4862419e 100644
--- a/src/cmd/godoc/linkify.go
+++ b/src/cmd/godoc/linkify.go
@@ -25,36 +25,41 @@ import (
 // formatted the same way as with FormatText.
 //
 func LinkifyText(w io.Writer, text []byte, n ast.Node) {
-	links := links(n)
+	links := linksFor(n)
 
-	i := 0        // links index
-	open := false // status of html tag
+	i := 0     // links index
+	prev := "" // prev HTML tag
 	linkWriter := func(w io.Writer, _ int, start bool) {
 		// end tag
 		if !start {
-			if open {
-				fmt.Fprintf(w, `</a>`)
-				open = false
+			if prev != "" {
+				fmt.Fprintf(w, `</%s>`, prev)
+				prev = ""
 			}
 			return
 		}
 
 		// start tag
-		open = false
+		prev = ""
 		if i < len(links) {
 			switch info := links[i]; {
-			case info.path != "" && info.ident == nil:
+			case info.path != "" && info.name == "":
 				// package path
 				fmt.Fprintf(w, `<a href="/pkg/%s/">`, info.path)
-				open = true
-			case info.path != "" && info.ident != nil:
+				prev = "a"
+			case info.path != "" && info.name != "":
 				// qualified identifier
-				fmt.Fprintf(w, `<a href="/pkg/%s/#%s">`, info.path, info.ident.Name)
-				open = true
-			case info.path == "" && info.ident != nil:
-				// locally declared identifier
-				fmt.Fprintf(w, `<a href="#%s">`, info.ident.Name)
-				open = true
+				fmt.Fprintf(w, `<a href="/pkg/%s/#%s">`, info.path, info.name)
+				prev = "a"
+			case info.path == "" && info.name != "":
+				// local identifier
+				if info.mode == identVal {
+					fmt.Fprintf(w, `<span id="%s">`, info.name)
+					prev = "span"
+				} else {
+					fmt.Fprintf(w, `<a href="#%s">`, info.name)
+					prev = "a"
+				}
 			}
 			i++
 		}
@@ -69,27 +74,34 @@ func LinkifyText(w io.Writer, text []byte, n ast.Node) {
 // The zero value of a link represents "no link".
 //
 type link struct {
-	path  string
-	ident *ast.Ident
+	mode       identMode
+	path, name string // package path, identifier name
 }
 
-// links returns the list of links for the identifiers used
+// linksFor returns the list of links for the identifiers used
 // by node in the same order as they appear in the source.
 //
-func links(node ast.Node) (list []link) {
-	defs := defs(node)
+func linksFor(node ast.Node) (list []link) {
+	modes := identModesFor(node)
 
 	// NOTE: We are expecting ast.Inspect to call the
 	//       callback function in source text order.
 	ast.Inspect(node, func(node ast.Node) bool {
 		switch n := node.(type) {
 		case *ast.Ident:
-			info := link{}
-			if !defs[n] {
+			m := modes[n]
+			info := link{mode: m}
+			switch m {
+			case identUse:
 				if n.Obj == nil && predeclared[n.Name] {
 					info.path = builtinPkgPath
 				}
-				info.ident = n
+				info.name = n.Name
+			case identDef:
+				// any declaration expect const or var - empty link
+			case identVal:
+				// const or var declaration
+				info.name = n.Name
 			}
 			list = append(list, info)
 			return false
@@ -107,28 +119,37 @@ func links(node ast.Node) (list []link) {
 							// and one for the qualified identifier.
 							info := link{path: path}
 							list = append(list, info)
-							info.ident = n.Sel
+							info.name = n.Sel.Name
 							list = append(list, info)
 							return false
 						}
 
 
@@ -121,28 +133,37 @@ func links(node ast.Node) (list []link) {
 	return
 }
 
-// defs returns the set of identifiers that are declared ("defined") by node.
-func defs(node ast.Node) map[*ast.Ident]bool {
-	m := make(map[*ast.Ident]bool)
+// The identMode describes how an identifier is "used" at its source location.
+type identMode int
+
+const (
+	identUse identMode = iota // identifier is used (must be zero value for identMode)
+	identDef                  // identifier is defined
+	identVal                  // identifier is defined in a const or var declaration
+)
+
+// identModesFor returns a map providing the identMode for each identifier used by node.
+func identModesFor(node ast.Node) map[*ast.Ident]identMode {
+	m := make(map[*ast.Ident]identMode)
 
 	ast.Inspect(node, func(node ast.Node) bool {
 		switch n := node.(type) {
 		case *ast.Field:
 			for _, n := range n.Names {
-				m[n] = true
+				m[n] = identDef
 			}
 		case *ast.ImportSpec:
 			if name := n.Name; name != nil {
-				m[name] = true
+				m[name] = identDef
 			}
 		case *ast.ValueSpec:
 			for _, n := range n.Names {
-				m[n] = true
+				m[n] = identVal
 			}
 		case *ast.TypeSpec:
-			m[n.Name] = true
+			m[n.Name] = identDef
 		case *ast.FuncDecl:
-			m[n.Name] = true
+			m[n.Name] = identDef
 		case *ast.AssignStmt:
 			// Short variable declarations only show up if we apply
 			// this code to all source code (as opposed to exported
@@ -155,7 +176,7 @@ func defs(node ast.Node) map[*ast.Ident]bool {
 					// Each lhs expression should be an
 					// ident, but we are conservative and check.
 					if n, _ := x.(*ast.Ident); n != nil {
-						m[n] = true
+						m[n] = identVal
 					}
 				}
 			}
@@ -167,6 +188,9 @@ func defs(node ast.Node) map[*ast.Ident]bool {
 }
 
 // The predeclared map represents the set of all predeclared identifiers.
+// TODO(gri) This information is also encoded in similar maps in go/doc,
+//           but not exported. Consider exporting an accessor and using
+//           it instead.
 var predeclared = map[string]bool{
 	"bool":       true,
 	"byte":       true,

また、src/cmd/godoc/main.go にもコメントの修正が1箇所あります。

diff --git a/src/cmd/godoc/main.go b/src/cmd/godoc/main.go
index d61141530e..ab792c8af0 100644
--- a/src/cmd/godoc/main.go
+++ b/src/cmd/godoc/main.go
@@ -411,7 +411,7 @@ func main() {
 		info.PDoc.ImportPath = flag.Arg(0)
 	}
 
-	// If we have more than one argument, use the remaining arguments for filtering
+	// If we have more than one argument, use the remaining arguments for filtering.
 	if flag.NArg() > 1 {
 		args := flag.Args()[1:]
 		rx := makeRx(args)

コアとなるコードの解説

src/cmd/godoc/linkify.go

  1. identMode 型の追加:

    type identMode int
    
    const (
    	identUse identMode = iota // identifier is used (must be zero value for identMode)
    	identDef                  // identifier is defined
    	identVal                  // identifier is defined in a const or var declaration
    )
    

    これは、識別子の「役割」を明確にするための新しい列挙型です。identValが追加されたことで、定数や変数の宣言を特別扱いできるようになりました。

  2. link 構造体の変更:

    type link struct {
    	mode       identMode
    	path, name string // package path, identifier name
    }
    

    以前はident *ast.Identフィールドを持っていましたが、modename stringに置き換えられました。これにより、ast.Identオブジェクトそのものを持つ必要がなくなり、より軽量で柔軟なデータ構造になりました。nameは識別子の文字列名、modeはその識別子の種類を示します。

  3. links から linksFor へのリネームとロジック変更:

    func linksFor(node ast.Node) (list []link) {
    	modes := identModesFor(node) // 新しいidentModesForを呼び出す
    	// ...
    	case *ast.Ident:
    		m := modes[n] // 識別子のモードを取得
    		info := link{mode: m}
    		switch m {
    		case identUse:
    			// ...
    			info.name = n.Name
    		case identDef:
    			// ...
    		case identVal:
    			info.name = n.Name // 定数・変数の場合は名前を設定
    		}
    		list = append(list, info)
    		// ...
    }
    

    この関数は、ASTノードからリンク情報を抽出する役割を担います。identModesForから取得したモード情報に基づいて、link構造体のmodenameフィールドを設定するようになりました。特にidentValの場合にnameを設定することで、後続の処理でその名前を使ってidを生成できるようになります。

  4. defs から identModesFor へのリネームとロジック変更:

    func identModesFor(node ast.Node) map[*ast.Ident]identMode {
    	m := make(map[*ast.Ident]identMode)
    	ast.Inspect(node, func(node ast.Node) bool {
    		switch n := node.(type) {
    		// ...
    		case *ast.ValueSpec: // const, var 宣言
    			for _, n := range n.Names {
    				m[n] = identVal // identVal モードを設定
    			}
    		// ...
    		case *ast.AssignStmt: // 短い変数宣言 (:=)
    			// ...
    			if n, _ := x.(*ast.Ident); n != nil {
    				m[n] = identVal // identVal モードを設定
    			}
    		// ...
    		}
    		return true
    	})
    	return m
    }
    

    この関数は、ASTを走査して各識別子が「定義」されているかどうか、そしてその定義がどのような種類か(特に定数や変数か)を判断します。ast.ValueSpecconstvarキーワードによる宣言)やast.AssignStmt:=による短い変数宣言)の場合に、対応する識別子にidentValモードを割り当てるロジックが追加されました。これにより、godocは定数や変数の宣言箇所を正確に識別できるようになります。

  5. LinkifyText 関数のHTML生成ロジックの変更:

    func LinkifyText(w io.Writer, text []byte, n ast.Node) {
    	// ...
    	linkWriter := func(w io.Writer, _ int, start bool) {
    		// ...
    		if i < len(links) {
    			switch info := links[i]; {
    			// ...
    			case info.path == "" && info.name != "": // ローカル識別子
    				if info.mode == identVal { // 定数または変数の場合
    					fmt.Fprintf(w, `<span id="%s">`, info.name) // spanタグとidを生成
    					prev = "span"
    				} else {
    					fmt.Fprintf(w, `<a href="#%s">`, info.name) // 既存のaタグ
    					prev = "a"
    				}
    			}
    			// ...
    		}
    	}
    	// ...
    }
    

    この部分が、実際にHTMLを生成する際の重要な変更点です。link構造体のmodeフィールドがidentValである場合(つまり、定数または変数である場合)、<a>タグの代わりに<span id="%s">という形式の<span>タグを生成するようになりました。これにより、定数や変数にも一意のidが割り当てられ、アンカーリンクのターゲットとして機能するようになります。<span>タグが選ばれたのは、<a>タグのようにクリック可能なリンクとしてではなく、単に識別子としてマークアップし、idを付与することが目的だからです。

src/cmd/godoc/main.go

  • コメントの修正: // If we have more than one argument, use the remaining arguments for filtering// If we have more than one argument, use the remaining arguments for filtering. に変更されました。これは機能的な変更ではなく、単なるコメントの句読点修正です。

これらの変更により、godocは定数や変数の宣言箇所にHTML idを付与し、それらへの直接リンクを可能にすることで、ドキュメントのナビゲーション性を大幅に向上させました。

関連リンク

参考にした情報源リンク

  • GitHub Issue #5077: https://github.com/golang/go/issues/5077
  • Gerrit Change-Id: I611e8dbf52a10cd8a4340545e1f6296b671f1fcc (コミットメッセージ内の https://golang.org/cl/8021044 に対応するGoのコードレビューシステムへのリンク)
    • GoのGerritシステムは現在GitHubに移行しているため、直接アクセスしてもリダイレクトされる可能性があります。
  • Go言語のソースコード: https://github.com/golang/go
  • Go言語のドキュメント生成に関する一般的な情報 (Go Blogなど):