zoukankan      html  css  js  c++  java
  • 用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可以把这两种方式做成可配置就好了。

  • 相关阅读:
    mac单机 k8s minikube ELK yaml 详细配置 踩坑
    springboot es 配置, ElasticsearchRepository接口使用
    Docker 搭建 ELK 日志记录
    空杯心态
    与友人谈
    mac单机, jenkins-master在集群k8s外, k8s内部署动态jenkins-slave, jnlp方式. 踩坑+吐血详细总结
    Anyproxy 代理前端请求并mock返回 二次开发 持续集成
    Oracle 设置TO_DATE('13-OCT-20', 'dd-MON-yy'), 报错 ORA-01843: 无效的月份
    allure-java 二次开发 添加自定义注解, 并修改@step相关aop问题
    Appium添加Listener运行报错
  • 原文地址:https://www.cnblogs.com/babe/p/2441636.html
Copyright © 2011-2022 走看看