[インデックス 11109] ファイルの概要
Go言語のgo/scanner
パッケージから、エクスポートされていたInsertSemis
モードが削除されました。これは、Goの自動セミコロン挿入機能が導入された時代からの名残であり、テスト目的でのみ非エクスポートのスイッチとして保持されることになりました。
コミット
commit 276f177b9c45218303bd29be128be58602d2afa9
Author: Robert Griesemer <gri@golang.org>
Date: Wed Jan 11 10:06:44 2012 -0800
go/scanner: remove (exported) InsertSemis mode
This is a relic from the times when we switched
to automatic semicolon insertion. It's still use-
ful to have a non-exported switch for testing.
R=golang-dev, r, rsc
CC=golang-dev
https://golang.org/cl/5528077
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/276f177b9c45218303bd29be128be58602d2afa9
元コミット内容
go/scanner: remove (exported) InsertSemis mode
This is a relic from the times when we switched
to automatic semicolon insertion. It's still use-
ful to have a non-exported switch for testing.
R=golang-dev, r, rsc
CC=golang-dev
https://golang.org/cl/5528077
変更の背景
このコミットの主な目的は、Go言語のgo/scanner
パッケージから、エクスポートされていたInsertSemis
モードを削除することです。コミットメッセージによると、このモードはGoが自動セミコロン挿入(Automatic Semicolon Insertion, ASI)に切り替わった時期の名残であり、もはや外部に公開する必要がないと判断されました。
Go言語では、コードの可読性を高め、開発者がセミコロンの記述を意識せずに済むように、特定のルールに基づいて自動的にセミコロンが挿入されます。この機能が導入された初期段階では、スキャナーがこの自動挿入を行うかどうかを制御するためのInsertSemis
というフラグが存在していました。しかし、ASIがGo言語の標準的な動作として確立されたため、このフラグは冗長となり、外部から制御する必要がなくなりました。
ただし、スキャナーのテストなど、特定の内部的な目的のために、セミコロン挿入を無効にする機能は引き続き必要とされました。そのため、エクスポートされたInsertSemis
フラグは削除され、代わりに非エクスポートの(つまり、パッケージ内部からのみアクセス可能な)dontInsertSemis
というフラグが導入されました。これにより、外部からの誤用を防ぎつつ、内部的なテストの柔軟性を維持しています。
前提知識の解説
Go言語の自動セミコロン挿入 (Automatic Semicolon Insertion - ASI)
Go言語の構文は、C言語やJavaのような言語とは異なり、文の終わりに明示的なセミコロン(;
)を必要としません。その代わりに、Goコンパイラは特定のルールに基づいて自動的にセミコロンを挿入します。これが自動セミコロン挿入(ASI)です。
ASIの主なルールは以下の通りです。
- 改行の直前にあるトークンが、識別子、整数リテラル、浮動小数点リテラル、虚数リテラル、ルーンリテラル、文字列リテラル、キーワード(
break
,continue
,fallthrough
,return
)、インクリメント演算子(++
)、デクリメント演算子(--
)、または閉じ括弧()
、]
、}
)である場合、その改行の後にセミコロンが挿入されます。 - 複雑な文(
if
,for
,switch
など)のブロックの終わり(}
)の直後に改行がある場合も、セミコロンが挿入されます。
この機能により、Goのコードはより簡潔で読みやすくなりますが、開発者はASIのルールを理解しておく必要があります。例えば、return
文の後に改行を挟んで値を書くと、return
の後にセミコロンが挿入され、意図しない結果になることがあります。
go/scanner
パッケージ
go/scanner
パッケージは、Go言語のソースコードを字句解析(lexical analysis)するための機能を提供します。字句解析とは、ソースコードの文字列を、意味を持つ最小単位である「トークン」(識別子、キーワード、演算子、リテラルなど)のストリームに変換するプロセスです。
scanner.Scanner
型は、この字句解析を行うための主要な構造体です。Init
メソッドでソースコードを初期化し、Scan
メソッドを呼び出すことで、次のトークンとその位置情報を取得します。このパッケージは、Goコンパイラ、go/parser
、godoc
などのGoツールチェインの基盤となっています。
go/parser
パッケージ
go/parser
パッケージは、go/scanner
によって生成されたトークンのストリームを受け取り、Go言語の構文規則に基づいて抽象構文木(Abstract Syntax Tree - AST)を構築します。ASTは、ソースコードの構造を木構造で表現したもので、コンパイラのセマンティック解析やコード生成、あるいは静的解析ツールなどで利用されます。
parser.ParseFile
などの関数が提供されており、Goのソースファイルを解析してASTを返します。
token
パッケージ
token
パッケージは、Go言語のトークン(キーワード、演算子、識別子など)の定義と、ソースコード内の位置情報(ファイル名、行番号、列番号、オフセット)を扱うための型を提供します。token.Pos
型はソースコード内の位置を表し、token.FileSet
は複数のファイルにわたる位置情報を管理します。
scanner.Mode
scanner.Mode
は、go/scanner
パッケージのScanner.Init
メソッドに渡されるビットフラグのセットで、スキャナーの動作を制御します。例えば、scanner.ScanComments
フラグは、コメントを通常のトークンとしてスキャンするかどうかを制御します。
このコミット以前は、scanner.InsertSemis
というフラグも存在し、自動セミコロン挿入を有効にするかどうかを制御していました。
技術的詳細
このコミットは、Go言語の字句解析器(スキャナー)における自動セミコロン挿入の扱いを根本的に変更します。
-
InsertSemis
フラグの削除とdontInsertSemis
の導入:- 以前は
const ( ScanComments = 1 << iota; InsertSemis )
として定義されていたInsertSemis
定数が削除されました。 - 代わりに、
const ( ScanComments = 1 << iota; dontInsertSemis )
としてdontInsertSemis
が導入されました。dontInsertSemis
は小文字で始まるため、エクスポートされない(パッケージ外部からは見えない)定数となります。 - これにより、自動セミコロン挿入はスキャナーのデフォルトの動作となり、外部から明示的に有効にする必要がなくなりました。無効にする場合は、内部的に
dontInsertSemis
フラグを使用します。
- 以前は
-
Scanner.Init
メソッドの動作変更:scanner.go
内のScanner.Init
メソッドのコメントが更新され、mode
パラメータが「コメントの処理方法を決定する」とだけ記述されるようになりました。以前は「コメントとセミコロンの処理方法を決定する」と書かれていました。これは、セミコロン挿入がもはやmode
パラメータで制御されるものではなく、常にデフォルトで有効であることを示唆しています。- セミコロン挿入のロジックが
if S.mode&InsertSemis != 0 { S.insertSemi = insertSemi }
からif S.mode&dontInsertSemis == 0 { S.insertSemi = insertSemi }
に変更されました。これは、dontInsertSemis
フラグが設定されていない場合にのみセミコロンが挿入されることを意味します。つまり、デフォルトではセミコロンが挿入され、dontInsertSemis
が設定された場合のみ挿入が抑制されます。
-
go/parser
における変更:src/pkg/go/parser/parser.go
のscannerMode
関数では、以前はvar m uint = scanner.InsertSemis
としてInsertSemis
がデフォルトで設定されていましたが、これがvar m uint
となり、デフォルトでInsertSemis
が設定されなくなりました。これは、スキャナー自体がデフォルトでセミコロン挿入を行うようになったため、パーサー側で明示的に有効にする必要がなくなったことを反映しています。
-
godoc
における変更:src/cmd/godoc/format.go
では、s.Init(..., scanner.ScanComments+scanner.InsertSemis)
という呼び出しがs.Init(..., scanner.ScanComments)
に変更されました。godoc
もまた、スキャナーのデフォルトのセミコロン挿入動作に依存するようになりました。
-
テストコードの変更:
src/pkg/exp/types/check_test.go
では、テストコード内で自動挿入されたセミコロンを無視するロジックが追加されました。これは、スキャナーが常にセミコロンを挿入するようになったため、テストがその動作を考慮する必要があることを示しています。src/pkg/go/scanner/scanner_test.go
では、TestSemis
関数内のcheckSemi
呼び出しで、以前InsertSemis
が渡されていた箇所が0
(デフォルト動作)またはScanComments
に変更されました。また、TestScan
,TestLineComments
,TestInit
,TestStdErrorHander
,checkError
などのテスト関数でも、InsertSemis
の代わりにdontInsertSemis
が使用されるようになりました。これにより、テストが新しいスキャナーの動作(デフォルトでセミコロン挿入が有効)に適合し、必要に応じてdontInsertSemis
を使ってその動作を無効にできることを示しています。
これらの変更により、Go言語の字句解析器は、自動セミコロン挿入を常にデフォルトで有効にするように簡素化され、その制御は内部的なテスト目的のためにのみ保持されるようになりました。
コアとなるコードの変更箇所
--- a/src/cmd/godoc/format.go
+++ b/src/cmd/godoc/format.go
@@ -231,7 +231,7 @@ func commentSelection(src []byte) Selection {
var s scanner.Scanner
fset := token.NewFileSet()
file := fset.AddFile("", fset.Base(), len(src))
- s.Init(file, src, nil, scanner.ScanComments+scanner.InsertSemis)
+ s.Init(file, src, nil, scanner.ScanComments)
return func() (seg []int) {
for {
pos, tok, lit := s.Scan()
diff --git a/src/pkg/exp/types/check_test.go b/src/pkg/exp/types/check_test.go
index 35535ea406..ea9218ff51 100644
--- a/src/pkg/exp/types/check_test.go
+++ b/src/pkg/exp/types/check_test.go
@@ -111,7 +111,7 @@ func expectedErrors(t *testing.T, testname string, files map[string]*ast.File) m
// set otherwise the position information returned here will
// not match the position information collected by the parser
s.Init(getFile(filename), src, nil, scanner.ScanComments)
- var prev token.Pos // position of last non-comment token
+ var prev token.Pos // position of last non-comment, non-semicolon token
scanFile:
for {
@@ -124,6 +124,12 @@ func expectedErrors(t *testing.T, testname string, files map[string]*ast.File) m
if len(s) == 2 {
errors[prev] = string(s[1])
}
+ case token.SEMICOLON:
+ // ignore automatically inserted semicolon
+ if lit == "\n" {
+ break
+ }
+ fallthrough
default:
prev = pos
}
diff --git a/src/pkg/go/parser/parser.go b/src/pkg/go/parser/parser.go
index 9fbed2d2ca..8467b0e4e4 100644
--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -67,7 +67,7 @@ type parser struct {
// scannerMode returns the scanner mode bits given the parser's mode bits.
func scannerMode(mode uint) uint {
- var m uint = scanner.InsertSemis
+ var m uint
if mode&ParseComments != 0 {
m |= scanner.ScanComments
}
diff --git a/src/pkg/go/scanner/scanner.go b/src/pkg/go/scanner/scanner.go
index 34d0442635..c5d83eba58 100644
--- a/src/pkg/go/scanner/scanner.go
+++ b/src/pkg/go/scanner/scanner.go
@@ -90,8 +90,8 @@ func (S *Scanner) next() {
// They control scanner behavior.
//
const (
- ScanComments = 1 << iota // return comments as COMMENT tokens
- InsertSemis // automatically insert semicolons
+ ScanComments = 1 << iota // return comments as COMMENT tokens
+ dontInsertSemis // do not automatically insert semicolons - for testing only
)
// Init prepares the scanner S to tokenize the text src by setting the
@@ -104,7 +104,7 @@ const (
// Calls to Scan will use the error handler err if they encounter a
// syntax error and err is not nil. Also, for each error encountered,
// the Scanner field ErrorCount is incremented by one. The mode parameter
-// determines how comments and semicolons are handled.
+// determines how comments are handled.
//
// Note that Init may call err if there is an error in the first character
// of the file.
@@ -673,7 +673,7 @@ scanAgain:
}
}
- if S.mode&InsertSemis != 0 {
+ if S.mode&dontInsertSemis == 0 {
S.insertSemi = insertSemi
}
diff --git a/src/pkg/go/scanner/scanner_test.go b/src/pkg/go/scanner/scanner_test.go
index dc8ab2a748..fd3a7cf660 100644
--- a/src/pkg/go/scanner/scanner_test.go
+++ b/src/pkg/go/scanner/scanner_test.go
@@ -223,7 +223,7 @@ func TestScan(t *testing.T) {
// verify scan
var s Scanner
- s.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), &testErrorHandler{t}, ScanComments)
+ s.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), &testErrorHandler{t}, ScanComments|dontInsertSemis)
index := 0
epos := token.Position{"", 0, 1, 1} // expected position
for {
@@ -430,14 +430,14 @@ var lines = []string{
func TestSemis(t *testing.T) {
for _, line := range lines {
-\t\tcheckSemi(t, line, InsertSemis)\n-\t\tcheckSemi(t, line, InsertSemis|ScanComments)\n+\t\tcheckSemi(t, line, 0)\n+\t\tcheckSemi(t, line, ScanComments)\n
// if the input ended in newlines, the input must tokenize the
// same with or without those newlines
for i := len(line) - 1; i >= 0 && line[i] == '\n'; i-- {
-\t\t\tcheckSemi(t, line[0:i], InsertSemis)\n-\t\t\tcheckSemi(t, line[0:i], InsertSemis|ScanComments)\n+\t\t\tcheckSemi(t, line[0:i], 0)\n+\t\t\tcheckSemi(t, line[0:i], ScanComments)\n }
}
}
@@ -492,7 +492,7 @@ func TestLineComments(t *testing.T) {
// verify scan
var S Scanner
file := fset.AddFile(filepath.Join("dir", "TestLineComments"), fset.Base(), len(src))
-\tS.Init(file, []byte(src), nil, 0)
+\tS.Init(file, []byte(src), nil, dontInsertSemis)
for _, s := range segs {
p, _, lit := S.Scan()
pos := file.Position(p)
@@ -511,7 +511,7 @@ func TestInit(t *testing.T) {
// 1st init
src1 := "if true { }"
f1 := fset.AddFile("src1", fset.Base(), len(src1))
-\ts.Init(f1, []byte(src1), nil, 0)
+\ts.Init(f1, []byte(src1), nil, dontInsertSemis)
if f1.Size() != len(src1) {
t.Errorf("bad file size: got %d, expected %d", f1.Size(), len(src1))
}
@@ -525,7 +525,7 @@ func TestInit(t *testing.T) {
// 2nd init
src2 := "go true { ]"
f2 := fset.AddFile("src2", fset.Base(), len(src2))
-\ts.Init(f2, []byte(src2), nil, 0)
+\ts.Init(f2, []byte(src2), nil, dontInsertSemis)
if f2.Size() != len(src2) {
t.Errorf("bad file size: got %d, expected %d", f2.Size(), len(src2))
}
@@ -551,7 +551,7 @@ func TestStdErrorHander(t *testing.T) {
v := new(ErrorVector)
var s Scanner
-\ts.Init(fset.AddFile("File1", fset.Base(), len(src)), []byte(src), v, 0)
+\ts.Init(fset.AddFile("File1", fset.Base(), len(src)), []byte(src), v, dontInsertSemis)
for {
if _, tok, _ := s.Scan(); tok == token.EOF {
break
@@ -596,5 +596,5 @@ func (h *errorCollector) Error(pos token.Position, msg string) {
func checkError(t *testing.T, src string, tok token.Token, pos int, err string) {
var s Scanner
var h errorCollector
-\ts.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), &h, ScanComments)
+\ts.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), &h, ScanComments|dontInsertSemis)
_, tok0, _ := s.Scan()
_, tok1, _ := s.Scan()
if tok0 != tok {
コアとなるコードの解説
src/cmd/godoc/format.go
- s.Init(file, src, nil, scanner.ScanComments+scanner.InsertSemis)
+ s.Init(file, src, nil, scanner.ScanComments)
godoc
ツールがスキャナーを初期化する際に、以前はscanner.ScanComments
とscanner.InsertSemis
の両方のモードを明示的に指定していました。この変更により、scanner.InsertSemis
が削除され、scanner.ScanComments
のみが指定されるようになりました。これは、スキャナーがデフォルトで自動セミコロン挿入を行うようになったため、godoc
側で明示的にそのモードを有効にする必要がなくなったことを示しています。
src/pkg/exp/types/check_test.go
- var prev token.Pos // position of last non-comment token
+ var prev token.Pos // position of last non-comment, non-semicolon token
...
+ case token.SEMICOLON:
+ // ignore automatically inserted semicolon
+ if lit == "\n" {
+ break
+ }
+ fallthrough
このテストファイルでは、字句解析中に自動挿入されたセミコロンを特別に処理するロジックが追加されました。prev
変数のコメントが「最後の非コメントトークンの位置」から「最後の非コメント、非セミコロンのトークンの位置」に変更されています。また、token.SEMICOLON
がスキャンされた場合に、それが自動挿入された改行によるセミコロンであれば無視する処理が追加されています。これは、スキャナーが常にセミコロンを挿入するようになったため、テストがその動作を考慮し、自動挿入されたセミコロンをエラーとして扱わないようにする必要があることを示しています。
src/pkg/go/parser/parser.go
- var m uint = scanner.InsertSemis
+ var m uint
parser
パッケージ内のscannerMode
関数は、パーサーのモードに基づいてスキャナーのモードビットを返します。以前は、scanner.InsertSemis
がデフォルトでm
に設定されていました。この変更により、m
の初期化からscanner.InsertSemis
が削除されました。これは、スキャナー自体がデフォルトで自動セミコロン挿入を行うようになったため、パーサー側で明示的にそのモードを有効にする必要がなくなったことを反映しています。
src/pkg/go/scanner/scanner.go
- ScanComments = 1 << iota // return comments as COMMENT tokens
- InsertSemis // automatically insert semicolons
+ ScanComments = 1 << iota // return comments as COMMENT tokens
+ dontInsertSemis // do not automatically insert semicolons - for testing only
InsertSemis
定数が削除され、代わりにdontInsertSemis
が導入されました。dontInsertSemis
は小文字で始まるため、パッケージ外部からはアクセスできない(非エクスポートの)定数です。これにより、自動セミコロン挿入はスキャナーのデフォルト動作となり、テスト目的でのみ無効にできる内部的なフラグが提供されることになりました。
-// determines how comments and semicolons are handled.
+// determines how comments are handled.
Scanner.Init
メソッドのコメントが更新され、mode
パラメータが「コメントの処理方法を決定する」とだけ記述されるようになりました。以前は「コメントとセミコロンの処理方法を決定する」と書かれていました。これは、セミコロン挿入がもはやmode
パラメータで制御されるものではなく、常にデフォルトで有効であることを示唆しています。
- if S.mode&InsertSemis != 0 {
+ if S.mode&dontInsertSemis == 0 {
セミコロン挿入の実際のロジックが変更されました。以前はInsertSemis
フラグが設定されている場合にセミコロンを挿入していましたが、この変更によりdontInsertSemis
フラグが設定されていない場合にセミコロンを挿入するようになりました。これは、自動セミコロン挿入がデフォルトで有効になり、dontInsertSemis
が設定された場合のみ挿入が抑制されるという、動作の反転を意味します。
src/pkg/go/scanner/scanner_test.go
- s.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), &testErrorHandler{t}, ScanComments)
+ s.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), &testErrorHandler{t}, ScanComments|dontInsertSemis)
TestScan
関数では、スキャナーの初期化時にScanComments
のみが指定されていましたが、ScanComments|dontInsertSemis
に変更されました。これは、このテストケースでは自動セミコロン挿入を無効にしたいことを明示しています。
- checkSemi(t, line, InsertSemis)
- checkSemi(t, line, InsertSemis|ScanComments)
+ checkSemi(t, line, 0)
+ checkSemi(t, line, ScanComments)
...
- checkSemi(t, line[0:i], InsertSemis)
- checkSemi(t, line[0:i], InsertSemis|ScanComments)
+ checkSemi(t, line[0:i], 0)
+ checkSemi(t, line[0:i], ScanComments)
TestSemis
関数では、checkSemi
呼び出しでInsertSemis
が渡されていた箇所が0
(デフォルト動作)またはScanComments
に変更されました。これは、スキャナーがデフォルトでセミコロン挿入を行うようになったため、テストがその動作に依存し、必要に応じてdontInsertSemis
(または0
でデフォルト動作)を使用するように変更されたことを示しています。
- S.Init(file, []byte(src), nil, 0)
+ S.Init(file, []byte(src), nil, dontInsertSemis)
...
- s.Init(f1, []byte(src1), nil, 0)
+ s.Init(f1, []byte(src1), nil, dontInsertSemis)
...
- s.Init(f2, []byte(src2), nil, 0)
+ s.Init(f2, []byte(src2), nil, dontInsertSemis)
...
- s.Init(fset.AddFile("File1", fset.Base(), len(src)), []byte(src), v, 0)
+ s.Init(fset.AddFile("File1", fset.Base(), len(src)), []byte(src), v, dontInsertSemis)
...
- s.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), &h, ScanComments)
+ s.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), &h, ScanComments|dontInsertSemis)
他のテスト関数でも、スキャナーの初期化時に0
(デフォルト動作)が指定されていた箇所がdontInsertSemis
に変更されています。これは、これらのテストが自動セミコロン挿入を無効にしたい場合に、新しい非エクスポートフラグを使用するように更新されたことを示しています。また、ScanComments
のみが指定されていた箇所がScanComments|dontInsertSemis
に変更されているのは、コメントのスキャンとセミコロン挿入の無効化を同時に行いたい場合に対応するためです。
これらの変更は、Go言語の自動セミコロン挿入が言語のコア機能として確立され、スキャナーが常にその動作を行うように簡素化されたことを明確に示しています。
関連リンク
- Go CL 5528077: https://golang.org/cl/5528077
参考にした情報源リンク
- Go言語のソースコード(上記コミットの差分)
- Go言語の自動セミコロン挿入に関する一般的な情報 (Go言語の公式ドキュメントやチュートリアルなど)
- Go言語の
go/scanner
,go/parser
,token
パッケージに関する情報 (Go言語の公式ドキュメント)