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

[インデックス 18746] ファイルの概要

このコミットは、Go言語のパーサー(go/parserパッケージ)におけるエラー報告の精度向上に関するものです。具体的には、goステートメントやdeferステートメントで関数が正しく呼び出されていない場合に報告されるエラーの位置が不正確であった問題を修正しています。また、パースエラー時にトークンの終了位置が有効な範囲外になる可能性があった問題も同時に解決しています。

コミット

commit 1624c73c9d98ad3466db0648a8462e8720cfa4aa
Author: Robert Griesemer <gri@golang.org>
Date:   Tue Mar 4 14:10:30 2014 -0800

    go/parser: better error position for non-invoked gp/defer functions
    
    Added test cases and expanded test harness to handle token end
    positions.
    
    Also: Make sure token end positions are never outside the valid
          position range, as was possible in case of parse errors.
    
    Fixes #7458.
    
    LGTM=bradfitz
    R=bradfitz
    CC=golang-codereviews
    https://golang.org/cl/70190046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/1624c73c9d98ad3466db0648a8462e8720cfa4aa

元コミット内容

Go言語のパーサーにおいて、goおよびdeferステートメント内で関数が呼び出されていない場合の、エラー位置の報告を改善する。テストケースを追加し、トークンの終了位置を扱うためのテストハーネスを拡張した。また、パースエラーの場合にトークンの終了位置が有効な位置範囲外になる可能性があった問題を修正した。

変更の背景

Go言語のコンパイラやツールチェーンにおいて、コードの構文解析(パース)は非常に重要なステップです。パーサーはソースコードを読み込み、抽象構文木(AST)を構築します。この過程で構文エラーが検出された場合、ユーザーに正確なエラーメッセージとエラーが発生したコード上の位置(行番号、列番号など)を報告する必要があります。

このコミット以前は、goステートメントやdeferステートメントにおいて、関数呼び出しが期待される場所で実際には関数が呼び出されていない(例: go fではなくgo f()とすべきところをgo fと記述した場合)場合に、エラーメッセージは出力されるものの、そのエラーが指し示す位置が不正確であるという問題がありました。具体的には、エラーが報告されるべき場所(関数名の直後)ではなく、ステートメントの開始位置など、より広範な範囲を指してしまうことがありました。

また、パーサーがASTノードの終了位置を計算する際に、トークンの開始位置に長さを加算することで終了位置を導出します。しかし、パースエラーが発生した場合など、トークンの開始位置自体が不正な値であると、計算された終了位置がファイルの有効な範囲(EOF以降)を超えてしまう可能性がありました。このような無効な位置情報が後続の処理で利用されると、パニック(プログラムの異常終了)を引き起こす原因となり得ました。

これらの問題を解決し、よりユーザーフレンドリーなエラー報告と、パーサーの堅牢性を向上させることがこのコミットの目的です。

前提知識の解説

  • goステートメント: Go言語の並行処理を記述するためのキーワードです。goキーワードの後に続く関数呼び出しを新しいゴルーチン(軽量スレッド)として実行します。例: go myFunction()
  • deferステートメント: Go言語の遅延実行を記述するためのキーワードです。deferキーワードの後に続く関数呼び出しを、その関数が属する関数の終了時(return時、パニック時など)に実行するようにスケジュールします。例: defer file.Close()
  • token.Pos: Go言語のgo/tokenパッケージで定義されている型で、ソースコード内の位置を表します。通常、ファイル内のバイトオフセットとして表現されます。
  • ast.BadExpr / ast.BadStmt: Go言語のgo/astパッケージで定義されているASTノードです。パーサーが構文エラーを検出した際に、不正な式やステートメントの代わりにこれらのノードをASTに挿入します。これにより、エラーが発生してもパーサーが処理を続行し、可能な限り多くのエラーを一度に報告できるようになります。FromフィールドとToフィールドを持ち、不正なコードの範囲を示します。
  • scanner.ScanComments: go/scannerパッケージのオプションで、コメントもスキャン対象に含めることを指定します。これにより、テストハーネスがソースコード内の特別なコメント(例: /* ERROR "rx" */)を解析して、期待されるエラーメッセージと位置を抽出できます。
  • 正規表現 (Regular Expression): テキスト内のパターンを記述するための強力なツールです。このコミットでは、テストハーネスがエラーコメントから期待されるエラーメッセージを抽出するために使用しています。

技術的詳細

このコミットは、主に以下の2つの技術的改善を導入しています。

  1. safePos関数の導入: parser.gosafePosという新しいメソッドが追加されました。この関数は、与えられたtoken.Posが有効なファイル位置であるかを検証し、もし無効であればファイルのEOF(End Of File)位置を返します。

    • 動作原理: p.file.Offset(pos)を呼び出すことで、posp.file(現在のファイル)の有効な範囲内にあるかを間接的にチェックします。Offsetメソッドは、無効な位置が与えられた場合にパニックを引き起こす可能性があります。safePosdeferrecoverを使ってこのパニックを捕捉し、パニックが発生した場合はp.file.Base() + p.file.Size()(ファイルのEOF位置)を返します。これにより、ASTノードのToフィールドなどに設定される位置情報が常に有効な範囲内に収まることが保証されます。
    • 適用箇所: ast.BadExprast.BadStmtToフィールドを設定する際に、既存のx.End()list[n-1].End()の代わりにp.safePos(...)が使用されるようになりました。これにより、パースエラーによって生成される不正なASTノードの範囲が、ファイルの有効な範囲を超えないようになります。
  2. error_test.goのテストハーネスの拡張: エラー位置のテストをより正確に行うために、error_test.goのテストハーネスが拡張されました。

    • /* ERROR HERE "rx" */コメントの導入: 新しいコメント形式/* ERROR HERE "rx" */が導入されました。これは、エラーメッセージが特定のトークンの直後に発生する場合(例: go ffの直後)に、そのトークンの終了位置をエラー位置として指定するために使用されます。
    • here変数の追加: expectedErrors関数内にhereという新しいtoken.Pos変数が追加されました。これは、直前の非コメント・非セミコロンのトークンの直後の位置(つまり、そのトークンの終了位置)を追跡します。
    • トークン長の計算: scanner.Scan()で取得したトークン(tok)とリテラル(lit)から、そのトークンの長さを計算し、prev(トークンの開始位置)に加算することでhereの位置を更新します。これにより、ERROR HEREコメントが正確なエラー位置を指し示すことができるようになりました。
    • 正規表現の更新: errRx正規表現が更新され、HEREキーワードをオプションで捕捉できるようになりました。

これらの変更により、パーサーはより正確なエラー位置を報告できるようになり、特にgodeferステートメントにおける関数呼び出しの欠落といった一般的な構文エラーに対して、開発者が問題を特定しやすくなりました。また、無効な位置情報によるパニックの可能性も排除され、パーサー全体の堅牢性が向上しています。

コアとなるコードの変更箇所

src/pkg/go/parser/error_test.go

  • errRx正規表現の変更: ^/\* *ERROR *(HERE)? *\"([^\"]*)\" *\*/$
  • expectedErrors関数内の変更:
    • var here token.Posの追加。
    • if len(s) == 3ブロック内でs[1] == "HERE"の場合にpos = hereを設定するロジックの追加。
    • defaultケースでhereの位置を計算するロジックの追加:
      			var l int // token length
      			if tok.IsLiteral() {
      				l = len(lit)
      			} else {
      				l = len(tok.String())
      			}
      			here = prev + token.Pos(l)
      

src/pkg/go/parser/parser.go

  • safePosメソッドの追加:
    func (p *parser) safePos(pos token.Pos) (res token.Pos) {
    	defer func() {
    		if recover() != nil {
    			res = token.Pos(p.file.Base() + p.file.Size()) // EOF position
    		}
    	}()
    	_ = p.file.Offset(pos) // trigger a panic if position is out-of-range
    	return pos
    }
    
  • ast.BadExprast.BadStmtToフィールド設定箇所でsafePosの利用:
    • parseFieldDecl: typ = &ast.BadExpr{From: pos, To: p.safePos(list[n-1].End())}
    • checkExpr: x = &ast.BadExpr{From: x.Pos(), To: p.safePos(x.End())}
    • checkExprOrType: x = &ast.BadExpr{From: x.Pos(), To: p.safePos(x.End())}
    • makeExpr: return &ast.BadExpr{From: s.Pos(), To: p.safePos(s.End())}
    • parseForStmt: return &ast.BadStmt{From: pos, To: p.safePos(body.End())}
    • parseReceiver: {Type: &ast.BadExpr{From: recv.Pos(), To: p.safePos(recv.End())}},
  • parseCallExprのシグネチャ変更とエラーメッセージの改善:
    • func (p *parser) parseCallExpr() *ast.CallExpr から func (p *parser) parseCallExpr(callType string) *ast.CallExpr へ変更。
    • エラーメッセージをp.errorExpected(x.Pos(), "function/method call")からp.error(p.safePos(x.End()), fmt.Sprintf("function must be invoked in %s statement", callType))へ変更。エラー位置にp.safePos(x.End())を使用し、エラーメッセージにcallType("go"または"defer")を含めるようにした。
  • parseGoStmtparseDeferStmtでのparseCallExprの呼び出し変更:
    • call := p.parseCallExpr() から call := p.parseCallExpr("go")
    • call := p.parseCallExpr() から call := p.parseCallExpr("defer")

src/pkg/go/parser/short_test.go

  • 新しいテストケースの追加:
    	`package p; func f() { go f /* ERROR HERE "function must be invoked" */ }`,
    	`package p; func f() { defer func() {} /* ERROR HERE "function must be invoked" */ }`,
    	`package p; func f() { go func() { func() { f(x func /* ERROR "expected ')'" */ (){}) } } }`,
    

コアとなるコードの解説

このコミットの核心は、パーサーがエラーを報告する際の「位置」の正確性を向上させることにあります。

  1. safePos関数: この関数は、GoのパーサーがASTノードの終了位置を計算する際に、その位置がファイルの有効な範囲内にあることを保証するための安全装置として機能します。パーサーは通常、トークンの開始位置にそのトークンの長さを加算することで終了位置を導出しますが、パースエラーによって開始位置自体が不正な場合、計算結果の終了位置がファイルの実際の終端を超えてしまうことがあります。safePosはこのような「人工的な」無効な位置を検出し、代わりにファイルのEOF位置を返すことで、後続の処理でのパニックを防ぎます。これは、deferrecoverを用いたGo特有のエラーハンドリングパターンを効果的に利用しています。

  2. parseCallExprの変更とgo/deferステートメントのエラー報告: 以前は、go fdefer fのように関数が呼び出されていない場合、エラーメッセージは「function/method callが期待される」という一般的なもので、エラー位置もステートメントの開始位置を指すことがありました。 この変更により、parseCallExprcallType("go"または"defer")を受け取るようになり、エラーメッセージが「goステートメントでは関数が呼び出されなければならない」のように、より具体的になりました。 さらに重要なのは、エラー位置がp.safePos(x.End())、つまり関数名(f)の直後を指すようになったことです。これにより、開発者はコードのどこを修正すべきか、より直感的に理解できるようになります。

  3. error_test.goのテストハーネスの拡張: /* ERROR HERE "rx" */コメントとhere変数の導入は、エラー位置のテストを非常に強力にしました。これにより、エラーが特定のトークンの「直後」に発生する場合でも、その正確な位置をテストケースで指定できるようになりました。これは、パーサーが報告するエラー位置の粒度を細かくし、テストの信頼性を高める上で不可欠な変更です。

これらの変更は、Go言語のツールチェーンが提供するエラーメッセージの品質を向上させ、開発者がより効率的にコードの誤りを修正できるようにするための重要な改善です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (go.dev)
  • Go言語のソースコード (github.com/golang/go)
  • Go言語のgo/parserパッケージのドキュメント
  • Go言語のgo/tokenパッケージのドキュメント
  • Go言語のgo/astパッケージのドキュメント
  • Go言語のgo/scannerパッケージのドキュメント
  • Go言語のdeferステートメントに関する公式ブログ記事やチュートリアル
  • Go言語のgoステートメント(ゴルーチン)に関する公式ブログ記事やチュートリアル
  • 正規表現に関する一般的な情報源