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

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

このコミットは、Go言語のgofixツールにおけるエラー処理の修正と改善に焦点を当てています。特に、os.Errorインターフェースから組み込みのerrorインターフェースへの移行を支援するための変更が中心です。この修正により、gofixはより多くのコードパターンを認識し、自動的に新しいエラー処理の慣習に適合させる能力が向上しました。

コミット

commit 758200f219641b2ca8af1a5264456a72124a1b21
Author: Russ Cox <rsc@golang.org>
Date:   Tue Nov 1 21:45:21 2011 -0400

    gofix: error fix
    
    To make the error fix more useful, expand typecheck to gather
    more information about struct fields, typecheck range statements,
    typecheck indirect and index of named types, and collect information
    about assignment conversions.
    
    Also, change addImport to rename top-level uses of a to-be-imported
    identifier to avoid conflicts.  This duplicated some of the code in
    the url fix, so that fix is now shorter.
    
    R=iant, r, r
    CC=golang-dev
    https://golang.org/cl/5305066

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

https://github.com/golang/go/commit/758200f219641b2ca8af1a5264456a72124a1b21

元コミット内容

このコミットの主な目的は、gofixツールの「エラー修正」機能を強化することです。具体的には、以下の改善が行われました。

  1. 型チェックの拡張: 構造体フィールドに関する情報の収集、rangeステートメントの型チェック、名前付き型の間接参照とインデックスの型チェック、および代入変換に関する情報の収集を行うようにtypecheck関数が拡張されました。これにより、gofixはより複雑なコード構造におけるエラー関連のパターンを正確に識別できるようになります。
  2. インポート時の名前衝突回避: addImport関数が変更され、インポートされる識別子とトップレベルの既存の識別子との名前衝突を避けるために、既存の識別子をリネームするようになりました。これにより、url修正における重複コードが削減され、コードベースがより簡潔になりました。

変更の背景

このコミットが行われた2011年11月は、Go言語がまだ比較的新しく、活発に開発が進められていた時期です。Go言語の初期バージョンでは、エラー処理にos.Errorというインターフェースが使用されていました。しかし、Go 1のリリースに向けて、エラー処理のメカニズムがより汎用的な組み込みのerrorインターフェースへと変更されることになりました。

この変更は、Go言語のエラー処理をよりシンプルで一貫性のあるものにするための重要なステップでした。しかし、既存のGoコードベースはos.Errorを使用しているものが多く、これらのコードを新しいerrorインターフェースに移行させる必要がありました。

gofixは、Go言語のバージョンアップに伴うコードの自動修正を支援するためのツールです。このコミットは、gofixos.Errorからerrorへの移行をより効果的に処理できるようにするための機能強化の一環として行われました。特に、os.Errorを使用しているコードが多岐にわたるため、gofixがより多くのコンテキストを理解し、正確な修正を適用できるように、型チェックのロジックを大幅に改善する必要がありました。

また、addImport関数の改善は、gofixがコードを修正する際に発生しうる名前衝突の問題を解決し、より堅牢な自動修正を可能にすることを目的としています。

前提知識の解説

Go言語のエラー処理の変遷 (os.Errorからerrorへ)

Go言語の初期のバージョンでは、エラーを表すためにos.Errorというインターフェースが使われていました。これは以下のように定義されていました。

package os

type Error interface {
    String() string
}

このインターフェースは、エラーメッセージを文字列として返すString()メソッドを持っていました。しかし、Go 1のリリースに向けて、エラー処理の設計が見直され、よりシンプルで組み込みのerrorインターフェースが導入されました。

package builtin

type error interface {
    Error() string
}

主な変更点は、メソッド名がString()からError()に変わったこと、そしてosパッケージに依存しない組み込みのインターフェースになったことです。この変更により、エラー処理が言語のより基本的な部分に統合され、より柔軟なエラー表現が可能になりました。

gofixツール

gofixは、Go言語のコードベースを新しいGoのバージョンや慣習に合わせて自動的に修正するためのコマンドラインツールです。Go言語の進化に伴い、APIの変更や言語仕様の変更が発生することがあります。gofixは、これらの変更に対応するために、古いコードパターンを新しいものに自動的に書き換える機能を提供します。これにより、開発者は手動で大量のコードを修正する手間を省き、スムーズに新しいバージョンに移行することができます。

GoのAST (Abstract Syntax Tree)

Go言語のコンパイラやツール(gofixも含む)は、Goのソースコードを直接操作するのではなく、その抽象構文木(AST: Abstract Syntax Tree)を操作します。ASTは、ソースコードの構造を木構造で表現したものです。各ノードは、変数宣言、関数呼び出し、式などのコード要素に対応します。

gofixは、ソースコードをASTにパースし、ASTを走査(walk)しながら特定のパターンを見つけ、そのパターンに対応するASTノードを修正します。修正後、ASTは再びソースコードに変換(プリント)されます。このコミットでは、go/astパッケージを使用してASTを操作し、コードの変更を行っています。

型チェック (Type Checking)

プログラミング言語における型チェックは、プログラムが型規則に従っているかを確認するプロセスです。Go言語のような静的型付け言語では、コンパイル時に型チェックが行われ、型の不一致などのエラーが検出されます。

gofixのようなツールがコードを自動修正する際には、単に文字列置換を行うだけでは不十分な場合があります。コードのセマンティクス(意味)を理解し、正しい型変換やリネームを行うためには、型情報が必要です。このコミットでは、gofix内部の型チェッカーが拡張され、より詳細な型情報を収集できるようになりました。これにより、gofixはよりインテリジェントなコード修正が可能になります。

技術的詳細

このコミットは、gofixerror修正ロジックを大幅に強化しています。

error.goの追加とerrorFnのロジック

src/cmd/gofix/error.goが新規追加され、errorFixというfix構造体が定義されています。このfix構造体は、gofixが実行する特定の修正(この場合はエラー関連の修正)を定義します。

errorFn関数は、このエラー修正の主要なロジックを含んでいます。この関数は、GoのAST(*ast.File)を受け取り、ファイル内のos.Error関連のパターンを識別し、修正を適用します。

errorFnの主要な処理は以下の通りです。

  1. osパッケージのインポートチェック: まず、ファイルがosパッケージをインポートしているか、または-force=errorフラグが指定されているかを確認します。これにより、不要な修正の実行を避けます。
  2. ヒューリスティックな修正の適用:
    • errorという名前のトップレベル識別子のリネーム: ファイル内にerrorという名前のトップレベル関数、変数、または定数がある場合、それらをerror_にリネームします。これは、組み込みのerrorインターフェースとの名前衝突を避けるためです。
    • 型チェックの実行: typecheck関数を呼び出し、ファイル内の型情報を収集します。この型チェックは、os.Errorの実装を特定するために重要です。
    • os.Error実装の特定: typecheckの結果と、errType"Error"または".*Error"にマッチする正規表現)を使用して、os.Errorインターフェースを実装している型を特定します。これには、os.Errorに代入される値の型や、名前にErrorを含む型が含まれます。
    • メソッドのリネーム: os.Errorを実装する型(またはそのポインタ)のメソッドについて、以下のリネームを行います。
      • String()メソッドをError()にリネームします。これは、os.Errorから組み込みerrorへの移行で、エラーメッセージを返すメソッド名が変わったためです。
      • Error()メソッドをErr()にリネームします。これは、String()Error()にリネームされた結果、既存のError()メソッドと衝突するのを避けるためです。
    • 構造体フィールドのリネーム: os.Errorを実装する型の構造体フィールドで、Errorという名前のフィールドがある場合、それをErrにリネームします。これもメソッド名との衝突を避けるためです。
    • セレクタ式のリネーム: os.Error実装型の値に対するセレクタ式(例: myError.String())について、.String.Errorに、.Error.Errにリネームします。
    • 複合リテラルのフィールドリネーム: os.Error型の複合リテラル(例: MyError{Error: someValue})において、Error:というフィールド名をErr:にリネームします。
    • 型アサーションの修正: err.(*os.Waitmsg)のような型アサーションをerr.(*exec.ExitError)に修正します。これは、os.Waitmsgexec.ExitErrorに置き換えられたことによるものです。この修正には、execパッケージのインポートも含まれます。
  3. 基本的な書き換えの適用:
    • os.Errorerrorに書き換えます。
    • os.NewErrorerrors.Newに書き換え、必要に応じてerrorsパッケージをインポートします。
    • os.EOFio.EOFに書き換え、必要に応じてioパッケージをインポートします。
  4. osパッケージの削除: 修正が適用され、かつファイル内でosパッケージが使用されなくなった場合、osパッケージのインポートを削除します。

typecheck.goの拡張

src/cmd/gofix/typecheck.gotypecheck関数が大幅に拡張されています。

  • Type構造体のDefフィールド追加: Type構造体にDefフィールドが追加され、名前付き型の定義(例: type MyInt intの場合のint)を保持できるようになりました。
  • assignマップの導入: typecheck関数は、typeofマップに加えてassignマップを返すようになりました。assignマップは、特定の型に代入された式を追跡するために使用されます。これは、os.Errorの実装を特定するヒューリスティックなロジックで活用されます。
  • 構造体フィールド情報の収集: typecheckは、構造体宣言を走査し、そのフィールド名と型をType構造体のFieldマップに収集するようになりました。これにより、gofixは構造体フィールドのリネーム(例: ErrorからErrへのリネーム)を正確に行うことができます。
  • rangeステートメントの型チェック: typecheck1関数内でast.RangeStmtの型チェックロジックが追加されました。これにより、rangeループのキーと値の型が正しく推論され、必要に応じて修正が適用されます。
  • 間接参照とインデックスの型チェックの改善: ast.IndexExpr(インデックスアクセス)とast.StarExpr(ポインタの間接参照)の型チェックロジックが改善され、名前付き型の場合でも正確な型推論が行われるようになりました。expand関数が導入され、名前付き型の基底型を展開して型情報を取得できるようになっています。

fix.gorenameTopaddImportの変更

  • renameTop関数の追加: renameTop関数が新しく追加されました。この関数は、指定された古い名前(old)を持つトップレベルの識別子(関数、変数、定数、インポート名など)を新しい名前(new)にリネームします。これは、errorという名前のトップレベル識別子をerror_にリネームするために使用されます。
  • addImportの改善: addImport関数が変更され、インポートを追加する際に、インポートされるパッケージ名と既存のトップレベル識別子との名前衝突を自動的に解決するようになりました。具体的には、インポートされるパッケージ名と同じ名前のトップレベル識別子が存在する場合、その識別子を_サフィックスを付けてリネームします。この変更により、url修正など、他の修正ロジックで重複していた名前衝突解決のコードが削減されました。

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

src/cmd/gofix/error.go (新規追加)

このファイルは、os.Errorからerrorへの移行に関するすべての修正ロジックをカプセル化しています。

// Copyright 2011 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"go/ast"
	"regexp"
	"strings"
)

func init() {
	fixes = append(fixes, errorFix)
}

var errorFix = fix{
	"error",
	errorFn,
	`Use error instead of os.Error.
...
`,
}

// ... (errVar, errType, errorTypeConfig の定義) ...

func errorFn(f *ast.File) bool {
	if !imports(f, "os") && !force["error"] {
		return false
	}

	var fixed bool
	var didHeuristic bool
	heuristic := func() {
		if didHeuristic {
			return
		}
		didHeuristic = true

		// Rename error to error_ to make room for error.
		fixed = renameTop(f, "error", "error_") || fixed

		// Use type checker to build list of error implementations.
		typeof, assign := typecheck(errorTypeConfig, f)

		// ... (isError, isErrorImpl, isErrorVar のロジック) ...

		walk(f, func(n interface{}) {
			// In method declaration on error implementation type,
			// rename String() to Error() and Error() to Err().
			// ...
			// In type definition of an error implementation type,
			// rename Error field to Err to make room for method.
			// ...
			// For values that are an error implementation type,
			// rename .Error to .Err and .String to .Error
			// ...
			// Assume x.Err is an error value and rename .String to .Error
			// ...
			// For values that are an error variable, rename .String to .Error.
			// ...
			// Rewrite composite literal of error type to turn Error: into Err:
			// ...
			// Rename os.Waitmsg to exec.ExitError
			// when used in a type assertion on an error.
			// ...
		})
	}

	// ... (fix, force["error"] のロジック) ...

	walk(f, func(n interface{}) {
		p, ok := n.(*ast.Expr)
		if !ok {
			return
		}
		sel, ok := (*p).(*ast.SelectorExpr)
		if !ok {
			return
		}
		switch {
		case isPkgDot(sel, "os", "Error"):
			fix()
			*p = &ast.Ident{NamePos: sel.Pos(), Name: "error"}
		case isPkgDot(sel, "os", "NewError"):
			fix()
			addImport(f, "errors")
			sel.X.(*ast.Ident).Name = "errors"
			sel.Sel.Name = "New"
		case isPkgDot(sel, "os", "EOF"):
			fix()
			addImport(f, "io")
			sel.X.(*ast.Ident).Name = "io"
		}
	})

	if fixed && !usesImport(f, "os") {
		deleteImport(f, "os")
	}

	return fixed
}

// ... (typeName の定義) ...

src/cmd/gofix/typecheck.go (変更)

typecheck関数のシグネチャと内部ロジックが変更され、より詳細な型情報を収集できるようになりました。

// ... (Type struct の変更) ...

// It returns two maps with type information:
// typeof maps AST nodes to type information in gofmt string form.
// assign maps type strings to lists of expressions that were assigned
// to values of another type that were assigned to that type.
func typecheck(cfg *TypeConfig, f *ast.File) (typeof map[interface{}]string, assign map[string][]interface{}) {
	typeof = make(map[interface{}]string)
	assign = make(map[string][]interface{})
	cfg1 := &TypeConfig{}
	*cfg1 = *cfg // make copy so we can add locally

	// ... (関数宣言の収集) ...

	// gather struct declarations
	for _, decl := range f.Decls {
		d, ok := decl.(*ast.GenDecl)
		if ok {
			for _, s := range d.Specs {
				switch s := s.(type) {
				case *ast.TypeSpec:
					// ... (構造体フィールド情報の収集ロジック) ...
				}
			}
		}
	}

	typecheck1(cfg1, f, typeof, assign)
	return typeof, assign
}

// ... (typecheck1 の変更) ...

func typecheck1(cfg *TypeConfig, f interface{}, typeof map[interface{}]string, assign map[string][]interface{}) {
	// set sets the type of n to typ.
	// If isDecl is true, n is being declared.
	set := func(n ast.Expr, typ string, isDecl bool) {
		if typeof[n] != "" || typ == "" {
			if typeof[n] != typ {
				assign[typ] = append(assign[typ], n) // <-- assign マップへの追加
			}
			return
		}
		typeof[n] = typ
	}

	// ... (expand 関数の追加) ...

	walkBeforeAfter(f, before, after)
}

src/cmd/gofix/fix.go (変更)

renameTop関数が追加され、addImport関数が変更されました。

// ... (既存のコード) ...

// renameTop renames all references to the top-level name top.
// It returns true if it makes any changes.
func renameTop(f *ast.File, old, new string) bool {
	var fixed bool

	// Rename any conflicting imports
	// ...
	// Rename any top-level declarations.
	// ...
	// Rename top-level old to new, both unresolved names
	// ...
	return fixed
}

// addImport adds the import path to the file f, if absent.
func addImport(f *ast.File, ipath string) {
	if imports(f, ipath) {
		return
	}

	// Determine name of import.
	// Assume added imports follow convention of using last element.
	_, name := path.Split(ipath)

	// Rename any conflicting top-level references from name to name_.
	renameTop(f, name, name+"_") // <-- 名前衝突解決ロジックの追加

	newImport := &ast.ImportSpec{
		Path: &ast.BasicLit{
			Kind:  token.STRING,
			Value: strconv.Quote(ipath),
		},
	}

	// ... (既存のインポート追加ロジック) ...
}

コアとなるコードの解説

error.goerrorFn

errorFnは、gofixos.Error関連のコードを修正する際の中心的な関数です。

  • ヒューリスティックなアプローチ: この関数は、単なる文字列置換ではなく、ASTと型情報に基づいたヒューリスティックなアプローチを採用しています。これは、os.Errorからerrorへの移行が単なる名前の変更だけでなく、関連するメソッドや構造体フィールドの変更も伴うためです。
  • renameTopの活用: renameTop(f, "error", "error_")は、ファイル内のerrorという名前のトップレベル識別子をerror_にリネームすることで、組み込みのerrorインターフェースとの名前衝突を回避します。これは、gofixがコードを修正する際に、既存のコードのセマンティクスを壊さないようにするための重要なステップです。
  • 型チェックとos.Error実装の特定: typecheck(errorTypeConfig, f)は、ファイル内の型情報を収集し、os.Errorインターフェースを実装している型を特定します。isErrorImpl関数は、この情報と正規表現(errType)を組み合わせて、os.Errorの実装をより広範に識別します。これにより、gofixは、明示的にos.Errorとして宣言されていないが、そのように振る舞う型(例: 名前にErrorを含む構造体)も修正対象とすることができます。
  • メソッドとフィールドのリネーム: walk関数内でASTを走査し、os.Error実装型のString()メソッドをError()に、Error()メソッドをErr()にリネームします。同様に、構造体フィールドのErrorErrにリネームします。これらのリネームは、Goのエラーインターフェースの変更に合わせたものです。
  • 型アサーションの修正: err.(*os.Waitmsg)のような特定の型アサーションをerr.(*exec.ExitError)に修正するロジックは、Goの標準ライブラリの変更に対応しています。これは、os.Waitmsgexec.ExitErrorに置き換えられたためです。
  • 基本的な置換: isPkgDot関数とswitch文を使用して、os.Erroros.NewErroros.EOFといった古い識別子を、それぞれerrorerrors.Newio.EOFに置き換えます。この際、必要に応じてerrorsioパッケージをインポートします。

typecheck.gotypechecktypecheck1

これらの関数は、gofixがコードのセマンティクスを理解するための基盤を提供します。

  • assignマップ: assignマップの導入は、型チェックの精度を向上させます。これにより、ある型が別の型に代入される際に、その代入がどのような型変換を伴うかを追跡できます。これは、os.Errorの実装をヒューリスティックに特定する際に役立ちます。
  • 構造体フィールド情報の収集: 構造体宣言からフィールド名と型を収集する機能は、gofixが構造体フィールドのリネーム(例: ErrorからErr)を正確に行うために不可欠です。
  • rangeステートメントの型チェック: rangeステートメントの型チェックロジックの追加は、gofixrangeループ内の変数(キーと値)の型を正しく推論し、それに応じて修正を適用できるようにします。
  • expand関数: expand関数は、名前付き型の基底型(例: type MyInt intの場合のint)を取得するために使用されます。これにより、型チェックがより汎用的になり、名前付き型に対しても正確な型情報を取得できるようになります。

fix.gorenameTopaddImport

これらの関数は、gofixがコードを修正する際のユーティリティ機能を提供します。

  • renameTop: この関数は、トップレベルの名前衝突を解決するための汎用的なメカニズムを提供します。errorFnerrorerror_にリネームする際に使用されるだけでなく、他のgofixの修正でも名前衝突を避けるために再利用できます。
  • addImportの自動名前衝突解決: addImport関数が、インポートを追加する際に自動的に名前衝突を解決するようになったのは重要な改善です。これにより、gofixはより堅牢になり、手動での介入なしにコードを修正できるようになります。以前は、url修正のように、各修正が独自の名前衝突解決ロジックを持つ必要がありましたが、この変更によりコードの重複が削減されました。

これらの変更は、gofixがGo言語の進化に対応し、より複雑なコードの自動修正を可能にするための重要なステップでした。特に、os.Errorからerrorへの移行はGo言語の大きな変更点の一つであり、gofixがこの移行をスムーズに行えるようにすることは、Goコミュニティにとって非常に価値のあることでした。

関連リンク

  • Go言語のerrorインターフェースに関する公式ドキュメント: https://pkg.go.dev/builtin#error
  • gofixツールの概要 (Goの公式ブログ記事など): 関連する公式ブログ記事やドキュメントは、Goのバージョンアップに伴うgofixの役割について説明している可能性があります。

参考にした情報源リンク