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

[インデックス 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の挙動が変わり、期待するトークンが返されなくなったため、識別子の判定が正しく行われなくなりました。このコミットは、この壊れたロジックを、より直接的な識別子判定ロジックに置き換えることで修正しています。

前提知識の解説

  1. godoc: Go言語のソースコードからドキュメンテーションを生成し、表示するためのツールです。Goの標準ライブラリやサードパーティのパッケージのドキュメントを閲覧する際に広く利用されます。コード内のコメントや宣言から情報を抽出し、整形されたHTML形式で提供します。
  2. go/scannerパッケージ: Go言語のソースコードを字句解析(lexical analysis)するためのパッケージです。ソースコードの文字列を入力として受け取り、それをトークン(識別子、キーワード、演算子、リテラルなど)のストリームに変換します。
  3. 自動セミコロン挿入 (Automatic Semicolon Insertion: ASI): Go言語の構文規則の一つで、特定の状況下で改行の後に自動的にセミコロンを挿入する仕組みです。これにより、開発者は通常、各ステートメントの終わりにセミコロンを明示的に記述する必要がありません。しかし、この挙動は字句解析器の内部ロジックに影響を与えます。
  4. go/tokenパッケージ: go/scannergo/parserなどのGo言語のツールで使われるトークン(字句)の種類を定義するパッケージです。
    • token.IDENT: 識別子(変数名、関数名など)を表すトークンタイプです。
    • token.EOF: ファイルの終端(End Of File)を表すトークンタイプです。字句解析器が入力の最後まで到達したことを示します。
  5. go/astパッケージ: Go言語の抽象構文木(Abstract Syntax Tree: AST)を表現するためのパッケージです。go/parserパッケージがソースコードを解析してASTを生成します。
  6. unicodeパッケージ: Unicode文字に関する機能を提供するGo言語の標準パッケージです。unicode.IsLetterunicode.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
}

このロジックは以下のステップで動作していました。

  1. scanner.Scannerのインスタンスを作成し、入力文字列sで初期化します。
  2. 最初のS.Scan()呼び出しで、文字列sの最初のトークンを読み取ります。
  3. もし最初のトークンがtoken.IDENT(識別子)であれば、次のステップに進みます。
  4. 2回目のS.Scan()呼び出しで、次のトークンを読み取ります。
  5. もし2回目のトークンがtoken.EOF(ファイルの終端)であれば、それは文字列sが単一の識別子で構成されていることを意味するため、trueを返します。
  6. それ以外の場合はfalseを返します。

このアプローチは、go/scannerInsertSemisフラグが有効であった(またはデフォルトでセミコロンを挿入しない)場合に機能していました。しかし、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
}

この新しいロジックは以下のステップで動作します。

  1. 入力文字列sが空であれば、有効な識別子ではないため、len(s) > 0のチェックでfalseを返します(ループの後に評価)。
  2. 文字列sの各文字(ルーンch)をループで検査します。
  3. 各文字について、以下の条件をチェックします。
    • unicode.IsLetter(ch): その文字がUnicodeの文字(アルファベット)であるか。
    • ch == ' ': その文字がスペースであるか。(Goの識別子にはスペースは含まれませんが、このコードではなぜか許可されています。これはおそらく、godocが内部的に識別子を扱う際の特殊な要件か、あるいは単純なバグである可能性があります。Go言語の仕様では識別子にスペースは含まれません。)
    • i > 0 && unicode.IsDigit(ch): その文字が数字であり、かつ文字列の最初の文字ではないか。(Goの識別子は数字で始まることはできませんが、2文字目以降は数字を含めることができます。)
  4. 上記のいずれかの条件を満たさない文字が見つかった場合、その文字列は有効な識別子ではないと判断し、直ちにfalseを返します。
  5. ループが最後まで実行され、すべての文字が上記の条件を満たした場合、かつ文字列が空でなければ(len(s) > 0)、trueを返します。

この変更により、godocgo/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の内部的な挙動に依存せず、より直接的かつ堅牢な方法で識別子を判定するようになりました。

関連リンク

参考にした情報源リンク

  • CL 5528077に関するWeb検索結果: go/scannerパッケージ内のInsertSemis関数が、閉じ括弧)の直前にセミコロンを挿入しないように変更されたことを示しています。この変更が、本コミットで修正されたバグの根本原因となりました。