[インデックス 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
を付与するロジックを追加することです。
具体的には、以下の点が変更されています。
-
identMode
の導入: 識別子(ast.Ident
)がコード内でどのように使用されているか(使用されているだけか、宣言されているか、特に定数や変数として宣言されているか)を区別するための新しい列挙型identMode
が導入されました。identUse
: 識別子が単に使用されている場合。identDef
: 識別子が宣言されている場合(関数、型、フィールドなど、定数や変数以外)。identVal
: 識別子が定数または変数として宣言されている場合。 このidentMode
は、godoc
がどの識別子にid
を付与すべきかを正確に判断するために使用されます。
-
link
構造体の変更: 既存のlink
構造体が変更され、identMode
型のmode
フィールドと、*ast.Ident
の代わりにstring
型のname
フィールドを持つようになりました。これにより、リンク情報がより汎用的に、かつidentMode
に基づいて処理できるようになります。 -
identModesFor
関数の導入: 以前のdefs
関数(識別子が「定義されている」かどうかを判断する)がidentModesFor
に置き換えられました。この新しい関数は、ASTを走査し、各ast.Ident
がどのidentMode
に該当するかをマッピングとして返します。特に、ast.ValueSpec
(定数や変数の宣言)やast.AssignStmt
(短い変数宣言:=
)の場合にidentVal
モードを割り当てるロロジックが追加されました。 -
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
-
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
が追加されたことで、定数や変数の宣言を特別扱いできるようになりました。 -
link
構造体の変更:type link struct { mode identMode path, name string // package path, identifier name }
以前は
ident *ast.Ident
フィールドを持っていましたが、mode
とname string
に置き換えられました。これにより、ast.Ident
オブジェクトそのものを持つ必要がなくなり、より軽量で柔軟なデータ構造になりました。name
は識別子の文字列名、mode
はその識別子の種類を示します。 -
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
構造体のmode
とname
フィールドを設定するようになりました。特にidentVal
の場合にname
を設定することで、後続の処理でその名前を使ってid
を生成できるようになります。 -
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.ValueSpec
(const
やvar
キーワードによる宣言)やast.AssignStmt
(:=
による短い変数宣言)の場合に、対応する識別子にidentVal
モードを割り当てるロジックが追加されました。これにより、godoc
は定数や変数の宣言箇所を正確に識別できるようになります。 -
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
を付与し、それらへの直接リンクを可能にすることで、ドキュメントのナビゲーション性を大幅に向上させました。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
godoc
コマンドのドキュメント: https://pkg.go.dev/cmd/godocgo/ast
パッケージのドキュメント: https://pkg.go.dev/go/ast
参考にした情報源リンク
- 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など):
- "GoDoc: Godoc: documenting Go code": https://go.dev/blog/godoc
- "GoDoc: Godoc: documenting Go code (Part 2)": https://go.dev/blog/godoc-part2
- "GoDoc: Godoc: documenting Go code (Part 3)": https://go.dev/blog/godoc-part3
(これらのブログ記事は
godoc
の基本的な機能や設計思想を理解するのに役立ちます。)