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

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

このコミットは、Go言語の初期のコード整形ツール(プリティプリンター)の一部である usr/gri/pretty/parser.gousr/gri/pretty/printer.go に関連する変更を含んでいます。parser.go はソースコードを抽象構文木(AST)に解析する役割を担い、printer.go はそのASTを整形されたソースコードとして出力する役割を担っています。このコミットの主な目的は、コメントの整形と空白文字の制御を改善し、より見栄えの良いコード出力を実現することにあります。

コミット

commit 8bbd873c340e9b495262cdd5eacc46daba960e53
Author: Robert Griesemer <gri@golang.org>
Date:   Mon Dec 1 14:03:20 2008 -0800

    - better comment formatting, starting to look good
    - comment printing still disabled by default because idempotency test fails
    - whitespace control better but not perfect yet
    - snapshot before making some heuristics changes
    
    R=r
    OCL=20151
    CL=20151

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

https://github.com/golang/go/commit/8bbd873c340e9b495262cdd5eacc46daba960e53

元コミット内容

- better comment formatting, starting to look good
- comment printing still disabled by default because idempotency test fails
- whitespace control better but not perfect yet
- snapshot before making some heuristics changes

変更の背景

このコミットは、Go言語の公式なコードフォーマッタである gofmt の前身、またはその開発過程における初期段階の変更点を示しています。当時のGo言語はまだ開発初期段階にあり、言語仕様だけでなく、その周辺ツール(パーサー、プリティプリンターなど)も活発に開発されていました。

コードフォーマッタにおいて、コメントの扱いは非常に複雑な課題です。コメントはコードの実行には影響しませんが、人間がコードを理解する上で不可欠な要素であり、その配置や整形は可読性に大きく影響します。また、空白文字の制御も同様に、コードの視覚的な構造と可読性を決定づける重要な要素です。

コミットメッセージにある「idempotency test fails」という記述は、このプリティプリンターがまだ「冪等性」を満たしていないことを示唆しています。コードフォーマッタにおける冪等性とは、「コードを一度フォーマットし、その結果を再度フォーマットしても、結果が変わらない」という性質を指します。これは、フォーマッタが安定しており、予測可能な出力を生成するために非常に重要です。コメントの整形や空白文字の挿入・削除は、この冪等性を破りやすい要素であり、開発者はこれらの課題に直面していたと考えられます。

このコミットは、これらの課題に対処し、より堅牢で高品質なコード整形ツールを構築するための試みの一環として行われました。特に、コメントの整形ロジックと、改行やインデントといった空白文字の制御メカニズムの改善に焦点が当てられています。

前提知識の解説

Go言語のAST (Abstract Syntax Tree)

ASTは、ソースコードの構造を木構造で表現したものです。パーサーはソースコードを読み込み、その構文構造を解析してASTを生成します。プリティプリンターは、このASTを受け取り、それを基に整形されたソースコードを生成します。ASTは、コメントや空白文字といった「非構造的な情報」を直接的には保持しないことが多いため、プリティプリンターはこれらの情報を別途管理し、適切に再配置する必要があります。

Go言語のパーサーとプリティプリンター

  • パーサー: ソースコードを解析し、ASTを構築するコンポーネントです。このコミットでは usr/gri/pretty/parser.go がこれに該当します。
  • プリティプリンター: ASTを受け取り、整形されたソースコードを出力するコンポーネントです。このコミットでは usr/gri/pretty/printer.go がこれに該当します。gofmt のようなツールは、このパーサーとプリティプリンターの組み合わせによって実現されています。

tabwriter.Writer

Go言語の標準ライブラリ text/tabwriter パッケージに含まれる tabwriter.Writer は、タブ区切りのテキストを整形するためのライターです。指定されたタブストップに基づいて、テキストをカラム状に揃える機能を提供します。プリティプリンターにおいて、コードのインデントやアライメントを制御するために利用されることがあります。

コメントの扱いと冪等性

コードフォーマッタにとって、コメントの扱いは非常にデリケートな問題です。コメントはコードの論理的な構造の一部ではないため、フォーマッタがコメントを移動させたり、整形したりする際に、元の意図を損なわないように細心の注意を払う必要があります。

  • 行コメント (//): 通常、行の残りの部分をコメントアウトするために使用されます。コードの右側に配置されることが多いです。
  • ブロックコメント (/* */): 複数行にわたるコメントや、コードの一部を一時的に無効化するために使用されます。

「冪等性」は、フォーマッタの品質を測る重要な指標です。フォーマット処理が冪等でない場合、ユーザーはコードをフォーマットするたびに異なる結果を得る可能性があり、これは非常に混乱を招きます。コメントや空白文字の微妙な変更が、この冪等性を破る原因となることがあります。

技術的詳細

このコミットの技術的な変更は、主に usr/gri/pretty/printer.go におけるコード整形ロジックの改善に集中しています。

  1. 状態管理の変更:

    • 以前の Printer 構造体にあった inline, lineend, funcend といった定数で表現されていた「状態」が削除されました。これは、より柔軟な改行制御を可能にするための変更と考えられます。
    • 代わりに、newlines という新しいフィールドが Printer 構造体に追加されました。これは、保留中の改行の数を明示的に管理するためのものです。これにより、以前の離散的な状態ではなく、連続的な改行数を制御できるようになりました。
    • indent フィールドが indentation に名称変更され、より意味が明確になりました。
  2. Newline 関数の強化:

    • Newline 関数が n int という引数を受け取るように変更されました。これにより、一度に複数の改行を出力できるようになりました。
    • maxnl 定数(値は2)が導入され、連続する改行の最大数が2に制限されました。これは、過剰な空白行の生成を防ぎ、コードの視覚的な密度を適切に保つためのヒューリスティックです。
    • 改行後には、現在の indentation レベルに応じたタブ文字が出力されるようになりました。
  3. コメント処理の改善:

    • PendingComment(pos int) bool という新しいヘルパー関数が追加されました。これは、指定された位置 pos の前に処理すべきコメントがあるかどうかを効率的に判断するために使用されます。
    • String 関数内のコメント挿入ロジックが大幅に修正されました。
      • 以前の trailing_blanktrailing_tab といったブール値のフラグが trailing_char という単一の整数変数に置き換えられました。これにより、直前に出力された空白文字の種類(スペース、タブ、なし)をより統一的に管理できるようになりました。
      • // スタイルのコメントと /* */ スタイルのコメントの扱いが区別され、それぞれに適した空白の挿入ロジックが適用されるようになりました。特に、// コメントは通常、行末に配置され、その後に改行が続くことが期待されます。/* */ コメントは、コードブロック内に埋め込まれる場合があり、その前後にスペースが必要となることがあります。
      • コメントの後に改行が必要な場合、以前の P.state = lineend のような状態遷移ではなく、P.newlines = 1 のように直接 newlines フィールドを設定するようになりました。
  4. 空白文字とセパレータの制御:

    • String 関数におけるセパレータ(空白、タブ、カンマ、セミコロン)の出力ロジックが調整されました。特に、カンマやセミコロンの後にスペースを挿入するかどうかの判断が、以前の P.state == inline から P.newlines == 0 に変更されました。これは、改行が保留されていない場合にのみスペースを挿入するという、より直感的なロジックです。
    • String 関数の最後に、保留中の改行 (P.newlines) を出力し、その後 P.newlines をリセットする処理が追加されました。これにより、改行の出力がより集中管理されるようになりました。
  5. スコープとブロックの整形:

    • OpenScope 関数が pos int 引数を受け取るように変更され、スコープ開始文字(例: {, ()の正確な位置情報が渡されるようになりました。
    • OpenScope および CloseScope 関数内で、インデントレベルの増減が P.indent++ / P.indent-- から P.indentation++ / P.indentation-- に変更されました。
    • Block 関数も pos int 引数を受け取るように変更され、ブロック開始位置の情報を利用できるようになりました。
    • Fields, StatementList, Stat, Declaration, Program といった関数内で、以前の P.state = lineendP.state = inline といった状態設定が、P.newlines = 1P.newlines = 0 といった newlines フィールドへの直接的な設定に置き換えられました。これにより、改行の制御がより統一的かつ明示的になりました。
    • 特に、Declaration 関数では、関数宣言の後に2つの改行 (P.newlines = 2) を挿入するロジックが追加されました。これは、関数定義間の視覚的な区切りを明確にするための整形ルールと考えられます。
  6. usr/gri/pretty/parser.go の変更:

    • ParseSwitchStat 関数に s.end = P.pos; という行が追加されました。これは、スイッチ文のASTノード (s) に対して、その終了位置 (P.pos) を正確に記録するためのものです。パーサーが正確な位置情報をASTに付与することは、プリティプリンターが元のコードの構造を忠実に再現し、コメントなどを適切に配置するために不可欠です。

これらの変更は、Go言語のコード整形ツールが、より複雑なコード構造(特にコメントと空白文字)を正確かつ美しく整形できるようにするための、初期段階における重要な改善を示しています。状態ベースの制御から、より明示的な改行数ベースの制御への移行は、整形ロジックの柔軟性と保守性を高める上で有効なアプローチです。

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

usr/gri/pretty/parser.go

--- a/usr/gri/pretty/parser.go
+++ b/usr/gri/pretty/parser.go
@@ -1183,6 +1183,7 @@ func (P *Parser) ParseSwitchStat() *AST.Stat {
 	for P.tok != Scanner.RBRACE && P.tok != Scanner.EOF {
 		s.block.Push(P.ParseCaseClause());
 	}
+	s.end = P.pos;
 	P.Expect(Scanner.RBRACE);
 	P.opt_semi = true;
 

usr/gri/pretty/printer.go

Printer 構造体の変更

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -36,15 +36,6 @@ const (
 )
 
 
-// Additional printing state to control the output. Fine-tuning
-// can be achieved by adding more specific state.\n-const (
-// 	inline = iota;
-// 	lineend;
-// 	funcend;
-// )
-//
-//
 type Printer struct {
 	// output
 	twriter *tabwriter.Writer;
@@ -54,12 +45,19 @@ type Printer struct {
 	cindex int;
 	cpos int;
 
-	// formatting control
+	// current state
 	lastpos int;  // pos after last string
 	level int;  // true scope level
-	indent int;  // indentation level
+	indentation int;  // indentation level
+	
+	// formatting control
 	separator int;  // pending separator
-	state int;  // state info
+	newlines int;  // pending newlines
+}
+
+
+func (P *Printer) PendingComment(pos int) bool {
+	return comments.BVal() && P.cpos < pos;
 }
  
  

Newline 関数の変更

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -101,10 +99,18 @@ func (P *Printer) Printf(format string, s ...) {
 }
 
 
-func (P *Printer) Newline() {
-	P.Printf("\n");
-	for i := P.indent; i > 0; i-- {
-		P.Printf("\t");
+func (P *Printer) Newline(n int) {
+	const maxnl = 2;
+	if n > 0 {
+		if n > maxnl {
+			n = maxnl;
+		}
+		for ; n > 0; n-- {
+			P.Printf("\n");
+		}
+		for i := P.indentation; i > 0; i-- {
+			P.Printf("\t");
+		}
 	}
 }
 

String 関数の変更(一部抜粋)

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -118,28 +124,27 @@ func (P *Printer) String(pos int, s string) {
 	// --------------------------------
 	// print pending separator, if any
 	// - keep track of white space printed for better comment formatting
-	trailing_blank := false;
-	trailing_tab := false;
+	trailing_char := 0;
 	switch P.separator {
 	case none:	// nothing to do
 	case blank:
 		P.Printf(" ");
-		trailing_blank = true;
+		trailing_char = ' ';
 	case tab:
 		P.Printf("\t");
-		trailing_tab = true;
+		trailing_char = '\t';
 	case comma:
 		P.Printf(",");
-		if P.state == inline {
+		if P.newlines == 0 {
 			P.Printf(" ");
-			trailing_blank = true;
+			trailing_char = ' ';
 		}
 	case semicolon:
 		if P.level > 0 {	// no semicolons at level 0
 			P.Printf(";");
-			if P.state == inline {
+			if P.newlines == 0 {
 				P.Printf(" ");
-				trailing_blank = true;
+				trailing_char = ' ';
 			}
 		}
 	default:	panic("UNREACHABLE");
@@ -149,7 +154,7 @@ func (P *Printer) String(pos int, s string) {
 	// --------------------------------
 	// interleave comments, if any
 	nlcount := 0;
-	for comments.BVal() && P.cpos < pos {
+	for P.PendingComment(pos) {
 		// we have a comment/newline that comes before the string
 		comment := P.comments.At(P.cindex).(*AST.Comment);
 		ctext := comment.text;
@@ -165,19 +170,19 @@ func (P *Printer) String(pos int, s string) {
 				// only white space before comment on this line
 				// or file starts with comment
 				// - indent
-				P.Newline();
+				P.Newline(nlcount);
 			} else {
 				// black space before comment on this line
 				if ctext[1] == '/' {
 					//-style comment
 					// - put in next cell
-					if !trailing_tab {
+					if trailing_char != '\t' {
 						P.Printf("\t");
 					}
 				} else {
 					/*-style comment */
 					// - print surrounded by blanks
-					if !trailing_blank && !trailing_tab {
+					if trailing_char == 0 {
 						P.Printf(" ");
 					}
 					ctext += " ";
@@ -191,16 +196,8 @@ func (P *Printer) String(pos int, s string) {
 
 			if ctext[1] == '/' {
 				//-style comments must end in newline
-				if P.state == inline {  // don't override non-inline states
-					P.state = lineend;
+				if P.newlines == 0 {  // don't add newlines if not needed
+					P.newlines = 1;
 				}
 			}
 			
@@ -208,16 +205,8 @@ func (P *Printer) String(pos int, s string) {
 
 	// --------------------------------
 	// adjust formatting depending on state
-	switch P.state {
-	case inline:	// nothing to do
-	case funcend:
-		P.Printf("\n\n");
-		fallthrough;
-	case lineend:
-		P.Newline();
-	default:	panic("UNREACHABLE");
-	}\n-	P.state = inline;
+	P.Newline(P.newlines);
+	P.newlines = 0;
 
 	// --------------------------------
 	// print string

OpenScope, CloseScope, Block などの変更(一部抜粋)

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -239,16 +236,16 @@ func (P *Printer) Token(pos int, tok int) {
 }
 
 
-func (P *Printer) OpenScope(paren string) {
-	P.String(0, paren);
+func (P *Printer) OpenScope(pos int, paren string) {
+	P.String(pos, paren);
 	P.level++;
-	P.indent++;
-	P.state = lineend;
+	P.indentation++;
+	P.newlines = 1;
 }
 
 
 func (P *Printer) CloseScope(pos int, paren string) {
-	P.indent--;
+	P.indentation--;
 	P.String(pos, paren);
 	P.level--;
 }
@@ -289,7 +286,7 @@ func (P *Printer) Parameters(pos int, list *array.Array) {
 
 
 func (P *Printer) Fields(list *array.Array, end int) {
-\tP.OpenScope(\"{\");
+\tP.OpenScope(0, \"{\");
  \tif list != nil {
  \t\tvar prev int;
  \t\tfor i, n := 0, list.Len(); i < n; i++ {\
@@ -297,7 +294,7 @@ func (P *Printer) Fields(list *array.Array, end int) {\
  \t\t\tif i > 0 {\
  \t\t\t\tif prev == Scanner.TYPE && x.tok != Scanner.STRING || prev == Scanner.STRING {\
  \t\t\t\t\tP.separator = semicolon;\
-\t\t\t\t\tP.state = lineend;\
+\t\t\t\t\tP.newlines = 1;\
  \t\t\t\t} else if prev == x.tok {\
  \t\t\t\t\tP.separator = comma;\
  \t\t\t\t} else {\
@@ -307,7 +304,7 @@ func (P *Printer) Fields(list *array.Array, end int) {\
  \t\t\tP.Expr(x);\
  \t\t\tprev = x.tok;\
  \t\t}\
-\t\tP.state = lineend;\
+\t\tP.newlines = 1;\
  \t}\
  \tP.CloseScope(end, \"}\");
  }
@@ -372,7 +369,7 @@ func (P *Printer) Type(t *AST.Type) {\
  // ----------------------------------------------------------------------------
  // Expressions
  
-func (P *Printer) Block(list *array.Array, end int, indent bool);\
+func (P *Printer) Block(pos int, list *array.Array, end int, indent bool);\
  
  func (P *Printer) Expr1(x *AST.Expr, prec1 int) {\
  \tif x == nil {\
@@ -392,8 +389,8 @@ func (P *Printer) Expr1(x *AST.Expr, prec1 int) {\
  \t\t// function literal
  \t\tP.String(x.pos, \"func\");
  \t\tP.Type(x.t);\
-\t\tP.Block(x.block, x.end, true);\
-\t\tP.state = inline;\
+\t\tP.Block(0, x.block, x.end, true);\
+\t\tP.newlines = 0;\
  
  \tcase Scanner.COMMA:\
  \t\t// list
@@ -476,20 +473,20 @@ func (P *Printer) StatementList(list *array.Array) {\
  \tif list != nil {\
  \t\tfor i, n := 0, list.Len(); i < n; i++ {\
  \t\t\tP.Stat(list.At(i).(*AST.Stat));
-\t\t\tP.state = lineend;\
+\t\t\tP.newlines = 1;\
  \t\t}\
  \t}\
  }
  
  
-func (P *Printer) Block(list *array.Array, end int, indent bool) {\
-\tP.OpenScope(\"{\");
+func (P *Printer) Block(pos int, list *array.Array, end int, indent bool) {\
+\tP.OpenScope(pos, \"{\");
  \tif !indent {\
-\t\tP.indent--;
+\t\tP.indentation--;
  \t}\
  \tP.StatementList(list);\
  \tif !indent {\
-\t\tP.indent++;
+\t\tP.indentation++;
  \t}\
  \tP.separator = none;\
  \tP.CloseScope(end, \"}\");
@@ -541,10 +538,10 @@ func (P *Printer) Stat(s *AST.Stat) {\
  
  \tcase Scanner.COLON:\
  \t\t// label declaration
-\t\tP.indent--;
+\t\tP.indentation--;
  \t\tP.Expr(s.expr);\
  \t\tP.Token(s.pos, s.tok);\
-\t\tP.indent++;
+\t\tP.indentation++;
  \t\tP.separator = none;\
  \t\t\n \tcase Scanner.CONST, Scanner.TYPE, Scanner.VAR:\
  \t\tP.Token(s.pos, s.tok);\
@@ -558,12 +555,12 @@ func (P *Printer) Stat(s *AST.Stat) {\
  
  \tcase Scanner.LBRACE:\
  \t\t// block
-\t\tP.Block(s.block, s.end, true);\
+\t\tP.Block(s.pos, s.block, s.end, true);\
  
  \tcase Scanner.IF:\
  \t\tP.String(s.pos, \"if\");
  \t\tP.ControlClause(s);\
-\t\tP.Block(s.block, s.end, true);\
+\t\tP.Block(0, s.block, s.end, true);\
  \t\tif s.post != nil {\
  \t\t\tP.separator = blank;\
  \t\t\tP.String(0, \"else\");
@@ -574,12 +571,12 @@ func (P *Printer) Stat(s *AST.Stat) {\
  \tcase Scanner.FOR:\
  \t\tP.String(s.pos, \"for\");
  \t\tP.ControlClause(s);\
-\t\tP.Block(s.block, s.end, true);\
+\t\tP.Block(0, s.block, s.end, true);\
  
  \tcase Scanner.SWITCH, Scanner.SELECT:\
  \t\tP.Token(s.pos, s.tok);\
  \t\tP.ControlClause(s);\
-\t\tP.Block(s.block, s.end, false);\
+\t\tP.Block(0, s.block, s.end, false);\
  
  \tcase Scanner.CASE, Scanner.DEFAULT:\
  \t\tP.Token(s.pos, s.tok);\
@@ -588,11 +585,11 @@ func (P *Printer) Stat(s *AST.Stat) {\
  \t\t\tP.Expr(s.expr);\
  \t\t}\
  \t\tP.String(0, \":\");
-\t\tP.indent++;
-\t\tP.state = lineend;\
+\t\tP.indentation++;
+\t\tP.newlines = 1;\
  \t\tP.StatementList(s.block);\
-\t\tP.indent--;
-\t\tP.state = lineend;\
+\t\tP.indentation--;
+\t\tP.newlines = 1;\
  
  \tcase Scanner.GO, Scanner.RETURN, Scanner.FALLTHROUGH, Scanner.BREAK, Scanner.CONTINUE, Scanner.GOTO:\
  \t\tP.Token(s.pos, s.tok);\
@@ -611,11 +608,11 @@ func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {\
  \t}\
  
  \tif d.tok != Scanner.FUNC && d.list != nil {\
-\t\tP.OpenScope(\"(\");
+\t\tP.OpenScope(0, \"(\");
  \t\tfor i := 0; i < d.list.Len(); i++ {\
  \t\t\tP.Declaration(d.list.At(i).(*AST.Decl), true);\
  \t\t\tP.separator = semicolon;\
-\t\t\tP.state = lineend;\
+\t\t\tP.newlines = 1;\
  \t\t}\
  \t\tP.CloseScope(d.end, \")\");
  
@@ -658,11 +654,7 @@ func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {\
  \t\t\t\tpanic(\"must be a func declaration\");
  \t\t\t}\
  \t\t\tP.separator = blank;\
-\t\t\tP.Block(d.list, d.end, true);\
+\t\t\tP.Block(0, d.list, d.end, true);\
  \t\t}\
  \t\t\n \t\tif d.tok != Scanner.TYPE {\
  \t\t\tP.separator = semicolon;\
@@ -666,11 +658,7 @@ func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {\
  \t\t}\
  \t}\
  \t\n-\tif d.tok == Scanner.FUNC {\
-\t\tP.state = funcend;\
-\t} else {\
-\t\tP.state = lineend;\
-\t}\
+\tP.newlines = 2;\
  }
  
  
@@ -680,11 +672,11 @@ func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {\
  func (P *Printer) Program(p *AST.Program) {\
  \tP.String(p.pos, \"package \");
  \tP.Expr(p.ident);\
-\tP.state = lineend;\
+\tP.newlines = 1;\
  \tfor i := 0; i < p.decls.Len(); i++ {\
  \t\tP.Declaration(p.decls.At(i), false);\
  \t}\
-\tP.state = lineend;\
+\tP.newlines = 1;\
  }
  
  

コアとなるコードの解説

usr/gri/pretty/parser.go の変更

ParseSwitchStat 関数は、Go言語の switch ステートメントを解析し、そのAST表現を構築する役割を担っています。追加された s.end = P.pos; の一行は、解析中のスイッチステートメントのASTノード s に対して、その終了位置 (P.pos) を記録しています。

この変更の重要性は、プリティプリンターが正確なコードを生成するために、ASTノードがソースコード内の正確な開始位置と終了位置を持つ必要があるという点にあります。特に、コメントや空白文字はASTには直接含まれないため、プリティプリンターはこれらの位置情報に基づいて、コメントを元のコードの適切な場所に再配置する必要があります。スイッチステートメントの終了位置を正確に記録することで、そのブロック内や直後のコメントの整形がより正確に行えるようになります。

usr/gri/pretty/printer.go の変更

printer.go の変更は、プリティプリンターの内部状態管理と、コメントおよび空白文字の整形ロジックの根本的な改善を目的としています。

  1. 状態管理の刷新:

    • 以前の inline, lineend, funcend といった列挙型による「状態」は、コードの整形における特定の状況(例: 行末、関数末尾)を表していました。しかし、これらの状態は柔軟性に欠け、複雑な整形ルールに対応しにくいという問題がありました。
    • 新しい newlines フィールドは、出力すべき保留中の改行の数を直接的に保持します。これにより、プリティプリンターは、特定の状態に縛られることなく、必要に応じて1行、2行、あるいはそれ以上の改行を柔軟に挿入できるようになりました。maxnl による最大改行数の制限は、過剰な空白行を防ぐための実用的なヒューリスティックです。
    • indent から indentation への名称変更は、単なるインデントレベルではなく、より広範な「字下げ」の概念を表現するためのものです。
  2. Newline 関数の機能拡張:

    • Newline(n int) は、n の値に応じて複数の改行を出力し、その後、現在の indentation レベルに応じたタブ文字を挿入します。これにより、コードブロックの開始や関数定義の後に、適切な数の空白行とインデントを自動的に挿入できるようになりました。
  3. String 関数におけるコメントと空白の制御:

    • String 関数は、プリティプリンターの中核であり、文字列(トークン)を出力する際に、その前後の空白やコメントを適切に処理します。
    • trailing_blanktrailing_tabtrailing_char に統合したことで、直前に出力された文字の種類(スペース、タブ、なし)をより簡潔に管理できるようになりました。これは、コメントを挿入する際に、既存の空白との重複を避け、適切な間隔を確保するために重要です。
    • P.PendingComment(pos) の導入により、コメントの処理がよりモジュール化され、可読性が向上しました。
    • コメントの整形ロジックは、// スタイルと /* */ スタイルで異なる振る舞いをします。// コメントは通常、行末に配置され、その後に改行が続くことが期待されるため、P.newlines = 1 が設定されます。/* */ コメントは、コードの途中に挿入されることがあり、その前後にスペースが必要となる場合があります。これらの違いを考慮することで、コメントがコードの可読性を損なうことなく、適切に配置されるようになります。
    • switch P.state ブロックの削除と、P.Newline(P.newlines); P.newlines = 0; による置き換えは、改行の出力が String 関数の最後に一元化されたことを意味します。これにより、整形ロジックがより予測可能になり、デバッグが容易になります。
  4. スコープ、ブロック、宣言の整形:

    • OpenScope, CloseScope, Block, Fields, StatementList, Stat, Declaration, Program といった関数における P.state の設定が P.newlines の設定に置き換えられました。これは、これらの構文要素の開始や終了時に、必要な改行とインデントをより正確に制御するためのものです。
    • 特に、関数宣言の後に P.newlines = 2 を設定する変更は、関数定義間に常に2行の空白行を挿入するという整形ルールを強制します。これは、Go言語の慣習的なフォーマットスタイルに合致し、コードのセクション間の視覚的な分離を強化します。

これらの変更は、Go言語のプリティプリンターが、単に構文的に正しいコードを出力するだけでなく、Goコミュニティで広く受け入れられているスタイルガイドに沿った、視覚的に美しく、読みやすいコードを生成するための重要なステップでした。特に、コメントと空白文字の複雑な相互作用を適切に処理する能力は、高品質なコードフォーマッタの実現に不可欠です。

関連リンク

参考にした情報源リンク

  • Go言語の初期開発に関する情報(一般的な知識として)
  • コードフォーマッタにおける冪等性に関する一般的な概念
  • Go言語の go/printer パッケージの設計思想に関する議論(一般的な知識として)
  • text/tabwriter パッケージのドキュメント```markdown

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

このコミットは、Go言語の初期のコード整形ツール(プリティプリンター)の一部である usr/gri/pretty/parser.gousr/gri/pretty/printer.go に関連する変更を含んでいます。parser.go はソースコードを抽象構文木(AST)に解析する役割を担い、printer.go はそのASTを整形されたソースコードとして出力する役割を担っています。このコミットの主な目的は、コメントの整形と空白文字の制御を改善し、より見栄えの良いコード出力を実現することにあります。

コミット

commit 8bbd873c340e9b495262cdd5eacc46daba960e53
Author: Robert Griesemer <gri@golang.org>
Date:   Mon Dec 1 14:03:20 2008 -0800

    - better comment formatting, starting to look good
    - comment printing still disabled by default because idempotency test fails
    - whitespace control better but not perfect yet
    - snapshot before making some heuristics changes
    
    R=r
    OCL=20151
    CL=20151

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

https://github.com/golang/go/commit/8bbd873c340e9b495262cdd5eacc46daba960e53

元コミット内容

- better comment formatting, starting to look good
- comment printing still disabled by default because idempotency test fails
- whitespace control better but not perfect yet
- snapshot before making some heuristics changes

変更の背景

このコミットは、Go言語の公式なコードフォーマッタである gofmt の前身、またはその開発過程における初期段階の変更点を示しています。当時のGo言語はまだ開発初期段階にあり、言語仕様だけでなく、その周辺ツール(パーサー、プリティプリンターなど)も活発に開発されていました。

コードフォーマッタにおいて、コメントの扱いは非常に複雑な課題です。コメントはコードの実行には影響しませんが、人間がコードを理解する上で不可欠な要素であり、その配置や整形は可読性に大きく影響します。また、空白文字の制御も同様に、コードの視覚的な構造と可読性を決定づける重要な要素です。

コミットメッセージにある「idempotency test fails」という記述は、このプリティプリンターがまだ「冪等性」を満たしていないことを示唆しています。コードフォーマッタにおける冪等性とは、「コードを一度フォーマットし、その結果を再度フォーマットしても、結果が変わらない」という性質を指します。これは、フォーマッタが安定しており、予測可能な出力を生成するために非常に重要です。コメントの整形や空白文字の挿入・削除は、この冪等性を破りやすい要素であり、開発者はこれらの課題に直面していたと考えられます。

このコミットは、これらの課題に対処し、より堅牢で高品質なコード整形ツールを構築するための試みの一環として行われました。特に、コメントの整形ロジックと、改行やインデントといった空白文字の制御メカニズムの改善に焦点が当てられています。

前提知識の解説

Go言語のAST (Abstract Syntax Tree)

ASTは、ソースコードの構造を木構造で表現したものです。パーサーはソースコードを読み込み、その構文構造を解析してASTを生成します。プリティプリンターは、このASTを受け取り、それを基に整形されたソースコードを生成します。ASTは、コメントや空白文字といった「非構造的な情報」を直接的には保持しないことが多いため、プリティプリンターはこれらの情報を別途管理し、適切に再配置する必要があります。

Go言語のパーサーとプリティプリンター

  • パーサー: ソースコードを解析し、ASTを構築するコンポーネントです。このコミットでは usr/gri/pretty/parser.go がこれに該当します。
  • プリティプリンター: ASTを受け取り、整形されたソースコードを出力するコンポーネントです。このコミットでは usr/gri/pretty/printer.go がこれに該当します。gofmt のようなツールは、このパーサーとプリティプリンターの組み合わせによって実現されています。

tabwriter.Writer

Go言語の標準ライブラリ text/tabwriter パッケージに含まれる tabwriter.Writer は、タブ区切りのテキストを整形するためのライターです。指定されたタブストップに基づいて、テキストをカラム状に揃える機能を提供します。プリティプリンターにおいて、コードのインデントやアライメントを制御するために利用されることがあります。

コメントの扱いと冪等性

コードフォーマッタにとって、コメントの扱いは非常にデリケートな問題です。コメントはコードの論理的な構造の一部ではないため、フォーマッタがコメントを移動させたり、整形したりする際に、元の意図を損なわないように細心の注意を払う必要があります。

  • 行コメント (//): 通常、行の残りの部分をコメントアウトするために使用されます。コードの右側に配置されることが多いです。
  • ブロックコメント (/* */): 複数行にわたるコメントや、コードの一部を一時的に無効化するために使用されます。

「冪等性」は、フォーマッタの品質を測る重要な指標です。フォーマット処理が冪等でない場合、ユーザーはコードをフォーマットするたびに異なる結果を得る可能性があり、これは非常に混乱を招きます。コメントや空白文字の微妙な変更が、この冪等性を破る原因となることがあります。

技術的詳細

このコミットの技術的な変更は、主に usr/gri/pretty/printer.go におけるコード整形ロジックの改善に集中しています。

  1. 状態管理の変更:

    • 以前の Printer 構造体にあった inline, lineend, funcend といった定数で表現されていた「状態」が削除されました。これは、より柔軟な改行制御を可能にするための変更と考えられます。
    • 代わりに、newlines という新しいフィールドが Printer 構造体に追加されました。これは、保留中の改行の数を明示的に管理するためのものです。これにより、以前の離散的な状態ではなく、連続的な改行数を制御できるようになりました。
    • indent フィールドが indentation に名称変更され、より意味が明確になりました。
  2. Newline 関数の強化:

    • Newline 関数が n int という引数を受け取るように変更されました。これにより、一度に複数の改行を出力できるようになりました。
    • maxnl 定数(値は2)が導入され、連続する改行の最大数が2に制限されました。これは、過剰な空白行の生成を防ぎ、コードの視覚的な密度を適切に保つためのヒューリスティックです。
    • 改行後には、現在の indentation レベルに応じたタブ文字が出力されるようになりました。
  3. コメント処理の改善:

    • PendingComment(pos int) bool という新しいヘルパー関数が追加されました。これは、指定された位置 pos の前に処理すべきコメントがあるかどうかを効率的に判断するために使用されます。
    • String 関数内のコメント挿入ロジックが大幅に修正されました。
      • 以前の trailing_blanktrailing_tab といったブール値のフラグが trailing_char という単一の整数変数に置き換えられました。これにより、直前に出力された空白文字の種類(スペース、タブ、なし)をより統一的に管理できるようになりました。
      • // スタイルのコメントと /* */ スタイルのコメントの扱いが区別され、それぞれに適した空白の挿入ロジックが適用されるようになりました。特に、// コメントは通常、行末に配置され、その後に改行が続くことが期待されます。/* */ コメントは、コードブロック内に埋め込まれる場合があり、その前後にスペースが必要となることがあります。
      • コメントの後に改行が必要な場合、以前の P.state = lineend のような状態遷移ではなく、P.newlines = 1 のように直接 newlines フィールドを設定するようになりました。
  4. 空白文字とセパレータの制御:

    • String 関数におけるセパレータ(空白、タブ、カンマ、セミコロン)の出力ロジックが調整されました。特に、カンマやセミコロンの後にスペースを挿入するかどうかの判断が、以前の P.state == inline から P.newlines == 0 に変更されました。これは、改行が保留されていない場合にのみスペースを挿入するという、より直感的なロジックです。
    • String 関数の最後に、保留中の改行 (P.newlines) を出力し、その後 P.newlines をリセットする処理が追加されました。これにより、改行の出力がより集中管理されるようになりました。
  5. スコープとブロックの整形:

    • OpenScope 関数が pos int 引数を受け取るように変更され、スコープ開始文字(例: {, ()の正確な位置情報が渡されるようになりました。
    • OpenScope および CloseScope 関数内で、インデントレベルの増減が P.indent++ / P.indent-- から P.indentation++ / P.indentation-- に変更されました。
    • Block 関数も pos int 引数を受け取るように変更され、ブロック開始位置の情報を利用できるようになりました。
    • Fields, StatementList, Stat, Declaration, Program といった関数内で、以前の P.state = lineendP.state = inline といった状態設定が、P.newlines = 1P.newlines = 0 といった newlines フィールドへの直接的な設定に置き換えられました。これにより、改行の制御がより統一的かつ明示的になりました。
    • 特に、Declaration 関数では、関数宣言の後に2つの改行 (P.newlines = 2) を挿入するロジックが追加されました。これは、関数定義間の視覚的な区切りを明確にするための整形ルールと考えられます。
  6. usr/gri/pretty/parser.go の変更:

    • ParseSwitchStat 関数に s.end = P.pos; という行が追加されました。これは、スイッチ文のASTノード (s) に対して、その終了位置 (P.pos) を正確に記録するためのものです。パーサーが正確な位置情報をASTに付与することは、プリティプリンターが元のコードの構造を忠実に再現し、コメントなどを適切に配置するために不可欠です。

これらの変更は、Go言語のコード整形ツールが、より複雑なコード構造(特にコメントと空白文字)を正確かつ美しく整形できるようにするための、初期段階における重要な改善を示しています。状態ベースの制御から、より明示的な改行数ベースの制御への移行は、整形ロジックの柔軟性と保守性を高める上で有効なアプローチです。

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

usr/gri/pretty/parser.go

--- a/usr/gri/pretty/parser.go
+++ b/usr/gri/pretty/parser.go
@@ -1183,6 +1183,7 @@ func (P *Parser) ParseSwitchStat() *AST.Stat {
 	for P.tok != Scanner.RBRACE && P.tok != Scanner.EOF {
 		s.block.Push(P.ParseCaseClause());
 	}
+	s.end = P.pos;
 	P.Expect(Scanner.RBRACE);
 	P.opt_semi = true;
 

usr/gri/pretty/printer.go

Printer 構造体の変更

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -36,15 +36,6 @@ const (
 )
 
 
-// Additional printing state to control the output. Fine-tuning
-// can be achieved by adding more specific state.\n-const (
-// 	inline = iota;
-// 	lineend;
-// 	funcend;
-// )
-//
-//
 type Printer struct {
 	// output
 	twriter *tabwriter.Writer;
@@ -54,12 +45,19 @@ type Printer struct {
 	cindex int;
 	cpos int;
 
-	// formatting control
+	// current state
 	lastpos int;  // pos after last string
 	level int;  // true scope level
-	indent int;  // indentation level
+	indentation int;  // indentation level
+	
+	// formatting control
 	separator int;  // pending separator
-	state int;  // state info
+	newlines int;  // pending newlines
+}
+
+
+func (P *Printer) PendingComment(pos int) bool {
+	return comments.BVal() && P.cpos < pos;
 }
  
  

Newline 関数の変更

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -101,10 +99,18 @@ func (P *Printer) Printf(format string, s ...) {
 }
 
 
-func (P *Printer) Newline() {
-	P.Printf("\n");
-	for i := P.indent; i > 0; i-- {
-		P.Printf("\t");
+func (P *Printer) Newline(n int) {
+	const maxnl = 2;
+	if n > 0 {
+		if n > maxnl {
+			n = maxnl;
+		}
+		for ; n > 0; n-- {
+			P.Printf("\n");
+		}
+		for i := P.indentation; i > 0; i-- {
+			P.Printf("\t");
+		}
 	}
 }
 

String 関数の変更(一部抜粋)

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -118,28 +124,27 @@ func (P *Printer) String(pos int, s string) {
 	// --------------------------------
 	// print pending separator, if any
 	// - keep track of white space printed for better comment formatting
-	trailing_blank := false;
-	trailing_tab := false;
+	trailing_char := 0;
 	switch P.separator {
 	case none:	// nothing to do
 	case blank:
 		P.Printf(" ");
-		trailing_blank = true;
+		trailing_char = ' ';
 	case tab:
 		P.Printf("\t");
-		trailing_tab = true;
+		trailing_char = '\t';
 	case comma:
 		P.Printf(",");
-		if P.state == inline {
+		if P.newlines == 0 {
 			P.Printf(" ");
-			trailing_blank = true;
+			trailing_char = ' ';
 		}
 	case semicolon:
 		if P.level > 0 {	// no semicolons at level 0
 			P.Printf(";");
-			if P.state == inline {
+			if P.newlines == 0 {
 				P.Printf(" ");
-				trailing_blank = true;
+				trailing_char = ' ';
 			}
 		}
 	default:	panic("UNREACHABLE");
@@ -149,7 +154,7 @@ func (P *Printer) String(pos int, s string) {
 	// --------------------------------
 	// interleave comments, if any
 	nlcount := 0;
-	for comments.BVal() && P.cpos < pos {
+	for P.PendingComment(pos) {
 		// we have a comment/newline that comes before the string
 		comment := P.comments.At(P.cindex).(*AST.Comment);
 		ctext := comment.text;
@@ -165,19 +170,19 @@ func (P *Printer) String(pos int, s string) {
 				// only white space before comment on this line
 				// or file starts with comment
 				// - indent
-				P.Newline();
+				P.Newline(nlcount);
 			} else {
 				// black space before comment on this line
 				if ctext[1] == '/' {
 					//-style comment
 					// - put in next cell
-					if !trailing_tab {
+					if trailing_char != '\t' {
 						P.Printf("\t");
 					}
 				} else {
 					/*-style comment */
 					// - print surrounded by blanks
-					if !trailing_blank && !trailing_tab {
+					if trailing_char == 0 {
 						P.Printf(" ");
 					}
 					ctext += " ";
@@ -191,16 +196,8 @@ func (P *Printer) String(pos int, s string) {
 
 			if ctext[1] == '/' {
 				//-style comments must end in newline
-				if P.state == inline {  // don't override non-inline states
-					P.state = lineend;
+				if P.newlines == 0 {  // don't add newlines if not needed
+					P.newlines = 1;
 				}
 			}
 			
@@ -208,16 +205,8 @@ func (P *Printer) String(pos int, s string) {
 
 	// --------------------------------
 	// adjust formatting depending on state
-	switch P.state {
-	case inline:	// nothing to do
-	case funcend:
-		P.Printf("\n\n");
-		fallthrough;
-	case lineend:
-		P.Newline();
-	default:	panic("UNREACHABLE");
-	}\n-	P.state = inline;
+	P.Newline(P.newlines);
+	P.newlines = 0;
 
 	// --------------------------------
 	// print string

OpenScope, CloseScope, Block などの変更(一部抜粋)

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -239,16 +236,16 @@ func (P *Printer) Token(pos int, tok int) {
 }
 
 
-func (P *Printer) OpenScope(paren string) {
-	P.String(0, paren);
+func (P *Printer) OpenScope(pos int, paren string) {
+	P.String(pos, paren);
 	P.level++;
-	P.indent++;
-	P.state = lineend;
+	P.indentation++;
+	P.newlines = 1;
 }
 
 
 func (P *Printer) CloseScope(pos int, paren string) {
-	P.indent--;
+	P.indentation--;
 	P.String(pos, paren);
 	P.level--;
 }
@@ -289,7 +286,7 @@ func (P *Printer) Parameters(pos int, list *array.Array) {
 
 
 func (P *Printer) Fields(list *array.Array, end int) {
-\tP.OpenScope(\"{\");
+\tP.OpenScope(0, \"{\");
  \tif list != nil {
  \t\tvar prev int;
  \t\tfor i, n := 0, list.Len(); i < n; i++ {\
@@ -297,7 +294,7 @@ func (P *Printer) Fields(list *array.Array, end int) {\
  \t\t\tif i > 0 {\
  \t\t\t\tif prev == Scanner.TYPE && x.tok != Scanner.STRING || prev == Scanner.STRING {\
  \t\t\t\t\tP.separator = semicolon;\
-\t\t\t\t\tP.state = lineend;\
+\t\t\t\t\tP.newlines = 1;\
  \t\t\t\t} else if prev == x.tok {\
  \t\t\t\t\tP.separator = comma;\
  \t\t\t\t} else {\
@@ -307,7 +304,7 @@ func (P *Printer) Fields(list *array.Array, end int) {\
  \t\t\tP.Expr(x);\
  \t\t\tprev = x.tok;\
  \t\t}\
-\t\tP.state = lineend;\
+\t\tP.newlines = 1;\
  \t}\
  \tP.CloseScope(end, \"}\");
  }
@@ -372,7 +369,7 @@ func (P *Printer) Type(t *AST.Type) {\
  // ----------------------------------------------------------------------------
  // Expressions
  
-func (P *Printer) Block(list *array.Array, end int, indent bool);\
+func (P *Printer) Block(pos int, list *array.Array, end int, indent bool);\
  
  func (P *Printer) Expr1(x *AST.Expr, prec1 int) {\
  \tif x == nil {\
@@ -392,8 +389,8 @@ func (P *Printer) Expr1(x *AST.Expr, prec1 int) {\
  \t\t// function literal
  \t\tP.String(x.pos, \"func\");
  \t\tP.Type(x.t);\
-\t\tP.Block(x.block, x.end, true);\
-\t\tP.state = inline;\
+\t\tP.Block(0, x.block, x.end, true);\
+\t\tP.newlines = 0;\
  
  \tcase Scanner.COMMA:\
  \t\t// list
@@ -476,20 +473,20 @@ func (P *Printer) StatementList(list *array.Array) {\
  \tif list != nil {\
  \t\tfor i, n := 0, list.Len(); i < n; i++ {\
  \t\t\tP.Stat(list.At(i).(*AST.Stat));
-\t\t\tP.state = lineend;\
+\t\t\tP.newlines = 1;\
  \t\t}\
  \t}\
  }
  
  
-func (P *Printer) Block(list *array.Array, end int, indent bool) {\
-\tP.OpenScope(\"{\");
+func (P *Printer) Block(pos int, list *array.Array, end int, indent bool) {\
+\tP.OpenScope(pos, \"{\");
  \tif !indent {\
-\t\tP.indent--;
+\t\tP.indentation--;
  \t}\
  \tP.StatementList(list);\
  \tif !indent {\
-\t\tP.indent++;
+\t\tP.indentation++;
  \t}\
  \tP.separator = none;\
  \tP.CloseScope(end, \"}\");
@@ -541,10 +538,10 @@ func (P *Printer) Stat(s *AST.Stat) {\
  
  \tcase Scanner.COLON:\
  \t\t// label declaration
-\t\tP.indent--;
+\t\tP.indentation--;
  \t\tP.Expr(s.expr);\
  \t\tP.Token(s.pos, s.tok);\
-\t\tP.indent++;
+\t\tP.indentation++;
  \t\tP.separator = none;\
  \t\t\n \tcase Scanner.CONST, Scanner.TYPE, Scanner.VAR:\
  \t\tP.Token(s.pos, s.tok);\
@@ -558,12 +555,12 @@ func (P *Printer) Stat(s *AST.Stat) {\
  
  \tcase Scanner.LBRACE:\
  \t\t// block
-\t\tP.Block(s.block, s.end, true);\
+\t\tP.Block(s.pos, s.block, s.end, true);\
  
  \tcase Scanner.IF:\
  \t\tP.String(s.pos, \"if\");
  \t\tP.ControlClause(s);\
-\t\tP.Block(s.block, s.end, true);\
+\t\tP.Block(0, s.block, s.end, true);\
  \t\tif s.post != nil {\
  \t\t\tP.separator = blank;\
  \t\t\tP.String(0, \"else\");
@@ -574,12 +571,12 @@ func (P *Printer) Stat(s *AST.Stat) {\
  \tcase Scanner.FOR:\
  \t\tP.String(s.pos, \"for\");
  \t\tP.ControlClause(s);\
-\t\tP.Block(s.block, s.end, true);\
+\t\tP.Block(0, s.block, s.end, true);\
  
  \tcase Scanner.SWITCH, Scanner.SELECT:\
  \t\tP.Token(s.pos, s.tok);\
  \t\tP.ControlClause(s);\
-\t\tP.Block(s.block, s.end, false);\
+\t\tP.Block(0, s.block, s.end, false);\
  
  \tcase Scanner.CASE, Scanner.DEFAULT:\
  \t\tP.Token(s.pos, s.tok);\
@@ -588,11 +585,11 @@ func (P *Printer) Stat(s *AST.Stat) {\
  \t\t\tP.Expr(s.expr);\
  \t\t}\
  \t\tP.String(0, \":\");
-\t\tP.indent++;
-\t\tP.state = lineend;\
+\t\tP.indentation++;
+\t\tP.newlines = 1;\
  \t\tP.StatementList(s.block);\
-\t\tP.indent--;
-\t\tP.state = lineend;\
+\t\tP.indentation--;
+\t\tP.newlines = 1;\
  
  \tcase Scanner.GO, Scanner.RETURN, Scanner.FALLTHROUGH, Scanner.BREAK, Scanner.CONTINUE, Scanner.GOTO:\
  \t\tP.Token(s.pos, s.tok);\
@@ -611,11 +608,11 @@ func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {\
  \t}\
  
  \tif d.tok != Scanner.FUNC && d.list != nil {\
-\t\tP.OpenScope(\"(\");
+\t\tP.OpenScope(0, \"(\");
  \t\tfor i := 0; i < d.list.Len(); i++ {\
  \t\t\tP.Declaration(d.list.At(i).(*AST.Decl), true);\
  \t\t\tP.separator = semicolon;\
-\t\t\tP.state = lineend;\
+\t\t\tP.newlines = 1;\
  \t\t}\
  \t\tP.CloseScope(d.end, \")\");
  
@@ -658,11 +654,7 @@ func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {\
  \t\t\t\tpanic(\"must be a func declaration\");
  \t\t\t}\
  \t\t\tP.separator = blank;\
-\t\t\tP.Block(d.list, d.end, true);\
+\t\t\tP.Block(0, d.list, d.end, true);\
  \t\t}\
  \t\t\n \t\tif d.tok != Scanner.TYPE {\
  \t\t\tP.separator = semicolon;\
@@ -666,11 +658,7 @@ func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {\
  \t\t}\
  \t}\
  \t\n-\tif d.tok == Scanner.FUNC {\
-\t\tP.state = funcend;\
-\t} else {\
-\t\tP.state = lineend;\
-\t}\
+\tP.newlines = 2;\
  }
  
  
@@ -680,11 +672,11 @@ func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {\
  func (P *Printer) Program(p *AST.Program) {\
  \tP.String(p.pos, \"package \");
  \tP.Expr(p.ident);\
-\tP.state = lineend;\
+\tP.newlines = 1;\
  \tfor i := 0; i < p.decls.Len(); i++ {\
  \t\tP.Declaration(p.decls.At(i), false);\
  \t}\
-\tP.state = lineend;\
+\tP.newlines = 1;\
  }
  
  

コアとなるコードの解説

usr/gri/pretty/parser.go の変更

ParseSwitchStat 関数は、Go言語の switch ステートメントを解析し、そのAST表現を構築する役割を担っています。追加された s.end = P.pos; の一行は、解析中のスイッチステートメントのASTノード s に対して、その終了位置 (P.pos) を記録しています。

この変更の重要性は、プリティプリンターが正確なコードを生成するために、ASTノードがソースコード内の正確な開始位置と終了位置を持つ必要があるという点にあります。特に、コメントや空白文字はASTには直接含まれないため、プリティプリンターはこれらの位置情報に基づいて、コメントを元のコードの適切な場所に再配置する必要があります。スイッチステートメントの終了位置を正確に記録することで、そのブロック内や直後のコメントの整形がより正確に行えるようになります。

usr/gri/pretty/printer.go の変更

printer.go の変更は、プリティプリンターの内部状態管理と、コメントおよび空白文字の整形ロジックの根本的な改善を目的としています。

  1. 状態管理の刷新:

    • 以前の inline, lineend, funcend といった列挙型による「状態」は、コードの整形における特定の状況(例: 行末、関数末尾)を表していました。しかし、これらの状態は柔軟性に欠け、複雑な整形ルールに対応しにくいという問題がありました。
    • 新しい newlines フィールドは、出力すべき保留中の改行の数を直接的に保持します。これにより、プリティプリンターは、特定の状態に縛られることなく、必要に応じて1行、2行、あるいはそれ以上の改行を柔軟に挿入できるようになりました。maxnl による最大改行数の制限は、過剰な空白行を防ぐための実用的なヒューリスティックです。
    • indent から indentation への名称変更は、単なるインデントレベルではなく、より広範な「字下げ」の概念を表現するためのものです。
  2. Newline 関数の機能拡張:

    • Newline(n int) は、n の値に応じて複数の改行を出力し、その後、現在の indentation レベルに応じたタブ文字を挿入します。これにより、コードブロックの開始や関数定義の後に、適切な数の空白行とインデントを自動的に挿入できるようになりました。
  3. String 関数におけるコメントと空白の制御:

    • String 関数は、プリティプリンターの中核であり、文字列(トークン)を出力する際に、その前後の空白やコメントを適切に処理します。
    • trailing_blanktrailing_tabtrailing_char に統合したことで、直前に出力された文字の種類(スペース、タブ、なし)をより簡潔に管理できるようになりました。これは、コメントを挿入する際に、既存の空白との重複を避け、適切な間隔を確保するために重要です。
    • P.PendingComment(pos) の導入により、コメントの処理がよりモジュール化され、可読性が向上しました。
    • コメントの整形ロジックは、// スタイルと /* */ スタイルで異なる振る舞いをします。// コメントは通常、行末に配置され、その後に改行が続くことが期待されるため、P.newlines = 1 が設定されます。/* */ コメントは、コードの途中に挿入されることがあり、その前後にスペースが必要となる場合があります。これらの違いを考慮することで、コメントがコードの可読性を損なうことなく、適切に配置されるようになります。
    • switch P.state ブロックの削除と、P.Newline(P.newlines); P.newlines = 0; による置き換えは、改行の出力が String 関数の最後に一元化されたことを意味します。これにより、整形ロジックがより予測可能になり、デバッグが容易になります。
  4. スコープ、ブロック、宣言の整形:

    • OpenScope, CloseScope, Block, Fields, StatementList, Stat, Declaration, Program といった関数における P.state の設定が P.newlines の設定に置き換えられました。これは、これらの構文要素の開始や終了時に、必要な改行とインデントをより正確に制御するためのものです。
    • 特に、関数宣言の後に P.newlines = 2 を設定する変更は、関数定義間に常に2行の空白行を挿入するという整形ルールを強制します。これは、Go言語の慣習的なフォーマットスタイルに合致し、コードのセクション間の視覚的な分離を強化します。

これらの変更は、Go言語のプリティプリンターが、単に構文的に正しいコードを出力するだけでなく、Goコミュニティで広く受け入れられているスタイルガイドに沿った、視覚的に美しく、読みやすいコードを生成するための重要なステップでした。特に、コメントと空白文字の複雑な相互作用を適切に処理する能力は、高品質なコードフォーマッタの実現に不可欠です。

関連リンク

参考にした情報源リンク

  • Go言語の初期開発に関する情報(一般的な知識として)
  • コードフォーマッタにおける冪等性に関する一般的な概念
  • Go言語の go/printer パッケージの設計思想に関する議論(一般的な知識として)
  • text/tabwriter パッケージのドキュメント