[インデックス 17283] ファイルの概要
このコミットは、Go言語のEmacsモード (go-mode.el
) におけるコードカバレッジ表示機能の改善とバグ修正に関するものです。具体的には、カバレッジ情報を表示するためのコードが複数の関数にリファクタリングされ、カバレッジの色のグラデーションが誤った最大値に基づいて計算されていたバグが修正されています。
コミット
commit 3495aa298dd4f9e8033557e35fa05a6792d24563
Author: Dominik Honnef <dominik.honnef@gmail.com>
Date: Thu Aug 15 22:37:16 2013 -0400
misc/emacs: Refactor coverage code into multiple functions
Also fixes color gradient being calculated against the wrong maximum.
R=adonovan
CC=golang-dev
https://golang.org/cl/12968043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3495aa298dd4f9e8033557e35fa05a6792d24563
元コミット内容
misc/emacs: Refactor coverage code into multiple functions
Also fixes color gradient being calculated against the wrong maximum.
このコミットは、EmacsのGoモードにおけるカバレッジ表示コードを複数の関数に分割してリファクタリングし、同時に色のグラデーションが誤った最大値に基づいて計算されていたバグを修正します。
変更の背景
Go言語には、go test -coverprofile
コマンドを使用してコードカバレッジ情報を生成する機能があります。Emacsの go-mode.el
は、このカバレッジプロファイルファイルを読み込み、Emacsバッファ内の対応するコード行にカバレッジ情報を視覚的にオーバーレイ表示する機能を提供しています。
このコミット以前は、カバレッジ情報を処理し、Emacsのオーバーレイとして表示するロジックが、go-coverage
という単一の大きな関数内に集約されていました。これにより、コードの可読性、保守性、およびテスト容易性が低下していました。
また、カバレッジの「ヒット数」に基づいてコード行に色を付ける際、色のグラデーションの計算に使用される最大ヒット数 (max-count
) が、カバレッジプロファイル全体(つまり、テスト対象のすべてのファイル)の最大値に基づいていました。しかし、視覚的なグラデーションは、現在表示しているファイル内の最大ヒット数に基づいて計算されるべきでした。この不一致により、カバレッジの色の表示が直感的でなく、誤解を招く可能性がありました。例えば、あるファイルではヒット数が少ないにもかかわらず、他のファイルで非常に高いヒット数があったために、そのファイルの色のグラデーションが適切に表示されない、といった問題が発生していました。
このコミットは、これらの問題を解決するために、コードのモジュール化とバグ修正を目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念に関する基本的な知識が必要です。
- Emacs Lisp (Elisp): Emacsエディタの拡張機能や設定を記述するために使用されるプログラミング言語です。Emacsのほとんどの機能はElispで実装されており、ユーザーはElispを使ってEmacsをカスタマイズできます。
- Emacsのオーバーレイ (Overlays): Emacsのオーバーレイは、バッファのテキストに視覚的な属性(色、フォント、カーソル形状など)を一時的に適用するためのメカニズムです。オーバーレイはテキスト自体を変更せず、その表示方法のみを変更します。コードカバレッジのハイライト表示には、このオーバーレイ機能が利用されます。
- Go言語のコードカバレッジ: Go言語のテストツールは、
go test -coverprofile=coverage.out
コマンドを使用して、テスト実行中にどのコードが実行されたか(カバレッジ)を記録したプロファイルファイルを生成できます。このファイルには、各コードブロックの開始・終了位置と、そのブロックが実行された回数(ヒット数)が含まれます。 go test -covermode
:go test
コマンドのオプションで、カバレッジの計測モードを指定します。set
: コードブロックが実行されたかどうか(0または1)のみを記録します。count
: コードブロックが実行された回数を記録します。atomic
:count
と同様ですが、並行実行環境での正確性を保証します。
log
関数 (自然対数): このコミットでは、カバレッジのヒット数を色のグラデーションにマッピングするために自然対数 (log
) が使用されています。これは、ヒット数が非常に広い範囲にわたる場合に、色の変化をより均等に分散させるためによく用いられる手法です。例えば、ヒット数が1から10000まである場合、線形に色を割り当てると、ヒット数が少ない部分での色の違いが分かりにくくなります。対数スケールを用いることで、ヒット数の小さな違いも視覚的に区別しやすくなります。
技術的詳細
このコミットの主要な技術的変更点は、go-mode.el
内の go-coverage
関数のリファクタリングと、カバレッジ色のグラデーション計算の修正です。
-
関数の分割とモジュール化:
go--coverage-face (count divisor)
: この新しいヘルパー関数は、与えられたカバレッジヒット数 (count
) とスケーリングのためのdivisor
に基づいて、Emacsのフェイス(表示スタイル、主に色)の名前を生成します。divisor
は、ヒット数を0から10の範囲に正規化するために使用されます。covermode=set
の場合(divisor
が0)、ヒット数に関わらず特定のフェイス(go-coverage-8
)を返します。それ以外の場合は、log(count) / divisor
を計算し、これを0から10の範囲にマッピングしてフェイス名を生成します。これにより、カバレッジのヒット数が多いほど色が濃くなるようなグラデーションが実現されます。go--coverage-make-overlay (range divisor)
: この関数は、特定のコード範囲 (range
) に対してカバレッジオーバーレイを作成します。go--coverage-face
を呼び出して適切なフェイスを取得し、そのフェイスをオーバーレイに適用します。また、オーバーレイにhelp-echo
プロパティを設定し、マウスカーソルを合わせたときにヒット数を表示するようにします。go--coverage-clear-overlays ()
: 既存のすべてのオーバーレイを削除し、バッファ全体に「未追跡(untracked)」を示す単一のオーバーレイを適用します。これは、新しいカバレッジ情報を表示する前にバッファをクリーンアップするために使用されます。go--coverage-parse-file (coverage-file file-name)
: この関数は、指定されたカバレッジプロファイルファイル (coverage-file
) を解析し、特定のファイル (file-name
) のカバレッジ情報を抽出します。以前はgo-coverage
関数内に直接埋め込まれていたファイル解析ロジックがここに移動されました。この関数は、解析されたカバレッジ範囲のリストと、そのファイル内の最大ヒット数に基づいて計算されたdivisor
を返します。
-
色のグラデーション計算の修正:
- 以前のコードでは、
max-count
がカバレッジプロファイル全体から取得されていました。このコミットでは、go--coverage-parse-file
関数が、現在処理しているファイルに限定されたmax-count
を計算し、それに基づいてdivisor
を返すように変更されました。 go-coverage
関数は、go--coverage-parse-file
から返されたranges-and-divisor
(カバレッジ範囲のリストと、そのファイルに特化したdivisor
)を受け取ります。- オーバーレイを作成する際に、
go--coverage-make-overlay
関数にこのファイル固有のdivisor
が渡されるようになり、go--coverage-face
関数が正しいdivisor
を使用して色のグラデーションを計算できるようになりました。これにより、「色のグラデーションが誤った最大値に基づいて計算されていた」というバグが修正されました。
- 以前のコードでは、
これらの変更により、go-coverage
関数はより簡潔になり、各ヘルパー関数が単一の責任を持つことで、コードの理解と保守が容易になりました。
コアとなるコードの変更箇所
misc/emacs/go-mode.el
ファイルが変更されています。
追加された関数:
go--coverage-face
go--coverage-make-overlay
go--coverage-clear-overlays
go--coverage-parse-file
変更された関数:
go-coverage
具体的な変更点(diffから抜粋):
--- a/misc/emacs/go-mode.el
+++ b/misc/emacs/go-mode.el
@@ -1014,22 +1014,49 @@ current coverage buffer or by prompting for it."
go--coverage-origin-buffer
(current-buffer))))
-(defun go-coverage (&optional coverage-file)
- "Open a clone of the current buffer and overlay it with
-coverage information gathered via go test -coverprofile=COVERAGE-FILE.
-
-If COVERAGE-FILE is nil, it will either be infered from the
-current buffer if it's already a coverage buffer, or be prompted
-for."
- (interactive)
- (setq coverage-file (or coverage-file (go--coverage-file)))
- (let* ((ranges '())
- (cur-buffer (current-buffer))
- (origin-buffer (go--coverage-origin-buffer))
- (file-name (file-name-nondirectory (buffer-file-name origin-buffer)))
- (gocov-buffer-name (concat (buffer-name origin-buffer) "<gocov>"))
- (max-count 0)
- divisor)
+(defun go--coverage-face (count divisor)
+ "Return the intensity face for COUNT when using DIVISOR
+to scale it to a range [0,10].
+
+DIVISOR scales the absolute cover count to values from 0 to 10.
+For DIVISOR = 0 the count will always translate to 8."
+ (let* ((count (go--covered-count range))
+ (norm (cond
+ ((= count 0)
+ -0.1) ;; Uncovered code, set to -0.1 so n becomes 0.
+ ((= divisor 0)
+ 0.8) ;; covermode=set, set to 0.8 so n becomes 8.
+ (t
+ (/ (log count) divisor))))
+ (n (1+ (floor (* norm 9))))) ;; Convert normalized count [0,1] to intensity [0,10]
+ (concat "go-coverage-" (number-to-string n))))
+
+(defun go--coverage-make-overlay (range divisor)
+ "Create a coverage overlay for a RANGE of covered/uncovered
+code. Uses DIVISOR to scale absolute counts to a [0,10] scale."
+ (let* ((count (go--covered-count range))
+ (face (go--coverage-face count divisor))
+ (ov (make-overlay (go--line-column-to-point (go--covered-start-line range)
+ (go--covered-start-column range))
+ (go--line-column-to-point (go--covered-end-line range)
+ (go--covered-end-column range)))))
+
+ (overlay-put ov 'face face)
+ (overlay-put ov 'help-echo (format "Count: %d" count))))
+
+(defun go--coverage-clear-overlays ()
+ "Remove existing overlays and put a single untracked overlay
+over the entire buffer."
+ (remove-overlays)
+ (overlay-put (make-overlay (point-min) (point-max))
+ 'face
+ 'go-coverage-untracked)))
+
+(defun go--coverage-parse-file (coverage-file file-name)
+ "Parse COVERAGE-FILE and extract coverage information and
+divisor for FILE-NAME."
+ (let (ranges
+ (max-count 0))
(with-temp-buffer
(insert-file-contents coverage-file)
(go--goto-line 2) ;; Skip over mode
@@ -1040,62 +1067,48 @@ for."
(destructuring-bind
(start-line start-column end-line end-column num count)
- (mapcar 'string-to-number rest)\n
- (if (> count max-count)
- (setq max-count count))\n
- (if (and (string= (file-name-nondirectory file) file-name))
- (push
- (make-go--covered
- :start-line start-line
- :start-column start-column
- :end-line end-line
- :end-column end-column
- :covered (/= count 0)
- :count count)\n
- ranges)))\n
+ (mapcar #'string-to-number rest)
+
+ (when (and (string= (file-name-nondirectory file) file-name))
+ (if (> count max-count)
+ (setq max-count count))
+ (push (make-go--covered :start-line start-line
+ :start-column start-column
+ :end-line end-line
+ :end-column end-column
+ :covered (/= count 0)
+ :count count)
+ ranges)))
(forward-line)))\n
- (if (> max-count 0)
- (setq divisor (log max-count))))\n
+ (list ranges (if (> max-count 0) (log max-count) 0))))))
- (with-current-buffer (or
- (get-buffer gocov-buffer-name)
- (make-indirect-buffer origin-buffer gocov-buffer-name t))\n
+(defun go-coverage (&optional coverage-file)
+ "Open a clone of the current buffer and overlay it with
+coverage information gathered via go test -coverprofile=COVERAGE-FILE.
+
+If COVERAGE-FILE is nil, it will either be infered from the
+current buffer if it's already a coverage buffer, or be prompted
+for."
+ (interactive)
+ (let* ((cur-buffer (current-buffer))
+ (origin-buffer (go--coverage-origin-buffer))
+ (gocov-buffer-name (concat (buffer-name origin-buffer) "<gocov>"))
+ (coverage-file (or coverage-file (go--coverage-file)))
+ (ranges-and-divisor (go--coverage-parse-file
+ coverage-file
+ (file-name-nondirectory (buffer-file-name origin-buffer)))))
+ (with-current-buffer (or (get-buffer gocov-buffer-name)
+ (make-indirect-buffer origin-buffer gocov-buffer-name t))
(set (make-local-variable 'go--coverage-origin-buffer) origin-buffer)
(set (make-local-variable 'go--coverage-current-file-name) coverage-file)
(save-excursion
- (remove-overlays)
- (overlay-put
- (make-overlay
- (point-min)
- (point-max))
- 'face 'go-coverage-untracked)
-
- (dolist (range ranges)
- (let* ((count (go--covered-count range))
- (norm (cond
- ((= count 0)
- -0.1)
- ((= max-count 1)
- 0.8)
- (t
- (/ (log count) divisor))))
- (n (1+ (floor (* norm 9))))
- (face (concat "go-coverage-" (number-to-string n)))
- (ov (make-overlay
- (go--line-column-to-point
- (go--covered-start-line range)
- (go--covered-start-column range))
- (go--line-column-to-point
- (go--covered-end-line range)
- (go--covered-end-column range)))))
-
- (overlay-put ov 'face face)
- (overlay-put ov 'help-echo (format "Count: %d" count)))))\n
+ (go--coverage-clear-overlays)
+ (dolist (range (car ranges-and-divisor))
+ (go--coverage-make-overlay range (cadr ranges-and-divisor))))
+
(if (not (eq cur-buffer (current-buffer)))
(display-buffer (current-buffer) 'display-buffer-reuse-window))))
コアとなるコードの解説
このコミットの核となる変更は、go-coverage
関数が担っていた複雑な処理を、新しく導入された4つのヘルパー関数に分散させた点です。
-
go--coverage-parse-file
:- この関数は、カバレッジプロファイルファイルから特定のGoファイルの情報を抽出する役割を担います。
with-temp-buffer
を使用して一時バッファでカバレッジファイルを読み込み、行ごとに解析します。destructuring-bind
を用いて、カバレッジプロファイルの各行からstart-line
,start-column
,end-line
,end-column
,num
,count
などの情報を効率的に抽出します。- 最も重要なのは、この関数が、現在処理しているファイルに限定された
max-count
を計算し、そのmax-count
の自然対数 (log max-count
) をdivisor
として返す点です。これにより、色のグラデーションがファイルごとに適切にスケーリングされるようになります。 - 最終的に、解析されたカバレッジ範囲のリスト (
ranges
) と計算されたdivisor
をリストとして返します。
-
go--coverage-face
:- カバレッジのヒット数 (
count
) と、go--coverage-parse-file
から渡されたファイル固有のdivisor
を受け取ります。 count
が0の場合は、未カバーコードとして-0.1
を正規化値とします。divisor
が0の場合(covermode=set
の場合)、正規化値を0.8
とし、常にgo-coverage-8
フェイスを返します。これは、set
モードではヒット数が1か0しかないため、グラデーションが不要だからです。- それ以外の場合、
(/ (log count) divisor)
を計算して正規化値 (norm
) を求めます。これにより、ヒット数が対数スケールで0から1の範囲に正規化されます。 - この正規化値 (
norm
) を0から10の整数値 (n
) に変換し、go-coverage-N
という形式のフェイス名を生成します。例えば、n
が1ならgo-coverage-1
、n
が10ならgo-coverage-10
となります。これにより、ヒット数が多いほど濃い色になるグラデーションが実現されます。
- カバレッジのヒット数 (
-
go--coverage-make-overlay
:- カバレッジ範囲 (
range
) とdivisor
を受け取り、その範囲にEmacsのオーバーレイを作成します。 go--coverage-face
を呼び出して、この範囲に適用すべきフェイス名を取得します。make-overlay
でオーバーレイを作成し、overlay-put
で取得したフェイスと、ヒット数を表示するhelp-echo
プロパティを設定します。
- カバレッジ範囲 (
-
go--coverage-clear-overlays
:- 既存のすべてのオーバーレイを
remove-overlays
で削除し、バッファ全体にgo-coverage-untracked
フェイスを持つ単一のオーバーレイを適用します。これは、新しいカバレッジ情報を表示する前の初期化ステップです。
- 既存のすべてのオーバーレイを
-
go-coverage
:- このメイン関数は、ユーザーがカバレッジ表示をトリガーするエントリポイントです。
go--coverage-file
を使ってカバレッジファイルパスを決定します。go--coverage-parse-file
を呼び出して、カバレッジ範囲のリストとファイル固有のdivisor
を取得します。- カバレッジ情報を表示するバッファ(元のバッファのクローン)に切り替えます。
go--coverage-clear-overlays
を呼び出して既存のオーバーレイをクリアします。- 取得したカバレッジ範囲のリストを
dolist
でループし、各範囲に対してgo--coverage-make-overlay
を呼び出してオーバーレイを作成します。この際、go--coverage-parse-file
から取得したファイル固有のdivisor
を渡すことで、正しい色のグラデーションが適用されます。
これらの変更により、コードの各部分がより明確な責任を持つようになり、バグの特定と修正が容易になりました。特に、max-count
の計算がファイルごとに分離されたことで、カバレッジの色の表示が正確かつ直感的になりました。
関連リンク
- Go言語のテストとカバレッジ: https://go.dev/blog/cover
- Emacs Lisp Manual: https://www.gnu.org/software/emacs/manual/html_node/elisp/
- Emacs Overlays: https://www.gnu.org/software/emacs/manual/html_node/elisp/Overlays.html
参考にした情報源リンク
- golang/go GitHubリポジトリ: https://github.com/golang/go
- Gerrit Code Review (golang.org/cl/12968043): https://golang.org/cl/12968043 (コミットメッセージに記載されている変更リストへのリンク)
- Go言語の公式ドキュメント
- Emacs Lispの公式ドキュメント
- コードカバレッジに関する一般的な情報源