[インデックス 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-facego--coverage-make-overlaygo--coverage-clear-overlaysgo--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の公式ドキュメント
- コードカバレッジに関する一般的な情報源