[インデックス 10576] ファイルの概要
このコミットは、Go言語のドキュメンテーションツールの一部である go/doc
パッケージ内の headscan
コマンドの機能改善を目的としています。具体的には、コメントから見出しを抽出する際の走査範囲をパッケージドキュメントだけでなく、定数、型、変数、関数のコメントにまで拡大し、出力の可読性を向上させ、抽出された見出しの総数をカウントする機能を追加しています。これにより、go/doc
パッケージのコメント解析ヒューリスティックにおける誤検出(false positives)をより効率的に特定し、デバッグできるようになります。
コミット
commit bc9ce6a129af4b99ec63810e61166e2b98285823
Author: Robert Griesemer <gri@golang.org>
Date: Thu Dec 1 11:50:15 2011 -0800
go/doc: better headscan
- scan all comments not just the package documentation
- declutter output so that false positives are more easily spotted
- count the number of headings to quickly see differences
- minor tweaks
R=golang-dev, r, r
CC=golang-dev
https://golang.org/cl/5450061
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bc9ce6a129af4b99ec63810e61166e2b98285823
元コミット内容
go/doc: better headscan
- scan all comments not just the package documentation
- declutter output so that false positives are more easily spotted
- count the number of headings to quickly see differences
- minor tweaks
変更の背景
Go言語のドキュメンテーションシステムは、ソースコード内のコメントから自動的にドキュメントを生成します。このシステムは、特定のパターン(例えば、空行で囲まれた非インデント行)を解析して見出し(HTMLの<h3>
タグ)として認識するヒューリスティックを使用しています。しかし、これらのヒューリスティックは完璧ではなく、意図しない行を見出しとして誤検出(false positive)したり、逆に本来見出しであるべき行を見逃したりする可能性があります。
headscan
コマンドは、このような見出し抽出の正確性を検証するために開発された内部ツールです。しかし、このコミット以前のheadscan
は、パッケージ全体のドキュメントコメント(d.Doc
)のみをスキャン対象としていました。Goのソースコードには、パッケージレベルのドキュメントだけでなく、定数、型、変数、関数といった個々の宣言にもドキュメントコメントが付与されます。これらのコメント内の見出し抽出が正しく行われているかを確認するためには、headscan
のスキャン範囲を拡大する必要がありました。
また、既存のheadscan
の出力は、見出しが検出されるたびにログメッセージを出力する形式であり、多数の見出しが検出されると非常に冗長で、誤検出を視覚的に特定するのが困難でした。そのため、より整理された出力形式が求められていました。さらに、変更前後の見出し抽出結果を定量的に比較するために、抽出された見出しの総数をカウントする機能も有用であると考えられました。
これらの課題に対処するため、headscan
コマンドの機能強化がこのコミットで行われました。
前提知識の解説
Go言語のドキュメンテーションシステム
Go言語は、ソースコードに記述されたコメントから自動的にドキュメントを生成する仕組みを持っています。これはgo doc
コマンドやgodoc
ツールによって利用されます。
- パッケージドキュメント: パッケージ宣言の直前に記述されたコメントは、そのパッケージ全体のドキュメントとなります。
- 宣言のドキュメント: 定数、変数、型、関数の宣言の直前に記述されたコメントは、それぞれの宣言のドキュメントとなります。
- 見出しの自動認識:
go/doc
パッケージは、コメント内の特定の書式(例えば、空行で囲まれた非インデント行)を解析し、HTMLの<h3>
タグとして見出しを自動的に生成するヒューリスティックを持っています。これは、長いドキュメントの構造化に役立ちます。
go/doc
パッケージ
go/doc
パッケージは、Goのソースコードからドキュメンテーションを抽出・整形するための標準ライブラリです。
doc.NewPackageDoc(pkg, path)
:go/parser
で解析されたパッケージ情報(pkg
)とファイルパス(path
)を受け取り、そのパッケージのドキュメント構造を表すPackageDoc
を生成します。PackageDoc
構造体には、パッケージ全体のドキュメント(Doc
フィールド)のほか、パッケージ内の定数(Consts
)、型(Types
)、変数(Vars
)、関数(Funcs
)といった各宣言のドキュメント情報が含まれています。それぞれの宣言情報もDoc
フィールドを持ち、個別のドキュメントコメントを保持しています。doc.ToHTML(w io.Writer, s []byte, words map[string]string)
: バイトスライスs
で与えられたコメントテキストをHTML形式に変換し、w
に書き出す関数です。この関数が、前述の見出し自動認識ヒューリスティックを適用し、<h3>
タグを挿入します。
go/parser
とgo/token
パッケージ
これらはGoのソースコードを解析するための基本的なパッケージです。
go/token.FileSet
: ソースファイルの位置情報を管理するためのオブジェクトです。go/parser.ParseDir
: 指定されたディレクトリ内のGoソースファイルを解析し、パッケージ情報を返します。parser.ParseComments
フラグを指定することで、コメントもAST(抽象構文木)に含めて解析させることができます。
headscan
コマンド
headscan
は、go/doc
パッケージの内部的なテスト・デバッグツールとして機能します。その主な目的は、go/doc
が見出しを正しく抽出しているか、特に誤検出がないかを確認することです。このツールは、Goのソースツリーを走査し、各ファイルのコメントからgo/doc
が生成する見出しを抽出し、その結果を表示します。
技術的詳細
このコミットにおける主要な技術的変更点は、src/pkg/go/doc/headscan.go
とsrc/pkg/go/doc/comment.go
に集中しています。
src/pkg/go/doc/headscan.go
の変更
-
スキャン範囲の拡大:
- 変更前は、
doc.NewPackageDoc
で取得したPackageDoc
のd.Doc
(パッケージ全体のドキュメント)のみを対象としていました。 - 変更後は、
d.Consts
、d.Types
、d.Vars
、d.Funcs
といった各宣言のドキュメントコメント(それぞれのDoc
フィールド)もappendHeadings
関数に渡して処理するようになりました。これにより、パッケージ内のあらゆる種類のコメントから見出しが抽出されるようになり、より包括的な検証が可能になりました。
- 変更前は、
-
出力の改善(Decluttering):
- 変更前は、見出しが検出されるたびに
log.Printf("%s: %s", path, line)
のように個別のログメッセージが出力されていました。 - 変更後は、
fmt.Printf("%s (package %s)\\n", path, pkg.Name)
でパッケージのパスと名前を一度だけ出力し、その下にfor _, h := range list { fmt.Printf("\\t%s\\n", h) }
というループで、そのパッケージから抽出されたすべての見出しをタブインデントで一覧表示する形式になりました。これにより、どのパッケージのどのコメントから見出しが抽出されたかが一目で分かりやすくなり、出力の冗長性が大幅に削減されました。
- 変更前は、見出しが検出されるたびに
-
見出しの総数カウント:
nheadings
という新しいカウンタ変数が導入されました。appendHeadings
関数が返す見出しのリストの長さ(len(list)
)をnheadings
に加算することで、処理された全ファイルから抽出された見出しの総数を集計します。main
関数の最後にfmt.Println(nheadings, "headings found")
として、この総数を表示するようになりました。これは、変更前後の結果を比較したり、特定のコード変更が見出し抽出に与える影響を定量的に把握したりするのに非常に役立ちます。
-
新しいヘルパー関数
appendHeadings
:- このコミットで新しく追加された
appendHeadings
関数は、コメント文字列をHTMLに変換し、そのHTMLの中から<h3>
タグで囲まれた見出しテキストを抽出する役割を担います。 - 内部では
doc.ToHTML
を使用してコメントをHTMLに変換し、bytes.Buffer
に書き込みます。 - その後、
strings.Index
を使って<h3>
と</h3>
タグの位置を検索し、その間のテキストを抽出してリストに追加します。これにより、見出し抽出ロジックがmain
関数から分離され、再利用性と可読性が向上しました。
- このコミットで新しく追加された
-
エラーハンドリングの改善:
log
パッケージの使用がfmt
パッケージとos.Stderr
への直接書き込み、およびos.Exit(1)
に置き換えられました。これは、Goのツールにおける一般的なエラー報告パターンへの移行です。
src/pkg/go/doc/comment.go
の変更
-
コメントの修正:
heading
関数内のコメント// allow ' for possessive 's only
が// allow "'\" for possessive "'s" only
に修正されました。これは、アポストロフィの扱いに関するコメントの明確化です。comment_test.go
でも同様に、テストケースのコメントが修正されています。
-
変数名のリファクタリング:
ToHTML
関数内で使用されていたブール変数lastNonblankWasHeading
がlastWasHeading
にリファクタリングされました。これは、変数の意図をより明確にするための変更であり、コードの可読性を向上させます。
-
lastWasHeading
フラグのリセットロジックの追加:ToHTML
関数内で、<pre>
ブロック(整形済みテキストブロック)の終了タグhtml_endpre
が書き込まれた直後にlastWasHeading = false
が追加されました。これは、整形済みテキストブロックの後に続く行が見出しとして誤って認識されるのを防ぐための重要な修正です。整形済みテキストブロックは通常、コード例などであり、その中の行が見出しとして扱われるべきではないため、このフラグのリセットは見出し検出ヒューリスティックの正確性を保つ上で不可欠です。
コアとなるコードの変更箇所
src/pkg/go/doc/headscan.go
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
The headscan command extracts comment headings from package files;
it is used to detect false positives which may require an adjustment
to the comment formatting heuristics in comment.go.
Usage: headscan [-root root_directory]
By default, the $GOROOT/src directory is scanned.
*/
package main
import (
"bytes"
"flag"
"fmt" // 追加
"go/doc"
"go/parser"
"go/token"
// "log" // 削除
"os"
"path/filepath"
"runtime" // 追加
"strings"
)
var (
root = flag.String("root", filepath.Join(runtime.GOROOT(), "src"), "root of filesystem tree to scan") // 変更
verbose = flag.Bool("v", false, "verbose mode") // 追加
)
const (
html_h = "<h3>" // 追加
html_endh = "</h3>\n" // 追加
)
// ... isGoFile 関数は変更なし ...
// appendHeadings 関数を新規追加
func appendHeadings(list []string, comment string) []string {
var buf bytes.Buffer
doc.ToHTML(&buf, []byte(comment), nil)
for s := buf.String(); ; {
i := strings.Index(s, html_h)
if i < 0 {
break
}
i += len(html_h)
j := strings.Index(s, html_endh)
if j < 0 {
list = append(list, s[i:]) // incorrect HTML
break
}
list = append(list, s[i:j])
s = s[j+len(html_endh):]
}
return list
}
func main() {
// fset := token.NewFileSet() // 変更なし
// rootDir := flag.String("root", "./", "root of filesystem tree to scan") // 削除
flag.Parse()
fset := token.NewFileSet() // 変更なし
nheadings := 0 // 追加
// err := filepath.Walk(*rootDir, func(path string, fi os.FileInfo, err error) error { // 削除
err := filepath.Walk(*root, func(path string, fi os.FileInfo, err error) error { // 変更
if !fi.IsDir() {
return nil
}
pkgs, err := parser.ParseDir(fset, path, isGoFile, parser.ParseComments)
if err != nil {
// log.Println(path, err) // 削除
if *verbose { // 追加
fmt.Fprintln(os.Stderr, err) // 変更
}
return nil
}
for _, pkg := range pkgs {
d := doc.NewPackageDoc(pkg, path)
// buf := new(bytes.Buffer) // 削除
// doc.ToHTML(buf, []byte(d.Doc), nil) // 削除
// b := buf.Bytes() // 削除
// for { // 削除
// i := bytes.Index(b, []byte("<h3>")) // 削除
// if i == -1 { // 削除
// break // 削除
// } // 削除
// line := bytes.SplitN(b[i:], []byte("\n"), 2)[0] // 削除
// log.Printf("%s: %s", path, line) // 削除
// b = b[i+len(line):] // 削除
// } // 削除
list := appendHeadings(nil, d.Doc) // 変更: パッケージドキュメントをスキャン
for _, d := range d.Consts { // 追加: 定数コメントをスキャン
list = appendHeadings(list, d.Doc)
}
for _, d := range d.Types { // 追加: 型コメントをスキャン
list = appendHeadings(list, d.Doc)
}
for _, d := range d.Vars { // 追加: 変数コメントをスキャン
list = appendHeadings(list, d.Doc)
}
for _, d := range d.Funcs { // 追加: 関数コメントをスキャン
list = appendHeadings(list, d.Doc)
}
if len(list) > 0 { // 追加: 見出しが見つかった場合のみ出力
// directories may contain multiple packages;
// print path and package name
fmt.Printf("%s (package %s)\n", path, pkg.Name) // 追加: パッケージ情報出力
for _, h := range list { // 追加: 見出しを一覧表示
fmt.Printf("\t%s\n", h)
}
nheadings += len(list) // 追加: 見出し数をカウント
}
}
return nil
})
if err != nil {
// log.Fatal(err) // 削除
fmt.Fprintln(os.Stderr, err) // 変更
os.Exit(1) // 追加
}
fmt.Println(nheadings, "headings found") // 追加: 総見出し数を出力
}
src/pkg/go/doc/comment.go
// ... heading 関数の一部変更 ...
func heading(line []byte) []byte {
// ...
// allow ' for possessive 's only // 削除
// b := line // 削除
// for { // 削除
// allow "'" for possessive "'s" only // 追加
for b := line; ; { // 変更
i := bytes.IndexRune(b, '\'')
// ...
}
// ...
}
func ToHTML(w io.Writer, s []byte, words map[string]string) {
inpara := false
lastWasBlank := false
// lastNonblankWasHeading := false // 削除
lastWasHeading := false // 変更
// ... close 関数は変更なし ...
for i := 0; i < len(lines); {
// ...
if isPre(lines[i]) {
// ...
w.Write(html_endpre)
lastWasHeading = false // 追加: <pre>ブロック後にフラグをリセット
continue
}
// if lastWasBlank && !lastNonblankWasHeading && i+2 < len(lines) && // 削除
if lastWasBlank && !lastWasHeading && i+2 < len(lines) && // 変更
isBlank(lines[i+1]) && !isBlank(lines[i+2]) && indentLen(lines[i+2]) == 0 {
// ...
if head := heading(lines[i]); head != nil {
// ...
w.Write(html_endh)
i += 2
// lastNonblankWasHeading = true // 削除
lastWasHeading = true // 変更
continue
}
}
// open paragraph
open()
lastWasBlank = false
// lastNonblankWasHeading = false // 削除
lastWasHeading = false // 変更
emphasize(w, lines[i], words, true) // nice text formatting
i++
}
// ...
}
コアとなるコードの解説
src/pkg/go/doc/headscan.go
appendHeadings
関数の導入: この関数は、go/doc.ToHTML
を使ってコメントをHTMLに変換し、そのHTML文字列から<h3>
タグで囲まれた見出しを抽出するロジックをカプセル化しています。これにより、main
関数内の見出し抽出ロジックが大幅に簡素化され、コードの再利用性が高まりました。main
関数の変更:- スキャン対象の拡大:
doc.NewPackageDoc
で取得したd
オブジェクトから、d.Doc
(パッケージドキュメント)だけでなく、d.Consts
、d.Types
、d.Vars
、d.Funcs
の各要素のDoc
フィールドもappendHeadings
に渡すようになりました。これにより、Goソースコード内のあらゆる種類のドキュメントコメントが見出し抽出の対象となり、headscan
の検証範囲が大幅に広がりました。 - 出力フォーマットの改善: 以前は各見出しが個別にログ出力されていましたが、変更後は
if len(list) > 0
の条件で、見出しが見つかったパッケージに対してのみ、そのパスとパッケージ名を一度出力し、その下に抽出されたすべての見出しをタブインデントで一覧表示するようになりました。この「パッケージごとの見出しリスト」形式は、出力の視認性を劇的に向上させ、誤検出の特定を容易にします。 - 総見出し数のカウント:
nheadings
変数を導入し、各パッケージから抽出された見出しの数を加算しています。これにより、headscan
の実行結果として、プロジェクト全体で見つかった見出しの総数が最後に表示されるようになり、変更前後の比較や、特定の変更が見出し抽出に与える影響を定量的に把握できるようになりました。
- スキャン対象の拡大:
src/pkg/go/doc/comment.go
lastWasHeading
変数へのリファクタリングと修正:ToHTML
関数内で、コメントの見出し検出ロジックを制御するブール変数lastNonblankWasHeading
がlastWasHeading
に名前が変更されました。これは、変数の役割をより直感的に理解できるようにするためのリファクタリングです。 さらに重要な変更として、<pre>
タグで囲まれた整形済みテキストブロックの処理後(w.Write(html_endpre)
の直後)にlastWasHeading = false
が追加されました。これは、整形済みテキストブロックの直後に続く行が、誤って見出しとして認識されるのを防ぐための修正です。整形済みテキストブロックは通常、コード例などであり、その内容が見出しとして扱われるべきではないため、このフラグのリセットはgo/doc
の見出し検出ヒューリスティックの正確性を維持するために不可欠です。
これらの変更により、headscan
ツールはより強力で使いやすいものとなり、go/doc
パッケージのコメント解析ロジックの品質向上に貢献しています。
関連リンク
- Go言語のドキュメンテーション: https://go.dev/doc/effective_go#commentary
go/doc
パッケージのドキュメント: https://pkg.go.dev/go/docgo/parser
パッケージのドキュメント: https://pkg.go.dev/go/parsergo/token
パッケージのドキュメント: https://pkg.go.dev/go/token
参考にした情報源リンク
- Go言語のソースコード(特に
src/pkg/go/doc
ディレクトリ) - Go言語の公式ドキュメンテーション
- Go言語のコミット履歴
- Go言語のコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/5450061 (コミットメッセージに記載)