[インデックス 11576] ファイルの概要
このコミットは、Go言語のドキュメンテーションツールであるgo/doc
パッケージにおけるURLリンクの生成に関するバグ修正です。具体的には、コメント内のURLが正しくHTMLリンクとして認識・変換されない問題を解決しています。
コミット
commit b9474de2bef8469a61b22ba17f18c28ab4c58fd8
Author: Gary Burd <gary@beagledreams.com>
Date: Thu Feb 2 17:02:05 2012 -0800
go/doc: Fix URL linking in ToHTML.
Fixes issue: 2832
R=rsc, gri
CC=golang-dev
https://golang.org/cl/5616060
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b9474de2bef8469a61b22ba17f18c28ab4c58fd8
元コミット内容
go/doc: Fix URL linking in ToHTML.
このコミットは、go/doc
パッケージがコメントをHTMLに変換する際に、URLのリンクが正しく機能しない問題を修正します。
変更の背景
Go言語のドキュメンテーションツールは、ソースコード内のコメントから自動的にドキュメントを生成する機能を持っています。この機能の一部として、コメント内に記述されたURLを自動的にHTMLの<a>
タグに変換し、クリック可能なリンクとして表示する処理が含まれています。
しかし、このコミットが修正する問題は、特定の条件下でURLが正しくリンクとして認識されず、単なるテキストとして表示されてしまうというものでした。これは、go/doc
パッケージがコメント内のパターンを識別する際に使用する正規表現の優先順位に起因していました。具体的には、URLパターンよりも識別子(identifier)パターンが先にマッチしてしまうことで、URLが識別子の一部として誤認識され、リンク化の処理がスキップされてしまっていたと考えられます。
この問題は、GoのIssueトラッカーでIssue 2832として報告されていました。ユーザーが期待するドキュメントの品質を保つため、このURLリンクの不具合を修正する必要がありました。
前提知識の解説
- Go言語のドキュメンテーションツール (
go/doc
): Go言語には、ソースコードのコメントから自動的にドキュメントを生成する標準ツールが用意されています。go doc
コマンドやgodoc
サーバーを通じて利用され、開発者がコードの意図を共有しやすくするために重要な役割を果たします。このツールは、Goのソースファイル内のパッケージ、関数、型、変数などの宣言に付随するコメントを解析し、構造化されたドキュメントを生成します。 - 正規表現 (Regular Expression): テキストの中から特定のパターンに合致する文字列を検索、置換、抽出するための強力なツールです。このコミットでは、コメント内のURLや識別子を識別するために正規表現が使用されています。正規表現エンジンは、定義されたパターンに基づいて入力文字列をスキャンし、マッチする部分を特定します。
regexp.MustCompile
: Go言語のregexp
パッケージで提供される関数で、正規表現文字列をコンパイルして*regexp.Regexp
型のオブジェクトを生成します。このオブジェクトは、その後のマッチング操作に使用されます。MustCompile
は、正規表現のコンパイルに失敗した場合にパニック(実行時エラー)を発生させるため、プログラムの初期化時など、正規表現が常に有効であることが保証される場合に使用されます。- キャプチャグループ (Capture Group): 正規表現において、括弧
()
で囲まれた部分を指します。これにより、マッチした文字列全体だけでなく、その部分文字列も個別に抽出することができます。例えば、(http://.*)
という正規表現では、http://
で始まるURL全体がキャプチャグループとして抽出されます。正規表現のマッチ結果は、通常、マッチした文字列全体と、各キャプチャグループにマッチした部分文字列の開始/終了インデックスの配列として返されます。 - HTMLの
<a>
タグ: HTMLにおいて、ハイパーリンクを定義するために使用される要素です。href
属性にリンク先のURLを指定することで、クリック可能なテキストや画像を作成できます。
技術的詳細
このコミットの核心は、src/pkg/go/doc/comment.go
ファイル内のmatchRx
という正規表現の定義と、その正規表現のマッチ結果を処理するロジックの変更にあります。
元のコードでは、matchRx
は以下のように定義されていました。
var matchRx = regexp.MustCompile(`(` + identRx + `)|(` + urlRx + `)`)
これは、identRx
(識別子をマッチさせる正規表現)とurlRx
(URLをマッチさせる正規表現)のいずれかにマッチするパターンを定義しています。|
は「または」を意味し、正規表現エンジンは左から右に評価を進めます。つまり、元のコードではidentRx
がurlRx
よりも先に評価されていました。
問題は、一部のURLがidentRx
のパターンにも合致してしまう可能性があったことです。例えば、http://example.com/foo
のようなURLは、example.com
やfoo
といった部分が識別子として誤認識される可能性がありました。正規表現エンジンは最初にマッチしたパターンを採用するため、URL全体がurlRx
として認識される前に、その一部がidentRx
としてマッチしてしまい、結果としてURLが正しくリンク化されませんでした。
この問題を解決するため、matchRx
の定義が以下のように変更されました。
var matchRx = regexp.MustCompile(`(` + urlRx + `)|(` + identRx + `)`)
この変更により、urlRx
がidentRx
よりも先に評価されるようになりました。これにより、正規表現エンジンはまずコメント内のURLパターンを優先的に探し、マッチすればそれをURLとして処理するようになります。
さらに、正規表現のマッチ結果を処理するemphasize
関数内のロジックも修正されました。正規表現のキャプチャグループのインデックスは、正規表現内の括弧の順序に依存します。matchRx
の定義が変更されたことで、キャプチャグループのインデックスの意味も変わりました。
元のコードでは、m[2] < 0
という条件で「最初の括弧付きサブ正規表現(identRx
)にマッチしなかった場合、urlRx
にマッチしたに違いない」と判断していました。しかし、urlRx
が最初のサブ正規表現になったため、この条件は逆転する必要があります。
修正後のコードでは、m[2] >= 0
という条件で「最初の括弧付きサブ正規表現(urlRx
)にマッチした場合、urlRx
にマッチしたに違いない」と判断するように変更されました。これにより、正規表現の優先順位の変更と、その結果としてのキャプチャグループのインデックスの変更が正しく反映されました。
また、src/pkg/go/doc/comment_test.go
には、TestEmphasize
という新しいテスト関数とemphasizeTests
というテストケースが追加されました。これらのテストケースは、様々な形式のURLが正しくHTMLリンクとして変換されることを検証します。特に、パスにピリオドが含まれるURLや、括弧で囲まれたURL、スラッシュがエスケープされたURLなど、以前問題を引き起こした可能性のあるエッジケースが網羅されています。これにより、将来的な回帰を防ぎ、URLリンク機能の堅牢性を高めています。
コアとなるコードの変更箇所
src/pkg/go/doc/comment.go
--- a/src/pkg/go/doc/comment.go
+++ b/src/pkg/go/doc/comment.go
@@ -56,7 +56,7 @@ const (
filePart + `([:.,]` + filePart + `)*`
)
-var matchRx = regexp.MustCompile(`(` + identRx + `)|(` + urlRx + `)`)
+var matchRx = regexp.MustCompile(`(` + urlRx + `)|(` + identRx + `)`)
var (
html_a = []byte(`<a href=\"`)
@@ -87,7 +87,7 @@ func emphasize(w io.Writer, line string, words map[string]string, nice bool) {
if m == nil {
break
}
- // m >= 6 (two parenthesized sub-regexps in matchRx, 1st one is identRx)
+ // m >= 6 (two parenthesized sub-regexps in matchRx, 1st one is urlRx)
// write text before match
commentEscape(w, line[0:m[0]], nice)
@@ -99,8 +99,8 @@ func emphasize(w io.Writer, line string, words map[string]string, nice bool) {
if words != nil {
url, italics = words[string(match)]
}
- if m[2] < 0 {
- // didn't match against first parenthesized sub-regexp; must be match against urlRx
+ if m[2] >= 0 {
+ // match against first parenthesized sub-regexp; must be match against urlRx
if !italics {
// no alternative URL in words list, use match instead
url = string(match)
src/pkg/go/doc/comment_test.go
--- a/src/pkg/go/doc/comment_test.go
+++ b/src/pkg/go/doc/comment_test.go
@@ -5,6 +5,7 @@
package doc
import (\n+\t\"bytes\"\n \t\"reflect\"\n \t\"testing\"\n )\n@@ -81,3 +82,28 @@ func TestBlocks(t *testing.T) {\n \t\t}\n \t}\n }\n+\n+var emphasizeTests = []struct {\n+\tin string\n+\tout string\n+}{\n+\t{\"http://www.google.com/\", `<a href=\"http://www.google.com/\">http://www.google.com/</a>`},\n+\t{\"https://www.google.com/\", `<a href=\"https://www.google.com/\">https://www.google.com/</a>`},\n+\t{\"http://www.google.com/path.\", `<a href=\"http://www.google.com/path\">http://www.google.com/path</a>.`},\n+\t{\"(http://www.google.com/)\", `(<a href=\"http://www.google.com/\">http://www.google.com/</a>)`},\n+\t{\"Foo bar http://example.com/ quux!\", `Foo bar <a href=\"http://example.com/\">http://example.com/</a> quux!`},\n+\t{\"Hello http://example.com/%2f/ /world.\", `Hello <a href=\"http://example.com/%2f/\">http://example.com/%2f/</a> /world.`},\n+\t{\"Lorem http: ipsum //host/path\", \"Lorem http: ipsum //host/path\"},\n+\t{\"javascript://is/not/linked\", \"javascript://is/not/linked\"},\n+}\n+\n+func TestEmphasize(t *testing.T) {\n+\tfor i, tt := range emphasizeTests {\n+\t\tvar buf bytes.Buffer\n+\t\temphasize(&buf, tt.in, nil, true)\n+\t\tout := buf.String()\n+\t\tif out != tt.out {\n+\t\t\tt.Errorf(\"#%d: mismatch\\nhave: %v\\nwant: %v\", i, out, tt.out)\n+\t\t}\n+\t}\n+}\n```
## コアとなるコードの解説
### `src/pkg/go/doc/comment.go`
1. **`matchRx`の正規表現の順序変更**:
* 変更前: `regexp.MustCompile(`(` + identRx + `)|(` + urlRx + `)`)`
* 変更後: `regexp.MustCompile(`(` + urlRx + `)|(` + identRx + `)`)`
* この変更が最も重要です。正規表現の`|`演算子は左から右に評価されるため、変更前は識別子パターン(`identRx`)がURLパターン(`urlRx`)よりも優先されていました。これにより、URLの一部が識別子として誤認識される可能性がありました。変更後はURLパターンが優先されるようになり、URLが正しく認識されるようになりました。
2. **キャプチャグループのインデックス判定ロジックの修正**:
* 変更前: `if m[2] < 0 { // didn't match against first parenthesized sub-regexp; must be match against urlRx`
* 変更後: `if m[2] >= 0 { // match against first parenthesized sub-regexp; must be match against urlRx`
* `regexp.MustCompile`で定義された正規表現のキャプチャグループのインデックスは、括弧の出現順序に依存します。`matchRx`の定義で`urlRx`と`identRx`の順序が入れ替わったため、`m[2]`が指すキャプチャグループの意味も変わりました。
* 元のコードでは、`m[2]`は`urlRx`のキャプチャグループに対応していました。`m[2] < 0`は`urlRx`にマッチしなかったことを意味し、その場合は`identRx`にマッチしたと判断していました。
* 変更後、`m[2]`は`identRx`のキャプチャグループに対応するようになりました。したがって、`m[2] >= 0`は`identRx`にマッチしたことを意味します。しかし、コメントの変更内容を見ると、`m[2]`が`urlRx`のキャプチャグループを指すというコメントが残っており、これは混乱を招く可能性があります。実際のロジックは、`m[2]`が`identRx`のキャプチャグループを指し、`m[2] >= 0`が`identRx`にマッチしたことを意味すると解釈できます。そして、その条件が真の場合(つまり`identRx`にマッチした場合)は、URLとして処理しないという意図が読み取れます。
* ただし、このコミットの意図はURLを優先することなので、`m[2]`が`urlRx`のキャプチャグループを指すというコメントは、変更後の`matchRx`の定義と矛盾しています。正しくは、`m[1]`が`urlRx`のキャプチャグループ、`m[2]`が`identRx`のキャプチャグループを指します。したがって、`m[1] >= 0`であればURLにマッチしたことになります。
* このコードの変更は、`m[2]`が`identRx`のキャプチャグループを指すようになったことを前提として、そのマッチング結果に基づいてURLとして処理するかどうかを判断しています。つまり、`m[2] >= 0`(`identRx`にマッチした場合)はURLとして処理しない、というロジックになっています。これは、`urlRx`が優先的にマッチし、もし`urlRx`にマッチしなかった場合に`identRx`にマッチするかどうかをチェックするという、正規表現の順序変更の意図と合致しています。
### `src/pkg/go/doc/comment_test.go`
1. **`emphasizeTests`の追加**:
* 様々な形式のURL文字列と、それらが`emphasize`関数によってHTML変換された場合の期待される出力が定義されています。
* 例: `{"http://www.google.com/", `<a href=\"http://www.google.com/\">http://www.google.com/</a>`}`
* これにより、URLが正しく`<a>`タグに変換されるか、またURLの末尾に句読点がある場合や括弧で囲まれている場合など、エッジケースも適切に処理されるかが検証されます。
* 特に注目すべきは、`"Lorem http: ipsum //host/path"`や`"javascript://is/not/linked"`のように、有効なURLではない文字列がリンク化されないことを確認するテストケースも含まれている点です。これは、過剰なリンク化を防ぐための重要なテストです。
2. **`TestEmphasize`関数の追加**:
* `emphasizeTests`で定義された各テストケースをループで実行し、`emphasize`関数にテスト入力文字列を渡してHTML変換を行います。
* 変換結果が期待される出力と一致するかどうかを`t.Errorf`で検証します。これにより、`emphasize`関数がコメント内のURLを正しくHTMLリンクに変換する機能が、変更後も期待通りに動作することを保証しています。
* `bytes.Buffer`を使用して`emphasize`関数の出力をキャプチャし、文字列として比較しています。
これらの変更により、`go/doc`パッケージはコメント内のURLをより正確に識別し、HTMLドキュメント内で適切なリンクとして表示できるようになりました。
## 関連リンク
* Go言語のドキュメンテーション: [https://go.dev/doc/](https://go.dev/doc/)
* Go言語の`regexp`パッケージ: [https://pkg.go.dev/regexp](https://pkg.go.dev/regexp)
* Go言語の`go/doc`パッケージ: [https://pkg.go.dev/go/doc](https://pkg.go.dev/go/doc)
## 参考にした情報源リンク
* Go言語のソースコード (GitHub): [https://github.com/golang/go](https://github.com/golang/go)
* Go言語のIssueトラッカー (GitHub): [https://github.com/golang/go/issues](https://github.com/golang/go/issues)
* 正規表現の基本: [https://ja.wikipedia.org/wiki/%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE](https://ja.wikipedia.org/wiki/%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE)
* HTML `<a>` タグ: [https://developer.mozilla.org/ja/docs/Web/HTML/Element/a](https://developer.mozilla.org/ja/docs/Web/HTML/Element/a)