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

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

このコミットは、Go言語の標準ツールである go fix コマンドに新しい修正 (cryptotype fix) を追加するものです。go fix は、Go言語のバージョンアップに伴うAPIの変更や非推奨になった機能に対して、既存のGoソースコードを自動的に更新するためのユーティリティです。この特定の修正は、crypto パッケージにおける型変更に対応し、ユーザーのコードが新しいAPIに準拠するように変換することを目的としています。

具体的には、crypto/aes および crypto/des パッケージ内の具体的な暗号型(例: *aes.Cipher, *des.Cipher, *des.TripleDESCipher)が、より汎用的な crypto/cipher.Block インターフェース型に統一された変更に対応しています。これにより、ユーザーは具体的な実装型ではなく、インターフェースを通じて暗号ブロックを扱うことが推奨されるようになります。

コミット

commit d3f9f21fdfbb8d1c7784a16825bd504bf7f04eee
Author: Russ Cox <rsc@golang.org>
Date:   Mon Feb 13 16:01:34 2012 -0500

    fix: add fix for crypto type change
    
    Fixes #2905.
    
    R=golang-dev, bradfitz, r
    CC=golang-dev
    https://golang.org/cl/5645088

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

https://github.com/golang/go/commit/d3f9f21fdfbb8d1c7784a16825bd504bf7f04eee

元コミット内容

fix: add fix for crypto type change

Fixes #2905.

R=golang-dev, bradfitz, r
CC=golang-dev
https://golang.org/cl/5645088

変更の背景

この変更の背景には、Go言語の crypto パッケージにおけるAPI設計の進化があります。初期のGo言語では、crypto/aescrypto/des のような具体的な暗号アルゴリズムの実装が、それぞれ独自の具体的な型(例: *aes.Cipher)を公開していました。しかし、より柔軟で汎用的な暗号処理を可能にするため、Goチームは crypto/cipher パッケージに Block インターフェースを導入し、具体的な暗号実装がこのインターフェースを満たすように変更しました。

この変更の目的は、ユーザーが特定の暗号アルゴリズムの実装詳細に依存することなく、Block インターフェースを通じて統一的に暗号ブロックを扱えるようにすることです。これにより、コードの再利用性、モジュール性、および将来の互換性が向上します。

しかし、このAPI変更は既存のコードベースに影響を与えます。具体的に *aes.Cipher*des.Cipher などの型を直接使用していたコードは、コンパイルエラーとなる可能性があります。この問題を解決し、ユーザーが手動でコードを修正する手間を省くために、go fix ツールにこの自動修正機能が追加されました。コミットメッセージにある Fixes #2905 は、この型変更に関連するGoのIssue 2905を修正するものであることを示しています。

前提知識の解説

go fix ツール

go fix は、Go言語のソースコードを自動的に更新するためのコマンドラインツールです。Go言語は後方互換性を非常に重視していますが、言語や標準ライブラリの進化に伴い、APIの変更や非推奨化が行われることがあります。go fix は、このような変更に対応するために、古いAPIの使用箇所を新しいAPIに自動的に書き換える機能を提供します。これにより、開発者は手動で大量のコードを修正する手間を省き、Goの新しいバージョンへの移行をスムーズに行うことができます。

go fix は、Goの抽象構文木(AST)を解析し、定義された「修正(fix)」ルールに基づいてコードを変換します。各修正は、特定のGoのバージョンやAPI変更に対応しており、go fix を実行すると、適用可能なすべての修正がコードに適用されます。

Goの crypto パッケージと cipher.Block インターフェース

Go言語の crypto パッケージは、暗号化、ハッシュ、署名などの暗号関連機能を提供します。このパッケージは、様々な暗号アルゴリズム(AES, DESなど)の実装を含んでいます。

crypto/cipher パッケージは、暗号ブロックの操作に関する汎用的なインターフェースを定義しています。その中でも特に重要なのが Block インターフェースです。

package cipher

// Block is an interface to a block cipher in ECB mode.
type Block interface {
	BlockSize() int
	Encrypt(dst, src []byte)
	Decrypt(dst, src []byte)
}

このインターフェースは、ブロック暗号が満たすべき基本的な操作(ブロックサイズ、暗号化、復号化)を定義しています。これにより、具体的な暗号アルゴリズム(例: AES)の実装は、この Block インターフェースを満たすことで、より抽象的なレベルで扱えるようになります。

例えば、以前は *aes.Cipher 型を直接使用していたコードは、この変更により cipher.Block 型として扱うことができるようになります。これは、Goのインターフェースの強力な機能の一つであり、ポリモーフィズムを実現し、コードの柔軟性と拡張性を高めます。

Goにおける型エイリアスとリファクタリング

Go言語では、型エイリアス(type alias)や構造体のフィールド変更など、様々なリファクタリングが行われることがあります。特に、ライブラリのAPIが成熟するにつれて、より良い設計や汎用性を目指して型定義が変更されることがあります。

このコミットのケースでは、具体的な暗号実装型から汎用インターフェース型への変更は、GoのAPI設計における一般的なリファクタリングパターンです。これにより、ユーザーは実装の詳細に縛られずに、より抽象的なインターフェースを通じて機能を利用できるようになります。

技術的詳細

このコミットは、go fix ツールがどのように動作し、特定の型変更を自動的に修正するかを示しています。主要な変更点は以下の通りです。

  1. src/cmd/fix/cryptotype.go の追加:

    • このファイルは、cryptotype という新しい fix ルールを定義しています。
    • cryptotypeFix 変数は fix 構造体として定義され、修正の名前、適用日、そして実際の修正ロジック (renameFix(cryptotypeReplace)) を含んでいます。
    • cryptotypeReplacerename 構造体のスライスであり、具体的な型置換のルールを定義しています。
      • OldImport: "crypto/aes", NewImport: "crypto/cipher", Old: "*aes.Cipher", New: "cipher.Block": crypto/aes をインポートしている場合に、*aes.Ciphercipher.Block に置き換え、必要に応じて crypto/cipher をインポートに追加します。
      • OldImport: "crypto/des", NewImport: "crypto/cipher", Old: "*des.Cipher", New: "cipher.Block": 同様に *des.Ciphercipher.Block に置き換えます。
      • OldImport: "crypto/des", NewImport: "crypto/cipher", Old: "*des.TripleDESCipher", New: "cipher.Block": 同様に *des.TripleDESCiphercipher.Block に置き換えます。
  2. src/cmd/fix/cryptotype_test.go の追加:

    • このファイルは、cryptotype 修正のテストケースを定義しています。
    • cryptotypeTeststestCase のスライスであり、修正前 (In) と修正後 (Out) のコードスニペットを含んでいます。これにより、go fix が正しく型を変換し、必要なインポートを追加・削除できることを検証します。
  3. src/cmd/fix/fix.go の変更:

    • fix.gogo fix ツールのコアロジックを含むファイルです。
    • renameFix 関数が追加されました。この関数は []rename スライスを受け取り、*ast.File を引数にとる func(*ast.File) bool 型の関数を返します。この返された関数が、ASTを走査して型置換を実行する実際のロジックです。
    • parseName 関数は、"pkg.Type" または "*pkg.Type" 形式の文字列を解析し、ポインタ型かどうか、パッケージ名、型名を抽出します。
    • renameFixTab 関数は、rename ルールに基づいてASTを走査し、型置換を実行する中心的なロジックです。
      • imports(f, t.OldImport) で古いインポートが存在するかを確認します。
      • walk(f, func(n interface{}) {...}) を使用してASTを走査し、*ast.Expr 型のノードをチェックします。
      • isPkgDot(x, opkg, onam) で、現在のASTノードが置換対象の型(例: aes.Cipher)であるかを確認します。
      • 条件が一致した場合、addImport(f, t.NewImport) で新しいインポート(例: crypto/cipher)を追加し、*np = expr(t.New) でASTノードを新しい型(例: cipher.Block)に置き換えます。
      • 最後に、usesImport(f, ipath) で古いインポートがもはや使用されていないことを確認し、deleteImport(f, ipath) で不要なインポートを削除します。
    • killPos 関数が追加されました。これはASTノードから位置情報(token.Pos)を削除するためのヘルパー関数です。テストケースでASTを比較する際に、位置情報の違いによる不一致を避けるために使用されます。

これらの変更により、go fixcrypto パッケージの型変更に自動的に対応できるようになり、開発者は手動でのコード修正なしにGoの新しいバージョンに移行できるようになります。

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

src/cmd/fix/cryptotype.go

// Copyright 2012 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

var cryptotypeFix = fix{
	"cryptotype",
	"2012-02-12",
	renameFix(cryptotypeReplace),
	`Rewrite uses of concrete cipher types to refer to the generic cipher.Block.

http://codereview.appspot.com/5625045/
`,
}

var cryptotypeReplace = []rename{
	{
		OldImport: "crypto/aes",
		NewImport: "crypto/cipher",
		Old:       "*aes.Cipher",
		New:       "cipher.Block",
	},
	{
		OldImport: "crypto/des",
		NewImport: "crypto/cipher",
		Old:       "*des.Cipher",
		New:       "cipher.Block",
	},
	{
		OldImport: "crypto/des",
		NewImport: "crypto/cipher",
		Old:       "*des.TripleDESCipher",
		New:       "cipher.Block",
	},
}

src/cmd/fix/cryptotype_test.go

// Copyright 2012 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

func init() {
	addTestCases(cryptotypeTests, cryptotypeFix.f)
}

var cryptotypeTests = []testCase{
	{
		Name: "cryptotype.0",
		In: `package main

import (
	"crypto/aes"
	"crypto/des"
)

var (
	_ *aes.Cipher
	_ *des.Cipher
	_ *des.TripleDESCipher
	_ = aes.New()
)
`,
		Out: `package main

import (
	"crypto/aes"
	"crypto/cipher"
)

var (
	_ cipher.Block
	_ cipher.Block
	_ cipher.Block
	_ = aes.New()
)
`,
	},
}

src/cmd/fix/fix.go

// ... (既存のimport文) ...
import (
	"fmt"
	"go/ast"
	"go/token"
	"os"
	"path"
	"reflect" // 追加
	"strconv"
	"strings"
)

// ... (既存のexpr関数) ...

var posType = reflect.TypeOf(token.Pos(0))

func killPos(v reflect.Value) {
	switch v.Kind() {
	case reflect.Ptr, reflect.Interface:
		if !v.IsNil() {
			killPos(v.Elem())
		}
	case reflect.Slice:
		n := v.Len()
		for i := 0; i < n; i++ {
			killPos(v.Index(i))
		}
	case reflect.Struct:
		n := v.NumField()
		for i := 0; i < n; i++ {
			f := v.Field(i)
			if f.Type() == posType {
				f.SetInt(0)
				continue
			}
			killPos(f)
		}
	}
}

// A Rename describes a single renaming.
type rename struct {
	OldImport string // only apply rename if this import is present
	NewImport string // add this import during rewrite
	Old       string // old name: p.T or *p.T
	New       string // new name: p.T or *p.T
}

func renameFix(tab []rename) func(*ast.File) bool {
	return func(f *ast.File) bool {
		return renameFixTab(f, tab)
	}
}

func parseName(s string) (ptr bool, pkg, nam string) {
	i := strings.Index(s, ".")
	if i < 0 {
		panic("parseName: invalid name " + s)
	}
	if strings.HasPrefix(s, "*") {
		ptr = true
		s = s[1:]
		i--
	}
	pkg = s[:i]
	nam = s[i+1:]
	return
}

func renameFixTab(f *ast.File, tab []rename) bool {
	fixed := false
	added := map[string]bool{}
	check := map[string]bool{}
	for _, t := range tab {
		if !imports(f, t.OldImport) {
			continue
		}
		optr, opkg, onam := parseName(t.Old)
		walk(f, func(n interface{}) {
			np, ok := n.(*ast.Expr)
			if !ok {
				return
			}
			x := *np
			if optr {
				p, ok := x.(*ast.StarExpr)
				if !ok {
					return
				}
				x = p.X
			}
			if !isPkgDot(x, opkg, onam) {
				return
			}
			if t.NewImport != "" && !added[t.NewImport] {
				addImport(f, t.NewImport)
				added[t.NewImport] = true
			}
			*np = expr(t.New)
			check[t.OldImport] = true
			fixed = true
		})
	}

	for ipath := range check {
		if !usesImport(f, ipath) {
			deleteImport(f, ipath)
		}
	}
	return fixed
}

コアとなるコードの解説

src/cmd/fix/cryptotype.go

このファイルは、go fix ツールが適用する新しい修正ルールを定義しています。

  • cryptotypeFix: これは fix 構造体のインスタンスで、この修正に関するメタデータ(名前、適用日、説明)と、実際に修正を行う関数 (renameFix(cryptotypeReplace)) を保持しています。renameFix は、後述する rename ルールに基づいてコードを書き換えるための汎用的な関数です。
  • cryptotypeReplace: これは rename 構造体のスライスです。各 rename エントリは、特定の型置換ルールを定義します。
    • OldImport: このインポートパスが存在する場合にのみ、この置換ルールを適用します。例えば、"crypto/aes" がインポートされている場合にのみ、*aes.Cipher の置換を試みます。
    • NewImport: 置換が適用された際に、このインポートパスをコードに追加します。例えば、"crypto/cipher" が追加されます。
    • Old: 置換対象となる古い型名です。"*aes.Cipher" のようにポインタ型も指定できます。
    • New: 置換後の新しい型名です。"cipher.Block" のように、パッケージプレフィックスを含む完全な型名が指定されます。

この定義により、go fixcrypto/aescrypto/des の具体的な暗号型が使用されている箇所を検出し、それらを crypto/cipher.Block インターフェース型に自動的に書き換えることができます。

src/cmd/fix/cryptotype_test.go

このファイルは、cryptotype 修正が正しく機能するかを検証するための単体テストです。

  • init() 関数: addTestCases を呼び出し、cryptotypeTests で定義されたテストケースを cryptotypeFix.f (修正関数) に関連付けます。これにより、go fix のテストフレームワークがこの修正をテスト対象として認識します。
  • cryptotypeTests: testCase 構造体のスライスです。
    • Name: テストケースの名前。
    • In: 修正前のGoソースコードの文字列。この例では、crypto/aescrypto/des をインポートし、それぞれの具体的な暗号型を変数宣言で使用しています。
    • Out: 修正後の期待されるGoソースコードの文字列。*aes.Cipher*des.Ciphercipher.Block に置き換えられ、crypto/cipher が新しいインポートとして追加されていることが期待されます。

このテストケースは、go fixcryptotype 修正を適用した際に、コードが正しく変換され、必要なインポートが追加され、不要なインポートが削除されることを保証します。

src/cmd/fix/fix.go

このファイルには、go fix ツールの汎用的な修正ロジックが追加されています。

  • killPos(v reflect.Value):

    • この関数は、GoのASTノードから位置情報(token.Pos)を再帰的に削除します。
    • go/ast パッケージで生成されるASTノードには、ソースコード内の位置を示す情報が含まれています。しかし、テストでASTを比較する際、位置情報が異なると比較が失敗する可能性があります。
    • この関数は reflect パッケージを使用して、AST構造を走査し、token.Pos 型のフィールドを見つけてその値をゼロに設定します。これにより、位置情報に依存しないASTの比較が可能になります。
  • rename 構造体:

    • cryptotype.go で使用されている rename 構造体の定義です。OldImport, NewImport, Old, New の各フィールドは、型置換のルールを詳細に記述します。
  • renameFix(tab []rename) func(*ast.File) bool:

    • これは高階関数で、rename ルールのスライス (tab) を受け取り、実際の修正ロジックをカプセル化した関数を返します。
    • 返される関数は *ast.File (GoソースファイルのAST) を引数にとり、修正が行われたかどうかを示す bool を返します。
  • parseName(s string) (ptr bool, pkg, nam string):

    • "pkg.Type" または "*pkg.Type" の形式の文字列を解析し、それがポインタ型であるか (ptr)、パッケージ名 (pkg)、および型名 (nam) を抽出します。
    • 例えば、"*aes.Cipher" を解析すると、ptr=true, pkg="aes", nam="Cipher" が返されます。
  • renameFixTab(f *ast.File, tab []rename) bool:

    • この関数が、実際の型置換ロジックの中心です。
    • tab で定義された各 rename ルールについてループします。
    • imports(f, t.OldImport) を呼び出して、現在のファイルが古いインポートパス(例: "crypto/aes")を使用しているかを確認します。使用していない場合は、このルールはスキップされます。
    • walk(f, func(n interface{}) {...}) を使用して、ファイルのAST全体を走査します。walkgo/ast パッケージのヘルパー関数で、ASTの各ノードを訪問するために使用されます。
    • 走査中に、*ast.Expr 型のノード(式を表すノード)をチェックします。
    • optr (古い型がポインタ型か) に応じて、*ast.StarExpr (ポインタ式) を処理します。
    • isPkgDot(x, opkg, onam) を使用して、現在の式が pkg.Type の形式であり、かつ置換対象の古い型名と一致するかを確認します。
    • 条件がすべて一致した場合、以下の処理を行います。
      • t.NewImport != "" かつ added[t.NewImport]false の場合、addImport(f, t.NewImport) を呼び出して新しいインポートパス(例: "crypto/cipher")をファイルに追加します。added マップは、同じインポートを複数回追加しないようにするために使用されます。
      • *np = expr(t.New) を使用して、ASTノードを新しい型名に置き換えます。expr 関数は文字列からAST式を生成するヘルパー関数です。
      • check[t.OldImport] = true を設定し、この古いインポートが修正によって影響を受けたことを記録します。
      • fixed = true を設定し、ファイルが修正されたことを示します。
    • すべての rename ルールが処理された後、check マップに記録された古いインポートパスについてループします。
    • !usesImport(f, ipath) を使用して、修正後にその古いインポートパスがファイル内で使用されなくなったかを確認します。
    • 使用されなくなった場合、deleteImport(f, ipath) を呼び出して、そのインポート文をファイルから削除します。
    • 最終的に、fixed の値を返します。

これらの関数とロジックの組み合わせにより、go fix はGoソースコード内の特定の型を自動的に検出し、新しい型に置き換え、関連するインポート文を適切に管理することができます。これは、Go言語の進化に伴うAPI変更への対応を自動化するための強力なメカニズムです。

関連リンク

参考にした情報源リンク