メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

#74958accepted

go/scanner: add \\`(\\*Scanner).End()\\`

ステータス変更: likely_accept accepted

要約

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

概要

go/scannerパッケージにScanner.End()メソッドを追加する提案です。このメソッドは、直前にスキャンしたトークンの終了位置をtoken.Posとして返すことで、トークンの正確な終了位置を簡単かつ確実に取得できるようにします。

ステータス変更

likely_acceptaccepted
この提案は2026年1月21日に「likely accept」となり、1週間の最終コメント期間を経て2026年1月28日に正式承認されました。Proposal Review Groupは、token.NoPosを初期値とすることに合意し、一貫性のためにPosTokLitのアクセサメソッドを追加することも検討しましたが、それは価値がないと判断しました。

技術的背景

現状の問題点

現在、go/scannerでトークンの終了位置を取得する標準的な方法が存在しません。Scanner.Scan()は開始位置のみを返すため、開発者は以下のようなワークアラウンドを使用せざるを得ませんでした:

pos, tok, lit := s.Scan()
tokLength := len(lit)
if !tok.IsLiteral() && tok != token.COMMENT {
    tokLength = len(tok.String())
}
tokEnd := pos + token.Pos(tokLength)

しかし、このアプローチには以下の重大な問題があります:

  1. キャリッジリターン(\r)の扱い: コメントやraw string literalでは、\rが実際のリテラル文字列から除外されるため、len(lit)が実際のソース上の長さと一致せず、終了位置の計算が不正確になります。
  2. 人工的なセミコロン: ファイル末尾でimpliedSemi==trueの場合、実際のソースには存在しない人工的なSEMICOLONトークンが生成されます。これにより、トークン間の空白を検査しようとするコードが予期しないパニックを起こす可能性があります。
// 問題例: ファイル末尾の人工セミコロンでパニック
const src = "package a; var a int"
// ... スキャナ初期化 ...
prevEndOff := 0
for {
    pos, tok, lit := s.Scan()
    off := file.Offset(pos)
    white := src[prevEndOff:off] // tok == EOF時にパニック!
    // ...
}

提案された解決策

新しいEnd()メソッドをScanner型に追加します:

package scanner // go/scanner
// End returns the position immediately after the last scanned token.
// If Scanner.Scan has not been called yet, End returns token.NoPos.
func (s *Scanner) End() token.Pos {
    return s.lastTokEnd
}

このメソッドは、スキャナが内部で正確に把握している終了位置を直接返すため、上記のようなワークアラウンドが不要になります。

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

  1. トークンの正確な範囲取得: トークンの開始位置(Scan())と終了位置(End())を正確に取得でき、ソースコード解析ツールやリファクタリングツールの精度が向上します。
  2. トークン間の空白/コメント解析: トークン間の正確な空白やコメントを解析できるようになり、フォーマッタやリンターの実装が簡単になります。
  3. エラー報告の改善: より正確な位置情報により、ユーザーフレンドリーなエラーメッセージを生成できます。

コード例

// Before: 従来の書き方(不正確なワークアラウンド)
pos, tok, lit := s.Scan()
tokLength := len(lit)
if !tok.IsLiteral() && tok != token.COMMENT {
    tokLength = len(tok.String())
}
tokEnd := pos + token.Pos(tokLength) // \rを含む場合に不正確
// After: 新APIを使った書き方
pos, tok, lit := s.Scan()
tokEnd := s.End() // 常に正確な終了位置
// 実用例: トークン間の空白を正確に取得
prevEnd := token.NoPos
for {
    pos, tok, lit := s.Scan()
    if tok == token.EOF {
        break
    }
    if prevEnd != token.NoPos {
        // トークン間の空白を正確に取得
        whitespace := src[file.Offset(prevEnd):file.Offset(pos)]
        fmt.Printf("Whitespace: %q\n", whitespace)
    }
    prevEnd = s.End() // 正確な終了位置を保存
}

議論のハイライト

  • Pos()からEnd()への変更: 当初は「次のスキャン開始位置」を返すPos()メソッドが提案されましたが、@adonovanの提案により「直前のトークンの終了位置」を返すEnd()に変更されました。これにより、raw string literal内のセミコロンでスキャナが「後退」する問題を回避できます。
  • ScanWithEnd()の検討: ScanWithEnd() (start Pos, _ Token, _ string, end Pos)という新メソッドも検討されましたが、後方互換性とシンプルさを重視してEnd()メソッドが選択されました。
  • 初期値の決定: Scan()未呼び出し時の戻り値として、token.NoPosfile.Base()file.Base() + BOMの長さ、未定義の4つが検討され、最もシンプルなtoken.NoPosが採用されました。
  • go/parserへの影響: go/parserも同様の問題(\rによるEnd()の不正確さ)を抱えていますが、この提案はそれを直接解決するものではありません。ただし、go/scannerを内部的に使用することで、go/parserのコメント終了位置計算も改善される見込みです(CL 694615、738700、738701で対応)。
  • 関連Issue: #54941(raw string literal内のコメント順序問題)、#41197/#69860/#69861(\rによるEnd()位置のずれ)など、複数の既知問題の解決に寄与します。

関連リンク