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

[インデックス 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に変換する際に、識別子を検出してその宣言へのリンクを挿入するメカニズムにあります。

  1. linkify.goの導入: 変更の大部分は、新しく追加されたsrc/cmd/godoc/linkify.goファイルに集約されています。このファイルには、識別子にリンクを付与するための主要なロジックが含まれています。
  2. LinkifyText関数: この関数が、HTMLエスケープされたソーステキストを受け取り、識別子にリンクを付与して出力する中心的な役割を担います。
    • links(n)関数を呼び出して、ASTノードn内の識別子に対応するリンク情報のリストを取得します。
    • tokenSelection(text, token.IDENT)tokenSelection(text, token.COMMENT)を使用して、ソーステキストから識別子とコメントの範囲を特定します。
    • FormatSelections関数(既存のgodocのフォーマット機能の一部)を利用し、識別子の位置に基づいて<a>タグを挿入します。
  3. link構造体: リンク情報を保持するための内部構造体です。path(パッケージパス)とident(ASTの識別子ノード)を持ちます。
  4. links関数: ASTノードを走査し、各識別子に対して適切なリンク情報を生成します。
    • defs(node)関数を呼び出して、ノード内で宣言されている識別子のセットを取得します。これにより、宣言されている識別子にはリンクを付与しないようにします(宣言自体がリンク先となるため)。
    • ast.Inspectを使用してASTを深さ優先で走査します。
    • *ast.Ident(単一の識別子)の場合、それが宣言ではない(!defs[n])かつ、組み込みの識別子(predeclared[n.Name])であれば、builtinPkgPath(おそらくbuiltinパッケージへのパス)をpathに設定し、それ以外はローカルな宣言へのリンクとしてidentを設定します。
    • *ast.SelectorExprpkg.Identのような修飾識別子)の場合、パッケージのインポートパスを解析し、パッケージ自体へのリンクと、修飾された識別子へのリンクの2つを生成します。
  5. defs関数: ASTノードを走査し、そのノード内で宣言されているすべての識別子を特定します。これには、フィールド、インポート、変数、型、関数宣言、短い変数宣言(:=)などが含まれます。
  6. predeclaredマップ: Go言語の組み込み識別子(bool, int, len, makeなど)を定義したマップです。これらの識別子もリンクの対象となります。
  7. src/cmd/godoc/format.goの変更:
    • commentSelection関数がtokenSelectionにリファクタリングされ、コメントだけでなく任意のトークンタイプ(この場合はtoken.COMMENTtoken.IDENT)の選択を汎用的に行えるようになりました。
  8. src/cmd/godoc/godoc.goの変更:
    • declLinksという新しいブール型フラグが追加され、デフォルトでtrueに設定されています。これにより、この機能を有効/無効にできます。
    • node_htmlFunc内で、*declLinkstrueの場合にLinkifyText関数が呼び出されるようになりました。
    • poorMansImporterというダミーのインポーター関数が追加されました。これは、完全な型チェックなしにパッケージ識別子を解決するために使用されます。ast.NewPackageの呼び出しでこのインポーターが使用され、未解決の識別子によるエラーを無視するようになっています。これは、godocが完全なコンパイラではないため、限定的な解決しか行えないことの現れです。

この機能は、godocが完全な型情報を持たないという制約の中で実装されており、すべてのケースを網羅するわけではないことがコメントで示唆されています(例: BUG(gri): When showing full source text (?m=src), identifier links are incorrect.)。しかし、一般的なブラウジングには十分な機能を提供します。

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

このコミットにおけるコアとなるコードの変更箇所は以下の3つのファイルです。

  1. src/cmd/godoc/format.go:
    • commentSelection関数がtokenSelectionに名称変更され、汎用的なトークン選択関数になりました。
    • FormatText関数内で、コメントの選択にtokenSelection(text, token.COMMENT)が使用されるようになりました。
  2. src/cmd/godoc/godoc.go:
    • declLinksという新しいコマンドラインフラグ(-links)が追加されました。
    • node_htmlFunc内で、declLinksフラグが有効な場合にLinkifyText関数が呼び出されるロジックが追加されました。
    • poorMansImporterというヘルパー関数が追加され、getPageInfo内でast.NewPackageに渡されるようになりました。
  3. 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.COMMENTtoken.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言語の組み込み識別子を定義しており、これらもリンクの対象となります。

関連リンク

参考にした情報源リンク

  • コミットハッシュ: 7cfebf7b1d0e02663a18225c04ca02f28e4fd6df
  • GitHubリポジトリ: https://github.com/golang/go
  • Go CL 7883044 (変更リスト): https://golang.org/cl/7883044 (ただし、このURLは直接的な内容を提供しない場合があります)
  • Issue #2063: godocのIssueトラッカーでこのコミットが解決した問題の詳細が確認できる可能性があります。