; Spelling fixes
[emacs.git] / lisp / progmodes / flymake.el
blob0b28dc31cfc80dad97c2c7e83113ed0370016d68
1 ;;; flymake.el --- A universal on-the-fly syntax checker -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2003-2017 Free Software Foundation, Inc.
5 ;; Author: Pavel Kobyakov <pk_at_work@yahoo.com>
6 ;; Maintainer: Leo Liu <sdl.web@gmail.com>
7 ;; Version: 0.3
8 ;; Keywords: c languages tools
10 ;; This file is part of GNU Emacs.
12 ;; GNU Emacs is free software: you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation, either version 3 of the License, or
15 ;; (at your option) any later version.
17 ;; GNU Emacs is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
25 ;;; Commentary:
27 ;; Flymake is a minor Emacs mode performing on-the-fly syntax checks.
29 ;; Flymake collects diagnostic information for multiple sources,
30 ;; called backends, and visually annotates the relevant portions in
31 ;; the buffer.
33 ;; This file contains the UI for displaying and interacting with the
34 ;; results produced by these backends, as well as entry points for
35 ;; backends to hook on to.
37 ;; The main entry points are `flymake-mode' and `flymake-start'
39 ;; The docstrings of these variables are relevant to understanding how
40 ;; Flymake works for both the user and the backend programmer:
42 ;; * `flymake-diagnostic-functions'
43 ;; * `flymake-diagnostic-types-alist'
45 ;;; Code:
47 (require 'cl-lib)
48 (require 'thingatpt) ; end-of-thing
49 (require 'warnings) ; warning-numeric-level, display-warning
50 (require 'compile) ; for some faces
51 (require 'subr-x) ; when-let*, if-let*, hash-table-keys, hash-table-values
53 (defgroup flymake nil
54 "Universal on-the-fly syntax checker."
55 :version "23.1"
56 :link '(custom-manual "(flymake) Top")
57 :group 'tools)
59 (defcustom flymake-error-bitmap '(flymake-double-exclamation-mark
60 compilation-error)
61 "Bitmap (a symbol) used in the fringe for indicating errors.
62 The value may also be a list of two elements where the second
63 element specifies the face for the bitmap. For possible bitmap
64 symbols, see `fringe-bitmaps'. See also `flymake-warning-bitmap'.
66 The option `flymake-fringe-indicator-position' controls how and where
67 this is used."
68 :version "24.3"
69 :type '(choice (symbol :tag "Bitmap")
70 (list :tag "Bitmap and face"
71 (symbol :tag "Bitmap")
72 (face :tag "Face"))))
74 (defcustom flymake-warning-bitmap '(exclamation-mark compilation-warning)
75 "Bitmap (a symbol) used in the fringe for indicating warnings.
76 The value may also be a list of two elements where the second
77 element specifies the face for the bitmap. For possible bitmap
78 symbols, see `fringe-bitmaps'. See also `flymake-error-bitmap'.
80 The option `flymake-fringe-indicator-position' controls how and where
81 this is used."
82 :version "24.3"
83 :type '(choice (symbol :tag "Bitmap")
84 (list :tag "Bitmap and face"
85 (symbol :tag "Bitmap")
86 (face :tag "Face"))))
88 (defcustom flymake-note-bitmap '(exclamation-mark compilation-info)
89 "Bitmap (a symbol) used in the fringe for indicating info notes.
90 The value may also be a list of two elements where the second
91 element specifies the face for the bitmap. For possible bitmap
92 symbols, see `fringe-bitmaps'. See also `flymake-error-bitmap'.
94 The option `flymake-fringe-indicator-position' controls how and where
95 this is used."
96 :version "26.1"
97 :type '(choice (symbol :tag "Bitmap")
98 (list :tag "Bitmap and face"
99 (symbol :tag "Bitmap")
100 (face :tag "Face"))))
102 (defcustom flymake-fringe-indicator-position 'left-fringe
103 "The position to put Flymake fringe indicator.
104 The value can be nil (do not use indicators), `left-fringe' or `right-fringe'.
105 See `flymake-error-bitmap' and `flymake-warning-bitmap'."
106 :version "24.3"
107 :type '(choice (const left-fringe)
108 (const right-fringe)
109 (const :tag "No fringe indicators" nil)))
111 (defcustom flymake-start-syntax-check-on-newline t
112 "Start syntax check if newline char was added/removed from the buffer."
113 :type 'boolean)
115 (defcustom flymake-no-changes-timeout 0.5
116 "Time to wait after last change before automatically checking buffer.
117 If nil, never start checking buffer automatically like this."
118 :type 'number)
120 (defcustom flymake-gui-warnings-enabled t
121 "Enables/disables GUI warnings."
122 :type 'boolean)
123 (make-obsolete-variable 'flymake-gui-warnings-enabled
124 "it no longer has any effect." "26.1")
126 (defcustom flymake-start-syntax-check-on-find-file t
127 "Start syntax check on find file."
128 :type 'boolean)
130 (defcustom flymake-log-level -1
131 "Obsolete and ignored variable."
132 :type 'integer)
133 (make-obsolete-variable 'flymake-log-level
134 "it is superseded by `warning-minimum-log-level.'"
135 "26.1")
137 (defcustom flymake-wrap-around t
138 "If non-nil, moving to errors wraps around buffer boundaries."
139 :type 'boolean)
141 (define-fringe-bitmap 'flymake-double-exclamation-mark
142 (vector #b00000000
143 #b00000000
144 #b00000000
145 #b00000000
146 #b01100110
147 #b01100110
148 #b01100110
149 #b01100110
150 #b01100110
151 #b01100110
152 #b01100110
153 #b01100110
154 #b00000000
155 #b01100110
156 #b00000000
157 #b00000000
158 #b00000000))
160 (defvar-local flymake-timer nil
161 "Timer for starting syntax check.")
163 (defvar-local flymake-check-start-time nil
164 "Time at which syntax check was started.")
166 (defun flymake--log-1 (level sublog msg &rest args)
167 "Do actual work for `flymake-log'."
168 (let (;; never popup the log buffer
169 (warning-minimum-level :emergency)
170 (warning-type-format
171 (format " [%s %s]"
172 (or sublog 'flymake)
173 (current-buffer))))
174 (display-warning (list 'flymake sublog)
175 (apply #'format-message msg args)
176 (if (numberp level)
177 (or (nth level
178 '(:emergency :error :warning :debug :debug) )
179 :error)
180 level)
181 "*Flymake log*")))
183 ;;;###autoload
184 (defmacro flymake-log (level msg &rest args)
185 "Log, at level LEVEL, the message MSG formatted with ARGS.
186 LEVEL is passed to `display-warning', which is used to display
187 the warning. If this form is included in a byte-compiled file,
188 the generated warning contains an indication of the file that
189 generated it."
190 (let* ((compile-file (and (boundp 'byte-compile-current-file)
191 (symbol-value 'byte-compile-current-file)))
192 (sublog (if (and
193 compile-file
194 (not load-file-name))
195 (intern
196 (file-name-nondirectory
197 (file-name-sans-extension compile-file))))))
198 `(flymake--log-1 ,level ',sublog ,msg ,@args)))
200 (defun flymake-error (text &rest args)
201 "Format TEXT with ARGS and signal an error for flymake."
202 (let ((msg (apply #'format-message text args)))
203 (flymake-log :error msg)
204 (error (concat "[Flymake] " msg))))
206 (cl-defstruct (flymake--diag
207 (:constructor flymake--diag-make))
208 buffer beg end type text backend)
210 ;;;###autoload
211 (defun flymake-make-diagnostic (buffer
214 type
215 text)
216 "Make a Flymake diagnostic for BUFFER's region from BEG to END.
217 TYPE is a key to `flymake-diagnostic-types-alist' and TEXT is a
218 description of the problem detected in this region."
219 (flymake--diag-make :buffer buffer :beg beg :end end :type type :text text))
221 (defun flymake-ler-make-ler (file line type text &optional full-file)
222 (let* ((file (or full-file file))
223 (buf (find-buffer-visiting file)))
224 (unless buf (flymake-error "No buffer visiting %s" file))
225 (pcase-let* ((`(,beg . ,end)
226 (with-current-buffer buf
227 (flymake-diag-region line nil))))
228 (flymake-make-diagnostic buf beg end type text))))
230 (make-obsolete 'flymake-ler-make-ler 'flymake-make-diagnostic "26.1")
232 (cl-defun flymake--overlays (&key beg end filter compare key)
233 "Get flymake-related overlays.
234 If BEG is non-nil and END is nil, consider only `overlays-at'
235 BEG. Otherwise consider `overlays-in' the region comprised by BEG
236 and END, defaulting to the whole buffer. Remove all that do not
237 verify FILTER, a function, and sort them by COMPARE (using KEY)."
238 (save-restriction
239 (widen)
240 (let ((ovs (cl-remove-if-not
241 (lambda (ov)
242 (and (overlay-get ov 'flymake)
243 (or (not filter)
244 (funcall filter ov))))
245 (if (and beg (null end))
246 (overlays-at beg t)
247 (overlays-in (or beg (point-min))
248 (or end (point-max)))))))
249 (if compare
250 (cl-sort ovs compare :key (or key
251 #'identity))
252 ovs))))
254 (defun flymake-delete-own-overlays (&optional filter)
255 "Delete all Flymake overlays in BUFFER."
256 (mapc #'delete-overlay (flymake--overlays :filter filter)))
258 (defface flymake-error
259 '((((supports :underline (:style wave)))
260 :underline (:style wave :color "Red1"))
262 :inherit error))
263 "Face used for marking error regions."
264 :version "24.4")
266 (defface flymake-warning
267 '((((supports :underline (:style wave)))
268 :underline (:style wave :color "deep sky blue"))
270 :inherit warning))
271 "Face used for marking warning regions."
272 :version "24.4")
274 (defface flymake-note
275 '((((supports :underline (:style wave)))
276 :underline (:style wave :color "yellow green"))
278 :inherit warning))
279 "Face used for marking note regions."
280 :version "26.1")
282 (define-obsolete-face-alias 'flymake-warnline 'flymake-warning "26.1")
283 (define-obsolete-face-alias 'flymake-errline 'flymake-error "26.1")
285 (defun flymake-diag-region (line &optional col)
286 "Compute region (BEG . END) corresponding to LINE and COL.
287 If COL is nil, return a region just for LINE.
288 Return nil if the region is invalid."
289 (condition-case-unless-debug _err
290 (let ((line (min (max line 1)
291 (line-number-at-pos (point-max) 'absolute))))
292 (save-excursion
293 (goto-char (point-min))
294 (forward-line (1- line))
295 (cl-flet ((fallback-bol
296 () (progn (back-to-indentation) (point)))
297 (fallback-eol
298 (beg)
299 (progn
300 (end-of-line)
301 (skip-chars-backward " \t\f\t\n" beg)
302 (if (eq (point) beg)
303 (line-beginning-position 2)
304 (point)))))
305 (if (and col (cl-plusp col))
306 (let* ((beg (progn (forward-char (1- col))
307 (point)))
308 (sexp-end (ignore-errors (end-of-thing 'sexp)))
309 (end (or (and sexp-end
310 (not (= sexp-end beg))
311 sexp-end)
312 (ignore-errors (goto-char (1+ beg)))))
313 (safe-end (or end
314 (fallback-eol beg))))
315 (cons (if end beg (fallback-bol))
316 safe-end))
317 (let* ((beg (fallback-bol))
318 (end (fallback-eol beg)))
319 (cons beg end))))))
320 (error (flymake-error "Invalid region line=%s col=%s" line col))))
322 (defvar flymake-diagnostic-functions nil
323 "Special hook of Flymake backends that check a buffer.
325 The functions in this hook diagnose problems in a buffer’s
326 contents and provide information to the Flymake user interface
327 about where and how to annotate problems diagnosed in a buffer.
329 Whenever Flymake or the user decides to re-check the buffer, each
330 function is called with an arbitrary number of arguments:
332 * the first argument is always REPORT-FN, a callback function
333 detailed below;
335 * the remaining arguments are keyword-value pairs in the
336 form (:KEY VALUE :KEY2 VALUE2...). Currently, Flymake provides
337 no such arguments, but backend functions must be prepared to
338 accept to accept and possibly ignore any number of them.
340 Backend functions are expected to initiate the buffer check, but
341 aren't required to complete it check before exiting: if the
342 computation involved is expensive, especially for large buffers,
343 that task can be scheduled for the future using asynchronous
344 processes or other asynchronous mechanisms.
346 In any case, backend functions are expected to return quickly or
347 signal an error, in which case the backend is disabled. Flymake
348 will not try disabled backends again for any future checks of
349 this buffer. Certain commands, like turning `flymake-mode' off
350 and on again, reset the list of disabled backends.
352 If the function returns, Flymake considers the backend to be
353 \"running\". If it has not done so already, the backend is
354 expected to call the function REPORT-FN with a single argument
355 REPORT-ACTION also followed by an optional list of keyword-value
356 pairs in the form (:REPORT-KEY VALUE :REPORT-KEY2 VALUE2...).
358 Currently accepted values for REPORT-ACTION are:
360 * A (possibly empty) list of diagnostic objects created with
361 `flymake-make-diagnostic', causing Flymake to annotate the
362 buffer with this information.
364 A backend may call REPORT-FN repeatedly in this manner, but
365 only until Flymake considers that the most recently requested
366 buffer check is now obsolete because, say, buffer contents have
367 changed in the meantime. The backend is only given notice of
368 this via a renewed call to the backend function. Thus, to
369 prevent making obsolete reports and wasting resources, backend
370 functions should first cancel any ongoing processing from
371 previous calls.
373 * The symbol `:panic', signaling that the backend has encountered
374 an exceptional situation and should be disabled.
376 Currently accepted REPORT-KEY arguments are:
378 * ‘:explanation’: value should give user-readable details of
379 the situation encountered, if any.
381 * ‘:force’: value should be a boolean suggesting that the Flymake
382 considers the report even if was somehow unexpected.")
384 (defvar flymake-diagnostic-types-alist
385 `((:error
386 . ((flymake-category . flymake-error)))
387 (:warning
388 . ((flymake-category . flymake-warning)))
389 (:note
390 . ((flymake-category . flymake-note))))
391 "Alist ((KEY . PROPS)*) of properties of Flymake error types.
392 KEY can be anything passed as `:type' to `flymake-diag-make'.
394 PROPS is an alist of properties that are applied, in order, to
395 the diagnostics of each type. The recognized properties are:
397 * Every property pertaining to overlays, except `category' and
398 `evaporate' (see Info Node `(elisp)Overlay Properties'), used
399 affect the appearance of Flymake annotations.
401 * `bitmap', an image displayed in the fringe according to
402 `flymake-fringe-indicator-position'. The value actually
403 follows the syntax of `flymake-error-bitmap' (which see). It
404 is overridden by any `before-string' overlay property.
406 * `severity', a non-negative integer specifying the diagnostic's
407 severity. The higher, the more serious. If the overlay
408 priority `priority' is not specified, `severity' is used to set
409 it and help sort overlapping overlays.
411 * `flymake-category', a symbol whose property list is considered
412 as a default for missing values of any other properties. This
413 is useful to backend authors when creating new diagnostic types
414 that differ from an existing type by only a few properties.")
416 (put 'flymake-error 'face 'flymake-error)
417 (put 'flymake-error 'bitmap 'flymake-error-bitmap)
418 (put 'flymake-error 'severity (warning-numeric-level :error))
419 (put 'flymake-error 'mode-line-face 'compilation-error)
421 (put 'flymake-warning 'face 'flymake-warning)
422 (put 'flymake-warning 'bitmap 'flymake-warning-bitmap)
423 (put 'flymake-warning 'severity (warning-numeric-level :warning))
424 (put 'flymake-warning 'mode-line-face 'compilation-warning)
426 (put 'flymake-note 'face 'flymake-note)
427 (put 'flymake-note 'bitmap 'flymake-note-bitmap)
428 (put 'flymake-note 'severity (warning-numeric-level :debug))
429 (put 'flymake-note 'mode-line-face 'compilation-info)
431 (defun flymake--lookup-type-property (type prop &optional default)
432 "Look up PROP for TYPE in `flymake-diagnostic-types-alist'.
433 If TYPE doesn't declare PROP in either
434 `flymake-diagnostic-types-alist' or in the symbol of its
435 associated `flymake-category' return DEFAULT."
436 (let ((alist-probe (assoc type flymake-diagnostic-types-alist)))
437 (cond (alist-probe
438 (let* ((alist (cdr alist-probe))
439 (prop-probe (assoc prop alist)))
440 (if prop-probe
441 (cdr prop-probe)
442 (if-let* ((cat (assoc-default 'flymake-category alist))
443 (plist (and (symbolp cat)
444 (symbol-plist cat)))
445 (cat-probe (plist-member plist prop)))
446 (cadr cat-probe)
447 default))))
449 default))))
451 (defun flymake--fringe-overlay-spec (bitmap &optional recursed)
452 (if (and (symbolp bitmap)
453 (boundp bitmap)
454 (not recursed))
455 (flymake--fringe-overlay-spec
456 (symbol-value bitmap) t)
457 (and flymake-fringe-indicator-position
458 bitmap
459 (propertize "!" 'display
460 (cons flymake-fringe-indicator-position
461 (if (listp bitmap)
462 bitmap
463 (list bitmap)))))))
465 (defun flymake--highlight-line (diagnostic)
466 "Highlight buffer with info in DIAGNOSTIC."
467 (when-let* ((ov (make-overlay
468 (flymake--diag-beg diagnostic)
469 (flymake--diag-end diagnostic))))
470 ;; First set `category' in the overlay, then copy over every other
471 ;; property.
473 (let ((alist (assoc-default (flymake--diag-type diagnostic)
474 flymake-diagnostic-types-alist)))
475 (overlay-put ov 'category (assoc-default 'flymake-category alist))
476 (cl-loop for (k . v) in alist
477 unless (eq k 'category)
478 do (overlay-put ov k v)))
479 ;; Now ensure some essential defaults are set
481 (cl-flet ((default-maybe
482 (prop value)
483 (unless (or (plist-member (overlay-properties ov) prop)
484 (let ((cat (overlay-get ov
485 'flymake-category)))
486 (and cat
487 (plist-member (symbol-plist cat) prop))))
488 (overlay-put ov prop value))))
489 (default-maybe 'bitmap 'flymake-error-bitmap)
490 (default-maybe 'face 'flymake-error)
491 (default-maybe 'before-string
492 (flymake--fringe-overlay-spec
493 (overlay-get ov 'bitmap)))
494 (default-maybe 'help-echo
495 (lambda (_window _ov pos)
496 (mapconcat
497 (lambda (ov)
498 (let ((diag (overlay-get ov 'flymake--diagnostic)))
499 (flymake--diag-text diag)))
500 (flymake--overlays :beg pos)
501 "\n")))
502 (default-maybe 'severity (warning-numeric-level :error))
503 (default-maybe 'priority (+ 100 (overlay-get ov 'severity))))
504 ;; Some properties can't be overridden.
506 (overlay-put ov 'evaporate t)
507 (overlay-put ov 'flymake t)
508 (overlay-put ov 'flymake--diagnostic diagnostic)))
510 ;; Nothing in Flymake uses this at all any more, so this is just for
511 ;; third-party compatibility.
512 (define-obsolete-function-alias 'flymake-display-warning 'message-box "26.1")
514 (defvar-local flymake--backend-state nil
515 "Buffer-local hash table of a Flymake backend's state.
516 The keys to this hash table are functions as found in
517 `flymake-diagnostic-functions'. The values are structures
518 of the type `flymake--backend-state', with these slots
520 `running', a symbol to keep track of a backend's replies via its
521 REPORT-FN argument. A backend is running if this key is
522 present. If the key is absent if the backend isn't expecting any
523 replies from the backend.
525 `diags', a (possibly empty) list of diagnostic objects created
526 with `flymake-make-diagnostic'. This key is absent if the
527 backend hasn't reported anything yet.
529 `reported-p', a boolean indicating if the backend has replied
530 since it last was contacted.
532 `disabled', a string with the explanation for a previous
533 exceptional situation reported by the backend. If this key is
534 present the backend is disabled.")
536 (cl-defstruct (flymake--backend-state
537 (:constructor flymake--make-backend-state))
538 running reported-p disabled diags)
540 (defmacro flymake--with-backend-state (backend state-var &rest body)
541 "Bind BACKEND's STATE-VAR to its state, run BODY."
542 (declare (indent 2) (debug (sexp sexp &rest form)))
543 (let ((b (make-symbol "b")))
544 `(let* ((,b ,backend)
545 (,state-var
546 (or (gethash ,b flymake--backend-state)
547 (puthash ,b (flymake--make-backend-state)
548 flymake--backend-state))))
549 ,@body)))
551 (defun flymake-is-running ()
552 "Tell if Flymake has running backends in this buffer"
553 (flymake-running-backends))
555 (cl-defun flymake--handle-report (backend token report-action
556 &key explanation force
557 &allow-other-keys)
558 "Handle reports from BACKEND identified by TOKEN.
560 BACKEND, REPORT-ACTION and EXPLANATION, and FORCE conform to the calling
561 convention described in `flymake-diagnostic-functions' (which
562 see). Optional FORCE says to handle a report even if TOKEN was
563 not expected."
564 (let* ((state (gethash backend flymake--backend-state))
565 (first-report (not (flymake--backend-state-reported-p state))))
566 (setf (flymake--backend-state-reported-p state) t)
567 (let (expected-token
568 new-diags)
569 (cond
570 ((null state)
571 (flymake-error
572 "Unexpected report from unknown backend %s" backend))
573 ((flymake--backend-state-disabled state)
574 (flymake-error
575 "Unexpected report from disabled backend %s" backend))
576 ((progn
577 (setq expected-token (flymake--backend-state-running state))
578 (null expected-token))
579 ;; should never happen
580 (flymake-error "Unexpected report from stopped backend %s" backend))
581 ((and (not (eq expected-token token))
582 (not force))
583 (flymake-error "Obsolete report from backend %s with explanation %s"
584 backend explanation))
585 ((eq :panic report-action)
586 (flymake--disable-backend backend explanation))
587 ((not (listp report-action))
588 (flymake--disable-backend backend
589 (format "Unknown action %S" report-action))
590 (flymake-error "Expected report, but got unknown key %s" report-action))
592 (setq new-diags report-action)
593 (save-restriction
594 (widen)
595 ;; only delete overlays if this is the first report
596 (when first-report
597 (flymake-delete-own-overlays
598 (lambda (ov)
599 (eq backend
600 (flymake--diag-backend
601 (overlay-get ov 'flymake--diagnostic))))))
602 (mapc (lambda (diag)
603 (flymake--highlight-line diag)
604 (setf (flymake--diag-backend diag) backend))
605 new-diags)
606 (setf (flymake--backend-state-diags state)
607 (append new-diags (flymake--backend-state-diags state)))
608 (when flymake-check-start-time
609 (flymake-log :debug "backend %s reported %d diagnostics in %.2f second(s)"
610 backend
611 (length new-diags)
612 (- (float-time) flymake-check-start-time)))))))))
614 (defun flymake-make-report-fn (backend &optional token)
615 "Make a suitable anonymous report function for BACKEND.
616 BACKEND is used to help Flymake distinguish different diagnostic
617 sources. If provided, TOKEN helps Flymake distinguish between
618 different runs of the same backend."
619 (let ((buffer (current-buffer)))
620 (lambda (&rest args)
621 (when (buffer-live-p buffer)
622 (with-current-buffer buffer
623 (apply #'flymake--handle-report backend token args))))))
625 (defun flymake--collect (fn)
626 (let (retval)
627 (maphash (lambda (backend state)
628 (when (funcall fn state) (push backend retval)))
629 flymake--backend-state)
630 retval))
632 (defun flymake-running-backends ()
633 "Compute running Flymake backends in current buffer."
634 (flymake--collect #'flymake--backend-state-running))
636 (defun flymake-disabled-backends ()
637 "Compute disabled Flymake backends in current buffer."
638 (flymake--collect #'flymake--backend-state-disabled))
640 (defun flymake-reporting-backends ()
641 "Compute reporting Flymake backends in current buffer."
642 (flymake--collect #'flymake--backend-state-reported-p))
644 (defun flymake--disable-backend (backend &optional explanation)
645 "Disable BACKEND because EXPLANATION.
646 If is is running also stop it."
647 (flymake-log :warning "Disabling backend %s because %s" backend explanation)
648 (flymake--with-backend-state backend state
649 (setf (flymake--backend-state-running state) nil
650 (flymake--backend-state-disabled state) explanation
651 (flymake--backend-state-reported-p state) t)))
653 (defun flymake--run-backend (backend)
654 "Run the backend BACKEND, reenabling if necessary."
655 (flymake-log :debug "Running backend %s" backend)
656 (let ((run-token (cl-gensym "backend-token")))
657 (flymake--with-backend-state backend state
658 (setf (flymake--backend-state-running state) run-token
659 (flymake--backend-state-disabled state) nil
660 (flymake--backend-state-diags state) nil
661 (flymake--backend-state-reported-p state) nil))
662 ;; FIXME: Should use `condition-case-unless-debug' here, for don't
663 ;; for two reasons: (1) that won't let me catch errors from inside
664 ;; `ert-deftest' where `debug-on-error' appears to be always
665 ;; t. (2) In cases where the user is debugging elisp somewhere
666 ;; else, and using flymake, the presence of a frequently
667 ;; misbehaving backend in the global hook (most likely the legacy
668 ;; backend) will trigger an annoying backtrace.
670 (condition-case err
671 (funcall backend
672 (flymake-make-report-fn backend run-token))
673 (error
674 (flymake--disable-backend backend err)))))
676 (defun flymake-start (&optional deferred force)
677 "Start a syntax check.
678 Start it immediately, or after current command if DEFERRED is
679 non-nil. With optional FORCE run even disabled backends.
681 Interactively, with a prefix arg, FORCE is t."
682 (interactive (list nil current-prefix-arg))
683 (cl-labels
684 ((start
686 (remove-hook 'post-command-hook #'start 'local)
687 (setq flymake-check-start-time (float-time))
688 (run-hook-wrapped
689 'flymake-diagnostic-functions
690 (lambda (backend)
691 (cond
692 ((and (not force)
693 (flymake--with-backend-state backend state
694 (flymake--backend-state-disabled state)))
695 (flymake-log :debug "Backend %s is disabled, not starting"
696 backend))
698 (flymake--run-backend backend)))
699 nil))))
700 (if (and deferred
701 this-command)
702 (add-hook 'post-command-hook #'start 'append 'local)
703 (start))))
705 (defvar flymake-mode-map
706 (let ((map (make-sparse-keymap))) map)
707 "Keymap for `flymake-mode'")
709 ;;;###autoload
710 (define-minor-mode flymake-mode nil
711 :group 'flymake :lighter flymake--mode-line-format :keymap flymake-mode-map
712 (cond
713 ;; Turning the mode ON.
714 (flymake-mode
715 (cond
716 ((not flymake-diagnostic-functions)
717 (flymake-error "No backends to check buffer %s" (buffer-name)))
719 (add-hook 'after-change-functions 'flymake-after-change-function nil t)
720 (add-hook 'after-save-hook 'flymake-after-save-hook nil t)
721 (add-hook 'kill-buffer-hook 'flymake-kill-buffer-hook nil t)
723 (setq flymake--backend-state (make-hash-table))
725 (when flymake-start-syntax-check-on-find-file
726 (flymake-start)))))
728 ;; Turning the mode OFF.
730 (remove-hook 'after-change-functions 'flymake-after-change-function t)
731 (remove-hook 'after-save-hook 'flymake-after-save-hook t)
732 (remove-hook 'kill-buffer-hook 'flymake-kill-buffer-hook t)
733 ;;+(remove-hook 'find-file-hook (function flymake-find-file-hook) t)
735 (flymake-delete-own-overlays)
737 (when flymake-timer
738 (cancel-timer flymake-timer)
739 (setq flymake-timer nil)))))
741 (defun flymake--schedule-timer-maybe ()
742 "(Re)schedule an idle timer for checking the buffer.
743 Do it only if `flymake-no-changes-timeout' is non-nil."
744 (when flymake-timer (cancel-timer flymake-timer))
745 (when flymake-no-changes-timeout
746 (setq
747 flymake-timer
748 (run-with-idle-timer
749 (seconds-to-time flymake-no-changes-timeout)
751 (lambda (buffer)
752 (when (buffer-live-p buffer)
753 (with-current-buffer buffer
754 (when (and flymake-mode
755 flymake-no-changes-timeout)
756 (flymake-log
757 :debug "starting syntax check after idle for %s seconds"
758 flymake-no-changes-timeout)
759 (flymake-start))
760 (setq flymake-timer nil))))
761 (current-buffer)))))
763 ;;;###autoload
764 (defun flymake-mode-on ()
765 "Turn Flymake mode on."
766 (flymake-mode 1))
768 ;;;###autoload
769 (defun flymake-mode-off ()
770 "Turn Flymake mode off."
771 (flymake-mode 0))
773 (make-obsolete 'flymake-mode-on 'flymake-mode "26.1")
774 (make-obsolete 'flymake-mode-off 'flymake-mode "26.1")
776 (defun flymake-after-change-function (start stop _len)
777 "Start syntax check for current buffer if it isn't already running."
778 (let((new-text (buffer-substring start stop)))
779 (when (and flymake-start-syntax-check-on-newline (equal new-text "\n"))
780 (flymake-log :debug "starting syntax check as new-line has been seen")
781 (flymake-start 'deferred))
782 (flymake--schedule-timer-maybe)))
784 (defun flymake-after-save-hook ()
785 (when flymake-mode
786 (flymake-log :debug "starting syntax check as buffer was saved")
787 (flymake-start)))
789 (defun flymake-kill-buffer-hook ()
790 (when flymake-timer
791 (cancel-timer flymake-timer)
792 (setq flymake-timer nil)))
794 (defun flymake-find-file-hook ()
795 (unless (or flymake-mode
796 (null flymake-diagnostic-functions))
797 (flymake-mode)
798 (flymake-log :warning "Turned on in `flymake-find-file-hook'")))
800 (defun flymake-goto-next-error (&optional n filter interactive)
801 "Go to Nth next Flymake error in buffer matching FILTER.
803 Interactively, always move to the next error. Interactively, and
804 with a prefix arg, skip any diagnostics with a severity less than
805 ‘:warning’.
807 If ‘flymake-wrap-around’ is non-nil, resumes search from top
808 at end of buffer.
810 FILTER is a list of diagnostic types found in
811 `flymake-diagnostic-types-alist', or nil, if no filter is to be
812 applied."
813 ;; TODO: let filter be a number, a severity below which diags are
814 ;; skipped.
815 (interactive (list 1
816 (if current-prefix-arg
817 '(:error :warning))
819 (let* ((n (or n 1))
820 (ovs (flymake--overlays :filter
821 (lambda (ov)
822 (let ((diag (overlay-get
824 'flymake--diagnostic)))
825 (and diag
826 (or (not filter)
827 (memq (flymake--diag-type diag)
828 filter)))))
829 :compare (if (cl-plusp n) #'< #'>)
830 :key #'overlay-start))
831 (tail (cl-member-if (lambda (ov)
832 (if (cl-plusp n)
833 (> (overlay-start ov)
834 (point))
835 (< (overlay-start ov)
836 (point))))
837 ovs))
838 (chain (if flymake-wrap-around
839 (if tail
840 (progn (setcdr (last tail) ovs) tail)
841 (and ovs (setcdr (last ovs) ovs)))
842 tail))
843 (target (nth (1- n) chain)))
844 (cond (target
845 (goto-char (overlay-start target))
846 (when interactive
847 (message
848 (funcall (overlay-get target 'help-echo)
849 nil nil (point)))))
850 (interactive
851 (user-error "No more Flymake errors%s"
852 (if filter
853 (format " of types %s" filter)
854 ""))))))
856 (defun flymake-goto-prev-error (&optional n filter interactive)
857 "Go to Nth previous Flymake error in buffer matching FILTER.
859 Interactively, always move to the previous error. Interactively,
860 and with a prefix arg, skip any diagnostics with a severity less
861 than ‘:warning’.
863 If ‘flymake-wrap-around’ is non-nil, resumes search from top
864 at end of buffer.
866 FILTER is a list of diagnostic types found in
867 `flymake-diagnostic-types-alist', or nil, if no filter is to be
868 applied."
869 (interactive (list 1 (if current-prefix-arg
870 '(:error :warning))
872 (flymake-goto-next-error (- (or n 1)) filter interactive))
875 ;;; Mode-line fanciness
877 (defvar flymake--mode-line-format `(:eval (flymake--mode-line-format)))
879 (put 'flymake--mode-line-format 'risky-local-variable t)
881 (defun flymake--mode-line-format ()
882 "Produce a pretty minor mode indicator."
883 (let* ((known (hash-table-keys flymake--backend-state))
884 (running (flymake-running-backends))
885 (disabled (flymake-disabled-backends))
886 (reported (flymake-reporting-backends))
887 (diags-by-type (make-hash-table))
888 (all-disabled (and disabled (null running)))
889 (some-waiting (cl-set-difference running reported)))
890 (maphash (lambda (_b state)
891 (mapc (lambda (diag)
892 (push diag
893 (gethash (flymake--diag-type diag)
894 diags-by-type)))
895 (flymake--backend-state-diags state)))
896 flymake--backend-state)
897 `((:propertize " Flymake"
898 mouse-face mode-line-highlight
899 help-echo
900 ,(concat (format "%s known backends\n" (length known))
901 (format "%s running\n" (length running))
902 (format "%s disabled\n" (length disabled))
903 "mouse-1: go to log buffer ")
904 keymap
905 ,(let ((map (make-sparse-keymap)))
906 (define-key map [mode-line mouse-1]
907 (lambda (_event)
908 (interactive "e")
909 (switch-to-buffer "*Flymake log*")))
910 map))
911 ,@(pcase-let ((`(,ind ,face ,explain)
912 (cond ((null known)
913 `("?" mode-line "No known backends"))
914 (some-waiting
915 `("Wait" compilation-mode-line-run
916 ,(format "Waiting for %s running backends"
917 (length running))))
918 (all-disabled
919 `("!" compilation-mode-line-run
920 "All backends disabled"))
922 `(nil nil nil)))))
923 (when ind
924 `((":"
925 (:propertize ,ind
926 face ,face
927 help-echo ,explain)))))
928 ,@(unless (or all-disabled
929 (null known))
930 (cl-loop
931 for (type . severity)
932 in (cl-sort (mapcar (lambda (type)
933 (cons type (flymake--lookup-type-property
934 type
935 'severity
936 (warning-numeric-level :error))))
937 (cl-union (hash-table-keys diags-by-type)
938 '(:error :warning)))
940 :key #'cdr)
941 for diags = (gethash type diags-by-type)
942 for face = (flymake--lookup-type-property type
943 'mode-line-face
944 'compilation-error)
945 when (or diags
946 (>= severity (warning-numeric-level :warning)))
947 collect `(:propertize
948 ,(format "%d" (length diags))
949 face ,face
950 mouse-face mode-line-highlight
951 keymap
952 ,(let ((map (make-sparse-keymap))
953 (type type))
954 (define-key map [mode-line mouse-4]
955 (lambda (_event)
956 (interactive "e")
957 (flymake-goto-prev-error 1 (list type) t)))
958 (define-key map [mode-line mouse-5]
959 (lambda (_event)
960 (interactive "e")
961 (flymake-goto-next-error 1 (list type) t)))
962 map)
963 help-echo
964 ,(concat (format "%s diagnostics of type %s\n"
965 (propertize (format "%d"
966 (length diags))
967 'face face)
968 (propertize (format "%s" type)
969 'face face))
970 "mouse-4/mouse-5: previous/next of this type\n"))
971 into forms
972 finally return
973 `((:propertize "[")
974 ,@(cl-loop for (a . rest) on forms by #'cdr
975 collect a when rest collect
976 '(:propertize " "))
977 (:propertize "]")))))))
979 (provide 'flymake)
981 (require 'flymake-proc)
983 ;;; flymake.el ends here