用flymake检测C/C++语法

1 前言

前段时间ahei使劲推荐flymake,而且在dea中还给出一段flymake配置,勾起了我学习flymake的兴趣。在此之前只是听说过flymake,偶尔浅尝一下还没学会怎么用就放弃了,这几天折腾flymae后觉得实在很给力。生活不是缺少美,而是缺少发现美的眼睛这话说得还真有点道理。

2 flymake基本用法

flymake是一个实时的语法检查工具,好像是从emacs22开始已经自带flymake,自带的flymake提供了对C,C++,XML,HTML,C#,perl,php,java,tex,idl的支持。查看flymake-allowed-file-name-masks这个变量可以得到支持语言的详细信息。想要了解其它语言的支持,可以看看http://www.emacswiki.org/FlyMake

在以下四种情况下,flymake会执行语法检测:

  • 打开文件时
  • 换行(可通过flymake-start-syntax-check-on-newline配置)
  • 代码改变0.5秒后(可通过flymake-no-changes-timeout配置)
  • 手工执行flymake-start-syntax-check

下面是flymake基本配置:

?View Code LISP
 
(autoload 'flymake-find-file-hook "flymake" "" t)
(add-hook 'find-file-hook 'flymake-find-file-hook)
(setq flymake-gui-warnings-enabled nil)
(setq flymake-log-level 0)

加上以上配置后,打开文件时会检测是否是flymake支持的语言,是的话就会自动打开flymake-mode。flymake-gui-warnings-enabled设置为nil表示出错时不弹个对话框显示错误;flymake-log-level设置为0表示记录错误日志。

本文主要讨论C/C++(因为别的我不会),自带的flymake对C/C++的支持是通过Makefile实现的,Makefile中必须有一个check-syntax目标,比如我在Linux下用automake,那我在Makefile.am中加了这么一段(其实就是调用gcc):

?View Code MAKEFILE
 
check-syntax:
    $(CXXCOMPILE) -Wall -Wextra -pedantic -fsyntax-only $(CHK_SOURCES)

如果不用automake,手写Makefile的话,相应地修改一下就行了。

有了这些设置后,打开项目中的cpp文件,应该会自动检测语法了,语法有错误的话会用颜色标识出错的行,M-x flymake-goto-next-error和M-xflymake-goto-prev-error可以在错误行间移动;鼠标在错误行上停留会用tooltip显示错误信息;M-x flymake-display-err-menu-for-current-line能弹出一个菜单显示错误。

另外,ahei写了几个函数可以在错误行间移动时在minibuffer显示出错误信息:

?View Code LISP
 
(defun flymake-display-current-error ()
  "Display errors/warnings under cursor."
  (interactive)
  (let ((ovs (overlays-in (point) (1+ (point)))))
    (catch 'found
      (dolist (ov ovs)
        (when (flymake-overlay-p ov)
          (message (overlay-get ov 'help-echo))
          (throw 'found t))))))
(defun flymake-goto-next-error-disp ()
  "Go to next error in err ring, then display error/warning."
  (interactive)
  (flymake-goto-next-error)
  (flymake-display-current-error))
(defun flymake-goto-prev-error-disp ()
  "Go to previous error in err ring, then display error/warning."
  (interactive)
  (flymake-goto-prev-error)
  (flymake-display-current-error))

我把它绑定到了这几个按键:

?View Code LISP
 
(defvar flymake-mode-map (make-sparse-keymap))
(define-key flymake-mode-map (kbd "C-c <f4>") 'flymake-goto-next-error-disp)
(define-key flymake-mode-map (kbd "C-c <S-f4>") 'flymake-goto-prev-error-disp)
(define-key flymake-mode-map (kbd "C-c <C-f4>")
  'flymake-display-err-menu-for-current-line)
(or (assoc 'flymake-mode minor-mode-map-alist)
    (setq minor-mode-map-alist
          (cons (cons 'flymake-mode flymake-mode-map)
                minor-mode-map-alist)))

编辑过程中,每次修改过代码0.5秒后,flymake都会检测错误,这就可以随时发现代码编写的错误了。

在这补充一下flymake检测的方法:

对于cpp文件,每次检测时,flymake是把buffer内的内容另存一份,再检测另存出来的文件。

而对于h文件,gcc没办法单独检查头文件,flymake会在flymake-master-file-dirs设定的目录中查找include过这个头文件的实现文件,把buffer另存为xxx_flymake.h,把查找到的第一个满足条件的实现文件另存yyy_flymake_master.cpp,并把里面的include语句改为include另存的文件,然后通过yyy_flymake_master.cpp来间接检测头文件。

3 ahei的改进

自带的flymake对C++只能通过Makefile来支持,还必须在Makefile中加入check-syntax目标,实在很麻烦。要是代码不是通过Makefile来管理的,flymake就无能为力了。

其实Makefile也是通过gcc来检测代码了,那要是跳过Makefile直接调用gcc来检测代码该多好啊,所以aheiDEA中配置了flymake直接调用gcc检测C++代码,具体代码在flymake-setting.el中,主要是以下几个函数:

?View Code LISP
 
(defvar flymake-makefile-filenames '("Makefile" "makefile" "GNUmakefile") "File names for make.")
 
(defun flymake-get-make-gcc-cmdline (source base-dir)
  (let (found)
    (dolist (makefile flymake-makefile-filenames)
      (if (file-readable-p (concat base-dir "/" makefile))
          (setq found t)))
    (if found
        (list "make"
              (list "-s"
                    "-C"
                    base-dir
                    (concat "CHK_SOURCES=" source)
                    "SYNTAX_CHECK_MODE=1"
                    "check-syntax"))
      (list (if (string= (file-name-extension source) "c") "gcc" "g++")
            (list "-o"
                  "/dev/null"
                  "-S"
                  source)))))
 
(defun flymake-simple-make-gcc-init-impl (create-temp-f use-relative-base-dir use-relative-source build-file-name get-cmdline-f)
  "Create syntax check command line for a directly checked source file.
Use CREATE-TEMP-F for creating temp copy."
  (let* ((args nil)
         (source-file-name buffer-file-name)
         (buildfile-dir (file-name-directory source-file-name)))
    (if buildfile-dir
        (let* ((temp-source-file-name  (flymake-init-create-temp-buffer-copy create-temp-f)))
          (setq args
                (flymake-get-syntax-check-program-args
                 temp-source-file-name
                 buildfile-dir
                 use-relative-base-dir
                 use-relative-source
                 get-cmdline-f))))
    args))
 
(defun flymake-simple-make-gcc-init ()
  (flymake-simple-make-gcc-init-impl 'flymake-create-temp-inplace t t "Makefile" 'flymake-get-make-gcc-cmdline))

主要思路就是先检测Makefile文件,如果存在就调用make,否则就直接调用gcc检测。

4 我的修改

ahei配置的gcc非常实用,我就是因为它喜欢上flymake的,不过用过几天后,发现ahei的配置里有几个问题没解决:

  • 没有对make或gcc进行检测 

    flymake进行语法检测都是通过调用外部程序来实现的,比如make或gcc,如果没有安装这两个程序,flymake还是会死心眼地启动一个process去调用。

  • 不支持用gcc直接检测头文件 

    ahei好像是把头文件忘掉了。

  • 目标没有权限写入时会出错 

    因为flymake检测文件时会把buffer内容另存到一个临时文件中再检测,如果我以普通用户身份打开/usr/include/下的头文件,或者/usr/src/下的实现文件时,flymake会报靠说权限有问题,并且你会发现这个文件没在emacs中被打开。

  • 不支持父目录中的Makefile 

    原始的flymake调用flymake-init-find-buildfile-dir来查找Makefile,它会从当前目录一直往上找,直到根目录为止,只要找到Makefile都可以。这有一个好处就是对一个有很多子目录的大工程,不需要对每个子目录下的Makefile文件都加上check-syntax目标,只需要在最顶层的Makefile中加就可以了。但ahei修改的时候可能是因为flymake-init-find-buildfile-dir找不到Makefile就会报错退出而无法转而使用gcc而放弃了这个函数,改为只在当前目录下找Makefile文件而不支持查找父目录了。

我的配置主要是在ahei的基础上进行修改,解决了这几个我发现的问题:

1.没有检测make或gcc存在的问题,我在配置文件中先检测有没有对应的外部程序,没有的话就不配置到flymake-allowed-file-name-masks中了。

2.我加了两个函数:flymake-master-make-gcc-header-init和flymake-master-make-gcc-init来支持直接用gcc检测头文件。

3.用ignore-error忽略掉权限错误,用flymake-report-fatal-status把错误通过minibuffer报告出来。

4.用flymake-find-buildfile来查找Makefile文件,能支持父目录Makefile查找。

修改后的代码如下(完整的配置见http://github.com/meteor1113/dotemacs/blob/master/init-basic.el):

?View Code LISP
 
(setq flymake-allowed-file-name-masks '())
(when (executable-find "texify")
  (add-to-list 'flymake-allowed-file-name-masks
               '("\\.tex\\'" flymake-simple-tex-init))
  (add-to-list 'flymake-allowed-file-name-masks
               '("[0-9]+\\.tex\\'"
                 flymake-master-tex-init flymake-master-cleanup)))
(when (executable-find "xml")
  (add-to-list 'flymake-allowed-file-name-masks
               '("\\.xml\\'" flymake-xml-init))
  (add-to-list 'flymake-allowed-file-name-masks
               '("\\.html?\\'" flymake-xml-init)))
(when (executable-find "perl")
  (add-to-list 'flymake-allowed-file-name-masks
               '("\\.p[ml]\\'" flymake-perl-init)))
(when (executable-find "php")
  (add-to-list 'flymake-allowed-file-name-masks
               '("\\.php[1]?\\'" flymake-php-init)))
(when (executable-find "make")
  (add-to-list 'flymake-allowed-file-name-masks
               '("\\.idl\\'" flymake-simple-make-init))
  (add-to-list 'flymake-allowed-file-name-masks
               '("\\.java\\'"
                 flymake-simple-make-java-init flymake-simple-java-cleanup))
  (add-to-list 'flymake-allowed-file-name-masks
               '("\\.cs\\'" flymake-simple-make-init)))
(when (or (executable-find "make")
          (executable-find "gcc")
          (executable-find "g++"))
  (defvar flymake-makefile-filenames '("Makefile" "makefile" "GNUmakefile")
    "File names for make.")
  (defun flymake-get-gcc-cmdline (source base-dir)
    (let ((cc (if (string= (file-name-extension source) "c") "gcc" "g++")))
      (list cc
            (list "-Wall"
                  "-Wextra"
                  "-pedantic"
                  "-fsyntax-only"
                  "-I.."
                  "-I../include"
                  "-I../inc"
                  "-I../common"
                  "-I../public"
                  "-I../.."
                  "-Ihttp://www.cnblogs.com/include"
                  "-Ihttp://www.cnblogs.com/inc"
                  "-Ihttp://www.cnblogs.com/common"
                  "-Ihttp://www.cnblogs.com/public"
                  source))))
  (defun flymake-init-find-makfile-dir (source-file-name)
    "Find Makefile, store its dir in buffer data and return its dir, if found."
    (let* ((source-dir (file-name-directory source-file-name))
           (buildfile-dir nil))
      (catch 'found
        (dolist (makefile flymake-makefile-filenames)
          (let ((found-dir (flymake-find-buildfile makefile source-dir)))
            (when found-dir
              (setq buildfile-dir found-dir)
              (setq flymake-base-dir buildfile-dir)
              (throw 'found t)))))
      buildfile-dir))
  (defun flymake-simple-make-gcc-init-impl (create-temp-f
                                            use-relative-base-dir
                                            use-relative-source)
    "Create syntax check command line for a directly checked source file.
Use CREATE-TEMP-F for creating temp copy."
    (let* ((args nil)
           (source-file-name buffer-file-name)
           (source-dir (file-name-directory source-file-name))
           (buildfile-dir
            (and (executable-find "make")
                 (flymake-init-find-makfile-dir source-file-name)))
           (cc (if (string= (file-name-extension source-file-name) "c")
                   "gcc"
                 "g++")))
      (if (or buildfile-dir (executable-find cc))
          (let* ((temp-source-file-name
                  (ignore-errors
                    (flymake-init-create-temp-buffer-copy create-temp-f))))
            (if temp-source-file-name
                (setq args
                      (flymake-get-syntax-check-program-args
                       temp-source-file-name
                       (if buildfile-dir buildfile-dir source-dir)
                       use-relative-base-dir
                       use-relative-source
                       (if buildfile-dir
                           'flymake-get-make-cmdline
                         'flymake-get-gcc-cmdline)))
              (flymake-report-fatal-status
               "TMPERR"
               (format "Can't create temp file for %s" source-file-name))))
        (flymake-report-fatal-status
         "NOMK" (format "No buildfile (%s) found for %s, or can't found %s"
                        "Makefile" source-file-name cc)))
      args))
  (defun flymake-simple-make-gcc-init ()
    (flymake-simple-make-gcc-init-impl 'flymake-create-temp-inplace t t))
  (defun flymake-master-make-gcc-init (get-incl-dirs-f
                                       master-file-masks
                                       include-regexp)
    "Create make command line for a source file
 checked via master file compilation."
    (let* ((args nil)
           (temp-master-file-name
            (ignore-errors
              (flymake-init-create-temp-source-and-master-buffer-copy
               get-incl-dirs-f
               'flymake-create-temp-inplace
               master-file-masks
               include-regexp)))
           (cc (if (string= (file-name-extension buffer-file-name) "c")
                   "gcc"
                 "g++")))
      (if temp-master-file-name
          (let* ((source-file-name buffer-file-name)
                 (source-dir (file-name-directory source-file-name))
                 (buildfile-dir
                  (and (executable-find "make")
                       (flymake-init-find-makfile-dir source-file-name))))
            (if (or buildfile-dir (executable-find cc))
                (setq args (flymake-get-syntax-check-program-args
                            temp-master-file-name
                            (if buildfile-dir buildfile-dir source-dir)
                            nil
                            nil
                            (if buildfile-dir
                                'flymake-get-make-cmdline
                              'flymake-get-gcc-cmdline)))
              (flymake-report-fatal-status
               "NOMK"
               (format "No buildfile (%s) found for %s, or can't found %s"
                       "Makefile" source-file-name cc))))
        (flymake-report-fatal-status
         "TMPERR" (format "Can't create temp file for %s" source-file-name)))
      args))
  (defun flymake-master-make-gcc-header-init ()
    (flymake-master-make-gcc-init
     'flymake-get-include-dirs
     '("\\.cpp\\'" "\\.c\\'")
     "[ \t]*#[ \t]*include[ \t]*\"\\([[:word:]0-9/\\_.]*%s\\)\""))
  (add-to-list 'flymake-allowed-file-name-masks
               '("\\.\\(?:h\\(?:pp\\)?\\)\\'"
                 flymake-master-make-gcc-header-init flymake-master-cleanup))
  (add-to-list 'flymake-allowed-file-name-masks
               '("\\.\\(?:c\\(?:pp\\|xx\\|\\+\\+\\)?\\|CC\\)\\'"
                 flymake-simple-make-gcc-init)))

5 检测python语法

因为偶尔我也用下python,所以我也希望flymake能把python也支持了。python有三个语法检测工具,我比较之后选择了pyflakes。

装好pyflakes后,加入以下配置就可以像检测cpp那样检测py文件了:

?View Code LISP
 
(when (executable-find "pyflakes")
  (defun flymake-pyflakes-init ()
    (let* ((temp-file (flymake-init-create-temp-buffer-copy
                       'flymake-create-temp-inplace))
           (local-file (file-relative-name
                        temp-file
                        (file-name-directory buffer-file-name))))
      (list "pyflakes" (list local-file))))
  (add-to-list 'flymake-allowed-file-name-masks
               '("\\.py\\'" flymake-pyflakes-init)))

如果是在windows下的话,可能会找不到pyflakes这个外部程序,因为C:\Python25\Scripts\pyflakes没被windows识别为可执行文件,我是在C:\Python25\Scripts\下加了个pyflakes.bat的文件,文件里写入以下内容就能正常检测了:

C:\Python25\python.exe C:\Python25\Scripts\pyflakes %*

6 遗留问题

有两个问题我还没解决:

一、对C++来说,找不到Makefile的时候自动改用gcc语法检测,但有时候看别人工程时会碰到有Makefile,但里面没有写check-syntax目标的问题(从网上下载的开源代码很少有在Makefile中写check-syntax目标的)。要是能配置成在Makefile中找不到check-syntax目标后也能自动改用gcc检测就好了。

二、flymake通过定时器,改变代码超过0.5秒后进行语法检测,其实我不太喜欢这种立即检测的方式,相比之下我更喜欢保存文件后进行检测。要是下个版本的flymake可以把这两种方式做成可配置就好了。

原文地址:https://www.cnblogs.com/babe/p/2441636.html