]> git.eshelyaron.com Git - emacs.git/commitdiff
New Flymake API variable flymake-diagnostic-functions
authorJoão Távora <joaotavora@gmail.com>
Mon, 25 Sep 2017 23:45:46 +0000 (00:45 +0100)
committerJoão Távora <joaotavora@gmail.com>
Mon, 2 Oct 2017 23:59:25 +0000 (00:59 +0100)
Lay groundwork for multiple active backends in the same buffer.

Backends are lisp functions called when flymake-mode sees fit.  They
are responsible for examining the current buffer and telling
flymake.el, via return value, if they can syntax check it.
Backends should return quickly and inexpensively, but they are also
passed a REPORT-FN argument which they may or may not call
asynchronously after performing more expensive work.

REPORT-FN's calling convention stipulates that a backend calls it with
a list of diagnostics as argument, or, alternatively, with a symbol
denoting an exceptional situation, usually some panic resulting from a
misconfigured backend.  In keeping with legacy behaviour,
flymake.el's response to a panic is to disable the issuing backend.

The flymake--diag object representing a diagnostic now also keeps
information about its source backend.  Among other uses, this allows
flymake to selectively cleanup overlays based on which backend is
updating its diagnostics.

* lisp/progmodes/flymake-proc.el (flymake-proc--report-fn):
New dynamic variable.
(flymake-proc--process): New variable.
(flymake-can-syntax-check-buffer): Remove.
(flymake-proc--process-sentinel): Simplify.  Use
unwind-protect.  Affect flymake-proc--processes here.
Bind flymake-proc--report-fn.
(flymake-proc--process-filter): Bind flymake-proc--report-fn.
(flymake-proc--post-syntax-check): Delete
(flymake-proc-start-syntax-check): Take mandatory
report-fn.  Rewrite.  Bind flymake-proc--report-fn.
(flymake-proc--process-sentinel): Rewrite and simplify.
(flymake-proc--panic): New helper.
(flymake-proc--start-syntax-check-process): Record report-fn
in process.  Use flymake-proc--panic.
(flymake-proc-stop-all-syntax-checks): Use mapc.  Don't affect
flymake-proc--processes here.  Record interruption reason.
(flymake-proc--init-find-buildfile-dir)
(flymake-proc--init-create-temp-source-and-master-buffer-copy):
Use flymake-proc--panic.
(flymake-diagnostic-functions): Add
flymake-proc-start-syntax-check.
(flymake-proc-compile): Call
flymake-proc-stop-all-syntax-checks with a reason.

* lisp/progmodes/flymake.el (flymake-backends): Delete.
(flymake-check-was-interrupted): Delete.
(flymake--diag): Add backend slot.
(flymake-delete-own-overlays): Take optional filter arg.
(flymake-diagnostic-functions): New user-visible variable.
(flymake--running-backends, flymake--disabled-backends): New
buffer-local variables.
(flymake-is-running): Now a function, not a variable.
(flymake-mode-line, flymake-mode-line-e-w)
(flymake-mode-line-status): Delete.
(flymake-lighter):  flymake's minor-mode "lighter".
(flymake-report): Delete.
(flymake--backend): Delete.
(flymake--can-syntax-check-buffer): Delete.
(flymake--handle-report, flymake--disable-backend)
(flymake--run-backend, flymake--run-backend):  New helpers.
(flymake-make-report-fn): Make a lambda.
(flymake--start-syntax-check): Iterate
flymake-diagnostic-functions.
(flymake-mode): Use flymake-lighter.  Simplify.  Initialize
flymake--running-backends and flymake--disabled-backends.
(flymake-find-file-hook): Simplify.

* test/lisp/progmodes/flymake-tests.el
(flymake-tests--call-with-fixture): Use flymake-is-running the
function.  Check if flymake-mode already active before activating it.
Add a thorough test for flymake multiple backends

* lisp/progmodes/flymake.el (flymake--start-syntax-check):
Don't use condition-case-unless-debug, use condition-case

* test/lisp/progmodes/flymake-tests.el
(flymake-tests--assert-set): New helper macro.
(dummy-backends): New test.

lisp/progmodes/flymake-proc.el
lisp/progmodes/flymake.el
test/lisp/progmodes/flymake-tests.el

index 05f2cab1af6cd131815740967d85bf1a1a617072..55f00955341d08aff46c868cb4076fcfa33922fc 100644 (file)
@@ -102,9 +102,15 @@ NAME is the file name function to use, default `flymake-proc-get-real-file-name'
                               (const :tag "flymake-proc-get-real-file-name" nil)
                               function))))
 
+(defvar-local flymake-proc--process nil
+  "Currently active flymake process for a buffer, if any.")
+
 (defvar flymake-proc--processes nil
   "List of currently active flymake processes.")
 
+(defvar flymake-proc--report-fn nil
+  "If bound, function used to report back to flymake's UI.")
+
 (defun flymake-proc--get-file-name-mode-and-masks (file-name)
   "Return the corresponding entry from `flymake-proc-allowed-file-name-masks'."
   (unless (stringp file-name)
@@ -118,11 +124,6 @@ NAME is the file name function to use, default `flymake-proc-get-real-file-name'
     (flymake-log 3 "file %s, init=%s" file-name (car mode-and-masks))
     mode-and-masks))
 
-(defun flymake-proc-can-syntax-check-file (file-name)
-  "Determine whether we can syntax check FILE-NAME.
-Return nil if we cannot, non-nil if we can."
-  (if (flymake-proc-get-init-function file-name) t nil))
-
 (defun flymake-proc--get-init-function (file-name)
   "Return init function to be used for the file."
   (let* ((init-f  (nth 0 (flymake-proc--get-file-name-mode-and-masks file-name))))
@@ -450,7 +451,9 @@ Create parent directories as needed."
   "Parse STRING and collect diagnostics info."
   (flymake-log 3 "received %d byte(s) of output from process %d"
                (length string) (process-id proc))
-  (let ((output-buffer (process-get proc 'flymake-proc--output-buffer)))
+  (let ((output-buffer (process-get proc 'flymake-proc--output-buffer))
+        (flymake-proc--report-fn
+         (process-get proc 'flymake-proc--report-fn)))
     (when (and (buffer-live-p (process-buffer proc))
                output-buffer)
       (with-current-buffer output-buffer
@@ -481,52 +484,56 @@ Create parent directories as needed."
             (process-put proc 'flymake-proc--unprocessed-mark
                          (point-marker))))))))
 
-(defun flymake-proc--process-sentinel (process _event)
+(defun flymake-proc--process-sentinel (proc _event)
   "Sentinel for syntax check buffers."
-  (when (memq (process-status process) '(signal exit))
-    (let* ((exit-status       (process-exit-status process))
-           (command           (process-command process))
-           (source-buffer     (process-buffer process))
-           (cleanup-f         (flymake-proc--get-cleanup-function
-                               (buffer-file-name source-buffer))))
-
+  (when (memq (process-status proc) '(signal exit))
+    (let* ((exit-status   (process-exit-status proc))
+           (command       (process-command proc))
+           (source-buffer (process-buffer proc))
+           (flymake-proc--report-fn (process-get proc
+                                                 'flymake-proc--report-fn))
+           (cleanup-f (flymake-proc--get-cleanup-function
+                       (buffer-file-name source-buffer)))
+           (diagnostics (process-get
+                         proc
+                         'flymake-proc--collected-diagnostics))
+           (interrupted (process-get proc 'flymake-proc--interrupted)))
       (flymake-log 2 "process %d exited with code %d"
-                   (process-id process) exit-status)
-      (unless (> flymake-log-level 2)
-        (kill-buffer (process-get process 'flymake-proc--output-buffer)))
-      (condition-case err
-          (progn
+                   (process-id proc) exit-status)
+      (unwind-protect
+          (when (buffer-live-p source-buffer)
             (flymake-log 3 "cleaning up using %s" cleanup-f)
-            (when (buffer-live-p source-buffer)
-              (with-current-buffer source-buffer
-                (funcall cleanup-f)))
-
-            (delete-process process)
-            (setq flymake-proc--processes (delq process flymake-proc--processes))
-
-            (when (buffer-live-p source-buffer)
-              (with-current-buffer source-buffer
-                (flymake-proc--post-syntax-check
-                 exit-status command
-                 (process-get process 'flymake-proc--collected-diagnostics))
-                (setq flymake-is-running nil))))
-        (error
-         (let ((err-str (format "Error in process sentinel for buffer %s: %s"
-                                source-buffer (error-message-string err))))
-           (flymake-log 0 err-str)
-           (with-current-buffer source-buffer
-             (setq flymake-is-running nil))))))))
-
-(defun flymake-proc--post-syntax-check (exit-status command diagnostics)
-  (if (equal 0 exit-status)
-      (flymake-report diagnostics)
-    (if flymake-check-was-interrupted
-        (flymake-report-status nil "") ;; STOPPED
-      (if (null diagnostics)
-          (flymake-report-fatal-status
-           "CFGERR"
-           (format "Configuration error has occurred while running %s" command))
-        (flymake-report diagnostics)))))
+            (with-current-buffer source-buffer
+              (funcall cleanup-f)
+              (cond ((equal 0 exit-status)
+                     (funcall flymake-proc--report-fn diagnostics))
+                    (interrupted
+                     (flymake-proc--panic :stopped interrupted))
+                    ((null diagnostics)
+                     ;; non-zero exit but no errors is strange
+                     (flymake-proc--panic
+                      :configuration-error
+                      (format "Command %s errored, but no diagnostics"
+                              command)))
+                    (diagnostics
+                     (funcall flymake-proc--report-fn diagnostics)))))
+        (delete-process proc)
+        (setq flymake-proc--processes
+              (delq proc flymake-proc--processes))
+        (unless (> flymake-log-level 2)
+          (kill-buffer (process-get proc 'flymake-proc--output-buffer)))))))
+
+(defun flymake-proc--panic (problem explanation)
+  "Tell flymake UI about a fatal PROBLEM with this backend.
+May only be called in a dynamic environment where
+`flymake-proc--dynamic-report-fn' is bound"
+  (flymake-log 0 "%s: %s" problem explanation)
+  (if (and (boundp 'flymake-proc--report-fn)
+           flymake-proc--report-fn)
+      (funcall flymake-proc--report-fn :panic
+               :explanation (format "%s: %s" problem explanation))
+    (error "Trouble telling flymake-ui about problem %s(%s)"
+                   problem explanation)))
 
 (defun flymake-proc-reformat-err-line-patterns-from-compile-el (original-list)
   "Grab error line patterns from ORIGINAL-LIST in compile.el format.
@@ -679,34 +686,47 @@ expression. A match indicates `:warning' type, otherwise
     (error
      (flymake-log 1 "Failed to delete dir %s, error ignored" dir-name))))
 
-(defun flymake-proc-start-syntax-check ()
+
+(defun flymake-proc-start-syntax-check (report-fn &optional interactive)
   "Start syntax checking for current buffer."
-  (interactive)
-  (flymake-log 3 "flymake is running: %s" flymake-is-running)
-  (when (not (and flymake-is-running
-                  (flymake-proc-can-syntax-check-file buffer-file-name)))
-    (when (or (not flymake-proc-compilation-prevents-syntax-check)
-              (not (flymake-proc--compilation-is-running))) ;+ (flymake-rep-ort-status buffer "COMP")
+  ;; Interactively, behave as if flymake had invoked us through its
+  ;; `flymake-diagnostic-functions' with a suitable ID so flymake can
+  ;; clean up consistently
+  (interactive (list (flymake-make-report-fn 'flymake-proc-start-syntax-check)
+                     t))
+  (cond
+   ((process-live-p flymake-proc--process)
+    (when interactive
+      (user-error
+       "There's already a flymake process running in this buffer")))
+   ((and buffer-file-name
+         ;; Since we write temp files in current dir, there's no point
+         ;; trying if the directory is read-only (bug#8954).
+         (file-writable-p (file-name-directory buffer-file-name))
+         (or (not flymake-proc-compilation-prevents-syntax-check)
+             (not (flymake-proc--compilation-is-running))))
+    (let ((init-f (flymake-proc--get-init-function buffer-file-name)))
+      (unless init-f (error "Can find a suitable init function"))
       (flymake-proc--clear-buildfile-cache)
       (flymake-proc--clear-project-include-dirs-cache)
 
-      (setq flymake-check-was-interrupted nil)
-      (setq flymake-check-start-time (float-time))
-
-      (let* ((source-file-name  buffer-file-name)
-             (init-f (flymake-proc--get-init-function source-file-name))
-             (cleanup-f (flymake-proc--get-cleanup-function source-file-name))
+      (let* ((flymake-proc--report-fn report-fn)
+             (cleanup-f (flymake-proc--get-cleanup-function buffer-file-name))
              (cmd-and-args (funcall init-f))
              (cmd          (nth 0 cmd-and-args))
              (args         (nth 1 cmd-and-args))
              (dir          (nth 2 cmd-and-args)))
-        (if (not cmd-and-args)
-            (progn
-              (flymake-log 0 "init function %s for %s failed, cleaning up" init-f source-file-name)
-              (funcall cleanup-f))
-          (progn
-            (setq flymake-last-change-time nil)
-            (flymake-proc--start-syntax-check-process cmd args dir)))))))
+        (cond ((not cmd-and-args)
+               (progn
+                 (flymake-log 0 "init function %s for %s failed, cleaning up"
+                              init-f buffer-file-name)
+                 (funcall cleanup-f)))
+              (t
+               (setq flymake-last-change-time nil)
+               (flymake-proc--start-syntax-check-process cmd
+                                                         args
+                                                         dir)
+               t)))))))
 
 (defun flymake-proc--start-syntax-check-process (cmd args dir)
   "Start syntax check process."
@@ -721,15 +741,18 @@ expression. A match indicates `:warning' type, otherwise
                               :noquery t
                               :filter 'flymake-proc--process-filter
                               :sentinel 'flymake-proc--process-sentinel))))
-        (setf (process-get process 'flymake-proc--output-buffer)
-              (generate-new-buffer
-                (format " *flymake output for %s*" (current-buffer))))
+        (process-put process 'flymake-proc--output-buffer
+                     (generate-new-buffer
+                      (format " *flymake output for %s*" (current-buffer))))
+        (process-put process 'flymake-proc--report-fn
+                     flymake-proc--report-fn)
+
+        (setq-local flymake-proc--process process)
         (push process flymake-proc--processes)
 
         (setq flymake-is-running t)
         (setq flymake-last-change-time nil)
 
-        (flymake-report-status nil "*")
         (flymake-log 2 "started process %d, command=%s, dir=%s"
                      (process-id process) (process-command process)
                      default-directory)
@@ -743,22 +766,16 @@ expression. A match indicates `:warning' type, otherwise
             (cleanup-f        (flymake-proc--get-cleanup-function source-file-name)))
        (flymake-log 0 err-str)
        (funcall cleanup-f)
-       (flymake-report-fatal-status "PROCERR" err-str)))))
-
-(defun flymake-proc--kill-process (proc)
-  "Kill process PROC."
-  (kill-process proc)
-  (let* ((buf (process-buffer proc)))
-    (when (buffer-live-p buf)
-      (with-current-buffer buf
-       (setq flymake-check-was-interrupted t))))
-  (flymake-log 1 "killed process %d" (process-id proc)))
-
-(defun flymake-proc-stop-all-syntax-checks ()
+       (flymake-proc--panic :make-process-error err-str)))))
+
+(defun flymake-proc-stop-all-syntax-checks (&optional reason)
   "Kill all syntax check processes."
-  (interactive)
-  (while flymake-proc--processes
-    (flymake-proc--kill-process (pop flymake-proc--processes))))
+  (interactive (list "Interrupted by user"))
+  (mapc (lambda (proc)
+          (kill-process proc)
+          (process-put proc 'flymake-proc--interrupted reason)
+          (flymake-log 2 "killed process %d" (process-id proc)))
+        flymake-proc--processes))
 
 (defun flymake-proc--compilation-is-running ()
   (and (boundp 'compilation-in-progress)
@@ -767,7 +784,7 @@ expression. A match indicates `:warning' type, otherwise
 (defun flymake-proc-compile ()
   "Kill all flymake syntax checks, start compilation."
   (interactive)
-  (flymake-proc-stop-all-syntax-checks)
+  (flymake-proc-stop-all-syntax-checks "Stopping for proper compilation")
   (call-interactively 'compile))
 
 ;;;; general init-cleanup and helper routines
@@ -897,11 +914,11 @@ Return full-name.  Names are real, not patched."
   "Find buildfile, store its dir in buffer data and return its dir, if found."
   (let* ((buildfile-dir
           (flymake-proc--find-buildfile buildfile-name
-                                  (file-name-directory source-file-name))))
+                                        (file-name-directory source-file-name))))
     (if buildfile-dir
         (setq flymake-proc--base-dir buildfile-dir)
       (flymake-log 1 "no buildfile (%s) for %s" buildfile-name source-file-name)
-      (flymake-report-fatal-status
+      (flymake-proc--panic
        "NOMK" (format "No buildfile (%s) found for %s"
                       buildfile-name source-file-name)))))
 
@@ -917,7 +934,7 @@ Return full-name.  Names are real, not patched."
     (if (not master-and-temp-master)
        (progn
          (flymake-log 1 "cannot find master file for %s" source-file-name)
-          (flymake-report-status "!" "")       ; NOMASTER
+          (flymake-proc--panic "NOMASTER" "")  ; NOMASTER
           nil)
       (setq flymake-proc--master-file-name (nth 0 master-and-temp-master))
       (setq flymake-proc--temp-master-file-name (nth 1 master-and-temp-master)))))
@@ -1053,6 +1070,11 @@ Use CREATE-TEMP-F for creating temp copy."
         (list "val" (flymake-proc-init-create-temp-buffer-copy
                      'flymake-proc-create-temp-inplace))))
 
+\f
+;;;; Hook onto flymake-ui
+(add-to-list 'flymake-diagnostic-functions
+             'flymake-proc-start-syntax-check)
+
 \f
 ;;;;
 
@@ -1238,9 +1260,6 @@ Return its components if so, nil otherwise.")
   (define-obsolete-function-alias 'flymake-start-syntax-check
     'flymake-proc-start-syntax-check "26.1"
     "Start syntax checking for current buffer.")
-  (define-obsolete-function-alias 'flymake-kill-process
-    'flymake-proc--kill-process "26.1"
-    "Kill process PROC.")
   (define-obsolete-function-alias 'flymake-stop-all-syntax-checks
     'flymake-proc-stop-all-syntax-checks "26.1"
     "Kill all syntax check processes.")
index dba69eb8e6c644227d8fbe4967076ff7024b7539..ac4db75d8b4c3f4943b6c14e22d9d537f47fd6cb 100644 (file)
@@ -35,6 +35,7 @@
 (require 'cl-lib)
 (require 'thingatpt) ; end-of-thing
 (require 'warnings) ; warning-numeric-level
+(eval-when-compile (require 'subr-x)) ; when-let*, if-let*
 
 (defgroup flymake nil
   "Universal on-the-fly syntax checker."
@@ -119,15 +120,6 @@ See `flymake-error-bitmap' and `flymake-warning-bitmap'."
 (defvar-local flymake-check-start-time nil
   "Time at which syntax check was started.")
 
-(defvar-local flymake-check-was-interrupted nil
-  "Non-nil if syntax check was killed by `flymake-compile'.")
-
-(defvar-local flymake-err-info nil
-  "Sorted list of line numbers and lists of err info in the form (file, err-text).")
-
-(defvar-local flymake-new-err-info nil
-  "Same as `flymake-err-info', effective when a syntax check is in progress.")
-
 (defun flymake-log (level text &rest args)
   "Log a message at level LEVEL.
 If LEVEL is higher than `flymake-log-level', the message is
@@ -140,7 +132,7 @@ are the string substitutions (see the function `format')."
 
 (cl-defstruct (flymake--diag
                (:constructor flymake--diag-make))
-  buffer beg end type text)
+  buffer beg end type text backend)
 
 (defun flymake-make-diagnostic (buffer
                                 beg
@@ -186,9 +178,9 @@ verify FILTER, sort them by COMPARE (using KEY)."
                                          #'identity))
          ovs)))))
 
-(defun flymake-delete-own-overlays ()
+(defun flymake-delete-own-overlays (&optional filter)
   "Delete all flymake overlays in BUFFER."
-  (mapc #'delete-overlay (flymake--overlays)))
+  (mapc #'delete-overlay (flymake--overlays :filter filter)))
 
 (defface flymake-error
   '((((supports :underline (:style wave)))
@@ -252,6 +244,55 @@ Or nil if the region is invalid."
     (error (flymake-log 4 "Invalid region for diagnostic %s")
            nil)))
 
+(defvar flymake-diagnostic-functions nil
+  "List of flymake backends i.e. sources of flymake diagnostics.
+
+This variable holds an arbitrary number of \"backends\" or
+\"checkers\" providing the flymake UI's \"frontend\" with
+information about where and how to annotate problems diagnosed in
+a buffer.
+
+Backends are lisp functions sharing a common calling
+convention. Whenever flymake decides it is time to re-check the
+buffer, each backend is called with a single argument, a
+REPORT-FN callback, detailed below.  Backend functions are first
+expected to quickly and inexpensively announce the feasibility of
+checking the buffer (i.e. they aren't expected to immediately
+start checking the buffer):
+
+* If the backend function returns nil, flymake forgets about this
+  backend for the current check, but will call it again the next
+  time;
+
+* If the backend function returns non-nil, flymake expects this backend to
+  check the buffer and call its REPORT-FN callback function. If
+  the computation involved is inexpensive, the backend function
+  may do so synchronously before returning. If it is not, it may
+  do so after retuning, using idle timers, asynchronous
+  processes or other asynchronous mechanisms.
+
+* If the backend function signals an error, it is disabled, i.e. flymake
+  will not attempt it again for this buffer until `flymake-mode'
+  is turned off and on again.
+
+When calling REPORT-FN, the first argument passed to it decides
+how to proceed. Recognized values are:
+
+* A (possibly empty) list of objects created with
+  `flymake-make-diagnostic', causing flymake to annotate the
+  buffer with this information and consider the backend has
+  having finished its check normally.
+
+* The symbol `:progress', signalling that the backend is still
+  working and will call REPORT-FN again in the future.
+
+* The symbol `:panic', signalling that the backend has
+  encountered an exceptional situation and should be disabled.
+
+In the latter cases, it is also possible to provide REPORT-FN
+with a string as the keyword argument `:explanation'. The string
+should give human-readable details of the situation.")
+
 (defvar flymake-diagnostic-types-alist
   `((:error
      . ((flymake-category . flymake-error)))
@@ -376,16 +417,11 @@ If TYPE doesn't declare PROP in either
     (overlay-put ov 'flymake-overlay t)
     (overlay-put ov 'flymake--diagnostic diagnostic)))
 
-
-(defvar-local flymake-is-running nil
-  "If t, flymake syntax check process is running for the current buffer.")
-
 (defun flymake-on-timer-event (buffer)
   "Start a syntax check for buffer BUFFER if necessary."
   (when (buffer-live-p buffer)
     (with-current-buffer buffer
       (when (and flymake-mode
-                 (not flymake-is-running)
                 flymake-last-change-time
                 (> (- (float-time) flymake-last-change-time)
                     flymake-no-changes-timeout))
@@ -427,59 +463,123 @@ If TYPE doesn't declare PROP in either
     (when choice (goto-char (overlay-start choice)))))
 
 ;; flymake minor mode declarations
-(defvar-local flymake-mode-line nil)
-(defvar-local flymake-mode-line-e-w nil)
-(defvar-local flymake-mode-line-status nil)
-
-(defun flymake-report-status (e-w &optional status)
-  "Show status in mode line."
-  (when e-w
-    (setq flymake-mode-line-e-w e-w))
-  (when status
-    (setq flymake-mode-line-status status))
-  (let* ((mode-line " Flymake"))
-    (when (> (length flymake-mode-line-e-w) 0)
-      (setq mode-line (concat mode-line ":" flymake-mode-line-e-w)))
-    (setq mode-line (concat mode-line flymake-mode-line-status))
-    (setq flymake-mode-line mode-line)
-    (force-mode-line-update)))
+(defvar-local flymake-lighter nil)
+
+(defun flymake--update-lighter (info &optional extended)
+  "Update Flymake’s \"lighter\" with INFO and EXTENDED."
+  (setq flymake-lighter (format " Flymake(%s%s)"
+                                info
+                                (if extended
+                                    (format ",%s" extended)
+                                  ""))))
 
 ;; Nothing in flymake uses this at all any more, so this is just for
 ;; third-party compatibility.
 (define-obsolete-function-alias 'flymake-display-warning 'message-box "26.1")
 
-(defun flymake-report-fatal-status (status warning)
-  "Display a warning and switch flymake mode off."
-  ;; This first message was always shown by default, and flymake-log
-  ;; does nothing by default, hence the use of message.
-  ;; Another option is display-warning.
-  (if (< flymake-log-level 0)
-      (message "Flymake: %s. Flymake will be switched OFF" warning))
-  (flymake-mode 0)
-  (flymake-log 0 "switched OFF Flymake mode for buffer %s due to fatal status %s, warning %s"
-               (buffer-name) status warning))
+(defvar-local flymake--running-backends nil
+  "List of currently active flymake backends.
+An active backend is a member of `flymake-diagnostic-functions'
+that has been invoked but hasn't reported any final status yet.")
 
-(defun flymake-report (diagnostics)
-  (save-restriction
-    (widen)
-    (flymake-delete-own-overlays)
-    (mapc #'flymake--highlight-line diagnostics)
-    (let ((err-count (cl-count-if #'flymake--diag-errorp diagnostics))
-          (warn-count (cl-count-if-not #'flymake--diag-errorp diagnostics)))
-      (when flymake-check-start-time
-        (flymake-log 2 "%s: %d error(s), %d other(s) in %.2f second(s)"
-                     (buffer-name) err-count warn-count
-                     (- (float-time) flymake-check-start-time)))
-      (if (null diagnostics)
-          (flymake-report-status "" "")
-        (flymake-report-status (format "%d/%d" err-count warn-count) "")))))
+(defvar-local flymake--disabled-backends nil
+  "List of currently disabled flymake backends.
+A backend is disabled if it reported `:panic'.")
+
+(defun flymake-is-running ()
+  "Tell if flymake has running backends in this buffer"
+  flymake--running-backends)
+
+(defun flymake--disable-backend (backend action &optional explanation)
+  (cl-pushnew backend flymake--disabled-backends)
+  (flymake-log 0 "Disabled the backend %s due to reports of %s (%s)"
+               backend action explanation))
+
+(cl-defun flymake--handle-report (backend action &key explanation)
+  "Handle reports from flymake backend identified by BACKEND."
+  (cond
+   ((not (memq backend flymake--running-backends))
+    (error "Ignoring unexpected report from backend %s" backend))
+   ((eq action :progress)
+    (flymake-log 3 "Backend %s reports progress: %s" backend explanation))
+   ((eq :panic action)
+    (flymake--disable-backend backend action explanation))
+   ((listp action)
+    (let ((diagnostics action))
+      (save-restriction
+        (widen)
+        (flymake-delete-own-overlays
+         (lambda (ov)
+           (eq backend
+               (flymake--diag-backend
+                (overlay-get ov 'flymake--diagnostic)))))
+        (mapc (lambda (diag)
+                (flymake--highlight-line diag)
+                (setf (flymake--diag-backend diag) backend))
+              diagnostics)
+        (let ((err-count (cl-count-if #'flymake--diag-errorp diagnostics))
+              (warn-count (cl-count-if-not #'flymake--diag-errorp
+                                           diagnostics)))
+          (when flymake-check-start-time
+            (flymake-log 2 "%d error(s), %d other(s) in %.2f second(s)"
+                         err-count warn-count
+                         (- (float-time) flymake-check-start-time)))
+          (if (null diagnostics)
+              (flymake--update-lighter "[ok]")
+            (flymake--update-lighter
+             (format "%d/%d" err-count warn-count)))))))
+   (t
+    (flymake--disable-backend "?"
+                              :strange
+                              (format "unknown action %s (%s)"
+                                      action explanation))))
+  (unless (eq action :progress)
+    (flymake--stop-backend backend)))
+
+(defun flymake-make-report-fn (backend)
+  "Make a suitable anonymous report function for BACKEND.
+BACKEND is used to help flymake distinguish diagnostic
+sources."
+  (lambda (&rest args)
+    (apply #'flymake--handle-report backend args)))
+
+(defun flymake--stop-backend (backend)
+  "Stop the backend BACKEND."
+  (setq flymake--running-backends (delq backend flymake--running-backends)))
+
+(defun flymake--run-backend (backend)
+  "Run the backend BACKEND."
+  (push backend flymake--running-backends)
+  ;; FIXME: Should use `condition-case-unless-debug'
+  ;; here, but that won't let me catch errors during
+  ;; testing where `debug-on-error' is always t
+  (condition-case err
+      (unless (funcall backend
+                       (flymake-make-report-fn backend))
+        (flymake--stop-backend backend))
+    (error
+     (flymake--disable-backend backend :error
+                               err)
+     (flymake--stop-backend backend))))
 
 (defun flymake--start-syntax-check (&optional deferred)
-  (cl-labels ((start
-               ()
-               (remove-hook 'post-command-hook #'start 'local)
-               (setq flymake-check-start-time (float-time))
-               (flymake-proc-start-syntax-check)))
+  "Start a syntax check.
+Start it immediately, or after current command if DEFERRED is
+non-nil."
+  (cl-labels
+      ((start
+        ()
+        (remove-hook 'post-command-hook #'start 'local)
+        (setq flymake-check-start-time (float-time))
+        (dolist (backend flymake-diagnostic-functions)
+          (cond ((memq backend flymake--running-backends)
+                 (flymake-log 2 "Backend %s still running, not restarting"
+                              backend))
+                ((memq backend flymake--disabled-backends)
+                 (flymake-log 2 "Backend %s is disabled, not starting"
+                              backend))
+                (t
+                 (flymake--run-backend backend))))))
     (if (and deferred
              this-command)
         (add-hook 'post-command-hook #'start 'append 'local)
@@ -487,33 +587,27 @@ If TYPE doesn't declare PROP in either
 
 ;;;###autoload
 (define-minor-mode flymake-mode nil
-  :group 'flymake :lighter flymake-mode-line
+  :group 'flymake :lighter flymake-lighter
+  (setq flymake--running-backends nil
+        flymake--disabled-backends nil)
   (cond
-
    ;; Turning the mode ON.
    (flymake-mode
     (cond
-     ((not buffer-file-name)
-      (message "Flymake unable to run without a buffer file name"))
-     ((not (flymake-can-syntax-check-file buffer-file-name))
-      (flymake-log 2 "flymake cannot check syntax in buffer %s" (buffer-name)))
+     ((not flymake-diagnostic-functions)
+      (error "flymake cannot check syntax in buffer %s" (buffer-name)))
      (t
       (add-hook 'after-change-functions 'flymake-after-change-function nil t)
       (add-hook 'after-save-hook 'flymake-after-save-hook nil t)
       (add-hook 'kill-buffer-hook 'flymake-kill-buffer-hook nil t)
-      ;;+(add-hook 'find-file-hook 'flymake-find-file-hook)
 
-      (flymake-report-status "" "")
+      (flymake--update-lighter "*" "*")
 
       (setq flymake-timer
             (run-at-time nil 1 'flymake-on-timer-event (current-buffer)))
 
-      (when (and flymake-start-syntax-check-on-find-file
-                 ;; Since we write temp files in current dir, there's no point
-                 ;; trying if the directory is read-only (bug#8954).
-                 (file-writable-p (file-name-directory buffer-file-name)))
-        (with-demoted-errors
-            (flymake--start-syntax-check))))))
+      (when flymake-start-syntax-check-on-find-file
+        (flymake--start-syntax-check)))))
 
    ;; Turning the mode OFF.
    (t
@@ -526,9 +620,7 @@ If TYPE doesn't declare PROP in either
 
     (when flymake-timer
       (cancel-timer flymake-timer)
-      (setq flymake-timer nil))
-
-    (setq flymake-is-running nil))))
+      (setq flymake-timer nil)))))
 
 ;;;###autoload
 (defun flymake-mode-on ()
@@ -564,8 +656,8 @@ If TYPE doesn't declare PROP in either
 
 ;;;###autoload
 (defun flymake-find-file-hook ()
-  (when (and (not (local-variable-p 'flymake-mode (current-buffer)))
-            (flymake-can-syntax-check-file buffer-file-name))
+  (unless (or flymake-mode
+             (null flymake-diagnostic-functions))
     (flymake-mode)
     (flymake-log 3 "automatically turned ON flymake mode")))
 
@@ -600,8 +692,5 @@ If TYPE doesn't declare PROP in either
 
 (provide 'flymake)
 
-(declare-function flymake-proc-start-syntax-check "flymake-proc")
-(declare-function flymake-can-syntax-check-file "flymake-proc")
-
 (require 'flymake-proc)
 ;;; flymake.el ends here
index 5ecc87fc7e6fa588cfad6a34b121788cec14b603..c2deb1dc5c7cc5bcd38841a6499cdeb4b6b105f7 100644 (file)
@@ -1,4 +1,4 @@
-;;; flymake-tests.el --- Test suite for flymake
+;;; flymake-tests.el --- Test suite for flymake -*- lexical-binding: t -*-
 
 ;; Copyright (C) 2011-2017 Free Software Foundation, Inc.
 
@@ -53,7 +53,7 @@ SEVERITY-PREDICATE is used to setup
             (when sev-pred-supplied-p
               (setq-local flymake-proc-diagnostic-type-pred severity-predicate))
             (goto-char (point-min))
-            (flymake-mode 1)
+            (unless flymake-mode (flymake-mode 1))
             ;; Weirdness here...  http://debbugs.gnu.org/17647#25
             ;; ... meaning `sleep-for', and even
             ;; `accept-process-output', won't suffice as ways to get
@@ -63,7 +63,7 @@ SEVERITY-PREDICATE is used to setup
             ;; reading an input event, so, as a workaround, use a dummy
             ;; `read-event' with a very short timeout.
             (unless noninteractive (read-event "" nil 0.1))
-            (while (and flymake-is-running (< (setq i (1+ i)) 10))
+            (while (and (flymake-is-running) (< (setq i (1+ i)) 10))
               (unless noninteractive (read-event "" nil 0.1))
               (sleep-for (+ 0.5 flymake-no-changes-timeout)))
             (funcall fn)))
@@ -130,6 +130,121 @@ SEVERITY-PREDICATE is used to setup
     (should (eq 'flymake-error (face-at-point)))
     (should-error (flymake-goto-next-error nil t)) ))
 
+(defmacro flymake-tests--assert-set (set
+                                     should
+                                     should-not)
+  (declare (indent 1))
+  `(progn
+     ,@(cl-loop
+        for s in should
+        collect `(should (memq ,s ,set)))
+     ,@(cl-loop
+        for s in should-not
+        collect `(should-not (memq ,s ,set)))))
+
+(ert-deftest dummy-backends ()
+  "Test GCC warning via function predicate."
+  (with-temp-buffer
+    (cl-labels
+        ((diagnose
+          (report-fn type words)
+          (funcall
+           report-fn
+           (cl-loop
+            for word in words
+            append
+            (save-excursion
+              (goto-char (point-min))
+              (cl-loop while (word-search-forward word nil t)
+                       collect (flymake-make-diagnostic
+                                (current-buffer)
+                                (match-beginning 0)
+                                (match-end 0)
+                                type
+                                (concat word " is wrong")))))))
+         (error-backend
+          (report-fn)
+          (run-with-timer
+           0.5 nil
+           #'diagnose report-fn :error '("manha" "prognata")))
+         (warning-backend
+          (report-fn)
+          (run-with-timer
+           0.5 nil
+           #'diagnose report-fn :warning '("ut" "dolor")))
+         (sync-backend
+          (report-fn)
+          (diagnose report-fn :note '("quis" "commodo")))
+         (refusing-backend
+          (_report-fn)
+          nil)
+         (panicking-backend
+          (report-fn)
+          (run-with-timer
+           0.5 nil
+           report-fn :panic :explanation "The spanish inquisition!"))
+         (crashing-backend
+          (_report-fn)
+          ;; HACK: Shoosh log during tests
+          (setq-local warning-minimum-log-level :emergency)
+          (error "crashed")))
+      (insert "Lorem ipsum dolor sit amet, consectetur adipiscing
+    elit, sed do eiusmod tempor incididunt ut labore et dolore
+    manha aliqua. Ut enim ad minim veniam, quis nostrud
+    exercitation ullamco laboris nisi ut aliquip ex ea commodo
+    consequat. Duis aute irure dolor in reprehenderit in
+    voluptate velit esse cillum dolore eu fugiat nulla
+    pariatur. Excepteur sint occaecat cupidatat non prognata
+    sunt in culpa qui officia deserunt mollit anim id est
+    laborum.")
+      (let ((flymake-diagnostic-functions
+             (list #'error-backend #'warning-backend #'sync-backend
+                   #'refusing-backend #'panicking-backend
+                   #'crashing-backend
+                   )))
+        (flymake-mode)
+        ;; FIXME: accessing some flymake-ui's internals here...
+        (flymake-tests--assert-set flymake--running-backends
+          (#'error-backend #'warning-backend #'panicking-backend)
+          (#'sync-backend #'crashing-backend #'refusing-backend))
+
+        (flymake-tests--assert-set flymake--disabled-backends
+          (#'crashing-backend)
+          (#'error-backend #'warning-backend #'sync-backend
+                           #'panicking-backend #'refusing-backend))
+
+        (cl-loop repeat 10 while (flymake-is-running)
+                 unless noninteractive do (read-event "" nil 0.1)
+                 do (sleep-for (+ 0.5 flymake-no-changes-timeout)))
+
+        (should (eq flymake--running-backends '()))
+
+        (flymake-tests--assert-set flymake--disabled-backends
+          (#'crashing-backend #'panicking-backend)
+          (#'error-backend #'warning-backend #'sync-backend
+                           #'refusing-backend))
+
+        (goto-char (point-min))
+        (flymake-goto-next-error)
+        (should (eq 'flymake-warning (face-at-point))) ; dolor
+        (flymake-goto-next-error)
+        (should (eq 'flymake-warning (face-at-point))) ; ut
+        (flymake-goto-next-error)
+        (should (eq 'flymake-error (face-at-point))) ; manha
+        (flymake-goto-next-error)
+        (should (eq 'flymake-warning (face-at-point))) ; Ut
+        (flymake-goto-next-error)
+        (should (eq 'flymake-note (face-at-point))) ; quis
+        (flymake-goto-next-error)
+        (should (eq 'flymake-warning (face-at-point))) ; ut
+        (flymake-goto-next-error)
+        (should (eq 'flymake-note (face-at-point))) ; commodo
+        (flymake-goto-next-error)
+        (should (eq 'flymake-warning (face-at-point))) ; dolor
+        (flymake-goto-next-error)
+        (should (eq 'flymake-error (face-at-point))) ; prognata
+        (should-error (flymake-goto-next-error nil t))))))
+
 (provide 'flymake-tests)
 
 ;;; flymake.el ends here