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

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

このコミットは、Go言語の初期のコード整形ツール(prettyパッケージ)における重要な改善を含んでいます。主な目的は、コードの可読性を高めるために、構造体のフィールド型のアラインメントを改善し、コメントの整形機能を導入することです。特に、エラスティックタブストップアルゴリズムを実装し、これを用いてコード要素を動的に揃える機能が追加されました。また、テストスクリプトのバグ修正と、より高速なスモークテストの導入も行われています。

コミット

commit 3c2f0ae13294d2b818a28f98df372c9848fc1454
Author: Robert Griesemer <gri@golang.org>
Date:   Thu Nov 13 17:50:46 2008 -0800

    * pretty printing snapshot: towards printing comments nicely
    - implemented elastic tabstops algorithm, now correct and documented
    - first cut at printing comments (use -comments flag, disabled for now)
    - struct field types are now aligned (using elastic tab stops)
    - needs more fine-tuning
    
    * fixed a bug in test script
    * added quick smoke test to makefile and invoke it in run.bash
      instead of the full test
    
    R=r
    OCL=19220
    CL=19220

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

https://github.com/golang/go/commit/3c2f0ae13294d2b818a28f98df372c9848fc1454

元コミット内容

* pretty printing snapshot: towards printing comments nicely
- implemented elastic tabstops algorithm, now correct and documented
- first cut at printing comments (use -comments flag, disabled for now)
- struct field types are now aligned (using elastic tab stops)
- needs more fine-tuning

* fixed a bug in test script
* added quick smoke test to makefile and invoke it in run.bash
  instead of the full test

R=r
OCL=19220
CL=19220

変更の背景

Go言語の初期開発段階において、コードの可読性と一貫性を保証するための自動整形ツール(後のgofmt)の重要性は認識されていました。このコミットは、その整形ツールの「pretty printing snapshot」の一部として、特に以下の課題に対処するために行われました。

  1. コードのアラインメントの改善: 構造体のフィールド宣言など、複数の行にわたるコード要素を視覚的に揃えることは、コードの可読性を大幅に向上させます。従来の固定幅タブでは、異なる環境やエディタ設定で表示が崩れる問題がありました。これを解決するために、動的に列幅を調整する「エラスティックタブストップ」の導入が検討されました。
  2. コメントの適切な整形: ソースコード内のコメントは、その意図を伝える上で不可欠ですが、自動整形ツールがコメントをどのように扱うかは重要な課題です。コメントがコードの構造を壊さずに、かつ読みやすい形で出力されるようにするための機能が必要でした。
  3. 開発プロセスの効率化: テストスクリプトのバグ修正や、開発中のフルテストの代わりに高速なスモークテストを導入することで、開発サイクルを短縮し、より迅速なフィードバックを得ることを目指しました。

これらの変更は、Go言語のコードベース全体の品質と開発者の生産性を高めるための基盤を築くものでした。

前提知識の解説

Go言語の初期開発とgofmt

Go言語は、その設計思想の一つとして「シンプルさ」と「生産性」を掲げています。その一環として、コードのスタイルに関する議論を減らし、一貫したコードベースを維持するために、公式のコード整形ツールgofmtが開発されました。このコミットが行われた2008年11月は、Go言語がまだ一般に公開される前の非常に初期の段階であり、gofmtの原型となるprettyパッケージが開発されていました。この時期のコミットは、現在のgofmtの基礎となる重要な機能がどのように構築されていったかを示しています。

コードフォーマッタの役割

コードフォーマッタは、ソースコードのレイアウト(インデント、スペース、改行など)を自動的に調整するツールです。これにより、以下のような利点があります。

  • 一貫性: チーム内のすべてのコードが同じスタイルで書かれるため、誰が書いたコードでも読みやすくなります。
  • 可読性: 整然としたレイアウトは、コードの構造を理解しやすくし、バグの発見にも役立ちます。
  • レビューの効率化: スタイルに関する議論が不要になるため、コードレビューが本質的な内容に集中できます。

エラスティックタブストップ (Elastic Tabstops)

エラスティックタブストップは、Nick Gravgaardによって提唱された、タブ文字を用いたコードのアラインメントに関するアルゴリズムです。従来のタブストップが固定幅(例: 4文字または8文字)であるのに対し、エラスティックタブストップは、同じ「列」にある要素の幅に合わせて、タブの展開幅を動的に調整します。

概念: コード内でタブ文字 (\t) を使用して列を揃える際、その列の最も長い要素の幅に合わせて、すべての行のその列の幅が自動的に調整されます。これにより、異なるタブ幅設定のエディタで開いても、コードのアラインメントが崩れることがありません。

利点:

  • 優れた可読性: 構造体フィールド、変数宣言、コメントなどが美しく揃えられ、コードの視覚的な構造が明確になります。
  • エディタ設定への非依存性: 開発者個人のタブ幅設定に左右されず、常に意図したアラインメントが維持されます。
  • メンテナンス性の向上: 列の要素の長さを変更しても、自動的にアラインメントが調整されるため、手動での調整が不要になります。

このコミットでは、このエラスティックタブストップアルゴリズムがprettyパッケージに実装され、特に構造体フィールドの型のアラインメントに適用されました。

AST (Abstract Syntax Tree)

AST(抽象構文木)は、ソースコードの構文構造を木構造で表現したものです。コンパイラやインタープリタ、そしてコード整形ツールにおいて中心的な役割を果たします。

  • パーサー: ソースコードを解析し、ASTを構築します。
  • プリンター: ASTを受け取り、それを基に整形されたソースコードを生成します。

このコミットでは、prettyパッケージがGo言語のソースコードをASTとして内部的に表現し、そのASTを操作して整形された出力を生成しています。

スキャナー (Scanner) とパーサー (Parser)

  • スキャナー (Lexer/Tokenizer): ソースコードを読み込み、意味のある最小単位(トークン、例: 識別子、キーワード、演算子、リテラル、コメント)に分解します。
  • パーサー: スキャナーが生成したトークンのストリームを受け取り、言語の文法規則に従ってそれらを解析し、ASTを構築します。

このコミットでは、コメントの処理に関して、スキャナーとパーサーの両方に変更が加えられています。スキャナーはコメントをトークンとして識別し、パーサーはそれらのコメントをASTの一部として適切に処理し、プリンターに渡す役割を担います。

技術的詳細

エラスティックタブストップの実装 (printer.go)

このコミットの最も重要な技術的変更は、usr/gri/pretty/printer.goにおけるエラスティックタブストップアルゴリズムの実装です。

  • Buffer構造体の変更:

    • Bufferは、整形中のコードの行とセル(タブで区切られた部分)を保持するための構造体です。
    • 変更前はsegmentlinesのみでしたが、変更後はcell(現在のセル)、lines(行ごとのセルリスト)、widths(列ごとの幅リスト)を持つようになりました。
    • linesAST.List型で、各要素はAST.List(行)であり、その要素は文字列(セル)です。
    • widthsAST.List型で、各要素は整数(列幅)です。
  • Format関数の再帰的な実装:

    • Format(line0, line1 int)関数は、指定された行範囲[line0, line1)に対してエラスティックタブストップを適用します。
    • この関数は再帰的に動作し、各列の最適な幅を計算します。
    • column変数は現在処理している列を示し、b.widthsに計算された列幅が追加されていきます。
    • width変数は、現在の列におけるセルの最大幅を追跡します。
    • PrintLines関数を呼び出すことで、計算された幅に基づいて空白が挿入され、整形された行が出力されます。
  • PrintLines関数:

    • PrintLines(line0, line1 int)関数は、Bufferに格納された行を、b.widthsに格納された列幅に基づいて整形して出力します。
    • 各セルsの後に、b.widths.at(j).(int) - len(s)で計算された空白文字nsepが挿入されます。これにより、列が揃えられます。
  • Tab()Newline()メソッドの変更:

    • Tab()は、現在のcellの内容を現在の行のセルリストに追加し、cellをクリアします。
    • Newline()は、Tab()を呼び出して現在の行の最後のセルを確定した後、新しい行をBufferに追加します。
    • Newline()内で、現在の行が1つのセルしか持たない場合(つまり、最後のセルがその行の唯一の要素である場合)、Format関数が呼び出され、バッファの内容がフラッシュ(整形・出力)されます。これは、エラスティックタブストップの特性上、次の行の開始まで列幅が確定しないため、行が確定した時点で出力を行うためのロジックです。

コメントの取り扱い (parser.go, scanner.go, printer.go)

コメントの整形は、スキャナー、パーサー、プリンターの連携によって実現されます。

  • parser.goの変更:

    • Parser.Next()関数が変更され、連続するコメントを個別にAST.NewCommentとしてP.commentsリストに追加するようになりました。
    • 変更前は、連続するコメントを一つの文字列として結合していましたが、変更後は個々のコメントを独立したASTノードとして扱うことで、より柔軟な整形が可能になります。
  • scanner.goの変更:

    • Scanner.SkipWhitespace()関数が、空白文字をスキップするだけでなく、改行の位置をnlposとして返すようになりました。これは、コメントがどの位置(行頭、行の途中)にあるかを判断するために使用されます。
    • Scanner.ScanComment(nlpos int)関数が、コメントのテキストだけでなく、そのコメントがコードのどの位置にあるか(行頭、行の途中、空白のみの行)を示す情報を付加するようになりました。これは、コメント文字列の先頭に特別な文字( \n\t)を付加することで実現されます。
      • (スペース): コメントの前に空白以外の文字がある場合、または行の途中にコメントがある場合。
      • \n: コメントが行の先頭にある場合。
      • \t: コメントの前に空白のみがある場合。
    • Scan()関数がSkipWhitespace()から返されるnlposScanComment()に渡すようになりました。
  • printer.goのコメント処理ロジック:

    • Printer.String(pos int, s string)関数内で、comments.BVal()フラグが有効な場合、P.cpos(現在のコメントの位置)がpos(現在の文字列の位置)よりも小さい間、コメントを処理します。
    • comment.textの先頭文字( \n\t)に基づいてコメントの種類を分類し、適切な整形を行います。
      • の場合: 行コメント(//)であればTab()で次のセルに移動し、ブロックコメント(/*)であればスペースを挿入します。
      • \nまたは\tの場合: コメントが行頭にあるべきと判断し、必要であればNewline()を呼び出し、インデントを適用します。
    • コメントのテキスト自体はtext[1 : len(text)]として出力されます(先頭の分類文字は除外)。
    • 行コメント(//)の場合、その後にNewline()が強制され、次のコードが新しい行から始まるようにします。
    • -commentsフラグ(var comments = Flag.Bool("comments", false, nil, "enable printing of comments"))によって、コメントの出力が制御されます。デフォルトでは無効になっています。

構造体フィールドのアラインメント

エラスティックタブストップの具体的な適用例として、構造体フィールドの型がアラインされるようになりました。printer.goFields関数内で、各フィールドの型がタブで区切られたセルとして扱われ、エラスティックタブストップアルゴリズムによって自動的に揃えられます。

テスト関連の変更

  • src/run.bashusr/gri/pretty/Makefile:

    • run.bashスクリプトが、make testの代わりにmake smoketestを呼び出すように変更されました。
    • Makefilesmoketestターゲットが追加されました。これは./test.sh parser.goを実行するもので、フルテストよりも高速に基本的な機能を確認できます。
  • usr/gri/pretty/selftest2.goの追加:

    • この新しいファイルは、エラスティックタブストップとコメント整形機能をテストするためのサンプルコードを含んでいます。
    • 構造体定義(type T struct { ... })や変数宣言(var ( ... ))において、フィールドや値がアラインされることを確認できます。
    • ループ内の行コメント(// the indexなど)や、行末コメント(// limitなど)が含まれており、コメント整形が正しく機能するかを検証できます。

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

  • usr/gri/pretty/printer.go:

    • Buffer構造体の定義変更。
    • Buffer.Init(), Buffer.Tab(), Buffer.Newline(), Buffer.Print() メソッドのロジック変更。
    • Buffer.PrintLines() および Buffer.Format() メソッドの実装(エラスティックタブストップの核心)。
    • Printer.String() メソッドにおけるコメント処理ロジックの追加と変更。
    • Flag.Bool("comments", ...) の追加。
    • Printer.Tab() メソッドの追加。
    • Printer.Fields() メソッドでの P.Blank() から P.Tab() への変更。
    • Printer.Declaration() メソッドでの P.Blank() から P.Tab() への変更、および P.String(0, " = ") から P.String(0, "= ") への変更。
    • Printer.Program() メソッドでのバッファフラッシュロジックの変更。
  • usr/gri/pretty/parser.go:

    • Parser.Next() メソッドにおけるコメント処理ロジックの変更(連続コメントの個別追加)。
  • usr/gri/pretty/scanner.go:

    • Scanner.SkipWhitespace() の戻り値変更(nlposの追加)。
    • Scanner.ScanComment() の引数変更(nlposの追加)と、コメント文字列への分類文字の付加ロジック。
    • Scanner.Scan() メソッドでの nlpos の利用。
  • usr/gri/pretty/ast.go:

    • List.last() ヘルパー関数の追加。
  • usr/gri/pretty/selftest2.go:

    • 新規追加されたテストファイル。
  • src/run.bash:

    • make test から make smoketest への変更。
  • usr/gri/pretty/Makefile:

    • smoketest ターゲットの追加。
  • usr/gri/pretty/test.sh:

    • runtest 関数への引数 $2 の追加。

コアとなるコードの解説

usr/gri/pretty/printer.go

エラスティックタブストップの主要なロジックはBuffer構造体とそのメソッドに集約されています。

type Buffer struct {
	cell string;  // current cell (last cell in last line, not in lines yet)
	lines AST.List;  // list of lines; each line is a list of cells (strings)
	widths AST.List;  // list of column widths - (re-)used during formatting
}

func (b *Buffer) Format(line0, line1 int) {
	column := b.widths.len();
	
	last := line0;
	for this := line0; this < line1; this++ {
		line := b.Line(this);
		
		if column < line.len() - 1 {
			// cell exists in this column
			// (note that the last cell per line is ignored)
			
			// print unprinted lines until beginning of block
			b.PrintLines(last, this);
			last = this;
			
			// column block begin
			width := int(tabwith.IVal());  // minimal width
			for ; this < line1; this++ {
				line := b.Line(this);
				if column < line.len() - 1 {
					// cell exists in this column
					// update width
					w := len(line.at(column).(string)) + 1; // 1 = minimum space between cells
					if w > width {
						width = w;
					}
				} else {
					break
				}
			}
			// column block end

			// format and print all columns to the right of this column
			// (we know the widths of this column and all columns to the left)
			b.widths.Add(width);
			b.Format(last, this);
			b.widths.Pop();
			last = this;
		}
	}

	// print unprinted lines until end
	b.PrintLines(last, line1);
}

func (b *Buffer) PrintLines(line0, line1 int) {
	for i := line0; i < line1; i++ {
		line := b.Line(i);
		for j := 0; j < line.len(); j++ {
			s := line.at(j).(string);
			print(s);
			if j < b.widths.len() {
				nsep := b.widths.at(j).(int) - len(s);
				assert(nsep >= 0);
				PrintBlanks(nsep);
			} else {
				assert(j == b.widths.len());
			}
		}
		println();
	}
}

Format関数は再帰的に呼び出され、現在のcolumnにおけるセルの最大幅を計算し、その幅をb.widthsに追加します。その後、残りの行に対して再帰的にFormatを呼び出すことで、ネストされた列のアラインメントを処理します。PrintLines関数は、b.widthsに格納された計算済みの列幅に基づいて、各セルの後に必要な空白を挿入し、整形された行を出力します。

コメント処理はPrinter.Stringメソッド内で行われます。

func (P *Printer) String(pos int, s string) {
	// ... (既存のセミコロン処理など)

	at_line_begin := false;
	for comments.BVal() && P.cpos < pos {
		comment := P.clist.at(P.cindex).(*AST.Comment);
		text := comment.text;
		assert(len(text) >= 3);  // classification char + "//" or "/*"
		
		// classify comment
		switch text[0] {
		case ' ':
			// not only white space before comment on the same line
			// - put into next cell if //-style comment
			// - preceed with a space if /*-style comment
			if text[2] == '/' { // line comment
				P.buf.Tab();
			} else { // block comment
				P.buf.Print(" ");
			}
		case '\n', '\t':
			// only white space before comment on the same line
			// - indent
			if !P.buf.EmptyLine() {
				P.buf.Newline();
			}
			for i := P.indent; i > 0; i-- {
				P.buf.Tab();
			}
		default:
			panic("UNREACHABLE");
		}
		
		P.buf.Print(text[1 : len(text)]); // Print comment text without classification char
		if text[2] == '/' { // line comment
			// line comments must end in newline
			P.buf.Newline();
			for i := P.indent; i > 0; i-- {
				P.buf.Tab();
			}
			at_line_begin = true;
		}

		P.cindex++;
		// ... (P.cposの更新)
	}
	// ... (改行処理、文字列出力など)
}

このコードは、scanner.goで付加されたコメントの分類文字(text[0])を基に、コメントの整形方法を決定します。行コメント(//)はTab()で次の列に配置されるか、新しい行に配置され、ブロックコメント(/* */)はスペースを伴って出力されます。行コメントの後には強制的に改行が挿入され、次のコードが新しい行から始まるようにします。

usr/gri/pretty/parser.go

Parser.Next()メソッドは、スキャナーからトークンを読み込む際にコメントを処理します。

func (P *Parser) Next() {
	for P.Next0(); P.tok == Scanner.COMMENT; P.Next0() {
		P.comments.Add(AST.NewComment(P.pos, P.val));
	}
}

変更前は連続するコメントを結合していましたが、この変更により、P.Next0()がコメントトークンを返すたびに、そのコメントを個別のAST.CommentノードとしてP.commentsリストに追加するようになりました。これにより、プリンターが各コメントを独立して整形できるようになります。

usr/gri/pretty/scanner.go

Scanner.SkipWhitespace()Scanner.ScanComment()は、コメントの検出と分類に重要な役割を果たします。

func (S *Scanner) SkipWhitespace() int {
	pos := -1;  // no new line position yet
	
	if S.chpos == 0 {
		// file beginning is always start of a new line
		pos = 0;
	}
	
	for {
		switch S.ch {
		case '\t', '\r', ' ':  // nothing to do
		case '\n': pos = S.pos;  // remember start of new line
		default: goto exit;
		}
		S.Next();
	}
exit:
	return pos;
}

func (S *Scanner) ScanComment(nlpos int) string {
	// ... (コメントの実際のテキストをスキャンするロジック)

	comment := S.src[pos : S.chpos];

	if nlpos < 0 {
		// not only whitespace before comment on this line
		comment = " " + comment;
	} else if nlpos == pos {
		// comment starts at the beginning of the line
		comment = "\n" + comment;
	} else {
		// only whitespace before comment on this line
		comment = "\t" + comment;
	}
	return comment;
}

SkipWhitespace()は、空白をスキップしながら、最後に検出された改行の位置(nlpos)を返します。このnlposは、ScanComment()に渡され、コメントがコードのどの位置にあるかを判断するために使用されます。ScanComment()は、コメントの実際のテキストの前に、その分類を示す文字(スペース、改行、タブ)を付加して返します。これにより、プリンターはコメントの整形方法を適切に判断できます。

関連リンク

  • Go言語公式プロジェクト: https://go.dev/
  • gofmtに関する情報(Go言語公式ブログなど): Go言語の進化とともにgofmtも進化しており、このコミットはその初期段階を示しています。

参考にした情報源リンク