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

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

このコミットは、EmacsエディタのGo言語モード(go-mode)におけるシンタックスハイライトのバグを修正するものです。具体的には、コメント内や通常の文字列リテラル内に存在するバッククォート(`)が、Go言語のRaw String Literal(生文字列リテラル)の開始と誤って認識され、不適切なシンタックスハイライトが行われる問題を解決します。

コミット

commit 3d63ec240edfc596a8840f7a7e5218fa28e55c04
Author: Rui Ueyama <ruiu@google.com>
Date:   Wed Apr 9 12:28:27 2014 -0400

    misc/emacs: ignore backquote in comment or string
    
    go-mode on Emacs 23 wrongly recognizes a backquote in a comment or
    a string as a start of a raw string literal. Below is an example
    that go-mode does not work well. This patch is to fix that issue.
    
      // `
      var x = 1
      // `
    
    LGTM=dominik.honnef
    R=golang-codereviews, dominik.honnef, adonovan
    CC=golang-codereviews
    https://golang.org/cl/84900043

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

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

元コミット内容

misc/emacs: ignore backquote in comment or string

go-mode on Emacs 23 wrongly recognizes a backquote in a comment or
a string as a start of a raw string literal. Below is an example
that go-mode does not work well. This patch is to fix that issue.

  // `
  var x = 1
  // `

LGTM=dominik.honnef
R=golang-codereviews, dominik.honnef, adonovan
CC=golang-codereviews
https://golang.org/cl/84900043

変更の背景

Emacs 23のgo-modeにおいて、Go言語のソースコードを編集する際に、コメント内や通常の文字列リテラル内にバッククォート(`)が存在すると、go-modeがそれをGoのRaw String Literalの開始と誤認識してしまう問題がありました。この誤認識により、それ以降のコードのシンタックスハイライトが崩れてしまい、コードの可読性が著しく低下するというユーザーエクスペリエンス上の問題が発生していました。

例えば、以下のようなコードがあった場合、

// `
var x = 1
// `

go-modeは最初の// の後のバッククォートをRaw String Literalの開始と見なし、var x = 1の行もRaw String Literalの一部としてハイライトしてしまうため、開発者はコードの構造を正確に把握することが困難になっていました。このコミットは、この誤認識を修正し、正しいシンタックスハイライトを提供することを目的としています。

前提知識の解説

Emacs

Emacsは、強力なカスタマイズ性と拡張性を持つテキストエディタです。Lisp言語(Emacs Lisp)で記述されており、ユーザーはEmacs Lispを用いてエディタの挙動を細かく制御したり、新しい機能を追加したりすることができます。

go-mode

go-modeは、Emacs上でGo言語のソースコードを編集するためのメジャーモードです。Go言語のシンタックスハイライト、自動インデント、コード補完、フォーマットなどの機能を提供し、Go開発者の生産性を向上させます。

Font-lock mode

font-lock-modeは、Emacsの主要なシンタックスハイライト機能を提供するマイナーモードです。バッファ(開いているファイルの内容)を解析し、キーワード、文字列、コメント、変数名、関数名などの構文要素に異なる色やスタイルを適用します。これにより、コードの構造が視覚的に分かりやすくなり、可読性が向上します。font-lock-modeは、各メジャーモードが定義するfont-lock-syntactic-keywordsなどの変数に設定されたパターン(正規表現や関数)に基づいて、どの部分をどのようにハイライトするかを決定します。

Go言語のRaw String Literal (生文字列リテラル)

Go言語には、通常の文字列リテラル(ダブルクォート"で囲む)とは別に、Raw String Literal(生文字列リテラル)というものがあります。これはバッククォート(`)で囲まれた文字列で、その内部ではエスケープシーケンス(例: \n, \t)が一切解釈されず、記述された文字がそのまま文字列の値となります。複数行にわたる文字列や、正規表現、HTML/XMLなどの記述において、エスケープ文字を気にすることなく記述できるため非常に便利です。

例:

// 通常の文字列リテラル
s1 := "Hello\nWorld" // 改行文字が含まれる

// Raw String Literal
s2 := `Hello\nWorld` // "\n"がそのまま文字列に含まれる
s3 := `
<html>
  <body>
    <h1>Hello</h1>
  </body>
</html>
` // 複数行のHTMLをそのまま記述

技術的詳細

go-modeにおけるシンタックスハイライトは、Emacsのfont-lock-modeを利用して実現されています。font-lock-modeは、font-lock-syntactic-keywordsという変数に定義されたパターンリストを基に、バッファ内のテキストをスキャンし、対応する構文要素に適切なフェイス(色やスタイル)を適用します。

このコミット以前のgo-mode.elでは、Raw String Literalを認識するために、font-lock-syntactic-keywordsリスト内に以下のような正規表現パターンが直接定義されていました。

'("`\\(`\\)\\(.*\\)`\\(`\\)`"
   (1 (7 . ?`))
   (2 (15 . nil))  ;; 15 = "generic string"
   (3 (7 . ?`))))

この正規表現は、バッククォートで始まりバッククォートで終わる任意の文字列をRaw String Literalとしてマッチさせようとします。しかし、この単純な正規表現では、バッククォートがコメント内(///* */)や通常のダブルクォートで囲まれた文字列リテラル内に現れた場合でも、それをRaw String Literalの開始と誤って解釈してしまうという問題がありました。これは、正規表現が文脈(コメント内か、文字列内かなど)を考慮せずにパターンマッチングを行うためです。

このコミットでは、この問題を解決するために、Raw String Literalの認識方法を根本的に変更しました。具体的には、font-lock-syntactic-keywordsリストから直接正規表現を削除し、代わりにgo--match-raw-string-literalというEmacs Lisp関数を呼び出すように変更しました。

新しいgo--match-raw-string-literal関数は、バッククォートを検出した際に、そのバッククォートが実際にRaw String Literalの開始であるかどうかをより厳密に判断するロジックを含んでいます。この判断には、go-in-string-or-comment-pという補助関数が利用されます。この補助関数は、現在のカーソル位置がコメント内または文字列リテラル内にあるかどうかを判定するもので、これにより、コメントや通常の文字列内のバッククォートをRaw String Literalとして誤認識することを防ぎます。

つまり、変更前は「バッククォートで囲まれていればRaw String Literal」という単純なルールだったのに対し、変更後は「バッククォートで囲まれており、かつそれがコメント内や通常の文字列内ではない場合にのみRaw String Literal」という、より洗練された文脈依存のルールが適用されるようになりました。

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

misc/emacs/go-mode.el ファイルが変更されました。

--- a/misc/emacs/go-mode.el
+++ b/misc/emacs/go-mode.el
@@ -294,7 +294,7 @@ For mode=set, all covered lines will have this weight."
 (defconst go--font-lock-syntactic-keywords
   ;; Override syntax property of raw string literal contents, so that
   ;; backslashes have no special meaning in ``. Used in Emacs 23 or older.
-  '((go--match-raw-string-literal
+  '((go--match-raw-string-literal
      (1 (7 . ?`))
      (2 (15 . nil))  ;; 15 = "generic string"
      (3 (7 . ?`))))
@@ -367,6 +367,18 @@ STOP-AT-STRING is not true, over strings."
       (- (point-max)
          (point-min))))
 
+(defun go--match-raw-string-literal (end)
+  "Search for a raw string literal. Set point to the end of the
+occurence found on success. Returns nil on failure."
+  (when (search-forward "`" end t)
+    (goto-char (match-beginning 0))
+    (if (go-in-string-or-comment-p)
+        (progn (goto-char (match-end 0))
+               (go--match-raw-string-literal end))
+      (when (looking-at "`\\(`\\)\\(.*\\)`\\(`\\)`")
+        (goto-char (match-end 0))
+        t))))
+
 (defun go-previous-line-has-dangling-op-p ()
   "Returns non-nil if the current line is a continuation line."
   (let* ((cur-line (line-number-at-pos))

コアとなるコードの解説

go--font-lock-syntactic-keywords の変更

以前は、go--font-lock-syntactic-keywords の定義内で、Raw String Literalを認識するための正規表現が直接記述されていました。

'("`\\(`\\)\\(.*\\)`\\(`\\)`"
   (1 (7 . ?`))
   (2 (15 . nil))  ;; 15 = "generic string"
   (3 (7 . ?`))))

このコミットでは、この正規表現を削除し、代わりにgo--match-raw-string-literalという関数を呼び出すように変更されました。

'((go--match-raw-string-literal
   (1 (7 . ?`))
   (2 (15 . nil))  ;; 15 = "generic string"
   (3 (7 . ?`))))

これは、シンタックスハイライトのロジックを正規表現による単純なパターンマッチングから、より複雑な条件分岐を伴うEmacs Lisp関数へと移行したことを意味します。

新しく追加された関数 go--match-raw-string-literal

この関数が、Raw String Literalを正確に識別するための新しいロジックを実装しています。

(defun go--match-raw-string-literal (end)
  "Search for a raw string literal. Set point to the end of the
occurence found on success. Returns nil on failure."
  (when (search-forward "`" end t)
    (goto-char (match-beginning 0))
    (if (go-in-string-or-comment-p)
        (progn (goto-char (match-end 0))
               (go--match-raw-string-literal end))
      (when (looking-at "`\\(`\\)\\(.*\\)`\\(`\\)`")
        (goto-char (match-end 0))
        t))))

この関数の動作は以下の通りです。

  1. (when (search-forward "" end t): 現在のカーソル位置からend(検索範囲の終端)まで、バッククォート(`` ``)を前方検索します。バッククォートが見つかれば、その位置にカーソルを移動し、whenブロック内の処理を実行します。見つからなければnilを返します。
  2. (goto-char (match-beginning 0)): search-forwardで見つかったバッククォートの開始位置にカーソルを移動します。
  3. (if (go-in-string-or-comment-p): ここがこの修正の核心部分です。go-in-string-or-comment-p関数を呼び出し、現在のカーソル位置(バッククォートが見つかった位置)がコメント内または通常の文字列リテラル内にあるかどうかを判定します。
    • もしコメント内または文字列内であれば:
      • (progn (goto-char (match-end 0)) (go--match-raw-string-literal end)): prognは複数の式を順に評価します。まず(goto-char (match-end 0))で、見つかったバッククォートの直後にカーソルを移動します。次に、(go--match-raw-string-literal end)を再帰的に呼び出し、現在のバッククォートをスキップして、検索範囲の終端まで次のバッククォートを探し続けます。これにより、コメントや文字列内のバッククォートはRaw String Literalとして扱われなくなります。
    • もしコメント内または文字列内でなければ:
      • (when (looking-at "\(\\)\\(.*\\)\(\\)"): 現在のカーソル位置から、GoのRaw String Literalの正規表現パターン(バッククォートで始まりバッククォートで終わる)にマッチするかどうかを確認します。looking-at`は、現在のカーソル位置からパターンがマッチするかどうかをチェックします。
      • (goto-char (match-end 0)): もしパターンにマッチすれば、マッチしたRaw String Literalの終端にカーソルを移動します。
      • t: そしてt(真)を返します。font-lock-modeは、このtを受け取ると、マッチした部分をRaw String Literalとしてハイライトします。

このgo--match-raw-string-literal関数と、その中で利用されるgo-in-string-or-comment-p関数(このコミットでは変更されていませんが、既存のgo-modeの機能として存在します)の組み合わせにより、go-modeはバッククォートがRaw String Literalの一部であるべきか、それとも単なるコメントや文字列内の文字として無視すべきかを正確に判断できるようになりました。

関連リンク

参考にした情報源リンク

  • Emacs font-lock-modeに関する情報: Web search results for "Emacs font-lock-mode go-mode.el raw string literal"