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

[インデックス 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/parsergo/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.gosrc/pkg/go/doc/comment.goに集中しています。

src/pkg/go/doc/headscan.goの変更

  1. スキャン範囲の拡大:

    • 変更前は、doc.NewPackageDocで取得したPackageDocd.Doc(パッケージ全体のドキュメント)のみを対象としていました。
    • 変更後は、d.Constsd.Typesd.Varsd.Funcsといった各宣言のドキュメントコメント(それぞれのDocフィールド)もappendHeadings関数に渡して処理するようになりました。これにより、パッケージ内のあらゆる種類のコメントから見出しが抽出されるようになり、より包括的な検証が可能になりました。
  2. 出力の改善(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) }というループで、そのパッケージから抽出されたすべての見出しをタブインデントで一覧表示する形式になりました。これにより、どのパッケージのどのコメントから見出しが抽出されたかが一目で分かりやすくなり、出力の冗長性が大幅に削減されました。
  3. 見出しの総数カウント:

    • nheadingsという新しいカウンタ変数が導入されました。
    • appendHeadings関数が返す見出しのリストの長さ(len(list))をnheadingsに加算することで、処理された全ファイルから抽出された見出しの総数を集計します。
    • main関数の最後にfmt.Println(nheadings, "headings found")として、この総数を表示するようになりました。これは、変更前後の結果を比較したり、特定のコード変更が見出し抽出に与える影響を定量的に把握したりするのに非常に役立ちます。
  4. 新しいヘルパー関数 appendHeadings:

    • このコミットで新しく追加されたappendHeadings関数は、コメント文字列をHTMLに変換し、そのHTMLの中から<h3>タグで囲まれた見出しテキストを抽出する役割を担います。
    • 内部ではdoc.ToHTMLを使用してコメントをHTMLに変換し、bytes.Bufferに書き込みます。
    • その後、strings.Indexを使って<h3></h3>タグの位置を検索し、その間のテキストを抽出してリストに追加します。これにより、見出し抽出ロジックがmain関数から分離され、再利用性と可読性が向上しました。
  5. エラーハンドリングの改善:

    • logパッケージの使用がfmtパッケージとos.Stderrへの直接書き込み、およびos.Exit(1)に置き換えられました。これは、Goのツールにおける一般的なエラー報告パターンへの移行です。

src/pkg/go/doc/comment.goの変更

  1. コメントの修正:

    • heading関数内のコメント// allow ' for possessive 's only// allow "'\" for possessive "'s" onlyに修正されました。これは、アポストロフィの扱いに関するコメントの明確化です。
    • comment_test.goでも同様に、テストケースのコメントが修正されています。
  2. 変数名のリファクタリング:

    • ToHTML関数内で使用されていたブール変数lastNonblankWasHeadinglastWasHeadingにリファクタリングされました。これは、変数の意図をより明確にするための変更であり、コードの可読性を向上させます。
  3. 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関数の変更:
    1. スキャン対象の拡大: doc.NewPackageDocで取得したdオブジェクトから、d.Doc(パッケージドキュメント)だけでなく、d.Constsd.Typesd.Varsd.Funcsの各要素のDocフィールドもappendHeadingsに渡すようになりました。これにより、Goソースコード内のあらゆる種類のドキュメントコメントが見出し抽出の対象となり、headscanの検証範囲が大幅に広がりました。
    2. 出力フォーマットの改善: 以前は各見出しが個別にログ出力されていましたが、変更後はif len(list) > 0の条件で、見出しが見つかったパッケージに対してのみ、そのパスとパッケージ名を一度出力し、その下に抽出されたすべての見出しをタブインデントで一覧表示するようになりました。この「パッケージごとの見出しリスト」形式は、出力の視認性を劇的に向上させ、誤検出の特定を容易にします。
    3. 総見出し数のカウント: nheadings変数を導入し、各パッケージから抽出された見出しの数を加算しています。これにより、headscanの実行結果として、プロジェクト全体で見つかった見出しの総数が最後に表示されるようになり、変更前後の比較や、特定の変更が見出し抽出に与える影響を定量的に把握できるようになりました。

src/pkg/go/doc/comment.go

  • lastWasHeading変数へのリファクタリングと修正: ToHTML関数内で、コメントの見出し検出ロジックを制御するブール変数lastNonblankWasHeadinglastWasHeadingに名前が変更されました。これは、変数の役割をより直感的に理解できるようにするためのリファクタリングです。 さらに重要な変更として、<pre>タグで囲まれた整形済みテキストブロックの処理後(w.Write(html_endpre)の直後)にlastWasHeading = falseが追加されました。これは、整形済みテキストブロックの直後に続く行が、誤って見出しとして認識されるのを防ぐための修正です。整形済みテキストブロックは通常、コード例などであり、その内容が見出しとして扱われるべきではないため、このフラグのリセットはgo/docの見出し検出ヒューリスティックの正確性を維持するために不可欠です。

これらの変更により、headscanツールはより強力で使いやすいものとなり、go/docパッケージのコメント解析ロジックの品質向上に貢献しています。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(特にsrc/pkg/go/docディレクトリ)
  • Go言語の公式ドキュメンテーション
  • Go言語のコミット履歴
  • Go言語のコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/5450061 (コミットメッセージに記載)