[インデックス 15953] ファイルの概要
このコミットは、Go言語のドキュメンテーションツールであるgodoc
に、識別子をその宣言元にリンクする機能を追加するものです。これにより、godoc
で生成されたドキュメントを閲覧する際に、コード内の識別子をクリックすることで、その識別子がどこで定義されているか(変数、関数、型など)に直接ジャンプできるようになります。
コミット
commit 7cfebf7b1d0e02663a18225c04ca02f28e4fd6df
Author: Robert Griesemer <gri@golang.org>
Date: Tue Mar 26 11:14:30 2013 -0700
godoc: link identifiers to declarations
The changes are almost completely self-contained
in the new file linkify.go. The other changes are
minimal and should not disturb the currently
working godoc, in anticipation of Go 1.1.
To disable the feature in case of problems, set
-links=false.
Fixes #2063.
R=adg, r
CC=golang-dev
https://golang.org/cl/7883044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7cfebf7b1d0e02663a18225c04ca02f28e4fd6df
元コミット内容
godoc: link identifiers to declarations
The changes are almost completely self-contained
in the new file linkify.go. The other changes are
minimal and should not disturb the currently
working godoc, in anticipation of Go 1.1.
To disable the feature in case of problems, set
-links=false.
Fixes #2063.
変更の背景
この変更の背景には、godoc
が生成するドキュメントのユーザビリティ向上があります。Go言語のコードベースをブラウズする際、特定の識別子(変数名、関数名、型名など)がどこで定義されているかを知ることは、コードの理解に不可欠です。これまでは、手動で検索するか、IDEの機能に頼る必要がありました。このコミットは、godoc
が生成するHTMLドキュメント内で、識別子に自動的にリンクを付与することで、このプロセスを大幅に簡素化し、開発者がより効率的にコードを探索できるようにすることを目的としています。
特に、Go 1.1のリリースを控えていた時期であり、この新機能が既存のgodoc
の動作に悪影響を与えないよう、変更がlinkify.go
という新しいファイルにほぼ完全に自己完結するように設計されています。また、問題が発生した場合に備えて、-links=false
というフラグで機能を無効化できるオプションも提供されています。
Fixes #2063
という記述から、この機能がGitHubのIssue #2063に対応するものであることがわかります。これは、ユーザーからの要望やバグ報告に基づいて機能が追加されたことを示唆しています。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
- Go言語のAST (Abstract Syntax Tree): Go言語のソースコードは、コンパイラによって抽象構文木(AST)にパースされます。ASTは、コードの構造を木構造で表現したもので、プログラムの各要素(識別子、式、文など)がノードとして表現されます。
go/ast
パッケージは、GoのASTを操作するための機能を提供します。 go/token
パッケージ: Go言語の字句解析(トークン化)に関する機能を提供します。ソースコードを個々のトークン(識別子、キーワード、演算子など)に分解する際に使用されます。godoc
ツール: Go言語のソースコードからドキュメントを生成するツールです。コメントやコード構造を解析し、HTML形式などで表示可能なドキュメントを生成します。- HTMLの
<a>
タグ: ハイパーリンクを作成するためのHTML要素です。href
属性にURLを指定することで、クリック可能なリンクを作成します。 - 識別子の解決 (Identifier Resolution): プログラム内で使用されている識別子(変数名、関数名など)が、どの宣言に対応しているかを特定するプロセスです。コンパイラやリンター、IDEなどがこの処理を行います。
godoc
の場合、完全な型情報がないため、限定的な識別子解決が行われます。
技術的詳細
このコミットの主要な技術的詳細は、godoc
がGoソースコードをHTMLに変換する際に、識別子を検出してその宣言へのリンクを挿入するメカニズムにあります。
linkify.go
の導入: 変更の大部分は、新しく追加されたsrc/cmd/godoc/linkify.go
ファイルに集約されています。このファイルには、識別子にリンクを付与するための主要なロジックが含まれています。LinkifyText
関数: この関数が、HTMLエスケープされたソーステキストを受け取り、識別子にリンクを付与して出力する中心的な役割を担います。links(n)
関数を呼び出して、ASTノードn
内の識別子に対応するリンク情報のリストを取得します。tokenSelection(text, token.IDENT)
とtokenSelection(text, token.COMMENT)
を使用して、ソーステキストから識別子とコメントの範囲を特定します。FormatSelections
関数(既存のgodoc
のフォーマット機能の一部)を利用し、識別子の位置に基づいて<a>
タグを挿入します。
link
構造体: リンク情報を保持するための内部構造体です。path
(パッケージパス)とident
(ASTの識別子ノード)を持ちます。links
関数: ASTノードを走査し、各識別子に対して適切なリンク情報を生成します。defs(node)
関数を呼び出して、ノード内で宣言されている識別子のセットを取得します。これにより、宣言されている識別子にはリンクを付与しないようにします(宣言自体がリンク先となるため)。ast.Inspect
を使用してASTを深さ優先で走査します。*ast.Ident
(単一の識別子)の場合、それが宣言ではない(!defs[n]
)かつ、組み込みの識別子(predeclared[n.Name]
)であれば、builtinPkgPath
(おそらくbuiltin
パッケージへのパス)をpath
に設定し、それ以外はローカルな宣言へのリンクとしてident
を設定します。*ast.SelectorExpr
(pkg.Ident
のような修飾識別子)の場合、パッケージのインポートパスを解析し、パッケージ自体へのリンクと、修飾された識別子へのリンクの2つを生成します。
defs
関数: ASTノードを走査し、そのノード内で宣言されているすべての識別子を特定します。これには、フィールド、インポート、変数、型、関数宣言、短い変数宣言(:=
)などが含まれます。predeclared
マップ: Go言語の組み込み識別子(bool
,int
,len
,make
など)を定義したマップです。これらの識別子もリンクの対象となります。src/cmd/godoc/format.go
の変更:commentSelection
関数がtokenSelection
にリファクタリングされ、コメントだけでなく任意のトークンタイプ(この場合はtoken.COMMENT
とtoken.IDENT
)の選択を汎用的に行えるようになりました。
src/cmd/godoc/godoc.go
の変更:declLinks
という新しいブール型フラグが追加され、デフォルトでtrue
に設定されています。これにより、この機能を有効/無効にできます。node_htmlFunc
内で、*declLinks
がtrue
の場合にLinkifyText
関数が呼び出されるようになりました。poorMansImporter
というダミーのインポーター関数が追加されました。これは、完全な型チェックなしにパッケージ識別子を解決するために使用されます。ast.NewPackage
の呼び出しでこのインポーターが使用され、未解決の識別子によるエラーを無視するようになっています。これは、godoc
が完全なコンパイラではないため、限定的な解決しか行えないことの現れです。
この機能は、godoc
が完全な型情報を持たないという制約の中で実装されており、すべてのケースを網羅するわけではないことがコメントで示唆されています(例: BUG(gri): When showing full source text (?m=src), identifier links are incorrect.
)。しかし、一般的なブラウジングには十分な機能を提供します。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下の3つのファイルです。
src/cmd/godoc/format.go
:commentSelection
関数がtokenSelection
に名称変更され、汎用的なトークン選択関数になりました。FormatText
関数内で、コメントの選択にtokenSelection(text, token.COMMENT)
が使用されるようになりました。
src/cmd/godoc/godoc.go
:declLinks
という新しいコマンドラインフラグ(-links
)が追加されました。node_htmlFunc
内で、declLinks
フラグが有効な場合にLinkifyText
関数が呼び出されるロジックが追加されました。poorMansImporter
というヘルパー関数が追加され、getPageInfo
内でast.NewPackage
に渡されるようになりました。
src/cmd/godoc/linkify.go
(新規ファイル):LinkifyText
関数: 識別子にリンクを付与する主要なロジック。link
構造体: リンク情報を保持。links
関数: ASTを走査し、リンク情報を生成。defs
関数: 宣言されている識別子を特定。predeclared
マップ: 組み込み識別子を定義。
コアとなるコードの解説
src/cmd/godoc/format.go
// tokenSelection returns, as a selection, the sequence of
// consecutive occurrences of token sel in the Go src text.
//
func tokenSelection(src []byte, sel token.Token) Selection {
var s scanner.Scanner
fset := token.NewFileSet()
file := fset.AddFile("", fset.Base(), len(src))
s.Init(file, src, nil /* no error handler */, scanner.ScanComments)
var res Selection
for {
pos, tok, lit := s.Scan()
if tok == token.EOF {
break
}
offs := file.Offset(pos)
if tok == sel { // ここで指定されたトークンタイプ(sel)と一致するかをチェック
seg := []int{offs, offs + len(lit)}
res = append(res, seg)
}
}
return res
}
func FormatText(w io.Writer, text []byte, line int, goSource bool, pattern string, selection Selection) {
var comments, highlights Selection
if goSource {
comments = tokenSelection(text, token.COMMENT) // コメントの選択にtokenSelectionを使用
}
if pattern != "" {
highlights = regexpSelection(text, pattern)
}
FormatSelections(w, text, nil, comments, selectionTag, highlights)
}
tokenSelection
関数は、与えられたソーステキストsrc
から、指定されたtoken.Token
型のトークン(例: token.COMMENT
やtoken.IDENT
)の出現箇所をSelection
(開始オフセットと終了オフセットのペアのリスト)として返します。これにより、コメントや識別子などの特定の種類のトークンを効率的に抽出できるようになりました。
src/cmd/godoc/godoc.go
var (
// ... 既存のフラグ ...
declLinks = flag.Bool("links", true, "link identifiers to their declarations") // 新しいフラグ
// ... 既存のフラグ ...
)
func node_htmlFunc(node interface{}, fset *token.FileSet) string {
var buf1 bytes.Buffer
writeNode(&buf1, fset, node)
var buf2 bytes.Buffer
// BUG(gri): When showing full source text (?m=src),
// identifier links are incorrect.
// TODO(gri): Only linkify exported code snippets, not the
// full source text: identifier resolution is
// not sufficiently strong w/o type checking.
// Need to check if info.PAst != nil - requires
// to pass *PageInfo around instead of fset.
if n, _ := node.(ast.Node); n != nil && *declLinks { // declLinksフラグがtrueの場合にLinkifyTextを呼び出す
LinkifyText(&buf2, buf1.Bytes(), n)
} else {
FormatText(&buf2, buf1.Bytes(), -1, true, "", nil)
}
return buf2.String()
}
// poorMansImporter returns a (dummy) package object named
// by the last path component of the provided package path
// (as is the convention for packages). This is sufficient
// to resolve package identifiers without doing an actual
// import. It never returns an error.
//
func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
pkg := imports[path]
if pkg == nil {
// note that strings.LastIndex returns -1 if there is no "/"
pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
imports[path] = pkg
}
return pkg, nil
}
func (h *docServer) getPageInfo(abspath, relpath string, mode PageInfoMode) (info *PageInfo) {
// ... 既存のコード ...
// ignore any errors - they are due to unresolved identifiers
pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil) // poorMansImporterを使用
// ... 既存のコード ...
}
declLinks
フラグは、この新機能の有効/無効を切り替えるためのものです。node_htmlFunc
は、ASTノードをHTMLに変換する際に、このフラグが有効であればLinkifyText
を呼び出し、識別子にリンクを付与します。poorMansImporter
は、godoc
が完全なコンパイラではないため、パッケージのインポートを完全に解決できない場合に、ダミーのパッケージオブジェクトを作成してASTの解析を続行できるようにするためのものです。これにより、未解決の識別子があってもドキュメント生成が中断されないようになります。
src/cmd/godoc/linkify.go
(新規ファイル)
// LinkifyText HTML-escapes source text and writes it to w.
// Identifiers that are in a "use" position (i.e., that are
// not being declared), are wrapped with HTML links pointing
// to the respective declaration, if possible. Comments are
// formatted the same way as with FormatText.
//
func LinkifyText(w io.Writer, text []byte, n ast.Node) {
links := links(n) // ASTからリンク情報を取得
i := 0 // links index
open := false // status of html tag
linkWriter := func(w io.Writer, _ int, start bool) {
// end tag
if !start {
if open {
fmt.Fprintf(w, `</a>`)
open = false
}
return
}
// start tag
open = false
if i < len(links) {
switch info := links[i]; {
case info.path != "" && info.ident == nil:
// package path
fmt.Fprintf(w, `<a href="/pkg/%s/">`, info.path)
open = true
case info.path != "" && info.ident != nil:
// 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
}
i++
}
}
idents := tokenSelection(text, token.IDENT) // 識別子の位置を取得
comments := tokenSelection(text, token.COMMENT) // コメントの位置を取得
FormatSelections(w, text, linkWriter, idents, selectionTag, comments) // リンクを挿入してフォーマット
}
// A link describes the (HTML) link information for an identifier.
// The zero value of a link represents "no link".
//
type link struct {
path string
ident *ast.Ident
}
// links 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) // 宣言されている識別子を取得
// 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] { // 宣言ではない識別子のみを対象
if n.Obj == nil && predeclared[n.Name] {
info.path = builtinPkgPath
}
info.ident = n
}
list = append(list, info)
return false
case *ast.SelectorExpr:
// Detect qualified identifiers of the form pkg.ident.
// ...
// Register two links, one for the package
// and one for the qualified identifier.
// ...
}
return true
})
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)
ast.Inspect(node, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.Field:
for _, n := range n.Names {
m[n] = true
}
case *ast.ImportSpec:
if name := n.Name; name != nil {
m[name] = true
}
case *ast.ValueSpec:
for _, n := range n.Names {
m[n] = true
}
case *ast.TypeSpec:
m[n.Name] = true
case *ast.FuncDecl:
m[n.Name] = true
case *ast.AssignStmt:
if n.Tok == token.DEFINE { // := の場合
for _, x := range n.Lhs {
if n, _ := x.(*ast.Ident); n != nil {
m[n] = true
}
}
}
}
return true
})
return m
}
// The predeclared map represents the set of all predeclared identifiers.
var predeclared = map[string]bool{
// ... 組み込み識別子のリスト ...
}
linkify.go
は、この機能の中核をなすファイルです。LinkifyText
関数は、ソースコードのテキストとASTノードを受け取り、links
関数で生成されたリンク情報に基づいて、識別子をHTMLの<a>
タグで囲みます。links
関数は、ASTを走査し、各識別子が宣言されているか(defs
関数で判定)どうか、組み込み識別子かどうか、修飾識別子かどうかなどを判断し、適切なリンクURLを生成します。defs
関数は、ASTノード内で宣言されているすべての識別子を特定し、それらがリンクの対象とならないようにするために使用されます。predeclared
マップは、Go言語の組み込み識別子を定義しており、これらもリンクの対象となります。
関連リンク
- Go言語のドキュメンテーションツール
godoc
: https://pkg.go.dev/golang.org/x/tools/cmd/godoc - Go言語のASTパッケージ
go/ast
: https://pkg.go.dev/go/ast - Go言語のトークンパッケージ
go/token
: https://pkg.go.dev/go/token
参考にした情報源リンク
- コミットハッシュ:
7cfebf7b1d0e02663a18225c04ca02f28e4fd6df
- GitHubリポジトリ: https://github.com/golang/go
- Go CL 7883044 (変更リスト): https://golang.org/cl/7883044 (ただし、このURLは直接的な内容を提供しない場合があります)
- Issue #2063:
godoc
のIssueトラッカーでこのコミットが解決した問題の詳細が確認できる可能性があります。