[インデックス 16568] ファイルの概要
このコミットは、Go言語の抽象構文木(AST)を定義する go/ast
パッケージにおいて、型アサーション式(TypeAssertExpr
)の構造を改善し、その正確なソースコード上の終了位置(End()
)を特定できるようにするための変更です。具体的には、型アサーション式の括弧 (
と )
の位置をASTノードに明示的に保持するようにし、go/parser
がこれらの位置を正しく設定し、go/printer
がこれらを利用して正確にコードを整形できるようにしています。これにより、GoのツールチェインにおけるASTの正確性と堅牢性が向上しました。
コミット
commit 568c4617ec55b8f3634c002c9c04779338c0cf7a
Author: Alan Donovan <adonovan@google.com>
Date: Thu Jun 13 14:41:44 2013 -0400
go/ast: add {L,R}paren token.Pos field to ast.TypeAssertExpr.
These are required to correctly determine the End() of the node.
Also set these fields in go/parser and use them in go/printer.
This is a backward-compatible API change.
R=gri, r
CC=golang-dev
https://golang.org/cl/10189043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/568c4617ec55b8f3634c002c9c04779338c0cf7a
元コミット内容
このコミットは、Go言語の go/ast
パッケージにおける ast.TypeAssertExpr
(型アサーション式、例: x.(T)
)の定義に、左括弧 (
と右括弧 )
のソースコード上の位置を示す token.Pos
型のフィールド Lparen
と Rparen
を追加するものです。
これらのフィールドは、TypeAssertExpr
ノード全体の正確な終了位置(End()
メソッドが返す値)を決定するために必要とされます。以前の実装では、型アサーションの終了位置の計算が不正確になる可能性がありました。
また、この変更に伴い、Goのソースコードを解析してASTを生成する go/parser
パッケージが、これらの新しいフィールドに正しい位置情報を設定するように更新され、ASTをGoのソースコードとして整形・出力する go/printer
パッケージが、これらのフィールドを利用して正確な出力を行うように修正されています。
このAPI変更は後方互換性があり、既存のコードに影響を与えないとされています。
変更の背景
Go言語のコンパイラや各種ツール(リンター、フォーマッター、IDEなど)は、ソースコードを抽象構文木(AST)として内部的に表現し、そのASTを操作することで様々な処理を行います。ASTの各ノードは、ソースコード上の開始位置と終了位置を持つことが一般的であり、これはエラー報告、コードのハイライト、リファクタリングなど、多くの機能で不可欠な情報となります。
go/ast
パッケージの TypeAssertExpr
は、x.(T)
のような型アサーションや、switch x.(type)
のような型スイッチの式を表します。このノードの End()
メソッドは、式がソースコード上でどこで終わるかを正確に返す必要があります。しかし、このコミット以前の TypeAssertExpr
の定義には、型アサーションを囲む括弧 (
と )
の位置情報が直接含まれていませんでした。
従来の TypeAssertExpr.End()
の実装は、x.Type
が存在する場合はその End()
を返し、そうでない場合は x.X.End()
を返すというロジックでした。これは、x.(type)
のような型スイッチの場合や、型アサーションの型部分が複雑な式である場合に、式全体の終了位置を正確に特定できない可能性がありました。特に、括弧の存在が式の終了位置に影響を与えるため、括弧の位置情報がないと、End()
の計算が不正確になることが問題でした。
この不正確さは、go/printer
のようなASTをソースコードに変換するツールや、ソースコードの範囲に基づいて操作を行う他のツールにおいて、誤った整形や不正確な動作を引き起こす可能性がありました。したがって、型アサーション式の正確な範囲を特定するために、括弧の開始位置と終了位置をASTノードに直接保持することが必要とされました。
前提知識の解説
抽象構文木 (Abstract Syntax Tree, AST)
ASTは、プログラミング言語のソースコードの抽象的な構文構造を木構造で表現したものです。コンパイラやインタプリタは、ソースコードを解析(パース)してASTを生成し、そのASTを基にコードの検証、最適化、コード生成などを行います。Go言語では、標準ライブラリの go/ast
パッケージがASTのデータ構造を定義しています。各ノードは、変数、式、文、関数定義など、ソースコードの特定の要素に対応します。
token.Pos
token.Pos
は、Goの go/token
パッケージで定義されている型で、ソースコード内の特定の文字位置(バイトオフセット)を表します。これは、ファイル名、行番号、列番号といった情報と組み合わせて、ソースコード上の正確な位置を特定するために使用されます。ASTノードには、通常、そのノードが表すソースコード要素の開始位置(Pos()
)と終了位置(End()
)が関連付けられています。
ast.TypeAssertExpr
ast.TypeAssertExpr
は、Go言語の型アサーション式 x.(T)
を表すASTノードです。ここで x
はインターフェース型の式、T
はアサートされる型です。また、型スイッチの switch x.(type)
の x.(type)
の部分もこのノードで表現されます。
例:
var i interface{} = "hello"
s := i.(string) // TypeAssertExpr
go/parser
パッケージ
go/parser
パッケージは、Goのソースコードを解析し、go/ast
パッケージで定義されたASTを生成する役割を担います。このパッケージは、字句解析(トークン化)と構文解析(パース)を行い、ソースコードの構造をASTとして表現します。
go/printer
パッケージ
go/printer
パッケージは、go/ast
パッケージで定義されたASTを受け取り、それをGoのソースコードとして整形して出力する役割を担います。これは、gofmt
のようなGoのコードフォーマッターの基盤となっています。ASTから元のソースコードを正確に再構築するためには、各ASTノードがソースコード上の正確な範囲情報を持っていることが重要です。
End()
メソッド
go/ast
パッケージの多くのASTノードには End()
メソッドが定義されており、そのノードが表すソースコード要素の終了位置(token.Pos
)を返します。このメソッドは、コードの範囲選択、エラーメッセージの表示、コードの自動生成など、様々なツールで利用されます。
技術的詳細
このコミットの技術的な核心は、ast.TypeAssertExpr
の構造に、型アサーション式の構文上重要な要素である括弧の位置情報を直接組み込むことです。
-
ast.TypeAssertExpr
構造体の変更:src/pkg/go/ast/ast.go
において、TypeAssertExpr
構造体に以下の2つのフィールドが追加されました。type TypeAssertExpr struct { X Expr // expression Lparen token.Pos // position of "(" Type Expr // asserted type; nil means type switch X.(type) Rparen token.Pos // position of ")" }
これにより、型アサーション式
X.(Type)
における(
の位置がLparen
に、)
の位置がRparen
にそれぞれ格納されるようになります。 -
TypeAssertExpr.End()
メソッドの簡素化と正確化:src/pkg/go/ast/ast.go
のTypeAssertExpr.End()
メソッドの実装が変更されました。 変更前:func (x *TypeAssertExpr) End() token.Pos { if x.Type != nil { return x.Type.End() } return x.X.End() }
変更後:
func (x *TypeAssertExpr) End() token.Pos { return x.Rparen + 1 }
この変更により、
TypeAssertExpr
の終了位置は、常に右括弧)
の直後の位置として定義されるようになりました。これは、型アサーション式が常に)
で終わるという構文規則に基づいています。以前の実装では、Type
フィールドがnil
の場合(x.(type)
のような型スイッチの場合)にX.End()
を返しており、これは)
の位置を考慮していませんでした。新しい実装は、Rparen
フィールドが正確な)
の位置を保持していることを前提として、よりシンプルかつ正確な終了位置を提供します。 -
go/parser
におけるLparen
とRparen
の設定:src/pkg/go/parser/parser.go
において、parseTypeAssertion
関数が型アサーション式を解析する際に、左括弧と右括弧のtoken.Pos
を取得し、それを新しく追加されたLparen
とRparen
フィールドに設定するように変更されました。 変更前:p.expect(token.LPAREN) // ... p.expect(token.RPAREN) return &ast.TypeAssertExpr{X: x, Type: typ}
変更後:
lparen := p.expect(token.LPAREN) // ... rparen := p.expect(token.RPAREN) return &ast.TypeAssertExpr{X: x, Type: typ, Lparen: lparen, Rparen: rparen}
p.expect()
メソッドは、期待するトークンを消費し、そのトークンの開始位置を返します。この戻り値を利用して、正確な括弧の位置がASTノードに格納されるようになりました。 -
go/printer
におけるLparen
とRparen
の利用:src/pkg/go/printer/nodes.go
において、printer
がast.TypeAssertExpr
を出力する際に、新しく追加されたLparen
とRparen
フィールドを利用するように変更されました。 変更前:p.print(token.PERIOD, token.LPAREN) // ... p.print(token.RPAREN)
変更後:
p.print(token.PERIOD, x.Lparen, token.LPAREN) // ... p.print(x.Rparen, token.RPAREN)
p.print()
メソッドは、引数としてtoken.Pos
を受け取ることができ、これにより指定された位置にトークンを出力する際の整形をより細かく制御できます。この変更により、go/printer
はASTノードに格納された正確な括弧の位置情報に基づいて、より忠実に元のソースコードの整形を再現できるようになります。
これらの変更は、GoのASTの正確性を高め、それを基盤とするツールチェイン全体の信頼性と堅牢性を向上させるものです。特に、ソースコードの正確な範囲情報を必要とする静的解析ツールやコードフォーマッターにとって、この変更は非常に重要です。
コアとなるコードの変更箇所
src/pkg/go/ast/ast.go
--- a/src/pkg/go/ast/ast.go
+++ b/src/pkg/go/ast/ast.go
@@ -304,8 +304,10 @@ type (
// type assertion.
//
TypeAssertExpr struct {
- \tX Expr // expression
- \tType Expr // asserted type; nil means type switch X.(type)
+ \tX Expr // expression
+ \tLparen token.Pos // position of "("
+ \tType Expr // asserted type; nil means type switch X.(type)
+ \tRparen token.Pos // position of ")"
}
// A CallExpr node represents an expression followed by an argument list.
@@ -456,26 +458,21 @@ func (x *Ellipsis) End() token.Pos {
}\n \treturn x.Ellipsis + 3 // len(\"...\")
}\n-func (x *BasicLit) End() token.Pos { return token.Pos(int(x.ValuePos) + len(x.Value)) }\n-func (x *FuncLit) End() token.Pos { return x.Body.End() }\n-func (x *CompositeLit) End() token.Pos { return x.Rbrace + 1 }\n-func (x *ParenExpr) End() token.Pos { return x.Rparen + 1 }\n-func (x *SelectorExpr) End() token.Pos { return x.Sel.End() }\n-func (x *IndexExpr) End() token.Pos { return x.Rbrack + 1 }\n-func (x *SliceExpr) End() token.Pos { return x.Rbrack + 1 }\n-func (x *TypeAssertExpr) End() token.Pos {\n-\tif x.Type != nil {\n-\t\treturn x.Type.End()\n-\t}\n-\treturn x.X.End()\n-}\n-func (x *CallExpr) End() token.Pos { return x.Rparen + 1 }\n-func (x *StarExpr) End() token.Pos { return x.X.End() }\n-func (x *UnaryExpr) End() token.Pos { return x.X.End() }\n-func (x *BinaryExpr) End() token.Pos { return x.Y.End() }\n-func (x *KeyValueExpr) End() token.Pos { return x.Value.End() }\n-func (x *ArrayType) End() token.Pos { return x.Elt.End() }\n-func (x *StructType) End() token.Pos { return x.Fields.End() }\
+func (x *BasicLit) End() token.Pos { return token.Pos(int(x.ValuePos) + len(x.Value)) }\n+func (x *FuncLit) End() token.Pos { return x.Body.End() }\n+func (x *CompositeLit) End() token.Pos { return x.Rbrace + 1 }\n+func (x *ParenExpr) End() token.Pos { return x.Rparen + 1 }\n+func (x *SelectorExpr) End() token.Pos { return x.Sel.End() }\n+func (x *IndexExpr) End() token.Pos { return x.Rbrack + 1 }\n+func (x *SliceExpr) End() token.Pos { return x.Rbrack + 1 }\n+func (x *TypeAssertExpr) End() token.Pos { return x.Rparen + 1 }\n+func (x *CallExpr) End() token.Pos { return x.Rparen + 1 }\n+func (x *StarExpr) End() token.Pos { return x.X.End() }\n+func (x *UnaryExpr) End() token.Pos { return x.X.End() }\n+func (x *BinaryExpr) End() token.Pos { return x.Y.End() }\n+func (x *KeyValueExpr) End() token.Pos { return x.Value.End() }\n+func (x *ArrayType) End() token.Pos { return x.Elt.End() }\n+func (x *StructType) End() token.Pos { return x.Fields.End() }\
func (x *FuncType) End() token.Pos {
\tif x.Results != nil {
\t\treturn x.Results.End()
src/pkg/go/parser/parser.go
--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -1150,7 +1150,7 @@ func (p *parser) parseTypeAssertion(x ast.Expr) ast.Expr {
\tdefer un(trace(p, \"TypeAssertion\"))
\t}
-\tp.expect(token.LPAREN)\n+\tlparen := p.expect(token.LPAREN)\n \tvar typ ast.Expr
\tif p.tok == token.TYPE {
\t\t// type switch: typ == nil
\t} else {
@@ -1158,9 +1158,9 @@ func (p *parser) parseTypeAssertion(x ast.Expr) ast.Expr {
\t\ttyp = p.parseType()\n \t}\n-\tp.expect(token.RPAREN)\n+\trparen := p.expect(token.RPAREN)\n
-\treturn &ast.TypeAssertExpr{X: x, Type: typ}\n+\treturn &ast.TypeAssertExpr{X: x, Type: typ, Lparen: lparen, Rparen: rparen}\n }
func (p *parser) parseIndexOrSlice(x ast.Expr) ast.Expr {
src/pkg/go/printer/nodes.go
--- a/src/pkg/go/printer/nodes.go
+++ b/src/pkg/go/printer/nodes.go
@@ -754,13 +754,13 @@ func (p *printer) expr1(expr ast.Expr, prec1, depth int) {
\tcase *ast.TypeAssertExpr:\n \t\tp.expr1(x.X, token.HighestPrec, depth)\n-\t\tp.print(token.PERIOD, token.LPAREN)\n+\t\tp.print(token.PERIOD, x.Lparen, token.LPAREN)\n \t\tif x.Type != nil {\n \t\t\tp.expr(x.Type)\n \t\t} else {\n \t\t\tp.print(token.TYPE)\n \t\t}\n-\t\tp.print(token.RPAREN)\n+\t\tp.print(x.Rparen, token.RPAREN)\n
\tcase *ast.IndexExpr:\n \t\t// TODO(gri): should treat[] like parentheses and undo one level of depth
コアとなるコードの解説
src/pkg/go/ast/ast.go
の変更
-
TypeAssertExpr
構造体へのフィールド追加:TypeAssertExpr
構造体にLparen token.Pos
とRparen token.Pos
が追加されました。これにより、型アサーション式x.(T)
の(
と)
のソースコード上の正確な位置が、ASTノード自体に直接保持されるようになります。これは、ASTノードが表現する構文要素の完全な範囲情報を保持するために不可欠です。 -
TypeAssertExpr.End()
メソッドの変更:TypeAssertExpr
のEnd()
メソッドがreturn x.Rparen + 1
に変更されました。 以前のバージョンでは、x.Type
がnil
でない場合はx.Type.End()
を、そうでない場合はx.X.End()
を返していました。このロジックは、x.(type)
のような型スイッチの場合に、type
キーワードの終了位置や、x
の終了位置を返すことになり、式全体の終了位置(つまり)
の直後)を正確に反映していませんでした。 新しい実装では、Rparen
フィールドに)
の位置が格納されるため、その位置に1
を加えることで、式全体の終了位置を正確に計算できるようになりました。これは、Goの構文規則において型アサーション式が常に)
で閉じられるという事実に基づいています。
src/pkg/go/parser/parser.go
の変更
parseTypeAssertion
関数における括弧位置の取得と設定:parseTypeAssertion
関数は、Goのソースコードを解析してTypeAssertExpr
を構築する役割を担っています。 変更前は単にp.expect(token.LPAREN)
とp.expect(token.RPAREN)
を呼び出して括弧トークンを消費していましたが、変更後はこれらの呼び出しの戻り値を変数lparen
とrparen
に格納しています。p.expect()
は、消費したトークンの開始位置(token.Pos
)を返します。 そして、ast.TypeAssertExpr
を生成する際に、これらのlparen
とrparen
の値を、新しく追加されたLparen
とRparen
フィールドに渡しています。これにより、パーサーがソースコードから読み取った正確な括弧の位置情報がASTノードに埋め込まれるようになりました。
src/pkg/go/printer/nodes.go
の変更
printer
における括弧位置の利用:printer
のexpr1
メソッド内でast.TypeAssertExpr
を処理する部分が変更されました。 変更前は、p.print(token.PERIOD, token.LPAREN)
とp.print(token.RPAREN)
のように、単にtoken.LPAREN
とtoken.RPAREN
というトークン型を渡して出力していました。 変更後は、p.print(token.PERIOD, x.Lparen, token.LPAREN)
とp.print(x.Rparen, token.RPAREN)
のように、x.Lparen
とx.Rparen
というtoken.Pos
の値を引数に追加しています。go/printer
は、token.Pos
の引数を受け取ると、その位置情報に基づいて出力の整形を調整します。これにより、ASTからソースコードを再構築する際に、元のソースコードにおける括弧の正確な位置(例えば、括弧の前後の空白など)をより忠実に再現できるようになります。これは、gofmt
のようなフォーマッターが、より一貫性のある正確な出力を生成するために重要です。
これらの変更は、GoのASTがソースコードの構造をより詳細かつ正確に表現できるようにするためのものであり、Goのツールチェイン全体の信頼性と機能性を向上させる基盤となります。
関連リンク
- Go CL 10189043: https://golang.org/cl/10189043
参考にした情報源リンク
- Go言語の公式ドキュメント(
go/ast
,go/token
,go/parser
,go/printer
パッケージに関する情報) - Go言語のソースコード(特に
src/go
ディレクトリ以下のパッケージ) - 抽象構文木 (AST) に関する一般的なプログラミング言語理論の知識
- Go言語の型アサーションに関する言語仕様