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

[インデックス 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の主なルールは以下の通りです。

  1. 改行の直前にあるトークンが、識別子、整数リテラル、浮動小数点リテラル、虚数リテラル、ルーンリテラル、文字列リテラル、キーワード(break, continue, fallthrough, return)、インクリメント演算子(++)、デクリメント演算子(--)、または閉じ括弧()]})である場合、その改行の後にセミコロンが挿入されます。
  2. 複雑な文(if, for, switchなど)のブロックの終わり(})の直後に改行がある場合も、セミコロンが挿入されます。

この機能により、Goのコードはより簡潔で読みやすくなりますが、開発者はASIのルールを理解しておく必要があります。例えば、return文の後に改行を挟んで値を書くと、returnの後にセミコロンが挿入され、意図しない結果になることがあります。

go/scannerパッケージ

go/scannerパッケージは、Go言語のソースコードを字句解析(lexical analysis)するための機能を提供します。字句解析とは、ソースコードの文字列を、意味を持つ最小単位である「トークン」(識別子、キーワード、演算子、リテラルなど)のストリームに変換するプロセスです。

scanner.Scanner型は、この字句解析を行うための主要な構造体です。Initメソッドでソースコードを初期化し、Scanメソッドを呼び出すことで、次のトークンとその位置情報を取得します。このパッケージは、Goコンパイラ、go/parsergodocなどの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言語の字句解析器(スキャナー)における自動セミコロン挿入の扱いを根本的に変更します。

  1. InsertSemisフラグの削除とdontInsertSemisの導入:

    • 以前はconst ( ScanComments = 1 << iota; InsertSemis )として定義されていたInsertSemis定数が削除されました。
    • 代わりに、const ( ScanComments = 1 << iota; dontInsertSemis )としてdontInsertSemisが導入されました。dontInsertSemisは小文字で始まるため、エクスポートされない(パッケージ外部からは見えない)定数となります。
    • これにより、自動セミコロン挿入はスキャナーのデフォルトの動作となり、外部から明示的に有効にする必要がなくなりました。無効にする場合は、内部的にdontInsertSemisフラグを使用します。
  2. 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が設定された場合のみ挿入が抑制されます。
  3. go/parserにおける変更:

    • src/pkg/go/parser/parser.goscannerMode関数では、以前はvar m uint = scanner.InsertSemisとしてInsertSemisがデフォルトで設定されていましたが、これがvar m uintとなり、デフォルトでInsertSemisが設定されなくなりました。これは、スキャナー自体がデフォルトでセミコロン挿入を行うようになったため、パーサー側で明示的に有効にする必要がなくなったことを反映しています。
  4. godocにおける変更:

    • src/cmd/godoc/format.goでは、s.Init(..., scanner.ScanComments+scanner.InsertSemis)という呼び出しがs.Init(..., scanner.ScanComments)に変更されました。godocもまた、スキャナーのデフォルトのセミコロン挿入動作に依存するようになりました。
  5. テストコードの変更:

    • 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.ScanCommentsscanner.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言語の自動セミコロン挿入が言語のコア機能として確立され、スキャナーが常にその動作を行うように簡素化されたことを明確に示しています。

関連リンク

参考にした情報源リンク