[インデックス 11595] ファイルの概要
このコミットは、Go言語のドキュメンテーションツールであるgodoc
における識別子検索のバグを修正するものです。具体的には、go/scanner
パッケージの変更(CL 5528077)によって、セミコロン挿入の挙動が変わったことが原因で、godoc
の識別子判定ロジックが誤動作するようになった問題に対処しています。
コミット
commit f6f5ce87cdaad3ca4805f6a16bba3b6851fddf2d
Author: Robert Griesemer <gri@golang.org>
Date: Fri Feb 3 09:20:53 2012 -0800
godoc: fix identifier search
Thanks to Andrey Mirtchovski for tracking this down.
This was broken by CL 5528077 which removed the InsertSemis
flag from go/scanner - as a result, semicolons are now always
inserted and the respective indexer code checked for the
wrong token.
Replaced the code by a direct identifier test.
R=rsc
CC=golang-dev
https://golang.org/cl/5606065
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f6f5ce87cdaad3ca4805f6a16bba3b6851fddf2d
元コミット内容
godoc: fix identifier search
Thanks to Andrey Mirtchovski for tracking this down.
This was broken by CL 5528077 which removed the InsertSemis
flag from go/scanner - as a result, semicolons are now always
inserted and the respective indexer code checked for the
wrong token.
Replaced the code by a direct identifier test.
R=rsc
CC=golang-dev
https://golang.org/cl/5606065
変更の背景
この変更は、Go言語のgodoc
ツールにおける識別子検索機能の不具合を修正するために行われました。不具合の原因は、Goコンパイラの字句解析器(lexer)を提供するgo/scanner
パッケージに対する以前の変更(CL 5528077)にありました。
CL 5528077は、go/scanner
からInsertSemis
フラグを削除しました。このフラグは、Go言語の自動セミコロン挿入(Automatic Semicolon Insertion: ASI)の挙動を制御するためのものでした。このフラグが削除された結果、go/scanner
は常にセミコロンを挿入するようになりました。
godoc
の内部では、識別子を判定するためにgo/scanner
を利用していました。以前のisIdentifier
関数は、go/scanner
が特定のトークン(token.EOF
)を返すことを期待していましたが、InsertSemis
フラグの削除により、go/scanner
の挙動が変わり、期待するトークンが返されなくなったため、識別子の判定が正しく行われなくなりました。このコミットは、この壊れたロジックを、より直接的な識別子判定ロジックに置き換えることで修正しています。
前提知識の解説
godoc
: Go言語のソースコードからドキュメンテーションを生成し、表示するためのツールです。Goの標準ライブラリやサードパーティのパッケージのドキュメントを閲覧する際に広く利用されます。コード内のコメントや宣言から情報を抽出し、整形されたHTML形式で提供します。go/scanner
パッケージ: Go言語のソースコードを字句解析(lexical analysis)するためのパッケージです。ソースコードの文字列を入力として受け取り、それをトークン(識別子、キーワード、演算子、リテラルなど)のストリームに変換します。- 自動セミコロン挿入 (Automatic Semicolon Insertion: ASI): Go言語の構文規則の一つで、特定の状況下で改行の後に自動的にセミコロンを挿入する仕組みです。これにより、開発者は通常、各ステートメントの終わりにセミコロンを明示的に記述する必要がありません。しかし、この挙動は字句解析器の内部ロジックに影響を与えます。
go/token
パッケージ:go/scanner
やgo/parser
などのGo言語のツールで使われるトークン(字句)の種類を定義するパッケージです。token.IDENT
: 識別子(変数名、関数名など)を表すトークンタイプです。token.EOF
: ファイルの終端(End Of File)を表すトークンタイプです。字句解析器が入力の最後まで到達したことを示します。
go/ast
パッケージ: Go言語の抽象構文木(Abstract Syntax Tree: AST)を表現するためのパッケージです。go/parser
パッケージがソースコードを解析してASTを生成します。unicode
パッケージ: Unicode文字に関する機能を提供するGo言語の標準パッケージです。unicode.IsLetter
やunicode.IsDigit
などの関数は、与えられたルーン(Unicodeコードポイント)が文字であるか、数字であるかを判定するために使用されます。
技術的詳細
このコミットの核心は、src/cmd/godoc/index.go
ファイル内のisIdentifier
関数の修正です。この関数は、与えられた文字列がGo言語の有効な識別子であるかどうかを判定することを目的としていました。
変更前のisIdentifier
関数:
変更前の実装では、go/scanner
パッケージを使用して文字列をスキャンし、その結果に基づいて識別子であるかを判定していました。
func isIdentifier(s string) bool {
var S scanner.Scanner
fset := token.NewFileSet()
S.Init(fset.AddFile("", fset.Base(), len(s)), []byte(s), nil, 0)
if _, tok, _ := S.Scan(); tok == token.IDENT {
_, tok, _ := S.Scan()
return tok == token.EOF
}
return false
}
このロジックは以下のステップで動作していました。
scanner.Scanner
のインスタンスを作成し、入力文字列s
で初期化します。- 最初の
S.Scan()
呼び出しで、文字列s
の最初のトークンを読み取ります。 - もし最初のトークンが
token.IDENT
(識別子)であれば、次のステップに進みます。 - 2回目の
S.Scan()
呼び出しで、次のトークンを読み取ります。 - もし2回目のトークンが
token.EOF
(ファイルの終端)であれば、それは文字列s
が単一の識別子で構成されていることを意味するため、true
を返します。 - それ以外の場合は
false
を返します。
このアプローチは、go/scanner
のInsertSemis
フラグが有効であった(またはデフォルトでセミコロンを挿入しない)場合に機能していました。しかし、CL 5528077によってInsertSemis
フラグが削除され、go/scanner
が常に自動的にセミコロンを挿入するようになったため、単一の識別子の後にも仮想的なセミコロンが挿入されるようになりました。これにより、2回目のS.Scan()
がtoken.EOF
ではなく、仮想的なセミコロンを表すトークンを返すようになり、isIdentifier
関数が常にfalse
を返すというバグが発生しました。
変更後のisIdentifier
関数:
新しい実装では、go/scanner
に依存せず、unicode
パッケージの関数を使って文字列を直接検査することで、識別子であるかを判定します。
// isIdentifier reports whether s is a Go identifier.
func isIdentifier(s string) bool {
for i, ch := range s {
if unicode.IsLetter(ch) || ch == ' ' || i > 0 && unicode.IsDigit(ch) {
continue
}
return false
}
return len(s) > 0
}
この新しいロジックは以下のステップで動作します。
- 入力文字列
s
が空であれば、有効な識別子ではないため、len(s) > 0
のチェックでfalse
を返します(ループの後に評価)。 - 文字列
s
の各文字(ルーンch
)をループで検査します。 - 各文字について、以下の条件をチェックします。
unicode.IsLetter(ch)
: その文字がUnicodeの文字(アルファベット)であるか。ch == ' '
: その文字がスペースであるか。(Goの識別子にはスペースは含まれませんが、このコードではなぜか許可されています。これはおそらく、godoc
が内部的に識別子を扱う際の特殊な要件か、あるいは単純なバグである可能性があります。Go言語の仕様では識別子にスペースは含まれません。)i > 0 && unicode.IsDigit(ch)
: その文字が数字であり、かつ文字列の最初の文字ではないか。(Goの識別子は数字で始まることはできませんが、2文字目以降は数字を含めることができます。)
- 上記のいずれかの条件を満たさない文字が見つかった場合、その文字列は有効な識別子ではないと判断し、直ちに
false
を返します。 - ループが最後まで実行され、すべての文字が上記の条件を満たした場合、かつ文字列が空でなければ(
len(s) > 0
)、true
を返します。
この変更により、godoc
はgo/scanner
の内部的な挙動変更に影響されることなく、安定して識別子を判定できるようになりました。
コアとなるコードの変更箇所
--- a/src/cmd/godoc/index.go
+++ b/src/cmd/godoc/index.go
@@ -44,7 +44,6 @@ import (
"errors"
"go/ast"
"go/parser"
- "go/scanner"
"go/token"
"index/suffixarray"
"io"
@@ -54,6 +53,7 @@ import (
"sort"
"strings"
"time"
+ "unicode"
)
// ----------------------------------------------------------------------------
@@ -921,15 +921,15 @@ func (x *Index) lookupWord(w string) (match *LookupResult, alt *AltWords) {
return
}
+// isIdentifier reports whether s is a Go identifier.
func isIdentifier(s string) bool {
- var S scanner.Scanner
- fset := token.NewFileSet()
- S.Init(fset.AddFile("", fset.Base(), len(s)), []byte(s), nil, 0)
- if _, tok, _ := S.Scan(); tok == token.IDENT {
- _, tok, _ := S.Scan()
- return tok == token.EOF
+ for i, ch := range s {
+ if unicode.IsLetter(ch) || ch == ' ' || i > 0 && unicode.IsDigit(ch) {
+ continue
+ }
+ return false
}
- return false
+ return len(s) > 0
}
// For a given query, which is either a single identifier or a qualified
コアとなるコードの解説
-
import
文の変更:- "go/scanner"
:go/scanner
パッケージのインポートが削除されました。これは、isIdentifier
関数がこのパッケージに依存しなくなったためです。+ "unicode"
:unicode
パッケージが新しくインポートされました。これは、新しいisIdentifier
関数が文字の種別(文字、数字)を判定するためにこのパッケージの関数を使用するためです。
-
isIdentifier
関数の変更:- 旧実装の削除:
scanner.Scanner
の初期化、token.NewFileSet()
、S.Scan()
を用いたロジックが完全に削除されました。 - 新実装の追加:
for i, ch := range s
: 入力文字列s
をルーン(Unicodeコードポイント)ごとにループ処理します。i
はインデックス、ch
はルーンです。if unicode.IsLetter(ch) || ch == ' ' || i > 0 && unicode.IsDigit(ch) { continue }
: 各ルーンch
が以下のいずれかの条件を満たす場合、次のルーンの処理に進みます。unicode.IsLetter(ch)
:ch
が文字(アルファベット)である。ch == ' '
:ch
がスペースである。(Goの識別子には通常スペースは含まれませんが、このgodoc
の文脈では許容されているようです。)i > 0 && unicode.IsDigit(ch)
:ch
が数字であり、かつ文字列の最初の文字ではない(インデックスi
が0より大きい)場合。Goの識別子は数字で始まることはできませんが、2文字目以降に数字を含むことはできます。
return false
: 上記のどの条件も満たさない文字が見つかった場合、その文字列は有効な識別子ではないと判断し、直ちにfalse
を返します。return len(s) > 0
: ループが最後まで実行された場合(つまり、すべての文字が識別子のルールに合致した場合)、文字列s
が空でなければtrue
を返します。空文字列は有効な識別子ではありません。
- 旧実装の削除:
この変更により、isIdentifier
関数はgo/scanner
の内部的な挙動に依存せず、より直接的かつ堅牢な方法で識別子を判定するようになりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/f6f5ce87cdaad3ca4805f6a16bba3b6851fddf2d
- Go CL 5606065: https://golang.org/cl/5606065
参考にした情報源リンク
- CL 5528077に関するWeb検索結果:
go/scanner
パッケージ内のInsertSemis
関数が、閉じ括弧)
の直前にセミコロンを挿入しないように変更されたことを示しています。この変更が、本コミットで修正されたバグの根本原因となりました。