メインコンテンツへスキップ
#78995accepted

x/tools/go/ast/inspector: add Cursor.Unparen{,Enclosing} methods

ステータス変更: likely_accept accepted

要約

AIによる要約であり、誤りを含む場合があります。

概要

golang.org/x/tools/go/ast/inspector の Cursor 型に Unparen と UnparenEnclosing の2つのメソッドを追加する提案です。どちらも AST(抽象構文木)上で括弧式 (...) を読み飛ばすためのもので、静的解析ツール(analyzer)を正しく書くときによく必要になる処理を標準で使えるようにします。

ステータス変更

likely_accept → accepted
2026年11月4日の週次レビューで likely accept とされ、その後1週間反対意見が出なかったため、11月12日に aclements 氏が「No change in consensus」として承認しました。同じ処理はすでに x/tools 内部の internal/astutil で実装が進んでおり(CL 771080)、x/tools のメンテナである adonovan 氏も初期から設計に同意していました。論点が少なく、すんなり承認された提案です。

技術的背景

現状の問題点

Go のソースでは x、(x)、((x)) は意味が同じですが、AST の上では *ast.ParenExpr というノードが間に入るため、別の構造になります。そのため、cur.Node() の型で分岐したり、cur.ParentEdge() で親との関係を調べたりする解析コードは、括弧で包まれたコードを見落としやすくなります。
ast.Unparen(#60061 で追加)は ast.Expr に対しては使えますが、Cursor ベースのコードでは使えません。そのため、これまでは同じループを毎回自分で書く必要がありました。提案者は「AST を扱うときにこの処理を忘れやすい」ことを動機に挙げています。

提案された解決策

// 自身を包む括弧を内側へたどって取り除いたカーソルを返す(ast.Unparen の Cursor 版)
func (cur Cursor) Unparen() Cursor
// Enclosing(祖先)の列のうち、括弧の中に入っていない最初の要素を返す
// (自身を囲む ParenExpr を外側へたどる)
func (cur Cursor) UnparenEnclosing() Cursor
  • Unparen は、ノードが *ast.ParenExpr である間、その子へ降りていきます。Node() で型 switch する前に呼ぶ用途を想定しています。
  • UnparenEnclosing は、ParentEdgeKind() == edge.ParenExpr_X である間、親へ上がっていきます。ParentEdge() で分岐する前に呼ぶ用途を想定しています(ParentEdgeKind は #77195 で追加されたアクセサです)。

これによって何ができるようになるか

  • 呼び出しの検出: (f)(x) のような書き方でも、関数部分の識別子を確実に取り出せます。
  • 代入先や引数の判定: x が (x) = 1 の左辺かどうかなど、括弧を挟んだ親子関係を正しく判定できます。
  • 修正の提案(SuggestedFix)を作る analyzer: 括弧の有無にかかわらず同じパターンを検出できるため、偽陰性(本来検出すべきものを見逃すこと)が減ります。
    主に恩恵を受けるのは、gopls や vet、独自 linter などの静的解析ツールの開発者です。

コード例

// Before: 括弧を手作業で読み飛ばす
c := cur
for {
	if _, ok := c.Node().(*ast.ParenExpr); ok {
		c, _ = c.FirstChild()
		continue
	}
	break
}
switch n := c.Node().(type) {
case *ast.Ident:
	// ...
}
p := cur
for p.ParentEdgeKind() == edge.ParenExpr_X {
	p = p.Parent()
}
if p.ParentEdgeKind() == edge.CallExpr_Fun { /* 関数位置にある */ }
// After: メソッドチェーンで簡潔に書ける
switch n := cur.Unparen().Node().(type) {
case *ast.Ident:
	// ...
}
if cur.UnparenEnclosing().ParentEdgeKind() == edge.CallExpr_Fun { /* 関数位置にある */ }

議論のハイライト

  • 名前: 内部ヘルパーは UnparenCursor のように単独の関数として作られていました。adonovan 氏は、メソッドにするなら「Cursor」接尾辞は不要だと指摘し、Unparen / UnparenEnclosing に改名されました。
  • 値レシーバの採用: ポインタレシーバにしてその場で書き換える案もありました。しかし、それでは cur.Parent().UnparenEnclosing() のようなメソッドチェーンが書けません。adonovan 氏も「Cursor は値型であり、ポインタレシーバのメソッドは持つべきでない」と同意しました。その場で更新したい場合は cur = cur.UnparenEnclosing() と書けば済みます。
  • 実装の最適化: Unparen の中で使う cur.FirstChild() は、内部では cur.index++ で置き換えられるという指摘がありました(ParenExpr の最初の子は内側の式であるため)。
  • 既存 API の延長: ast.Unparen(#60061)、Cursor 本体(#70859)、ParentEdge{Kind,Index}(#77195)に続く、Cursor API を使いやすくする一連の改善の一つと位置づけられます。

関連リンク