用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基本配置:
(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):
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显示出错误信息:
(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)) |
我把它绑定到了这几个按键:
(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来检测代码该多好啊,所以ahei在DEA中配置了flymake直接调用gcc检测C++代码,具体代码在flymake-setting.el中,主要是以下几个函数:
(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):
(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文件了:
(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可以把这两种方式做成可配置就好了。