emacs-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[PATCH] Flymake support for C/C++


From: João Távora
Subject: [PATCH] Flymake support for C/C++
Date: Thu, 12 Oct 2017 16:09:15 +0100
User-agent: Gnus/5.13 (Gnus v5.13) Emacs/26.0.90 (gnu/linux)

Hi,

Here's a proposal for supporting Flymake in C/C++. This patch:

- Sets up Flymake in c-mode buffers (heads up Alan), but doesn't
  automatically enable it.

- Adds a special target to src/Makefile so that Flymake can work with
  Emacs's C sources (using a default cc-flymake-use-special-make-target
  method).

- The special target's name is 'check-syntax' and uses CHK_SOURCES,
  which lets older Emacsen edit new Emacs sources with old Flymake
  implementations (dubious but harmless advantage).

- Is customizable by the user to use a zero-configuration method that
  guesses GCC flags from Makefiles (see
  cc-flymake-use-cc-directly). This probably works in the simplest
  scenarios(*)

- Is programmable by the user to use any other self-configuration
  technique (this includes per-file/dir manual configuration using a
  file-local cc-flymake-command)

*: Sadly, GNU Hello no longer works since it uses a backquoted shell
  expression that the current implementation can't intercept (my old
  use-emacs-as-a-shell-parser )
  Here it is, for reference

gcc -DLOCALEDIR=\"/usr/local/share/locale\" -DHAVE_CONFIG_H -I. -I. -I.. -I. 
-I. -I.. -I../intl -I../intl    -g -O2 -c `test -f 'hello.c' || echo 
'./'`hello.c

Also, FTR, discovered that -M flags do not seem to harm syntax-checking.
No idea if dependencies files are still created anyway though.

João

>From 41ef8318c8c539e475080e3b240b9c941d39caa9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= <address@hidden>
Date: Thu, 12 Oct 2017 14:04:51 +0100
Subject: [PATCH] Add a Flymake backend for C(++)

* lisp/progmodes/cc-flymake.el: New file.

* lisp/progmodes/cc-mode.el (c-mode): Call c--setup-flymake.
(c--setup-flymake): New function.

* src/Makefile.in: Add check-syntax target.
---
 lisp/progmodes/cc-flymake.el | 214 +++++++++++++++++++++++++++++++++++++++++++
 lisp/progmodes/cc-mode.el    |   9 ++
 src/Makefile.in              |   5 +
 3 files changed, 228 insertions(+)
 create mode 100644 lisp/progmodes/cc-flymake.el

diff --git a/lisp/progmodes/cc-flymake.el b/lisp/progmodes/cc-flymake.el
new file mode 100644
index 0000000000..4baffdd3be
--- /dev/null
+++ b/lisp/progmodes/cc-flymake.el
@@ -0,0 +1,214 @@
+;;; cc-flymake.el --- Flymake backends for C/C++     -*- lexical-binding: t; 
-*-
+
+;; Copyright (C) 2017 Free Software Foundation, Inc.
+
+;; Author: João Távora <address@hidden>
+;; Keywords: languages, c
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Flymake support for C/C++.
+
+;;; Code:
+
+(require 'subr-x) ; when-let*
+
+(defcustom cc-flymake-command 'cc-flymake-use-special-make-target
+  "Command used by the CC flymake backend.
+A list of strings, or a symbol naming a function that produces one
+such list when called with no arguments in the buffer where the
+variable `flymake-mode' is active.
+
+The command should invoke a GNU-style compiler that checks the
+syntax of a (Obj)C(++) program passed to it via its standard
+input and prints the result on its standard output."
+  :type '(choice
+          (symbol :tag "Function")
+          ((repeat :) string))
+  :group 'cc-flymake)
+
+(defvar-local cc-flymake--cached-flags nil)
+
+(defun cc-flymake--guess-flags (cc-program &optional trash-cache)
+  "Guess C(++) flags for compiling current buffer.
+Do this with by finding a suitable Makefile and intercepting an
+invocation of CC-PROGRAM, a string naming a C(++) compiler, in
+the output of \"make --just-print <file.o>\".
+
+Cache the result. TRASH-CACHE of a change to the Makefile rests
+the cache.
+
+This function signals an error if it encounters any problems,
+which normally causes using backends to be disabled.
+
+Can also function interactively for debug purposes."
+  (interactive (list (read-from-minibuffer "Compiler? ")
+                     current-prefix-arg))
+  (when trash-cache (setq cc-flymake--cached-flags nil))
+  (catch 'retval
+    (unless (buffer-file-name)
+      ;; don't error and don't cache, so that when the buffer is saved
+      ;; we get another chance.
+      (throw 'retval nil))
+    (when-let* ((makefile-dir
+                 (locate-dominating-file default-directory "Makefile"))
+                (makefile (expand-file-name "Makefile" makefile-dir))
+                (mtime (file-attribute-modification-time
+                        (file-attributes makefile))))
+      (cond
+       ((equal (list cc-program makefile mtime)
+               (cdr cc-flymake--cached-flags))
+        (when (called-interactively-p 'interactive)
+          (message "cache HIT for %s flags: %S" cc-program
+                   (car cc-flymake--cached-flags)))
+        (throw 'retval (car cc-flymake--cached-flags)))
+       (t
+        (let*
+            ((sans-nothing
+              (file-name-nondirectory
+               (file-name-sans-extension
+                (buffer-file-name))))
+             (blob (shell-command-to-string
+                    (format "make -C %s -f %s --just-print %s.o"
+                            makefile-dir
+                            makefile
+                            sans-nothing)))
+             (match (string-match
+                     (format "%s[[:space:]]+\\(\\(?:-.*\\)*\\)%s"
+                             cc-program
+                             sans-nothing)
+                     blob))
+             (flag-string (and match
+                               (match-string 1 blob)))
+             (flags (and flag-string
+                         ;; FIXME: shell unescaping: Nothing here to
+                         ;; deal with simple backslash-escaped spaces,
+                         ;; like quoted and backquoted expressions,
+                         ;; etc.
+                         (split-string
+                          flag-string
+                          nil
+                          nil
+                          "[[:space:]]+"))))
+          (when (or flags (string= "" flag-string))
+            (setq cc-flymake--cached-flags
+                  (list flags cc-program makefile mtime))
+            (when (called-interactively-p 'interactive)
+              (message "cache MISS for %s flags: %S" cc-program flags))
+            (throw 'retval flags))))))
+    (error "Could not guess %s flags" cc-program)))
+
+(require 'compile)
+(defun cc-flymake--make-diagnostics (source)
+  "Parse the current buffer of compilation messages.
+Return a list of diagnostics for the source buffer SOURCE."
+  ;; TODO: if you can understand it, use `compilation-mode's regexps
+  ;; or even some of its machinery here.
+  ;;
+  ;;    (set (make-local-variable 'compilation-locs)
+  ;;         (make-hash-table :test 'equal :weakness 'value))
+  ;;    (compilation-parse-errors (point-min) (point-max)
+  ;;                              'gnu 'gcc-include)
+  ;;    (while (next-single-property-change 'compilation-message)
+  ;;       ...)
+  ;;
+  ;; For now, this works minimaly well.
+  (cl-loop
+   while
+   (search-forward-regexp
+    "^\\(In file included from 
\\)?<stdin>:\\([0-9]+\\):\\([0-9]+\\):\n?\\(.*\\): \\(.*\\)$"
+    nil t)
+   for msg = (match-string 5)
+   for (beg . end) = (flymake-diag-region
+                      source
+                      (string-to-number (match-string 2))
+                      (string-to-number (match-string 3)))
+   for type = (if (match-string 1)
+                  :error
+                (assoc-default
+                 (match-string 4)
+                 '(("error" . :error)
+                   ("note" . :note)
+                   ("warning" . :warning))
+                 #'string-match))
+   collect (flymake-make-diagnostic source beg end type msg)))
+
+(defun cc-flymake-use-special-make-target ()
+  "Build command for checking a file with Make directly."
+  (unless (executable-find "make") (error "Make not found"))
+  `("make" "check-syntax" "CHK_SOURCES=-x c -"))
+
+(defvar-local cc-flymake-program "cc"
+  "C(++) compiler used by `cc-flymake-use-cc-directly'.")
+
+(defun cc-flymake-use-cc-directly ()
+  "Build command for checking a file with a C(++) compiler."
+  (unless (executable-find cc-flymake-program)
+    (error "%s not found" cc-flymake-program))
+  `(,cc-flymake-program
+    "-fsyntax-only"
+    ,@(cc-flymake--guess-flags cc-flymake-program)
+    "-x" "c" "-"))
+
+(defvar-local cc-flymake--proc nil
+  "Internal variable for `flymake-gcc'")
+
+;;;###autoload
+(defun cc-flymake (report-fn &rest _args)
+  "Flymake backend for GNU-style C compilers.
+This backend uses `cc-flymake-command' (which see) to launch a
+process that is passed the current buffer's contents via stdin.
+REPORT-FN is Flymake's callback."
+  (when (process-live-p cc-flymake--proc)
+    (kill-process cc-flymake--proc))
+  (let ((source (current-buffer)))
+    (save-restriction
+      (widen)
+      (setq
+       cc-flymake--proc
+       (make-process
+        :name "gcc-flymake"
+        :buffer (generate-new-buffer "*gcc-flymake*")
+        :command (if (symbolp cc-flymake-command)
+                     (funcall cc-flymake-command)
+                   cc-flymake-command)
+        :noquery t :connection-type 'pipe
+        :sentinel
+        (lambda (p _ev)
+          (when (eq 'exit (process-status p))
+            (unwind-protect
+                (when (eq p cc-flymake--proc)
+                  (with-current-buffer (process-buffer p)
+                    (goto-char (point-min))
+                    (let ((diags
+                           (cc-flymake--make-diagnostics source)))
+                      (if (or diags
+                              (zerop (process-exit-status p)))
+                          (funcall report-fn diags)
+                        ;; non-zero exit with no diags is cause
+                        ;; for alarm
+                        (funcall report-fn
+                                 :panic :explanation
+                                 (buffer-substring
+                                  (point-min) (progn (goto-char (point-min))
+                                                     (line-end-position))))))))
+              ;; (display-buffer (process-buffer p)) ; for debug
+              (kill-buffer (process-buffer p)))))))
+      (process-send-region cc-flymake--proc (point-min) (point-max))
+      (process-send-eof cc-flymake--proc))))
+
+(provide 'cc-flymake)
+;;; cc-flymake.el ends here
diff --git a/lisp/progmodes/cc-mode.el b/lisp/progmodes/cc-mode.el
index b0e5fe47a7..37c8ecfa98 100644
--- a/lisp/progmodes/cc-mode.el
+++ b/lisp/progmodes/cc-mode.el
@@ -1842,6 +1842,7 @@ c-mode
   (c-common-init 'c-mode)
   (easy-menu-add c-c-menu)
   (cc-imenu-init cc-imenu-c-generic-expression)
+  (c--setup-flymake)
   (c-run-mode-hooks 'c-mode-common-hook))
 
 (defconst c-or-c++-mode--regexp
@@ -2297,6 +2298,14 @@ c-submit-bug-report
        (insert (format "Buffer Style: %s\nc-emacs-features: %s\n"
                        style c-features)))))))
 
+(defun c--setup-flymake ()
+  "Setup flymake for cc buffers."
+  (add-hook 'flymake-diagnostic-functions 'cc-flymake nil t)
+  (defvar flymake-proc-allowed-file-name-masks)
+  ;; hackinly convince the legacy `flymake-proc' backend to disable
+  ;; itself.
+  (setq-local flymake-proc-allowed-file-name-masks nil))
+

 (cc-provide 'cc-mode)
 
diff --git a/src/Makefile.in b/src/Makefile.in
index 9a8c9c85f0..66c259902f 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -746,3 +746,8 @@ bootstrap-emacs$(EXEEXT):
 endif
        @: Compile some files earlier to speed up further compilation.
        $(MAKE) -C ../lisp compile-first EMACS="$(bootstrap_exe)"
+
+### Flymake support (for C only)
+check-syntax:
+       $(AM_V_CC)$(CC) -c $(CPPFLAGS) $(ALL_CFLAGS) ${CHK_SOURCES} || true
+.PHONY: check-syntax
-- 
2.11.0


reply via email to

[Prev in Thread] Current Thread [Next in Thread]