aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoão Távora2017-09-30 17:32:53 +0100
committerJoão Távora2017-10-03 14:18:55 +0100
commitf6e909b41e927a6715bad5fc5386257f29e7c0bb (patch)
treeefc4ed256ed6f6ff96326285cc1abd6348330343
parent22a7372faba317a3589c49fef912e542f3197f0d (diff)
downloademacs-f6e909b41e927a6715bad5fc5386257f29e7c0bb.tar.gz
emacs-f6e909b41e927a6715bad5fc5386257f29e7c0bb.zip
Flymake backends can report multiple times per check
Rewrote a significant part of the Flymake backend API. Flymake now ignores the return value of backend functions: a function can either returns or errors. If it doesn't error, a backend is no longer constrained to call REPORT-FN exactly once. It may do so any number of times, cumulatively reporting diagnostics. Flymake keeps track of outdated REPORT-FN instances and disconsiders obsolete reports. Backends should avoid reporting obsolete data by cancelling any ongoing processing at every renewed call to the backend function. Consolidated flymake.el internal data structures to require less buffer-local variables. Adjusted Flymake's mode-line indicator to the new semantics. Adapted and simplified the implementation of elisp and legacy backends, fixing potential race conditions when calling backends in rapid succession. Added a new test for a backend that calls REPORT-FN multiple times. Simplify test infrastructure. * lisp/progmodes/flymake-elisp.el (flymake-elisp-checkdoc) (flymake-elisp-byte-compile): Error instead of returning nil if not in emacs-lisp-mode. (flymake-elisp--byte-compile-process): New buffer-local variable. (flymake-elisp-byte-compile): Mark (and kill) previous process obsolete process before starting a new one. Don't report if obsolete process. * lisp/progmodes/flymake-proc.el (flymake-proc--current-process): New buffer-local variable. (flymake-proc--processes): Remove. (flymake-proc--process-filter): Don't bind flymake-proc--report-fn. (flymake-proc--process-sentinel): Rewrite. Don't report if obsolete process. (flymake-proc-legacy-flymake): Rewrite. Mark (and kill) previous process obsolete process before starting a new one. Integrate flymake-proc--start-syntax-check-process helper. (flymake-proc--start-syntax-check-process): Delete. (flymake-proc-stop-all-syntax-checks): Don't use flymake-proc--processes, iterate buffers. (flymake-proc-compile): * lisp/progmodes/flymake.el (subr-x): Require it explicitly. (flymake-diagnostic-functions): Reword docstring. (flymake--running-backends, flymake--disabled-backends) (flymake--diagnostics-table): Delete. (flymake--backend-state): New buffer-local variable and new defstruct. (flymake--with-backend-state, flymake--collect) (flymake-running-backends, flymake-disabled-backends) (flymake-reporting-backends): New helpers. (flymake-is-running): Use flymake-running-backends. (flymake--handle-report): Rewrite. (flymake-make-report-fn): Ensure REPORT-FN runs in the correct buffer or not at all. (flymake--disable-backend, flymake--run-backend): Rewrite. (flymake-start): Rewrite. (flymake-mode): Set flymake--backend-state. (flymake--mode-line-format): Rewrite. * test/lisp/progmodes/flymake-tests.el (flymake-tests--wait-for-backends): New helper. (flymake-tests--call-with-fixture): Use it. (included-c-header-files): Fix whitespace. (flymake-tests--diagnose-words): New helper. (dummy-backends): Rewrite for new semantics. Use cl-letf. (flymake-tests--assert-set): Use quote. (recurrent-backend): New test.
-rw-r--r--lisp/progmodes/flymake-elisp.el94
-rw-r--r--lisp/progmodes/flymake-proc.el247
-rw-r--r--lisp/progmodes/flymake.el492
-rw-r--r--test/lisp/progmodes/flymake-tests.el246
4 files changed, 609 insertions, 470 deletions
diff --git a/lisp/progmodes/flymake-elisp.el b/lisp/progmodes/flymake-elisp.el
index 7797d278e3f..f54badfc833 100644
--- a/lisp/progmodes/flymake-elisp.el
+++ b/lisp/progmodes/flymake-elisp.el
@@ -48,14 +48,15 @@
48(defun flymake-elisp-checkdoc (report-fn) 48(defun flymake-elisp-checkdoc (report-fn)
49 "A flymake backend for `checkdoc'. 49 "A flymake backend for `checkdoc'.
50Calls REPORT-FN directly." 50Calls REPORT-FN directly."
51 (when (derived-mode-p 'emacs-lisp-mode) 51 (unless (derived-mode-p 'emacs-lisp-mode)
52 (funcall report-fn 52 (error "Can only work on `emacs-lisp-mode' buffers"))
53 (cl-loop for (text start end _unfixable) in 53 (funcall report-fn
54 (flymake-elisp--checkdoc-1) 54 (cl-loop for (text start end _unfixable) in
55 collect 55 (flymake-elisp--checkdoc-1)
56 (flymake-make-diagnostic 56 collect
57 (current-buffer) 57 (flymake-make-diagnostic
58 start end :note text))))) 58 (current-buffer)
59 start end :note text))))
59 60
60(defun flymake-elisp--byte-compile-done (report-fn 61(defun flymake-elisp--byte-compile-done (report-fn
61 origin-buffer 62 origin-buffer
@@ -94,40 +95,59 @@ Calls REPORT-FN directly."
94 (kill-buffer output-buffer) 95 (kill-buffer output-buffer)
95 (ignore-errors (delete-file temp-file)))) 96 (ignore-errors (delete-file temp-file))))
96 97
98(defvar-local flymake-elisp--byte-compile-process nil
99 "Buffer-local process started for byte-compiling the buffer.")
100
97(defun flymake-elisp-byte-compile (report-fn) 101(defun flymake-elisp-byte-compile (report-fn)
98 "A flymake backend for elisp byte compilation. 102 "A Flymake backend for elisp byte compilation.
99Spawn an Emacs process that byte-compiles a file representing the 103Spawn an Emacs process that byte-compiles a file representing the
100current buffer state and calls REPORT-FN when done." 104current buffer state and calls REPORT-FN when done."
101 (interactive (list (lambda (stuff) 105 (interactive (list (lambda (stuff)
102 (message "aha %s" stuff)))) 106 (message "aha %s" stuff))))
103 (when (derived-mode-p 'emacs-lisp-mode) 107 (unless (derived-mode-p 'emacs-lisp-mode)
104 (let ((temp-file (make-temp-file "flymake-elisp-byte-compile")) 108 (error "Can only work on `emacs-lisp-mode' buffers"))
105 (origin-buffer (current-buffer))) 109 (when flymake-elisp--byte-compile-process
106 (save-restriction 110 (process-put flymake-elisp--byte-compile-process 'flymake-elisp--obsolete t)
107 (widen) 111 (when (process-live-p flymake-elisp--byte-compile-process)
108 (write-region (point-min) (point-max) temp-file nil 'nomessage)) 112 (kill-process flymake-elisp--byte-compile-process)))
109 (let* ((output-buffer (generate-new-buffer " *flymake-elisp-byte-compile*"))) 113 (let ((temp-file (make-temp-file "flymake-elisp-byte-compile"))
110 (make-process 114 (origin-buffer (current-buffer)))
111 :name "flymake-elisp-byte-compile" 115 (save-restriction
112 :buffer output-buffer 116 (widen)
113 :command (list (expand-file-name invocation-name invocation-directory) 117 (write-region (point-min) (point-max) temp-file nil 'nomessage))
114 "-Q" 118 (let* ((output-buffer (generate-new-buffer " *flymake-elisp-byte-compile*")))
115 "--batch" 119 (setq
116 ;; "--eval" "(setq load-prefer-newer t)" ; for testing 120 flymake-elisp--byte-compile-process
117 "-L" default-directory 121 (make-process
118 "-l" "flymake-elisp" 122 :name "flymake-elisp-byte-compile"
119 "-f" "flymake-elisp--batch-byte-compile" 123 :buffer output-buffer
120 temp-file) 124 :command (list (expand-file-name invocation-name invocation-directory)
121 :connection-type 'pipe 125 "-Q"
122 :sentinel 126 "--batch"
123 (lambda (proc _event) 127 ;; "--eval" "(setq load-prefer-newer t)" ; for testing
124 (unless (process-live-p proc) 128 "-L" default-directory
125 (flymake-elisp--byte-compile-done report-fn 129 "-l" "flymake-elisp"
126 origin-buffer 130 "-f" "flymake-elisp--batch-byte-compile"
127 output-buffer 131 temp-file)
128 temp-file)))) 132 :connection-type 'pipe
129 :stderr null-device 133 :sentinel
130 :noquery t)))) 134 (lambda (proc _event)
135 (unless (process-live-p proc)
136 (unwind-protect
137 (cond
138 ((zerop (process-exit-status proc))
139 (flymake-elisp--byte-compile-done report-fn
140 origin-buffer
141 output-buffer
142 temp-file))
143 ((process-get proc 'flymake-elisp--obsolete)
144 (flymake-log 3 "proc %s considered obsolete" proc))
145 (t
146 (funcall report-fn
147 :panic
148 :explanation (format "proc %s died violently" proc)))))))))
149 :stderr null-device
150 :noquery t)))
131 151
132(defun flymake-elisp--batch-byte-compile (&optional file) 152(defun flymake-elisp--batch-byte-compile (&optional file)
133 "Helper for `flymake-elisp-byte-compile'. 153 "Helper for `flymake-elisp-byte-compile'.
diff --git a/lisp/progmodes/flymake-proc.el b/lisp/progmodes/flymake-proc.el
index 3ab5523128f..b0ee7d65025 100644
--- a/lisp/progmodes/flymake-proc.el
+++ b/lisp/progmodes/flymake-proc.el
@@ -109,12 +109,9 @@ NAME is the file name function to use, default `flymake-proc-get-real-file-name'
109 (const :tag "flymake-proc-get-real-file-name" nil) 109 (const :tag "flymake-proc-get-real-file-name" nil)
110 function)))) 110 function))))
111 111
112(defvar-local flymake-proc--process nil 112(defvar-local flymake-proc--current-process nil
113 "Currently active flymake process for a buffer, if any.") 113 "Currently active flymake process for a buffer, if any.")
114 114
115(defvar flymake-proc--processes nil
116 "List of currently active flymake processes.")
117
118(defvar flymake-proc--report-fn nil 115(defvar flymake-proc--report-fn nil
119 "If bound, function used to report back to flymake's UI.") 116 "If bound, function used to report back to flymake's UI.")
120 117
@@ -543,9 +540,7 @@ Create parent directories as needed."
543 "Parse STRING and collect diagnostics info." 540 "Parse STRING and collect diagnostics info."
544 (flymake-log 3 "received %d byte(s) of output from process %d" 541 (flymake-log 3 "received %d byte(s) of output from process %d"
545 (length string) (process-id proc)) 542 (length string) (process-id proc))
546 (let ((output-buffer (process-get proc 'flymake-proc--output-buffer)) 543 (let ((output-buffer (process-get proc 'flymake-proc--output-buffer)))
547 (flymake-proc--report-fn
548 (process-get proc 'flymake-proc--report-fn)))
549 (when (and (buffer-live-p (process-buffer proc)) 544 (when (and (buffer-live-p (process-buffer proc))
550 output-buffer) 545 output-buffer)
551 (with-current-buffer output-buffer 546 (with-current-buffer output-buffer
@@ -578,49 +573,55 @@ Create parent directories as needed."
578 573
579(defun flymake-proc--process-sentinel (proc _event) 574(defun flymake-proc--process-sentinel (proc _event)
580 "Sentinel for syntax check buffers." 575 "Sentinel for syntax check buffers."
581 (when (memq (process-status proc) '(signal exit)) 576 (let (debug
582 (let* ((exit-status (process-exit-status proc)) 577 (pid (process-id proc))
583 (command (process-command proc)) 578 (source-buffer (process-buffer proc)))
584 (source-buffer (process-buffer proc)) 579 (unwind-protect
585 (flymake-proc--report-fn (process-get proc 580 (when (buffer-live-p source-buffer)
586 'flymake-proc--report-fn)) 581 (with-current-buffer source-buffer
587 (cleanup-f (flymake-proc--get-cleanup-function 582 (cond ((process-get proc 'flymake-proc--obsolete)
588 (buffer-file-name source-buffer))) 583 (flymake-log 3 "proc %s considered obsolete"
589 (diagnostics (process-get 584 pid))
590 proc 585 ((process-get proc 'flymake-proc--interrupted)
591 'flymake-proc--collected-diagnostics)) 586 (flymake-log 3 "proc %s interrupted by user"
592 (interrupted (process-get proc 'flymake-proc--interrupted)) 587 pid))
593 (panic nil) 588 ((not (process-live-p proc))
594 (output-buffer (process-get proc 'flymake-proc--output-buffer))) 589 (let* ((exit-status (process-exit-status proc))
595 (flymake-log 2 "process %d exited with code %d" 590 (command (process-command proc))
596 (process-id proc) exit-status) 591 (diagnostics (process-get
597 (condition-case-unless-debug err 592 proc
598 (progn 593 'flymake-proc--collected-diagnostics)))
599 (flymake-log 3 "cleaning up using %s" cleanup-f) 594 (flymake-log 2 "process %d exited with code %d"
600 (with-current-buffer source-buffer 595 pid exit-status)
601 (funcall cleanup-f) 596 (cond
602 (cond ((equal 0 exit-status) 597 ((equal 0 exit-status)
603 (funcall flymake-proc--report-fn diagnostics)) 598 (funcall flymake-proc--report-fn diagnostics
604 (interrupted 599 :explanation (format "a gift from %s" (process-id proc))
605 (flymake-proc--panic :stopped interrupted)) 600 ))
606 (diagnostics 601 (diagnostics
607 ;; non-zero exit but some diagnostics is quite 602 ;; non-zero exit but some diagnostics is quite
608 ;; normal... 603 ;; normal...
609 (funcall flymake-proc--report-fn diagnostics)) 604 (funcall flymake-proc--report-fn diagnostics
610 ((null diagnostics) 605 :explanation (format "a gift from %s" (process-id proc))))
611 ;; ...but no diagnostics is strange, so panic. 606 ((null diagnostics)
612 (setq panic t) 607 ;; ...but no diagnostics is strange, so panic.
613 (flymake-proc--panic 608 (setq debug debug-on-error)
614 :configuration-error 609 (flymake-proc--panic
615 (format "Command %s errored, but no diagnostics" 610 :configuration-error
616 command)))))) 611 (format "Command %s errored, but no diagnostics"
617 (delete-process proc) 612 command)))))))))
618 (setq flymake-proc--processes 613 (let ((output-buffer (process-get proc 'flymake-proc--output-buffer)))
619 (delq proc flymake-proc--processes)) 614 (cond (debug
620 (if panic 615 (flymake-log 3 "Output buffer %s kept alive for debugging"
621 (flymake-log 1 "Output buffer %s kept alive for debugging" 616 output-buffer))
622 output-buffer) 617 (t
623 (kill-buffer output-buffer)))))) 618 (when (buffer-live-p source-buffer)
619 (with-current-buffer source-buffer
620 (let ((cleanup-f (flymake-proc--get-cleanup-function
621 (buffer-file-name))))
622 (flymake-log 3 "cleaning up using %s" cleanup-f)
623 (funcall cleanup-f))))
624 (kill-buffer output-buffer)))))))
624 625
625(defun flymake-proc--panic (problem explanation) 626(defun flymake-proc--panic (problem explanation)
626 "Tell flymake UI about a fatal PROBLEM with this backend. 627 "Tell flymake UI about a fatal PROBLEM with this backend.
@@ -729,87 +730,85 @@ can also be executed interactively independently of
729 diags 730 diags
730 (append args '(:force t)))) 731 (append args '(:force t))))
731 t)) 732 t))
732 (cond 733 (let ((proc flymake-proc--current-process)
733 ((process-live-p flymake-proc--process) 734 (flymake-proc--report-fn report-fn))
734 (when interactive 735 (when (processp proc)
735 (user-error 736 (process-put proc 'flymake-proc--obsolete t)
736 "There's already a flymake process running in this buffer"))) 737 (flymake-log 3 "marking %s obsolete" (process-id proc))
737 ((and buffer-file-name 738 (when (process-live-p proc)
738 ;; Since we write temp files in current dir, there's no point 739 (when interactive
739 ;; trying if the directory is read-only (bug#8954). 740 (user-error
740 (file-writable-p (file-name-directory buffer-file-name)) 741 "There's already a flymake process running in this buffer")
741 (or (not flymake-proc-compilation-prevents-syntax-check) 742 (kill-process proc))))
742 (not (flymake-proc--compilation-is-running)))) 743 (when
743 (let ((init-f (flymake-proc--get-init-function buffer-file-name))) 744 ;; A number of situations make us not want to error right away
744 (unless init-f (error "Can find a suitable init function")) 745 ;; (and disable ourselves), in case the situation changes in
745 (flymake-proc--clear-buildfile-cache) 746 ;; the near future.
746 (flymake-proc--clear-project-include-dirs-cache) 747 (and buffer-file-name
747 748 ;; Since we write temp files in current dir, there's no point
748 (let* ((flymake-proc--report-fn report-fn) 749 ;; trying if the directory is read-only (bug#8954).
749 (cleanup-f (flymake-proc--get-cleanup-function buffer-file-name)) 750 (file-writable-p (file-name-directory buffer-file-name))
750 (cmd-and-args (funcall init-f)) 751 (or (not flymake-proc-compilation-prevents-syntax-check)
751 (cmd (nth 0 cmd-and-args)) 752 (not (flymake-proc--compilation-is-running))))
752 (args (nth 1 cmd-and-args)) 753 (let ((init-f (flymake-proc--get-init-function buffer-file-name)))
753 (dir (nth 2 cmd-and-args))) 754 (unless init-f (error "Can find a suitable init function"))
754 (cond ((not cmd-and-args) 755 (flymake-proc--clear-buildfile-cache)
755 (progn 756 (flymake-proc--clear-project-include-dirs-cache)
756 (flymake-log 0 "init function %s for %s failed, cleaning up" 757
757 init-f buffer-file-name) 758 (let* ((cleanup-f (flymake-proc--get-cleanup-function buffer-file-name))
758 (funcall cleanup-f))) 759 (cmd-and-args (funcall init-f))
759 (t 760 (cmd (nth 0 cmd-and-args))
760 (setq flymake-last-change-time nil) 761 (args (nth 1 cmd-and-args))
761 (flymake-proc--start-syntax-check-process cmd 762 (dir (nth 2 cmd-and-args))
762 args 763 (success nil))
763 dir) 764 (unwind-protect
764 t))))))) 765 (cond
766 ((not cmd-and-args)
767 (flymake-log 0 "init function %s for %s failed, cleaning up"
768 init-f buffer-file-name))
769 (t
770 (setq flymake-last-change-time nil)
771 (setq proc
772 (let ((default-directory (or dir default-directory)))
773 (when dir
774 (flymake-log 3 "starting process on dir %s" dir))
775 (make-process
776 :name "flymake-proc"
777 :buffer (current-buffer)
778 :command (cons cmd args)
779 :noquery t
780 :filter
781 (lambda (proc string)
782 (let ((flymake-proc--report-fn report-fn))
783 (flymake-proc--process-filter proc string)))
784 :sentinel
785 (lambda (proc event)
786 (let ((flymake-proc--report-fn report-fn))
787 (flymake-proc--process-sentinel proc event))))))
788 (process-put proc 'flymake-proc--output-buffer
789 (generate-new-buffer
790 (format " *flymake output for %s*" (current-buffer))))
791 (setq flymake-proc--current-process proc)
792 (flymake-log 2 "started process %d, command=%s, dir=%s"
793 (process-id proc) (process-command proc)
794 default-directory)
795 (setq success t)))
796 (unless success
797 (funcall cleanup-f))))))))
765 798
766(define-obsolete-function-alias 'flymake-start-syntax-check 799(define-obsolete-function-alias 'flymake-start-syntax-check
767 'flymake-proc-legacy-flymake "26.1") 800 'flymake-proc-legacy-flymake "26.1")
768 801
769(defun flymake-proc--start-syntax-check-process (cmd args dir)
770 "Start syntax check process."
771 (condition-case-unless-debug err
772 (let* ((process
773 (let ((default-directory (or dir default-directory)))
774 (when dir
775 (flymake-log 3 "starting process on dir %s" dir))
776 (make-process :name "flymake-proc"
777 :buffer (current-buffer)
778 :command (cons cmd args)
779 :noquery t
780 :filter 'flymake-proc--process-filter
781 :sentinel 'flymake-proc--process-sentinel))))
782 (process-put process 'flymake-proc--output-buffer
783 (generate-new-buffer
784 (format " *flymake output for %s*" (current-buffer))))
785 (process-put process 'flymake-proc--report-fn
786 flymake-proc--report-fn)
787
788 (setq-local flymake-proc--process process)
789 (push process flymake-proc--processes)
790
791 (setq flymake-is-running t)
792 (setq flymake-last-change-time nil)
793
794 (flymake-log 2 "started process %d, command=%s, dir=%s"
795 (process-id process) (process-command process)
796 default-directory)
797 process)
798 (error
799 (flymake-proc--panic :make-process-error
800 (format-message
801 "Failed to launch syntax check process `%s' with args %s: %s"
802 cmd args (error-message-string err)))
803 (funcall (flymake-proc--get-cleanup-function buffer-file-name)))))
804
805(defun flymake-proc-stop-all-syntax-checks (&optional reason) 802(defun flymake-proc-stop-all-syntax-checks (&optional reason)
806 "Kill all syntax check processes." 803 "Kill all syntax check processes."
807 (interactive (list "Interrupted by user")) 804 (interactive (list "Interrupted by user"))
808 (mapc (lambda (proc) 805 (dolist (buf (buffer-list))
809 (kill-process proc) 806 (with-current-buffer buf
810 (process-put proc 'flymake-proc--interrupted reason) 807 (let (p flymake-proc--current-process)
811 (flymake-log 2 "killed process %d" (process-id proc))) 808 (when (process-live-p p)
812 flymake-proc--processes)) 809 (kill-process p)
810 (process-put p 'flymake-proc--interrupted reason)
811 (flymake-log 2 "killed process %d" (process-id p)))))))
813 812
814(defun flymake-proc--compilation-is-running () 813(defun flymake-proc--compilation-is-running ()
815 (and (boundp 'compilation-in-progress) 814 (and (boundp 'compilation-in-progress)
diff --git a/lisp/progmodes/flymake.el b/lisp/progmodes/flymake.el
index 1068b3889da..d1673977762 100644
--- a/lisp/progmodes/flymake.el
+++ b/lisp/progmodes/flymake.el
@@ -36,7 +36,7 @@
36(require 'thingatpt) ; end-of-thing 36(require 'thingatpt) ; end-of-thing
37(require 'warnings) ; warning-numeric-level, display-warning 37(require 'warnings) ; warning-numeric-level, display-warning
38(require 'compile) ; for some faces 38(require 'compile) ; for some faces
39(eval-when-compile (require 'subr-x)) ; when-let*, if-let* 39(require 'subr-x) ; when-let*, if-let*, hash-table-keys, hash-table-values
40 40
41(defgroup flymake nil 41(defgroup flymake nil
42 "Universal on-the-fly syntax checker." 42 "Universal on-the-fly syntax checker."
@@ -315,42 +315,39 @@ about where and how to annotate problems diagnosed in a buffer.
315 315
316Whenever Flymake or the user decides to re-check the buffer, each 316Whenever Flymake or the user decides to re-check the buffer, each
317function is called with a common calling convention, a single 317function is called with a common calling convention, a single
318REPORT-FN argument, detailed below. Backend functions are first 318REPORT-FN argument, detailed below. Backend functions are
319expected to quickly and inexpensively announce the feasibility of 319expected to initiate the buffer check, but aren't required to
320checking the buffer via the return value (i.e. they aren't 320complete it check before exiting: if the computation involved is
321required to immediately start checking the buffer): 321expensive, especially for large buffers, that task can be
322 322scheduled for the future using asynchronous processes or other
323* If the backend function returns nil, Flymake forgets about this 323asynchronous mechanisms.
324 backend for the current check, but will call it again for the 324
325 next one; 325In any case, backend functions are expected to return quickly or
326 326signal an error, in which case the backend is disabled. Flymake
327* If the backend function returns non-nil, Flymake expects this 327will not try disabled backends again for any future checks of
328 backend to check the buffer and call its REPORT-FN callback 328this buffer. Certain commands, like turning `flymake-mode' off
329 function exactly once. If the computation involved is 329and on again, reset the list of disabled backends.
330 inexpensive, the backend function may do so synchronously, 330
331 before returning. If it is not, it should do so after 331If the function returns, Flymake considers the backend to be
332 returning, using idle timers, asynchronous processes or other 332\"running\". If it has not done so already, the backend is
333 asynchronous mechanisms. 333expected to call the function REPORT-FN with a single argument
334 334ACTION followed by an optional list of keyword arguments and
335* If the backend function signals an error, it is disabled,
336 i.e. Flymake will not use it again for the current or any
337 future checks of this buffer. Certain commands, like turning
338 `flymake-mode' on and off again, resets the list of disabled
339 backends.
340
341Backends are required to call REPORT-FN with a single argument
342ACTION followed by an optional list of keywords parameters and
343their values (:KEY1 VALUE1 :KEY2 VALUE2...). 335their values (:KEY1 VALUE1 :KEY2 VALUE2...).
344 336
345The possible values for ACTION are. 337The possible values for ACTION are.
346 338
347* A (possibly empty) list of objects created with 339* A (possibly empty) list of diagnostic objects created with
348 `flymake-make-diagnostic', causing Flymake to annotate the 340 `flymake-make-diagnostic', causing Flymake to annotate the
349 buffer with this information and consider the backend has 341 buffer with this information.
350 having finished its check normally.
351 342
352* The symbol `:progress', signalling that the backend is still 343 A backend may call REPORT-FN repeatedly in this manner, but
353 working and will call REPORT-FN again in the future. 344 only until Flymake considers that the most recently requested
345 buffer check is now obsolete because, say, buffer contents have
346 changed in the meantime. The backend is only given notice of
347 this via a renewed call to the backend function. Thus, to
348 prevent making obsolete reports and wasting resources, backend
349 functions should first cancel any ongoing processing from
350 previous calls.
354 351
355* The symbol `:panic', signalling that the backend has 352* The symbol `:panic', signalling that the backend has
356 encountered an exceptional situation and should be disabled. 353 encountered an exceptional situation and should be disabled.
@@ -360,8 +357,8 @@ The recognized optional keyword arguments are:
360* ‘:explanation’: value should give user-readable details of 357* ‘:explanation’: value should give user-readable details of
361 the situation encountered, if any. 358 the situation encountered, if any.
362 359
363* ‘:force’: value should be a boolean forcing the Flymake UI 360* ‘:force’: value should be a boolean suggesting that the Flymake
364 to consider the report even if was somehow unexpected.") 361 considers the report even if was somehow unexpected.")
365 362
366(defvar flymake-diagnostic-types-alist 363(defvar flymake-diagnostic-types-alist
367 `((:error 364 `((:error
@@ -493,122 +490,189 @@ associated `flymake-category' return DEFAULT."
493;; third-party compatibility. 490;; third-party compatibility.
494(define-obsolete-function-alias 'flymake-display-warning 'message-box "26.1") 491(define-obsolete-function-alias 'flymake-display-warning 'message-box "26.1")
495 492
496(defvar-local flymake--running-backends nil 493(defvar-local flymake--backend-state nil
497 "List of currently active flymake backends. 494 "Buffer-local hash table of a Flymake backend's state.
498An active backend is a member of `flymake-diagnostic-functions' 495The keys to this hash table are functions as found in
499that has been invoked but hasn't reported any final status yet.") 496`flymake-diagnostic-functions'. The values are structures
500 497of the type `flymake--backend-state', with these slots
501(defvar-local flymake--disabled-backends nil 498
502 "List of currently disabled flymake backends. 499`running', a symbol to keep track of a backend's replies via its
503A backend is disabled if it reported `:panic'.") 500REPORT-FN argument. A backend is running if this key is
504 501present. If the key is absent if the backend isn't expecting any
505(defvar-local flymake--diagnostics-table nil 502replies from the backend.
506 "Hash table of all diagnostics indexed by backend.") 503
504`diags', a (possibly empty) list of diagnostic objects created
505with `flymake-make-diagnostic'. This key is absent if the
506backend hasn't reported anything yet.
507
508`reported-p', a boolean indicating if the backend has replied
509since it last was contacted.
510
511`disabled', a string with the explanation for a previous
512exceptional situation reported by the backend. If this key is
513present the backend is disabled.")
514
515(cl-defstruct (flymake--backend-state
516 (:constructor flymake--make-backend-state))
517 running reported-p disabled diags)
518
519(defmacro flymake--with-backend-state (backend state-var &rest body)
520 "Bind BACKEND's STATE-VAR to its state, run BODY."
521 (declare (indent 2) (debug (sexp sexp &rest form)))
522 (let ((b (make-symbol "b")))
523 `(let* ((,b ,backend)
524 (,state-var
525 (or (gethash ,b flymake--backend-state)
526 (puthash ,b (flymake--make-backend-state)
527 flymake--backend-state))))
528 ,@body)))
507 529
508(defun flymake-is-running () 530(defun flymake-is-running ()
509 "Tell if flymake has running backends in this buffer" 531 "Tell if flymake has running backends in this buffer"
510 flymake--running-backends) 532 (flymake-running-backends))
511 533
512(defun flymake--disable-backend (backend action &optional explanation) 534(cl-defun flymake--handle-report (backend token action &key explanation force)
513 (cl-pushnew backend flymake--disabled-backends) 535 "Handle reports from BACKEND identified by TOKEN.
514 (flymake-log :warning "Disabled the backend %s due to reports of %s (%s)" 536
515 backend action explanation)) 537BACKEND, ACTION and EXPLANATION, and FORCE conform to the calling
516 538convention described in `flymake-diagnostic-functions' (which
517(cl-defun flymake--handle-report (backend action &key explanation force) 539see). Optional FORCE says to handle a report even if TOKEN was
518 "Handle reports from flymake backend identified by BACKEND. 540not expected."
519 541 (let* ((state (gethash backend flymake--backend-state))
520BACKEND, ACTION and EXPLANATION conform to the calling convention 542 (first-report (not (flymake--backend-state-reported-p state))))
521described in `flymake-diagnostic-functions' (which see). Optional 543 (setf (flymake--backend-state-reported-p state) t)
522FORCE says to handle a report even if it was not expected." 544 (let (expected-token
523 (cond 545 new-diags)
524 ((and (not (memq backend flymake--running-backends)) 546 (cond
525 (not force)) 547 ((null state)
526 (flymake-error "Ignoring unexpected report from backend %s" backend)) 548 (flymake-error
527 ((eq action :progress) 549 "Unexpected report from unknown backend %s" backend))
528 (flymake-log 3 "Backend %s reports progress: %s" backend explanation)) 550 ((flymake--backend-state-disabled state)
529 ((eq :panic action) 551 (flymake-error
530 (flymake--disable-backend backend action explanation)) 552 "Unexpected report from disabled backend %s" backend))
531 ((listp action) 553 ((progn
532 (let ((diagnostics action)) 554 (setq expected-token (flymake--backend-state-running state))
533 (save-restriction 555 (null expected-token))
534 (widen) 556 ;; should never happen
535 (flymake-delete-own-overlays 557 (flymake-error "Unexpected report from stopped backend %s" backend))
536 (lambda (ov) 558 ((and (not (eq expected-token token))
537 (eq backend 559 (not force))
538 (flymake--diag-backend 560 (flymake-error "Obsolete report from backend %s with explanation %s"
539 (overlay-get ov 'flymake--diagnostic))))) 561 backend explanation))
540 (puthash backend diagnostics flymake--diagnostics-table) 562 ((eq :panic action)
541 (mapc (lambda (diag) 563 (flymake--disable-backend backend explanation))
542 (flymake--highlight-line diag) 564 ((not (listp action))
543 (setf (flymake--diag-backend diag) backend)) 565 (flymake--disable-backend backend
544 diagnostics) 566 (format "Unknown action %S" action))
545 (when flymake-check-start-time 567 (flymake-error "Expected report, but got unknown key %s" action))
546 (flymake-log 2 "backend %s reported %d diagnostics in %.2f second(s)" 568 (t
547 backend 569 (setq new-diags action)
548 (length diagnostics) 570 (save-restriction
549 (- (float-time) flymake-check-start-time)))))) 571 (widen)
550 (t 572 ;; only delete overlays if this is the first report
551 (flymake--disable-backend "?" 573 (when first-report
552 :strange 574 (flymake-delete-own-overlays
553 (format "unknown action %s (%s)" 575 (lambda (ov)
554 action explanation)))) 576 (eq backend
555 (unless (eq action :progress) 577 (flymake--diag-backend
556 (flymake--stop-backend backend))) 578 (overlay-get ov 'flymake--diagnostic))))))
557 579 (mapc (lambda (diag)
558(defun flymake-make-report-fn (backend) 580 (flymake--highlight-line diag)
581 (setf (flymake--diag-backend diag) backend))
582 new-diags)
583 (setf (flymake--backend-state-diags state)
584 (append new-diags (flymake--backend-state-diags state)))
585 (when flymake-check-start-time
586 (flymake-log :debug "backend %s reported %d diagnostics in %.2f second(s)"
587 backend
588 (length new-diags)
589 (- (float-time) flymake-check-start-time)))))))))
590
591(defun flymake-make-report-fn (backend &optional token)
559 "Make a suitable anonymous report function for BACKEND. 592 "Make a suitable anonymous report function for BACKEND.
560BACKEND is used to help flymake distinguish diagnostic 593BACKEND is used to help flymake distinguish different diagnostic
561sources." 594sources. If provided, TOKEN helps flymake distinguish between
562 (lambda (&rest args) 595different runs of the same backend."
563 (apply #'flymake--handle-report backend args))) 596 (let ((buffer (current-buffer)))
564 597 (lambda (&rest args)
565(defun flymake--stop-backend (backend) 598 (when (buffer-live-p buffer)
566 "Stop the backend BACKEND." 599 (with-current-buffer buffer
567 (setq flymake--running-backends (delq backend flymake--running-backends))) 600 (apply #'flymake--handle-report backend token args))))))
601
602(defun flymake--collect (fn)
603 (let (retval)
604 (maphash (lambda (backend state)
605 (when (funcall fn state) (push backend retval)))
606 flymake--backend-state)
607 retval))
608
609(defun flymake-running-backends ()
610 "Compute running Flymake backends in current buffer."
611 (flymake--collect #'flymake--backend-state-running))
612
613(defun flymake-disabled-backends ()
614 "Compute disabled Flymake backends in current buffer."
615 (flymake--collect #'flymake--backend-state-disabled))
616
617(defun flymake-reporting-backends ()
618 "Compute reporting Flymake backends in current buffer."
619 (flymake--collect #'flymake--backend-state-reported-p))
620
621(defun flymake--disable-backend (backend &optional explanation)
622 "Disable BACKEND because EXPLANATION.
623If is is running also stop it."
624 (flymake-log :warning "Disabling backend %s because %s" backend explanation)
625 (flymake--with-backend-state backend state
626 (setf (flymake--backend-state-running state) nil
627 (flymake--backend-state-disabled state) explanation
628 (flymake--backend-state-reported-p state) t)))
568 629
569(defun flymake--run-backend (backend) 630(defun flymake--run-backend (backend)
570 "Run the backend BACKEND." 631 "Run the backend BACKEND, reenabling if necessary."
571 (push backend flymake--running-backends) 632 (flymake-log :debug "Running backend %s" backend)
572 (remhash backend flymake--diagnostics-table) 633 (let ((run-token (cl-gensym "backend-token")))
573 ;; FIXME: Should use `condition-case-unless-debug' here, but that 634 (flymake--with-backend-state backend state
574 ;; won't let me catch errors from inside `ert-deftest' where 635 (setf (flymake--backend-state-running state) run-token
575 ;; `debug-on-error' is always t 636 (flymake--backend-state-disabled state) nil
576 (condition-case err 637 (flymake--backend-state-diags state) nil
577 (unless (funcall backend 638 (flymake--backend-state-reported-p state) nil))
578 (flymake-make-report-fn backend)) 639 ;; FIXME: Should use `condition-case-unless-debug' here, for don't
579 (flymake--stop-backend backend)) 640 ;; for two reasons: (1) that won't let me catch errors from inside
580 (error 641 ;; `ert-deftest' where `debug-on-error' appears to be always
581 (flymake--disable-backend backend :error 642 ;; t. (2) In cases where the user is debugging elisp somewhere
582 err) 643 ;; else, and using flymake, the presence of a frequently
583 (flymake--stop-backend backend)))) 644 ;; misbehaving backend in the global hook (most likely the legacy
584 645 ;; backend) will trigger an annoying backtrace.
585(defun flymake-start (&optional deferred interactive) 646 ;;
647 (condition-case err
648 (funcall backend
649 (flymake-make-report-fn backend run-token))
650 (error
651 (flymake--disable-backend backend err)))))
652
653(defun flymake-start (&optional deferred force)
586 "Start a syntax check. 654 "Start a syntax check.
587Start it immediately, or after current command if DEFERRED is 655Start it immediately, or after current command if DEFERRED is
588non-nil. With optional INTERACTIVE or interactively, clear any 656non-nil. With optional FORCE run even disabled backends.
589stale information about running and automatically disabled 657
590backends." 658Interactively, with a prefix arg, FORCE is t."
591 (interactive (list nil t)) 659 (interactive (list nil current-prefix-arg))
592 (cl-labels 660 (cl-labels
593 ((start 661 ((start
594 () 662 ()
595 (remove-hook 'post-command-hook #'start 'local) 663 (remove-hook 'post-command-hook #'start 'local)
596 (setq flymake-check-start-time (float-time)) 664 (setq flymake-check-start-time (float-time))
597 (when interactive
598 (setq flymake--diagnostics-table (make-hash-table)
599 flymake--running-backends nil
600 flymake--disabled-backends nil))
601 (run-hook-wrapped 665 (run-hook-wrapped
602 'flymake-diagnostic-functions 666 'flymake-diagnostic-functions
603 (lambda (backend) 667 (lambda (backend)
604 (cond ((memq backend flymake--running-backends) 668 (cond
605 (flymake-log :debug "Backend %s still running, not restarting" 669 ((and (not force)
606 backend)) 670 (flymake--with-backend-state backend state
607 ((memq backend flymake--disabled-backends) 671 (flymake--backend-state-disabled state)))
608 (flymake-log :debug "Backend %s is disabled, not starting" 672 (flymake-log :debug "Backend %s is disabled, not starting"
609 backend)) 673 backend))
610 (t 674 (t
611 (flymake--run-backend backend))) 675 (flymake--run-backend backend)))
612 nil)))) 676 nil))))
613 (if (and deferred 677 (if (and deferred
614 this-command) 678 this-command)
@@ -623,8 +687,6 @@ backends."
623;;;###autoload 687;;;###autoload
624(define-minor-mode flymake-mode nil 688(define-minor-mode flymake-mode nil
625 :group 'flymake :lighter flymake--mode-line-format :keymap flymake-mode-map 689 :group 'flymake :lighter flymake--mode-line-format :keymap flymake-mode-map
626 (setq flymake--running-backends nil
627 flymake--disabled-backends nil)
628 (cond 690 (cond
629 ;; Turning the mode ON. 691 ;; Turning the mode ON.
630 (flymake-mode 692 (flymake-mode
@@ -636,7 +698,7 @@ backends."
636 (add-hook 'after-save-hook 'flymake-after-save-hook nil t) 698 (add-hook 'after-save-hook 'flymake-after-save-hook nil t)
637 (add-hook 'kill-buffer-hook 'flymake-kill-buffer-hook nil t) 699 (add-hook 'kill-buffer-hook 'flymake-kill-buffer-hook nil t)
638 700
639 (setq flymake--diagnostics-table (make-hash-table)) 701 (setq flymake--backend-state (make-hash-table))
640 702
641 (when flymake-start-syntax-check-on-find-file 703 (when flymake-start-syntax-check-on-find-file
642 (flymake-start))))) 704 (flymake-start)))))
@@ -797,20 +859,26 @@ applied."
797 859
798(defun flymake--mode-line-format () 860(defun flymake--mode-line-format ()
799 "Produce a pretty minor mode indicator." 861 "Produce a pretty minor mode indicator."
800 (let ((running flymake--running-backends) 862 (let* ((known (hash-table-keys flymake--backend-state))
801 (reported (cl-plusp 863 (running (flymake-running-backends))
802 (hash-table-count flymake--diagnostics-table)))) 864 (disabled (flymake-disabled-backends))
865 (reported (flymake-reporting-backends))
866 (diags-by-type (make-hash-table))
867 (all-disabled (and disabled (null running)))
868 (some-waiting (cl-set-difference running reported)))
869 (maphash (lambda (_b state)
870 (mapc (lambda (diag)
871 (push diag
872 (gethash (flymake--diag-type diag)
873 diags-by-type)))
874 (flymake--backend-state-diags state)))
875 flymake--backend-state)
803 `((:propertize " Flymake" 876 `((:propertize " Flymake"
804 mouse-face mode-line-highlight 877 mouse-face mode-line-highlight
805 ,@(when (not reported)
806 `(face compilation-mode-line-fail))
807 help-echo 878 help-echo
808 ,(concat (format "%s registered backends\n" 879 ,(concat (format "%s known backends\n" (length known))
809 (length flymake-diagnostic-functions)) 880 (format "%s running\n" (length running))
810 (format "%s running\n" 881 (format "%s disabled\n" (length disabled))
811 (length running))
812 (format "%s disabled\n"
813 (length flymake--disabled-backends))
814 "mouse-1: go to log buffer ") 882 "mouse-1: go to log buffer ")
815 keymap 883 keymap
816 ,(let ((map (make-sparse-keymap))) 884 ,(let ((map (make-sparse-keymap)))
@@ -819,69 +887,73 @@ applied."
819 (interactive "e") 887 (interactive "e")
820 (switch-to-buffer "*Flymake log*"))) 888 (switch-to-buffer "*Flymake log*")))
821 map)) 889 map))
822 ,@(when running 890 ,@(pcase-let ((`(,ind ,face ,explain)
823 `(":" (:propertize "Run" 891 (cond ((null known)
824 face compilation-mode-line-run 892 `("?" mode-line "No known backends"))
825 help-echo 893 (some-waiting
826 ,(format "%s running backends" 894 `("Wait" compilation-mode-line-run
827 (length running))))) 895 ,(format "Waiting for %s running backends"
828 ,@(when reported 896 (length running))))
829 (let ((by-type (make-hash-table))) 897 (all-disabled
830 (maphash (lambda (_backend diags) 898 `("!" compilation-mode-line-run
831 (mapc (lambda (diag) 899 "All backends disabled"))
832 (push diag 900 (t
833 (gethash (flymake--diag-type diag) 901 `(nil nil nil)))))
834 by-type))) 902 (when ind
835 diags)) 903 `((":"
836 flymake--diagnostics-table) 904 (:propertize ,ind
837 (cl-loop 905 face ,face
838 for (type . severity) 906 help-echo ,explain)))))
839 in (cl-sort (mapcar (lambda (type) 907 ,@(unless (or all-disabled
840 (cons type (flymake--lookup-type-property 908 (null known))
841 type 909 (cl-loop
842 'severity 910 for (type . severity)
843 (warning-numeric-level :error)))) 911 in (cl-sort (mapcar (lambda (type)
844 (cl-union (hash-table-keys by-type) 912 (cons type (flymake--lookup-type-property
845 '(:error :warning))) 913 type
846 #'> 914 'severity
847 :key #'cdr) 915 (warning-numeric-level :error))))
848 for diags = (gethash type by-type) 916 (cl-union (hash-table-keys diags-by-type)
849 for face = (flymake--lookup-type-property type 917 '(:error :warning)))
850 'mode-line-face 918 #'>
851 'compilation-error) 919 :key #'cdr)
852 when (or diags 920 for diags = (gethash type diags-by-type)
853 (>= severity (warning-numeric-level :warning))) 921 for face = (flymake--lookup-type-property type
854 collect `(:propertize 922 'mode-line-face
855 ,(format "%d" (length diags)) 923 'compilation-error)
856 face ,face 924 when (or diags
857 mouse-face mode-line-highlight 925 (>= severity (warning-numeric-level :warning)))
858 keymap 926 collect `(:propertize
859 ,(let ((map (make-sparse-keymap)) 927 ,(format "%d" (length diags))
860 (type type)) 928 face ,face
861 (define-key map [mode-line mouse-4] 929 mouse-face mode-line-highlight
862 (lambda (_event) 930 keymap
863 (interactive "e") 931 ,(let ((map (make-sparse-keymap))
864 (flymake-goto-prev-error 1 (list type) t))) 932 (type type))
865 (define-key map [mode-line mouse-5] 933 (define-key map [mode-line mouse-4]
866 (lambda (_event) 934 (lambda (_event)
867 (interactive "e") 935 (interactive "e")
868 (flymake-goto-next-error 1 (list type) t))) 936 (flymake-goto-prev-error 1 (list type) t)))
869 map) 937 (define-key map [mode-line mouse-5]
870 help-echo 938 (lambda (_event)
871 ,(concat (format "%s diagnostics of type %s\n" 939 (interactive "e")
872 (propertize (format "%d" 940 (flymake-goto-next-error 1 (list type) t)))
873 (length diags)) 941 map)
874 'face face) 942 help-echo
875 (propertize (format "%s" type) 943 ,(concat (format "%s diagnostics of type %s\n"
876 'face face)) 944 (propertize (format "%d"
877 "mouse-4/mouse-5: previous/next of this type\n")) 945 (length diags))
878 into forms 946 'face face)
879 finally return 947 (propertize (format "%s" type)
880 `((:propertize "[") 948 'face face))
881 ,@(cl-loop for (a . rest) on forms by #'cdr 949 "mouse-4/mouse-5: previous/next of this type\n"))
882 collect a when rest collect 950 into forms
883 '(:propertize " ")) 951 finally return
884 (:propertize "]")))))))) 952 `((:propertize "[")
953 ,@(cl-loop for (a . rest) on forms by #'cdr
954 collect a when rest collect
955 '(:propertize " "))
956 (:propertize "]")))))))
885 957
886 958
887 959
diff --git a/test/lisp/progmodes/flymake-tests.el b/test/lisp/progmodes/flymake-tests.el
index 222c8f11848..5e042f2b082 100644
--- a/test/lisp/progmodes/flymake-tests.el
+++ b/test/lisp/progmodes/flymake-tests.el
@@ -36,6 +36,26 @@
36 36
37;; 37;;
38;; 38;;
39(defun flymake-tests--wait-for-backends ()
40 ;; Weirdness here... http://debbugs.gnu.org/17647#25
41 ;; ... meaning `sleep-for', and even
42 ;; `accept-process-output', won't suffice as ways to get
43 ;; process filters and sentinels to run, though they do work
44 ;; fine in a non-interactive batch session. The only thing
45 ;; that will indeed unblock pending process output is
46 ;; reading an input event, so, as a workaround, use a dummy
47 ;; `read-event' with a very short timeout.
48 (unless noninteractive (read-event "" nil 0.1))
49 (cl-loop repeat 5
50 for notdone = (cl-set-difference (flymake-running-backends)
51 (flymake-reporting-backends))
52 while notdone
53 unless noninteractive do (read-event "" nil 0.1)
54 do (sleep-for (+ 0.5 flymake-no-changes-timeout))
55 finally (when notdone (ert-fail
56 (format "Some backends not reporting yet %s"
57 notdone)))))
58
39(cl-defun flymake-tests--call-with-fixture (fn file 59(cl-defun flymake-tests--call-with-fixture (fn file
40 &key (severity-predicate 60 &key (severity-predicate
41 nil sev-pred-supplied-p)) 61 nil sev-pred-supplied-p))
@@ -46,7 +66,6 @@ SEVERITY-PREDICATE is used to setup
46 (visiting (find-buffer-visiting file)) 66 (visiting (find-buffer-visiting file))
47 (buffer (or visiting (find-file-noselect file))) 67 (buffer (or visiting (find-file-noselect file)))
48 (process-environment (cons "LC_ALL=C" process-environment)) 68 (process-environment (cons "LC_ALL=C" process-environment))
49 (i 0)
50 (warning-minimum-log-level :error)) 69 (warning-minimum-log-level :error))
51 (unwind-protect 70 (unwind-protect
52 (with-current-buffer buffer 71 (with-current-buffer buffer
@@ -55,18 +74,7 @@ SEVERITY-PREDICATE is used to setup
55 (setq-local flymake-proc-diagnostic-type-pred severity-predicate)) 74 (setq-local flymake-proc-diagnostic-type-pred severity-predicate))
56 (goto-char (point-min)) 75 (goto-char (point-min))
57 (unless flymake-mode (flymake-mode 1)) 76 (unless flymake-mode (flymake-mode 1))
58 ;; Weirdness here... http://debbugs.gnu.org/17647#25 77 (flymake-tests--wait-for-backends)
59 ;; ... meaning `sleep-for', and even
60 ;; `accept-process-output', won't suffice as ways to get
61 ;; process filters and sentinels to run, though they do work
62 ;; fine in a non-interactive batch session. The only thing
63 ;; that will indeed unblock pending process output is
64 ;; reading an input event, so, as a workaround, use a dummy
65 ;; `read-event' with a very short timeout.
66 (unless noninteractive (read-event "" nil 0.1))
67 (while (and (flymake-is-running) (< (setq i (1+ i)) 10))
68 (unless noninteractive (read-event "" nil 0.1))
69 (sleep-for (+ 0.5 flymake-no-changes-timeout)))
70 (funcall fn))) 78 (funcall fn)))
71 (and buffer 79 (and buffer
72 (not visiting) 80 (not visiting)
@@ -119,38 +127,37 @@ SEVERITY-PREDICATE is used to setup
119(ert-deftest different-diagnostic-types () 127(ert-deftest different-diagnostic-types ()
120 "Test GCC warning via function predicate." 128 "Test GCC warning via function predicate."
121 (skip-unless (and (executable-find "gcc") (executable-find "make"))) 129 (skip-unless (and (executable-find "gcc") (executable-find "make")))
122 (flymake-tests--with-flymake 130 (let ((flymake-wrap-around nil))
123 ("errors-and-warnings.c") 131 (flymake-tests--with-flymake
124 (flymake-goto-next-error) 132 ("errors-and-warnings.c")
125 (should (eq 'flymake-error (face-at-point))) 133 (flymake-goto-next-error)
126 (flymake-goto-next-error) 134 (should (eq 'flymake-error (face-at-point)))
127 (should (eq 'flymake-note (face-at-point))) 135 (flymake-goto-next-error)
128 (flymake-goto-next-error) 136 (should (eq 'flymake-note (face-at-point)))
129 (should (eq 'flymake-warning (face-at-point))) 137 (flymake-goto-next-error)
130 (flymake-goto-next-error) 138 (should (eq 'flymake-warning (face-at-point)))
131 (should (eq 'flymake-error (face-at-point))) 139 (flymake-goto-next-error)
132 (flymake-goto-next-error) 140 (should (eq 'flymake-error (face-at-point)))
133 (should (eq 'flymake-warning (face-at-point))) 141 (flymake-goto-next-error)
134 (flymake-goto-next-error) 142 (should (eq 'flymake-warning (face-at-point)))
135 (should (eq 'flymake-warning (face-at-point))) 143 (flymake-goto-next-error)
136 (let ((flymake-wrap-around nil)) 144 (should (eq 'flymake-warning (face-at-point)))
137 (should-error (flymake-goto-next-error nil nil t))) )) 145 (should-error (flymake-goto-next-error nil nil t)))))
138 146
139(ert-deftest included-c-header-files () 147(ert-deftest included-c-header-files ()
140 "Test inclusion of .h header files." 148 "Test inclusion of .h header files."
141 (skip-unless (and (executable-find "gcc") (executable-find "make"))) 149 (skip-unless (and (executable-find "gcc") (executable-find "make")))
142 (flymake-tests--with-flymake 150 (let ((flymake-wrap-around nil))
143 ("some-problems.h") 151 (flymake-tests--with-flymake
144 (flymake-goto-next-error) 152 ("some-problems.h")
145 (should (eq 'flymake-warning (face-at-point))) 153 (flymake-goto-next-error)
146 (flymake-goto-next-error) 154 (should (eq 'flymake-warning (face-at-point)))
147 (should (eq 'flymake-error (face-at-point))) 155 (flymake-goto-next-error)
148 (let ((flymake-wrap-around nil)) 156 (should (eq 'flymake-error (face-at-point)))
149 (should-error (flymake-goto-next-error nil nil t))) ) 157 (should-error (flymake-goto-next-error nil nil t)))
150 (flymake-tests--with-flymake 158 (flymake-tests--with-flymake
151 ("no-problems.h") 159 ("no-problems.h")
152 (let ((flymake-wrap-around nil)) 160 (should-error (flymake-goto-next-error nil nil t)))))
153 (should-error (flymake-goto-next-error nil nil t))) ))
154 161
155(defmacro flymake-tests--assert-set (set 162(defmacro flymake-tests--assert-set (set
156 should 163 should
@@ -159,19 +166,15 @@ SEVERITY-PREDICATE is used to setup
159 `(progn 166 `(progn
160 ,@(cl-loop 167 ,@(cl-loop
161 for s in should 168 for s in should
162 collect `(should (memq ,s ,set))) 169 collect `(should (memq (quote ,s) ,set)))
163 ,@(cl-loop 170 ,@(cl-loop
164 for s in should-not 171 for s in should-not
165 collect `(should-not (memq ,s ,set))))) 172 collect `(should-not (memq (quote ,s) ,set)))))
166 173
167(ert-deftest dummy-backends () 174(defun flymake-tests--diagnose-words
168 "Test GCC warning via function predicate." 175 (report-fn type words)
169 (with-temp-buffer 176 "Helper. Call REPORT-FN with diagnostics for WORDS in buffer."
170 (cl-labels 177 (funcall report-fn
171 ((diagnose
172 (report-fn type words)
173 (funcall
174 report-fn
175 (cl-loop 178 (cl-loop
176 for word in words 179 for word in words
177 append 180 append
@@ -184,32 +187,34 @@ SEVERITY-PREDICATE is used to setup
184 (match-end 0) 187 (match-end 0)
185 type 188 type
186 (concat word " is wrong"))))))) 189 (concat word " is wrong")))))))
187 (error-backend 190
188 (report-fn) 191(ert-deftest dummy-backends ()
189 (run-with-timer 192 "Test many different kinds of backends."
190 0.5 nil 193 (with-temp-buffer
191 #'diagnose report-fn :error '("manha" "prognata"))) 194 (cl-letf
192 (warning-backend 195 (((symbol-function 'error-backend)
193 (report-fn) 196 (lambda (report-fn)
194 (run-with-timer 197 (run-with-timer
195 0.5 nil 198 0.5 nil
196 #'diagnose report-fn :warning '("ut" "dolor"))) 199 #'flymake-tests--diagnose-words report-fn :error '("manha" "prognata"))))
197 (sync-backend 200 ((symbol-function 'warning-backend)
198 (report-fn) 201 (lambda (report-fn)
199 (diagnose report-fn :note '("quis" "commodo"))) 202 (run-with-timer
200 (refusing-backend 203 0.5 nil
201 (_report-fn) 204 #'flymake-tests--diagnose-words report-fn :warning '("ut" "dolor"))))
202 nil) 205 ((symbol-function 'sync-backend)
203 (panicking-backend 206 (lambda (report-fn)
204 (report-fn) 207 (flymake-tests--diagnose-words report-fn :note '("quis" "commodo"))))
205 (run-with-timer 208 ((symbol-function 'panicking-backend)
206 0.5 nil 209 (lambda (report-fn)
207 report-fn :panic :explanation "The spanish inquisition!")) 210 (run-with-timer
208 (crashing-backend 211 0.5 nil
209 (_report-fn) 212 report-fn :panic :explanation "The spanish inquisition!")))
210 ;; HACK: Shoosh log during tests 213 ((symbol-function 'crashing-backend)
211 (setq-local warning-minimum-log-level :emergency) 214 (lambda (_report-fn)
212 (error "crashed"))) 215 ;; HACK: Shoosh log during tests
216 (setq-local warning-minimum-log-level :emergency)
217 (error "crashed"))))
213 (insert "Lorem ipsum dolor sit amet, consectetur adipiscing 218 (insert "Lorem ipsum dolor sit amet, consectetur adipiscing
214 elit, sed do eiusmod tempor incididunt ut labore et dolore 219 elit, sed do eiusmod tempor incididunt ut labore et dolore
215 manha aliqua. Ut enim ad minim veniam, quis nostrud 220 manha aliqua. Ut enim ad minim veniam, quis nostrud
@@ -220,31 +225,27 @@ SEVERITY-PREDICATE is used to setup
220 sunt in culpa qui officia deserunt mollit anim id est 225 sunt in culpa qui officia deserunt mollit anim id est
221 laborum.") 226 laborum.")
222 (let ((flymake-diagnostic-functions 227 (let ((flymake-diagnostic-functions
223 (list #'error-backend #'warning-backend #'sync-backend 228 (list 'error-backend 'warning-backend 'sync-backend
224 #'refusing-backend #'panicking-backend 229 'panicking-backend
225 #'crashing-backend 230 'crashing-backend
226 ))) 231 ))
232 (flymake-wrap-around nil))
227 (flymake-mode) 233 (flymake-mode)
228 ;; FIXME: accessing some flymake-ui's internals here...
229 (flymake-tests--assert-set flymake--running-backends
230 (#'error-backend #'warning-backend #'panicking-backend)
231 (#'sync-backend #'crashing-backend #'refusing-backend))
232 234
233 (flymake-tests--assert-set flymake--disabled-backends 235 (flymake-tests--assert-set (flymake-running-backends)
234 (#'crashing-backend) 236 (error-backend warning-backend panicking-backend)
235 (#'error-backend #'warning-backend #'sync-backend 237 (crashing-backend))
236 #'panicking-backend #'refusing-backend))
237 238
238 (cl-loop repeat 10 while (flymake-is-running) 239 (flymake-tests--assert-set (flymake-disabled-backends)
239 unless noninteractive do (read-event "" nil 0.1) 240 (crashing-backend)
240 do (sleep-for (+ 0.5 flymake-no-changes-timeout))) 241 (error-backend warning-backend sync-backend
242 panicking-backend))
241 243
242 (should (eq flymake--running-backends '())) 244 (flymake-tests--wait-for-backends)
243 245
244 (flymake-tests--assert-set flymake--disabled-backends 246 (flymake-tests--assert-set (flymake-disabled-backends)
245 (#'crashing-backend #'panicking-backend) 247 (crashing-backend panicking-backend)
246 (#'error-backend #'warning-backend #'sync-backend 248 (error-backend warning-backend sync-backend))
247 #'refusing-backend))
248 249
249 (goto-char (point-min)) 250 (goto-char (point-min))
250 (flymake-goto-next-error) 251 (flymake-goto-next-error)
@@ -265,8 +266,55 @@ SEVERITY-PREDICATE is used to setup
265 (should (eq 'flymake-warning (face-at-point))) ; dolor 266 (should (eq 'flymake-warning (face-at-point))) ; dolor
266 (flymake-goto-next-error) 267 (flymake-goto-next-error)
267 (should (eq 'flymake-error (face-at-point))) ; prognata 268 (should (eq 'flymake-error (face-at-point))) ; prognata
268 (let ((flymake-wrap-around nil)) 269 (should-error (flymake-goto-next-error nil nil t))))))
269 (should-error (flymake-goto-next-error nil nil t))))))) 270
271(ert-deftest recurrent-backend ()
272 "Test a backend that calls REPORT-FN multiple times"
273 (with-temp-buffer
274 (let (tick)
275 (cl-letf
276 (((symbol-function 'eager-backend)
277 (lambda (report-fn)
278 (funcall report-fn nil :explanation "very eager but no diagnostics")
279 (display-buffer (current-buffer))
280 (run-with-timer
281 0.5 nil
282 (lambda ()
283 (flymake-tests--diagnose-words report-fn :warning '("consectetur"))
284 (setq tick t)
285 (run-with-timer
286 0.5 nil
287 (lambda ()
288 (flymake-tests--diagnose-words report-fn :error '("fugiat"))
289 (setq tick t))))))))
290 (insert "Lorem ipsum dolor sit amet, consectetur adipiscing
291 elit, sed do eiusmod tempor incididunt ut labore et dolore
292 manha aliqua. Ut enim ad minim veniam, quis nostrud
293 exercitation ullamco laboris nisi ut aliquip ex ea commodo
294 consequat. Duis aute irure dolor in reprehenderit in
295 voluptate velit esse cillum dolore eu fugiat nulla
296 pariatur. Excepteur sint occaecat cupidatat non prognata
297 sunt in culpa qui officia deserunt mollit anim id est
298 laborum.")
299 (let ((flymake-diagnostic-functions
300 (list 'eager-backend))
301 (flymake-wrap-around nil))
302 (flymake-mode)
303 (flymake-tests--assert-set (flymake-running-backends)
304 (eager-backend) ())
305 (cl-loop until tick repeat 4 do (sleep-for 0.2))
306 (setq tick nil)
307 (goto-char (point-max))
308 (flymake-goto-prev-error)
309 (should (eq 'flymake-warning (face-at-point))) ; consectetur
310 (should-error (flymake-goto-prev-error nil nil t))
311 (cl-loop until tick repeat 4 do (sleep-for 0.2))
312 (flymake-goto-next-error)
313 (should (eq 'flymake-error (face-at-point))) ; fugiat
314 (flymake-goto-prev-error)
315 (should (eq 'flymake-warning (face-at-point))) ; back at consectetur
316 (should-error (flymake-goto-prev-error nil nil t))
317 )))))
270 318
271(provide 'flymake-tests) 319(provide 'flymake-tests)
272 320