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

[インデックス 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 型のフィールド LparenRparen を追加するものです。

これらのフィールドは、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 の構造に、型アサーション式の構文上重要な要素である括弧の位置情報を直接組み込むことです。

  1. 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 にそれぞれ格納されるようになります。

  2. TypeAssertExpr.End() メソッドの簡素化と正確化: src/pkg/go/ast/ast.goTypeAssertExpr.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 フィールドが正確な ) の位置を保持していることを前提として、よりシンプルかつ正確な終了位置を提供します。

  3. go/parser における LparenRparen の設定: src/pkg/go/parser/parser.go において、parseTypeAssertion 関数が型アサーション式を解析する際に、左括弧と右括弧の token.Pos を取得し、それを新しく追加された LparenRparen フィールドに設定するように変更されました。 変更前:

    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ノードに格納されるようになりました。

  4. go/printer における LparenRparen の利用: src/pkg/go/printer/nodes.go において、printerast.TypeAssertExpr を出力する際に、新しく追加された LparenRparen フィールドを利用するように変更されました。 変更前:

    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 の変更

  1. TypeAssertExpr 構造体へのフィールド追加: TypeAssertExpr 構造体に Lparen token.PosRparen token.Pos が追加されました。これにより、型アサーション式 x.(T)() のソースコード上の正確な位置が、ASTノード自体に直接保持されるようになります。これは、ASTノードが表現する構文要素の完全な範囲情報を保持するために不可欠です。

  2. TypeAssertExpr.End() メソッドの変更: TypeAssertExprEnd() メソッドが return x.Rparen + 1 に変更されました。 以前のバージョンでは、x.Typenil でない場合は x.Type.End() を、そうでない場合は x.X.End() を返していました。このロジックは、x.(type) のような型スイッチの場合に、type キーワードの終了位置や、x の終了位置を返すことになり、式全体の終了位置(つまり ) の直後)を正確に反映していませんでした。 新しい実装では、Rparen フィールドに ) の位置が格納されるため、その位置に 1 を加えることで、式全体の終了位置を正確に計算できるようになりました。これは、Goの構文規則において型アサーション式が常に ) で閉じられるという事実に基づいています。

src/pkg/go/parser/parser.go の変更

  1. parseTypeAssertion 関数における括弧位置の取得と設定: parseTypeAssertion 関数は、Goのソースコードを解析して TypeAssertExpr を構築する役割を担っています。 変更前は単に p.expect(token.LPAREN)p.expect(token.RPAREN) を呼び出して括弧トークンを消費していましたが、変更後はこれらの呼び出しの戻り値を変数 lparenrparen に格納しています。p.expect() は、消費したトークンの開始位置(token.Pos)を返します。 そして、ast.TypeAssertExpr を生成する際に、これらの lparenrparen の値を、新しく追加された LparenRparen フィールドに渡しています。これにより、パーサーがソースコードから読み取った正確な括弧の位置情報がASTノードに埋め込まれるようになりました。

src/pkg/go/printer/nodes.go の変更

  1. printer における括弧位置の利用: printerexpr1 メソッド内で ast.TypeAssertExpr を処理する部分が変更されました。 変更前は、p.print(token.PERIOD, token.LPAREN)p.print(token.RPAREN) のように、単に token.LPARENtoken.RPAREN というトークン型を渡して出力していました。 変更後は、p.print(token.PERIOD, x.Lparen, token.LPAREN)p.print(x.Rparen, token.RPAREN) のように、x.Lparenx.Rparen という token.Pos の値を引数に追加しています。 go/printer は、token.Pos の引数を受け取ると、その位置情報に基づいて出力の整形を調整します。これにより、ASTからソースコードを再構築する際に、元のソースコードにおける括弧の正確な位置(例えば、括弧の前後の空白など)をより忠実に再現できるようになります。これは、gofmt のようなフォーマッターが、より一貫性のある正確な出力を生成するために重要です。

これらの変更は、GoのASTがソースコードの構造をより詳細かつ正確に表現できるようにするためのものであり、Goのツールチェイン全体の信頼性と機能性を向上させる基盤となります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント(go/ast, go/token, go/parser, go/printer パッケージに関する情報)
  • Go言語のソースコード(特に src/go ディレクトリ以下のパッケージ)
  • 抽象構文木 (AST) に関する一般的なプログラミング言語理論の知識
  • Go言語の型アサーションに関する言語仕様