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

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

このコミットは、Go言語の静的解析ツールである go vet に、無意味な代入(useless assignments)を検出する新しいチェック機能を追加するものです。特に、expr = expr の形式の自己代入(self-assignment)を検出する機能が導入されました。これにより、プログラマが意図しないバグや冗長なコードを早期に発見できるようになります。

コミット

  • コミットハッシュ: a22361d68d6d93ab2b06e4b608e19796e033218b
  • 作者: David Symonds dsymonds@golang.org
  • コミット日時: 2013年3月6日(水) 09:55:04 +1100

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

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

元コミット内容

vet: check for useless assignments.

The only check so far is for self-assignments of the form "expr = expr",
but even that found one instance in the standard library.

R=r, adg, mtj, rsc
CC=golang-dev
https://golang.org/cl/7455048

変更の背景

Go言語の go vet ツールは、Goプログラムの潜在的なバグや疑わしい構成を検出するための静的解析ツールです。コードの品質と信頼性を向上させるために、様々なチェック機能が組み込まれています。

このコミットが導入された背景には、プログラマが意図せず行ってしまう「無意味な代入」や「自己代入」といった一般的なコーディングミスを自動的に検出したいというニーズがありました。特に x = x のような自己代入は、多くの場合、s.x = x のように構造体のフィールドに代入するべきところを間違えてローカル変数に代入してしまった、といった論理的な誤りを示唆します。このようなミスはコンパイルエラーにはならないため、実行時まで気づかれにくいバグとなり得ます。

go vet にこのチェックを追加することで、開発者はコードレビューやテストの前に、より早い段階でこれらの潜在的な問題を特定し、修正できるようになります。コミットメッセージにもあるように、このチェックは標準ライブラリ内でも実際に1つのインスタンスを発見しており、その有効性が示されています。

前提知識の解説

go vet とは

go vet は、Go言語のソースコードを静的に解析し、潜在的なバグや疑わしいコード構成を報告するツールです。コンパイラが検出できないが、実行時に問題を引き起こす可能性のあるパターン(例: フォーマット文字列の不一致、ロックの誤用、到達不能なコードなど)を特定します。go vet はGoの標準ツールチェーンの一部であり、go test コマンドの一部としても実行されます。

抽象構文木 (AST)

Goコンパイラや静的解析ツールは、ソースコードを直接扱うのではなく、その構造を抽象化した「抽象構文木(Abstract Syntax Tree, AST)」に変換して解析を行います。ASTは、プログラムの構造を木構造で表現したもので、各ノードがプログラムの要素(変数宣言、関数呼び出し、代入文など)に対応します。go vet はこのASTを走査し、特定のパターンに合致するノードを見つけることで、問題のあるコードを検出します。

go/ast パッケージ

go/ast パッケージは、Go言語のソースコードの抽象構文木(AST)を表現するための型と関数を提供します。このパッケージを使うことで、Goプログラムの構造をプログラム的に検査・操作することができます。例えば、ast.AssignStmt は代入文を表すASTノードの型です。

go/token パッケージ

go/token パッケージは、Go言語のトークン(キーワード、識別子、演算子など)とソースコード上の位置情報を定義します。例えば、代入演算子 =token.ASSIGN、短い変数宣言 :=token.DEFINE といった定数で表されます。

reflect パッケージ

reflect パッケージは、Goプログラムの実行時に型情報を検査したり、変数の値を動的に操作したりするための機能を提供します。このコミットでは、代入の左辺と右辺の型が一致するかどうかを reflect.TypeOf を使って確認しています。

自己代入 (Self-assignment)

自己代入とは、x = x のように、変数や式がそれ自身に代入される操作を指します。多くの場合、これはプログラマの意図しないミスであり、例えば s.x = x と書くべきところを x = x と書いてしまった、といったケースが考えられます。自己代入は通常、プログラムの動作に影響を与えませんが、コードの冗長性や潜在的なバグの兆候となるため、検出して修正することが望ましいとされています。

gofmt と式の比較

gofmt はGo言語のコードフォーマッタですが、このコミットの文脈では、f.gofmt(expr) のように、ASTノードをGoの標準フォーマットで文字列に変換する内部ヘルパー関数を指していると考えられます。これにより、代入の左辺と右辺の式が、フォーマットされた文字列として完全に一致するかどうかを比較し、自己代入を検出しています。単にASTノードのポインタを比較するのではなく、その表現が同じであるかを文字列比較で確認することで、より堅牢なチェックを実現しています。

技術的詳細

このコミットで導入された無意味な代入チェックは、go vet ツールに新しい assign フラグを追加し、ast.AssignStmt(代入文)を解析することで実現されています。

新しいチェックの核となるロジックは、src/cmd/vet/assign.go に追加された checkAssignStmt 関数にあります。この関数は、GoプログラムのASTを走査する際に、各代入文 (ast.AssignStmt) に対して呼び出されます。

checkAssignStmt 関数は以下のステップで無意味な代入を検出します。

  1. vet("assign") の確認: まず、vet ツールが assign チェックを有効にしているかを確認します。これは、go vet -assign のように明示的に指定された場合にのみチェックが実行されるようにするためです。
  2. 代入演算子の確認: 代入文のトークンが token.ASSIGN (=) であることを確認します。これは、短い変数宣言 (:=) を含む代入文を除外するためです。短い変数宣言は新しい変数を宣言するため、自己代入の文脈では通常問題になりません。
  3. 左右辺の要素数の確認: 代入の左辺 (stmt.Lhs) と右辺 (stmt.Rhs) の要素数が異なる場合、それらは同じ式である可能性がないため、チェックをスキップします。これは、多重代入 (a, b = c, d) のようなケースを効率的に処理するためです。
  4. 各代入ペアの検査: 左辺と右辺の要素数が同じ場合、各ペア (lhs, rhs) について以下のチェックを行います。
    • 型の比較: reflect.TypeOf(lhs) != reflect.TypeOf(rhs) を使用して、左辺と右辺のASTノードの型が異なる場合、比較をスキップします。これは、異なる型のノードが同じ文字列表現を持つ可能性が低いため、より重い gofmt による文字列比較を避けるための最適化です。
    • 文字列表現の比較: f.gofmt(lhs)f.gofmt(rhs) を呼び出し、左辺と右辺の式を標準フォーマットの文字列に変換します。そして、これらの文字列が完全に一致するかどうかを比較します (le == re)。
    • 警告の報告: 文字列表現が一致した場合、それは自己代入であると判断し、f.Warnf を使用して警告メッセージを報告します。警告メッセージには、どの式が自己代入されているかが含まれます(例: "self-assignment of x to x")。

このチェックは、go/ast パッケージを使用してASTを解析し、go/token パッケージで代入の種類を識別し、reflect パッケージで型の比較を行い、そして内部的な gofmt ヘルパー関数で式の文字列表現を比較するという、Goの標準ライブラリの機能を活用した典型的な静的解析の実装パターンを示しています。

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

このコミットによる主要なコード変更は以下の3つのファイルにわたります。

  1. src/cmd/vet/assign.go (新規ファイル)

    • 無意味な代入をチェックするための新しいロジックを含むファイルです。
    • checkAssignStmt 関数が定義されており、これが自己代入の検出を行います。
  2. src/cmd/vet/main.go

    • go vet ツールのメインエントリポイントとなるファイルです。
    • 新しい assign チェックを有効にするためのフラグ定義が report マップに追加されました。
    • ASTを走査する File.walkAssignStmt 関数内で、新しく追加された f.checkAssignStmt(stmt) が呼び出されるようになりました。これにより、すべての代入文が新しいチェックの対象となります。
  3. src/cmd/vet/test_assign.go (新規ファイル)

    • assign チェックの動作を検証するためのテストケースを含むファイルです。
    • x = xs.x = s.x のような自己代入のパターンが記述されており、// ERROR コメントによって go vet が正しく警告を出すことを期待しています。

コアとなるコードの解説

src/cmd/vet/assign.go

// Copyright 2013 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.

/*
This file contains the code to check for useless assignments.
*/

package main

import (
	"go/ast"
	"go/token"
	"reflect"
)

// TODO: should also check for assignments to struct fields inside methods
// that are on T instead of *T.

// checkAssignStmt checks for assignments of the form "<expr> = <expr>".
// These are almost always useless, and even when they aren't they are usually a mistake.
func (f *File) checkAssignStmt(stmt *ast.AssignStmt) {
	if !vet("assign") { // (1) 'assign'チェックが有効か確認
		return
	}
	if stmt.Tok != token.ASSIGN { // (2) 代入演算子が '=' か確認 (':=' は除外)
		return // ignore :=
	}
	if len(stmt.Lhs) != len(stmt.Rhs) { // (3) 左辺と右辺の要素数が一致するか確認
		// If LHS and RHS have different cardinality, they can't be the same.
		return
	}
	for i, lhs := range stmt.Lhs { // (4) 各代入ペアをループ
		rhs := stmt.Rhs[i]
		if reflect.TypeOf(lhs) != reflect.TypeOf(rhs) { // (5) ASTノードの型が一致するか確認
			continue // short-circuit the heavy-weight gofmt check
		}
		le := f.gofmt(lhs) // (6) 左辺の式を文字列に変換
		re := f.gofmt(rhs) // (7) 右辺の式を文字列に変換
		if le == re { // (8) 文字列が一致するか比較
			f.Warnf(stmt.Pos(), "self-assignment of %s to %s", re, le) // (9) 警告を報告
		}
	}
}

この checkAssignStmt 関数が、無意味な代入、特に自己代入を検出する中心的なロジックです。

  • (1) if !vet("assign"): go vet -assign のように、assign チェックがコマンドラインで明示的に有効にされている場合にのみ、以下の処理が実行されます。
  • (2) if stmt.Tok != token.ASSIGN: Go言語には = (代入) と := (短い変数宣言と代入) の2種類の代入があります。このチェックは = による代入のみを対象とし、:= は新しい変数を宣言するため、自己代入の文脈では通常問題にならないため無視されます。
  • (3) if len(stmt.Lhs) != len(stmt.Rhs): a, b = c, d のような多重代入の場合、左辺と右辺の要素数が一致しないと、それらが同じ式である可能性はないため、早期にリターンします。
  • (4) for i, lhs := range stmt.Lhs: 代入文が複数の要素を持つ場合(例: a, b = a, b)、各ペアについて個別にチェックを行います。
  • (5) if reflect.TypeOf(lhs) != reflect.TypeOf(rhs): 左辺と右辺のASTノードの型が異なる場合、それらが同じ式である可能性は非常に低いため、gofmt による重い文字列変換と比較をスキップします。これはパフォーマンス最適化のためです。
  • (6), (7) le := f.gofmt(lhs) / re := f.gofmt(rhs): ここが重要な部分です。f.gofmt は、与えられたASTノード(式)をGoの標準フォーマットに従って文字列に変換する内部ヘルパー関数です。これにより、x(x) のように見た目は異なるが意味は同じ式を、同じ文字列表現として比較できるようになります。
  • (8) if le == re: 左辺と右辺の式の文字列表現が完全に一致する場合、それは自己代入であると判断されます。
  • (9) f.Warnf(...): 自己代入が検出された場合、go vet は指定されたソースコード上の位置 (stmt.Pos()) と共に警告メッセージを出力します。メッセージには、どの式が自己代入されているかが示されます。

src/cmd/vet/main.go

// ... (省略) ...

var report = map[string]*bool{
	"all":        flag.Bool("all", true, "check everything; disabled if any explicit check is requested"),
	"assign":     flag.Bool("assign", false, "check for useless assignments"), // 新しいフラグの追加
	"atomic":     flag.Bool("atomic", false, "check for common mistaken usages of the sync/atomic package"),
	// ... (省略) ...
}

// ... (省略) ...

// walkAssignStmt walks an assignment statement
func (f *File) walkAssignStmt(stmt *ast.AssignStmt) {
	f.checkAssignStmt(stmt) // 新しいチェック関数の呼び出し
	f.checkAtomicAssignment(stmt)
}

// ... (省略) ...

main.go では、go vet のコマンドラインオプションを定義する report マップに、"assign" という新しいエントリが追加されました。これにより、ユーザーは go vet -assign コマンドでこの新しいチェックを明示的に有効にできるようになります。デフォルトでは false に設定されているため、go vet を引数なしで実行した場合にはこのチェックは実行されません(ただし、-all フラグが指定されている場合は実行されます)。

また、ASTを走査する際に代入文 (ast.AssignStmt) を処理する walkAssignStmt 関数内で、新しく定義された f.checkAssignStmt(stmt) が呼び出されるようになりました。これにより、go vet がソースコードを解析する際に、すべての代入文が assign チェックの対象となります。

src/cmd/vet/test_assign.go

// Copyright 2013 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.

// This file contains tests for the useless-assignment checker.

// +build vet_test

package main

type ST struct {
	x int
}

func (s *ST) SetX(x int) {
	// Accidental self-assignment; it should be "s.x = x"
	x = x // ERROR "self-assignment of x to x"
	// Another mistake
	s.x = s.x // ERROR "self-assignment of s.x to s.x"
}

このテストファイルは、新しい assign チェックが正しく機能することを確認するためのものです。 +build vet_test ディレクティブにより、このファイルは go test コマンドで通常はビルドされず、go vet のテストスイートの一部としてのみビルド・実行されます。

テストケースでは、x = x のような単純なローカル変数の自己代入と、s.x = s.x のような構造体フィールドの自己代入の例が示されています。各行の末尾にある // ERROR "..." コメントは、go vet がその行で指定されたエラーメッセージを報告することを期待していることを示します。これにより、go vet がこれらの一般的な自己代入のパターンを正確に検出できることが保証されます。

関連リンク

  • Go CL 7455048: https://golang.org/cl/7455048 (このコミットに対応するGoのコードレビューシステム上のチェンジリスト)

参考にした情報源リンク