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

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

このコミットは、Go言語の実験的なEBNFリンター/検証ツールである exp/ebnflint パッケージにおいて、Go言語の仕様書 (go_spec.html) のEBNF定義を go test コマンドで直接テストできるようにするための変更を導入しています。これにより、カスタムのMakefileを使用する必要がなくなり、Goの標準的なテストワークフローに統合されます。

コミット

commit 4417bc3742a11b48a0ae07b883c9a0ed5d064e4b
Author: Russ Cox <rsc@golang.org>
Date:   Mon Jan 23 16:35:25 2012 -0500

    exp/ebnflint: test spec during 'go test'
    
    This avoids the need for a custom Makefile.
    
    R=gri
    CC=golang-dev
    https://golang.org/cl/5575045

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

https://github.com/golang/go/commit/4417bc3742a11b48a0ae07b883c9a0ed5d064e4b

元コミット内容

このコミットの目的は、exp/ebnflint ツールがGo言語の仕様書に含まれるEBNF定義を go test コマンドの一部として検証できるようにすることです。以前は、この検証を行うためにカスタムのMakefileが必要でしたが、この変更によりその必要がなくなります。

変更の背景

Go言語の開発プロセスにおいて、言語仕様の正確性は極めて重要です。Go言語の文法はEBNF(Extended Backus-Naur Form)で定義されており、このEBNF定義が正しく、かつ一貫していることを検証するツールが exp/ebnflint です。

このコミット以前は、exp/ebnflint を使ってGo言語仕様のEBNFを検証するには、おそらく特定のコマンドライン引数を与えたり、あるいは専用のMakefileスクリプトを実行する必要がありました。これは、Goプロジェクトの標準的な開発・テストワークフローから逸脱しており、CI/CDパイプラインへの統合や、開発者がローカルでテストを実行する際の利便性を損ねていました。

go test コマンドはGo言語の標準的なテスト実行ツールであり、Goエコシステムにおけるテストのデファクトスタンダードです。このツールにEBNF検証を統合することで、以下のメリットが生まれます。

  1. ワークフローの簡素化: 開発者は go test を実行するだけで、コードの単体テストとEBNF仕様の検証の両方を行えるようになります。
  2. CI/CDとの統合: 自動化されたビルドおよびテストシステムにおいて、EBNF検証を容易に組み込むことができます。
  3. 一貫性: Goプロジェクト全体でテスト実行方法の一貫性が保たれます。
  4. 依存関係の削減: カスタムMakefileの管理や、それに伴う潜在的な環境依存の問題を回避できます。

この変更は、Go言語のツールチェインと開発ワークフローの成熟を示すものであり、Go言語仕様の品質保証プロセスをより堅牢かつ効率的にするための重要なステップでした。

前提知識の解説

EBNF (Extended Backus-Naur Form)

EBNFは、プログラミング言語やマークアップ言語などの文法を記述するためのメタ言語(言語を記述するための言語)です。BNF(Backus-Naur Form)の拡張版であり、より簡潔で読みやすい記述が可能です。

  • 目的: 言語の構文規則を厳密かつ曖昧さなく定義するために使用されます。コンパイラやインタプリタのパーサー(構文解析器)を生成する際の基盤となります。
  • 基本的な要素:
    • 終端記号 (Terminal Symbols): 言語の実際の要素(キーワード、演算子、識別子など)。例: if, +, myVar
    • 非終端記号 (Non-terminal Symbols): 終端記号や他の非終端記号の組み合わせで定義される抽象的な構文カテゴリ。例: Expression, Statement
    • 規則 (Rules): 非終端記号がどのように構成されるかを定義します。NonTerminal = Definition . の形式で記述されます。
  • EBNFの主な記法:
    • = : 定義。
    • . : 規則の終わり。
    • | : 選択(OR)。例: A | B はAまたはB。
    • () : グループ化。
    • [] : オプション(0回または1回)。例: [ A ] はAがあってもなくてもよい。
    • {} : 繰り返し(0回以上)。例: { A } はAが0回以上繰り返される。
    • "" または '' : リテラル文字列(終端記号)。
    • /* ... */ : コメント。

Go言語の仕様書では、Go言語の構文がEBNFで記述されており、例えば以下のような規則が見られます。

SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .

これは、「SourceFilePackageClause の後にセミコロン、その後に0個以上の ImportDecl とセミコロンの繰り返し、さらにその後に0個以上の TopLevelDecl とセミコロンの繰り返しが続く」ことを意味します。

go test コマンド

go test はGo言語の標準的なテスト実行ツールです。

  • 機能:
    • Goパッケージ内のテスト関数(TestXxxBenchmarkXxxExampleXxx)を自動的に発見し、実行します。
    • テスト結果(成功/失敗、実行時間、カバレッジなど)を報告します。
    • 並行テスト実行、テストのフィルタリング、ベンチマークテストなど、豊富な機能を提供します。
  • テストファイルの命名規則: テストファイルは通常、テスト対象のソースファイルと同じディレクトリに配置され、ファイル名の末尾が _test.go となります。
  • テスト関数の命名規則: テスト関数は func TestXxx(t *testing.T) の形式で定義されます。*testing.T はテストの状態を管理し、エラー報告などを行うためのオブジェクトです。
  • 実行方法: パッケージのルートディレクトリで go test を実行すると、そのパッケージ内のすべてのテストが実行されます。

このコミットでは、ebnflint の検証ロジックを go test が認識するテスト関数 (TestSpec) としてラップすることで、標準的なテストワークフローに統合しています。

技術的詳細

このコミットの技術的な核心は、ebnflint のEBNF検証ロジックを再構築し、それをGoの標準テストフレームワーク (testing パッケージ) に適合させる点にあります。

  1. main 関数のリファクタリングと verify 関数の導入:

    • 元の ebnflint.gomain 関数は、コマンドライン引数からファイル名や標準入力を受け取り、EBNFのパースと検証を行っていました。このロジックは、コマンドラインツールとしての振る舞いを前提としていました。
    • このコミットでは、EBNFのパースと検証の中核ロジックを verify という新しい関数に切り出しています。
    • func verify(name, start string, r io.Reader) error というシグネチャを持つ verify 関数は、検証対象の名前 (name)、開始規則 (start)、そして入力データを提供する io.Reader (r) を引数として受け取ります。
    • io.Reader を受け取ることで、ファイルだけでなく、標準入力やメモリ上のデータなど、様々なソースからEBNF定義を読み込むことが可能になります。これは、テストケースで特定の文字列を検証する際にも非常に便利です。
    • rnil の場合、verify 関数は name をファイルパスとして解釈し、os.Open を使ってファイルを開くロジックを含んでいます。これにより、既存のファイルパス指定の動作も維持されます。
    • main 関数は、コマンドライン引数の解析後、この新しい verify 関数を呼び出すように変更されています。
  2. io.Reader インターフェースの活用:

    • Go言語の io.Reader インターフェースは、データを読み込むための抽象化されたインターフェースです。ファイル (os.File)、ネットワーク接続、標準入力 (os.Stdin)、バイトスライス (bytes.Buffer) など、多くのGoのI/Oソースがこのインターフェースを実装しています。
    • verify 関数が io.Reader を引数として受け取ることで、ebnflint の検証ロジックは特定の入力ソースに依存しなくなります。これは、テストコードからGo言語仕様のHTMLファイルを os.Open で開いて io.Reader として渡すことを可能にし、柔軟性を高めます。
  3. ebnflint_test.go の新規作成と TestSpec 関数の実装:

    • src/pkg/exp/ebnflint/ebnflint_test.go という新しいテストファイルが作成されました。Goのテストフレームワークは、_test.go で終わるファイル内の TestXxx 関数を自動的に発見し実行します。
    • このファイルには func TestSpec(t *testing.T) というテスト関数が定義されています。
    • TestSpec 関数内では、runtime.GOROOT() + "/doc/go_spec.html" を使ってGoのインストールディレクトリにある公式仕様書HTMLファイルのパスを取得しています。
    • このパスと、Go言語のEBNFの開始規則である "SourceFile"、そして nilio.Reader を引数として、新しく定義された verify 関数を呼び出しています。
    • verify 関数がエラーを返した場合、t.Fatal(err) を呼び出してテストを失敗させ、エラーメッセージを出力します。これにより、Go言語仕様のEBNFに問題があれば、go test の実行時に即座に検出されるようになります。

この一連の変更により、ebnflint のEBNF検証機能がGoの標準テストスイートにシームレスに統合され、開発者はより簡単にGo言語仕様のEBNFの健全性を確認できるようになりました。

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

src/pkg/exp/ebnflint/ebnflint.go

--- a/src/pkg/exp/ebnflint/ebnflint.go
+++ b/src/pkg/exp/ebnflint/ebnflint.go
@@ -11,6 +11,7 @@ import (
 	"fmt"
 	"go/scanner"
 	"go/token"
+	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -76,34 +77,46 @@ func main() {
 	flag.Parse()
 
 	var (
-		filename string
-		src      []byte
-		err      error
+		name string
+		r    io.Reader
 	)
 	switch flag.NArg() {
 	case 0:
-		filename = "<stdin>"
-		src, err = ioutil.ReadAll(os.Stdin)
+		name, r = "<stdin>", os.Stdin
 	case 1:
-		filename = flag.Arg(0)
-		src, err = ioutil.ReadFile(filename)
+		name = flag.Arg(0)
 	default:
 		usage()
 	}
-	if err != nil {
+
+	if err := verify(name, *start, r); err != nil {
 		report(err)
 	}
-
-	if filepath.Ext(filename) == ".html" || bytes.Index(src, open) >= 0 {
-		src = extractEBNF(src)
+
+func verify(name, start string, r io.Reader) error {
+	if r == nil {
+		f, err := os.Open(name)
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+		r = f
 	}
 
-	grammar, err := ebnf.Parse(filename, bytes.NewBuffer(src))
+	src, err := ioutil.ReadAll(r)
 	if err != nil {
-		report(err)
+		return err
 	}
 
-	if err = ebnf.Verify(grammar, *start); err != nil {
-		report(err)
+	if filepath.Ext(name) == ".html" || bytes.Index(src, open) >= 0 {
+		src = extractEBNF(src)
 	}
+
+	grammar, err := ebnf.Parse(name, bytes.NewBuffer(src))
+	if err != nil {
+		return err
+	}
+
+	return ebnf.Verify(grammar, start)
 }

src/pkg/exp/ebnflint/ebnflint_test.go (新規ファイル)

--- /dev/null
+++ b/src/pkg/exp/ebnflint/ebnflint_test.go
@@ -0,0 +1,16 @@
+// 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
+
+import (
+	"runtime"
+	"testing"
+)
+
+func TestSpec(t *testing.T) {
+	if err := verify(runtime.GOROOT()+"/doc/go_spec.html", "SourceFile", nil); err != nil {
+		t.Fatal(err)
+	}
+}

コアとなるコードの解説

ebnflint.go の変更点

  1. io パッケージのインポート: io.Reader を使用するために io パッケージが追加されました。
  2. main 関数のリファクタリング:
    • filenamesrc 変数が name (string) と r (io.Reader) に置き換えられました。
    • コマンドライン引数に応じて namer を設定するロジックが変更されました。標準入力の場合は name"<stdin>"ros.Stdin に設定します。ファイル名が指定された場合は name のみを設定し、rnil のままにします。
    • EBNFのパースと検証の実際の処理は、新しく導入された verify 関数に委譲されるようになりました。main 関数は verify 関数が返すエラーを report 関数で処理するだけになります。
  3. verify 関数の新規追加:
    • この関数は、EBNFの検証ロジックの新しいエントリポイントです。
    • if r == nil ブロック: io.Reader が提供されていない場合(つまり、ファイルパスが指定された場合)、os.Open(name) を使ってファイルを読み込み用に開きます。エラーが発生した場合はそれを返します。defer f.Close() でファイルが確実に閉じられるようにします。
    • src, err := ioutil.ReadAll(r): 提供された io.Reader からすべてのデータを読み込みます。これにより、ファイル、標準入力、またはテストコードから渡された任意の io.Reader からデータを取得できます。
    • filepath.Ext(name) == ".html" || bytes.Index(src, open) >= 0: 読み込んだデータがHTMLファイルであるか、またはEBNFの開始マーカー (open) を含む場合は、extractEBNF 関数を呼び出してHTMLからEBNF部分を抽出します。これは、Go言語仕様がHTML形式でEBNFを含んでいるためです。
    • grammar, err := ebnf.Parse(name, bytes.NewBuffer(src)): 抽出されたEBNFソースをパースして文法ツリーを構築します。
    • return ebnf.Verify(grammar, start): 構築された文法ツリーと指定された開始規則 (start) を使ってEBNFの検証を実行し、結果のエラーを返します。

ebnflint_test.go の新規追加

  1. パッケージ宣言: package main となっており、ebnflint 実行可能ファイルと同じパッケージに属することを示します。これにより、ebnflint.go で定義された verify 関数を直接呼び出すことができます。
  2. インポート:
    • runtime: runtime.GOROOT() を使用してGoのインストールディレクトリのパスを取得するためにインポートされます。
    • testing: Goの標準テストフレームワークであり、テスト関数を定義するために必要です。
  3. TestSpec 関数の定義:
    • func TestSpec(t *testing.T): go test コマンドによって自動的に発見され実行されるテスト関数です。*testing.T はテストユーティリティを提供します。
    • if err := verify(runtime.GOROOT()+"/doc/go_spec.html", "SourceFile", nil); err != nil:
      • runtime.GOROOT() + "/doc/go_spec.html": Goのインストールディレクトリにある公式のGo言語仕様書HTMLファイルへのパスを構築します。
      • "SourceFile": Go言語のEBNFの開始規則です。
      • nil: io.Readernil で渡すことで、verify 関数内でファイルパスから読み込むように指示します。
      • verify 関数を呼び出し、返されたエラーを err に格納します。
    • t.Fatal(err): verify 関数がエラーを返した場合(つまり、Go言語仕様のEBNFに問題があった場合)、t.Fatal を呼び出してテストを即座に失敗させ、エラーメッセージを出力します。

これらの変更により、ebnflint は単なるコマンドラインツールから、Goの標準テストスイートの一部として機能するモジュールへと進化しました。

関連リンク

参考にした情報源リンク