flymake.el 46.8 KB
Newer Older
1
;;; flymake.el --- A universal on-the-fly syntax checker  -*- lexical-binding: t; -*-
Eli Zaretskii's avatar
Eli Zaretskii committed
2

Paul Eggert's avatar
Paul Eggert committed
3
;; Copyright (C) 2003-2017 Free Software Foundation, Inc.
Eli Zaretskii's avatar
Eli Zaretskii committed
4

5
;; Author:  Pavel Kobyakov <pk_at_work@yahoo.com>
Leo Liu's avatar
Leo Liu committed
6
;; Maintainer: Leo Liu <sdl.web@gmail.com>
Eli Zaretskii's avatar
Eli Zaretskii committed
7 8 9 10 11
;; Version: 0.3
;; Keywords: c languages tools

;; This file is part of GNU Emacs.

12
;; GNU Emacs is free software: you can redistribute it and/or modify
Eli Zaretskii's avatar
Eli Zaretskii committed
13
;; it under the terms of the GNU General Public License as published by
14 15
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
Eli Zaretskii's avatar
Eli Zaretskii committed
16 17 18 19 20 21 22

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
23
;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
Eli Zaretskii's avatar
Eli Zaretskii committed
24 25 26

;;; Commentary:
;;
27 28
;; Flymake is a minor Emacs mode performing on-the-fly syntax checks.
;;
29 30 31 32
;; Flymake collects diagnostic information for multiple sources,
;; called backends, and visually annotates the relevant portions in
;; the buffer.
;;
33
;; This file contains the UI for displaying and interacting with the
34 35 36 37 38
;; results produced by these backends, as well as entry points for
;; backends to hook on to.
;;
;; The main entry points are `flymake-mode' and `flymake-start'
;;
Paul Eggert's avatar
Paul Eggert committed
39
;; The docstrings of these variables are relevant to understanding how
40 41 42 43
;; Flymake works for both the user and the backend programmer:
;;
;; * `flymake-diagnostic-functions'
;; * `flymake-diagnostic-types-alist'
44
;;
Eli Zaretskii's avatar
Eli Zaretskii committed
45 46
;;; 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
52 53 54 55 56 57 58

(defgroup flymake nil
  "Universal on-the-fly syntax checker."
  :version "23.1"
  :link '(custom-manual "(flymake) Top")
  :group 'tools)

59 60
(defcustom flymake-error-bitmap '(flymake-double-exclamation-mark
                                  compilation-error)
61 62 63 64 65 66 67 68 69 70 71 72 73
  "Bitmap (a symbol) used in the fringe for indicating errors.
The value may also be a list of two elements where the second
element specifies the face for the bitmap.  For possible bitmap
symbols, see `fringe-bitmaps'.  See also `flymake-warning-bitmap'.

The option `flymake-fringe-indicator-position' controls how and where
this is used."
  :version "24.3"
  :type '(choice (symbol :tag "Bitmap")
                 (list :tag "Bitmap and face"
                       (symbol :tag "Bitmap")
                       (face :tag "Face"))))

74
(defcustom flymake-warning-bitmap '(exclamation-mark compilation-warning)
75 76 77 78 79 80 81 82 83 84 85 86 87
  "Bitmap (a symbol) used in the fringe for indicating warnings.
The value may also be a list of two elements where the second
element specifies the face for the bitmap.  For possible bitmap
symbols, see `fringe-bitmaps'.  See also `flymake-error-bitmap'.

The option `flymake-fringe-indicator-position' controls how and where
this is used."
  :version "24.3"
  :type '(choice (symbol :tag "Bitmap")
                 (list :tag "Bitmap and face"
                       (symbol :tag "Bitmap")
                       (face :tag "Face"))))

88 89 90 91 92 93 94 95 96 97 98 99 100 101
(defcustom flymake-note-bitmap '(exclamation-mark compilation-info)
  "Bitmap (a symbol) used in the fringe for indicating info notes.
The value may also be a list of two elements where the second
element specifies the face for the bitmap.  For possible bitmap
symbols, see `fringe-bitmaps'.  See also `flymake-error-bitmap'.

The option `flymake-fringe-indicator-position' controls how and where
this is used."
  :version "26.1"
  :type '(choice (symbol :tag "Bitmap")
                 (list :tag "Bitmap and face"
                       (symbol :tag "Bitmap")
                       (face :tag "Face"))))

102
(defcustom flymake-fringe-indicator-position 'left-fringe
103
  "The position to put Flymake fringe indicator.
104 105 106 107 108 109 110 111 112 113 114 115
The value can be nil (do not use indicators), `left-fringe' or `right-fringe'.
See `flymake-error-bitmap' and `flymake-warning-bitmap'."
  :version "24.3"
  :type '(choice (const left-fringe)
		 (const right-fringe)
		 (const :tag "No fringe indicators" nil)))

(defcustom flymake-start-syntax-check-on-newline t
  "Start syntax check if newline char was added/removed from the buffer."
  :type 'boolean)

(defcustom flymake-no-changes-timeout 0.5
116 117
  "Time to wait after last change before automatically checking buffer.
If nil, never start checking buffer automatically like this."
118 119 120 121 122 123 124 125
  :type 'number)

(defcustom flymake-gui-warnings-enabled t
  "Enables/disables GUI warnings."
  :type 'boolean)
(make-obsolete-variable 'flymake-gui-warnings-enabled
			"it no longer has any effect." "26.1")

126
(defcustom flymake-start-on-flymake-mode t
127
  "Start syntax check when `flymake-mode'is enabled.
128
Specifically, start it when the buffer is actually displayed."
129 130
  :type 'boolean)

131 132 133
(define-obsolete-variable-alias 'flymake-start-syntax-check-on-find-file
  'flymake-start-on-flymake-mode "26.1")

134
(defcustom flymake-log-level -1
135
  "Obsolete and ignored variable."
136
  :type 'integer)
137 138 139
(make-obsolete-variable 'flymake-log-level
			"it is superseded by `warning-minimum-log-level.'"
                        "26.1")
140

141 142
(defcustom flymake-wrap-around t
  "If non-nil, moving to errors wraps around buffer boundaries."
143
  :type 'boolean)
144

João Távora's avatar
João Távora committed
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
(when (fboundp 'define-fringe-bitmap)
  (define-fringe-bitmap 'flymake-double-exclamation-mark
    (vector #b00000000
            #b00000000
            #b00000000
            #b00000000
            #b01100110
            #b01100110
            #b01100110
            #b01100110
            #b01100110
            #b01100110
            #b01100110
            #b01100110
            #b00000000
            #b01100110
            #b00000000
            #b00000000
            #b00000000)))
164

165 166 167 168 169 170
(defvar-local flymake-timer nil
  "Timer for starting syntax check.")

(defvar-local flymake-check-start-time nil
  "Time at which syntax check was started.")

171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
(defun flymake--log-1 (level sublog msg &rest args)
  "Do actual work for `flymake-log'."
  (let (;; never popup the log buffer
        (warning-minimum-level :emergency)
        (warning-type-format
         (format " [%s %s]"
                 (or sublog 'flymake)
                 (current-buffer))))
    (display-warning (list 'flymake sublog)
                     (apply #'format-message msg args)
                     (if (numberp level)
                         (or (nth level
                                  '(:emergency :error :warning :debug :debug) )
                             :error)
                       level)
                     "*Flymake log*")))

188 189 190 191 192
(defun flymake-switch-to-log-buffer ()
  "Go to the *Flymake log* buffer."
  (interactive)
  (switch-to-buffer "*Flymake log*"))

193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
;;;###autoload
(defmacro flymake-log (level msg &rest args)
  "Log, at level LEVEL, the message MSG formatted with ARGS.
LEVEL is passed to `display-warning', which is used to display
the warning.  If this form is included in a byte-compiled file,
the generated warning contains an indication of the file that
generated it."
  (let* ((compile-file (and (boundp 'byte-compile-current-file)
                            (symbol-value 'byte-compile-current-file)))
         (sublog (if (and
                      compile-file
                      (not load-file-name))
                     (intern
                      (file-name-nondirectory
                       (file-name-sans-extension compile-file))))))
    `(flymake--log-1 ,level ',sublog ,msg ,@args)))

(defun flymake-error (text &rest args)
211
  "Format TEXT with ARGS and signal an error for Flymake."
212 213 214
  (let ((msg (apply #'format-message text args)))
    (flymake-log :error msg)
    (error (concat "[Flymake] " msg))))
215

216
(cl-defstruct (flymake--diag
217
               (:constructor flymake--diag-make))
218
  buffer beg end type text backend)
219

220
;;;###autoload
221 222 223 224 225
(defun flymake-make-diagnostic (buffer
                                beg
                                end
                                type
                                text)
226
  "Make a Flymake diagnostic for BUFFER's region from BEG to END.
227 228 229 230
TYPE is a key to `flymake-diagnostic-types-alist' and TEXT is a
description of the problem detected in this region."
  (flymake--diag-make :buffer buffer :beg beg :end end :type type :text text))

231 232 233 234 235
(cl-defun flymake--overlays (&key beg end filter compare key)
  "Get flymake-related overlays.
If BEG is non-nil and END is nil, consider only `overlays-at'
BEG. Otherwise consider `overlays-in' the region comprised by BEG
and END, defaulting to the whole buffer.  Remove all that do not
236 237 238 239 240 241 242 243 244
verify FILTER, a function, and sort them by COMPARE (using KEY)."
  (save-restriction
    (widen)
    (let ((ovs (cl-remove-if-not
                (lambda (ov)
                  (and (overlay-get ov 'flymake)
                       (or (not filter)
                           (funcall filter ov))))
                (if (and beg (null end))
245 246
                    (overlays-at beg t)
                  (overlays-in (or beg (point-min))
247 248 249 250 251
                               (or end (point-max)))))))
      (if compare
          (cl-sort ovs compare :key (or key
                                        #'identity))
        ovs))))
252

253
(defun flymake-delete-own-overlays (&optional filter)
254
  "Delete all Flymake overlays in BUFFER."
255
  (mapc #'delete-overlay (flymake--overlays :filter filter)))
256 257

(defface flymake-error
258 259 260 261
  '((((supports :underline (:style wave)))
     :underline (:style wave :color "Red1"))
    (t
     :inherit error))
262
  "Face used for marking error regions."
263
  :version "24.4")
264

265
(defface flymake-warning
266
  '((((supports :underline (:style wave)))
267
     :underline (:style wave :color "deep sky blue"))
268 269
    (t
     :inherit warning))
270
  "Face used for marking warning regions."
271
  :version "24.4")
272

273 274 275 276 277 278
(defface flymake-note
  '((((supports :underline (:style wave)))
     :underline (:style wave :color "yellow green"))
    (t
     :inherit warning))
  "Face used for marking note regions."
279
  :version "26.1")
280

281 282 283
(define-obsolete-face-alias 'flymake-warnline 'flymake-warning "26.1")
(define-obsolete-face-alias 'flymake-errline 'flymake-error "26.1")

284 285 286 287 288
;;;###autoload
(defun flymake-diag-region (buffer line &optional col)
  "Compute BUFFER's region (BEG . END) corresponding to LINE and COL.
If COL is nil, return a region just for LINE.  Return nil if the
region is invalid."
289
  (condition-case-unless-debug _err
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
      (with-current-buffer buffer
        (let ((line (min (max line 1)
                         (line-number-at-pos (point-max) 'absolute))))
          (save-excursion
            (goto-char (point-min))
            (forward-line (1- line))
            (cl-flet ((fallback-bol
                       () (progn (back-to-indentation) (point)))
                      (fallback-eol
                       (beg)
                       (progn
                         (end-of-line)
                         (skip-chars-backward " \t\f\t\n" beg)
                         (if (eq (point) beg)
                             (line-beginning-position 2)
                           (point)))))
              (if (and col (cl-plusp col))
                  (let* ((beg (progn (forward-char (1- col))
                                     (point)))
                         (sexp-end (ignore-errors (end-of-thing 'sexp)))
                         (end (or (and sexp-end
                                       (not (= sexp-end beg))
                                       sexp-end)
                                  (ignore-errors (goto-char (1+ beg)))))
                         (safe-end (or end
                                       (fallback-eol beg))))
                    (cons (if end beg (fallback-bol))
                          safe-end))
                (let* ((beg (fallback-bol))
                       (end (fallback-eol beg)))
                  (cons beg end)))))))
321
    (error (flymake-error "Invalid region line=%s col=%s" line col))))
322

323
(defvar flymake-diagnostic-functions nil
324
  "Special hook of Flymake backends that check a buffer.
325

326
The functions in this hook diagnose problems in a buffer's
327
contents and provide information to the Flymake user interface
328
about where and how to annotate problems diagnosed in a buffer.
329

330 331
Each backend function must be prepared to accept an arbitrary
number of arguments:
332 333 334 335 336 337 338

* the first argument is always REPORT-FN, a callback function
  detailed below;

* the remaining arguments are keyword-value pairs in the
  form (:KEY VALUE :KEY2 VALUE2...).  Currently, Flymake provides
  no such arguments, but backend functions must be prepared to
339
  accept and possibly ignore any number of them.
340

341 342 343 344 345 346
Whenever Flymake or the user decides to re-check the buffer,
backend functions are called as detailed above and are expected
to initiate this check, but aren't required to complete it before
exiting: if the computation involved is expensive, especially for
large buffers, that task can be scheduled for the future using
asynchronous processes or other asynchronous mechanisms.
347 348 349 350 351 352 353 354 355 356

In any case, backend functions are expected to return quickly or
signal an error, in which case the backend is disabled.  Flymake
will not try disabled backends again for any future checks of
this buffer.  Certain commands, like turning `flymake-mode' off
and on again, reset the list of disabled backends.

If the function returns, Flymake considers the backend to be
\"running\". If it has not done so already, the backend is
expected to call the function REPORT-FN with a single argument
357 358
REPORT-ACTION also followed by an optional list of keyword-value
pairs in the form (:REPORT-KEY VALUE :REPORT-KEY2 VALUE2...).
359

360
Currently accepted values for REPORT-ACTION are:
361

362
* A (possibly empty) list of diagnostic objects created with
363
  `flymake-make-diagnostic', causing Flymake to annotate the
364
  buffer with this information.
365

366 367 368 369 370 371 372 373
  A backend may call REPORT-FN repeatedly in this manner, but
  only until Flymake considers that the most recently requested
  buffer check is now obsolete because, say, buffer contents have
  changed in the meantime. The backend is only given notice of
  this via a renewed call to the backend function. Thus, to
  prevent making obsolete reports and wasting resources, backend
  functions should first cancel any ongoing processing from
  previous calls.
374

Paul Eggert's avatar
Paul Eggert committed
375 376
* The symbol `:panic', signaling that the backend has encountered
  an exceptional situation and should be disabled.
377

378
Currently accepted REPORT-KEY arguments are:
379

380
* `:explanation' value should give user-readable details of
381 382
  the situation encountered, if any.

383
* `:force': value should be a boolean suggesting that Flymake
384
  consider the report even if it was somehow unexpected.")
385

386
(defvar flymake-diagnostic-types-alist
387
  `((:error
388
     . ((flymake-category . flymake-error)))
389 390 391 392
    (:warning
     . ((flymake-category . flymake-warning)))
    (:note
     . ((flymake-category . flymake-note))))
393 394 395
  "Alist ((KEY . PROPS)*) of properties of Flymake diagnostic types.
KEY designates a kind of diagnostic can be anything passed as
`:type' to `flymake-make-diagnostic'.
396 397

PROPS is an alist of properties that are applied, in order, to
398 399
the diagnostics of the type designated by KEY.  The recognized
properties are:
400 401 402

* Every property pertaining to overlays, except `category' and
  `evaporate' (see Info Node `(elisp)Overlay Properties'), used
403
  to affect the appearance of Flymake annotations.
404 405 406 407

* `bitmap', an image displayed in the fringe according to
  `flymake-fringe-indicator-position'.  The value actually
  follows the syntax of `flymake-error-bitmap' (which see).  It
Paul Eggert's avatar
Paul Eggert committed
408
  is overridden by any `before-string' overlay property.
409 410 411

* `severity', a non-negative integer specifying the diagnostic's
  severity.  The higher, the more serious.  If the overlay
412
  property `priority' is not specified, `severity' is used to set
413 414 415
  it and help sort overlapping overlays.

* `flymake-category', a symbol whose property list is considered
416 417
  a default for missing values of any other properties.  This is
  useful to backend authors when creating new diagnostic types
418 419 420
  that differ from an existing type by only a few properties.")

(put 'flymake-error 'face 'flymake-error)
421
(put 'flymake-error 'bitmap 'flymake-error-bitmap)
422
(put 'flymake-error 'severity (warning-numeric-level :error))
423
(put 'flymake-error 'mode-line-face 'compilation-error)
424 425

(put 'flymake-warning 'face 'flymake-warning)
426
(put 'flymake-warning 'bitmap 'flymake-warning-bitmap)
427
(put 'flymake-warning 'severity (warning-numeric-level :warning))
428
(put 'flymake-warning 'mode-line-face 'compilation-warning)
429 430

(put 'flymake-note 'face 'flymake-note)
431
(put 'flymake-note 'bitmap 'flymake-note-bitmap)
432
(put 'flymake-note 'severity (warning-numeric-level :debug))
433
(put 'flymake-note 'mode-line-face 'compilation-info)
434 435 436 437

(defun flymake--lookup-type-property (type prop &optional default)
  "Look up PROP for TYPE in `flymake-diagnostic-types-alist'.
If TYPE doesn't declare PROP in either
438 439
`flymake-diagnostic-types-alist' or in the symbol of its
associated `flymake-category' return DEFAULT."
440
  (let ((alist-probe (assoc type flymake-diagnostic-types-alist)))
441 442 443 444 445 446 447 448 449 450 451 452 453 454
    (cond (alist-probe
           (let* ((alist (cdr alist-probe))
                  (prop-probe (assoc prop alist)))
             (if prop-probe
                 (cdr prop-probe)
               (if-let* ((cat (assoc-default 'flymake-category alist))
                         (plist (and (symbolp cat)
                                     (symbol-plist cat)))
                         (cat-probe (plist-member plist prop)))
                   (cadr cat-probe)
                 default))))
          (t
           default))))

455 456 457 458 459 460 461 462 463 464 465 466 467
(defun flymake--fringe-overlay-spec (bitmap &optional recursed)
  (if (and (symbolp bitmap)
           (boundp bitmap)
           (not recursed))
      (flymake--fringe-overlay-spec
       (symbol-value bitmap) t)
    (and flymake-fringe-indicator-position
         bitmap
         (propertize "!" 'display
                     (cons flymake-fringe-indicator-position
                           (if (listp bitmap)
                               bitmap
                             (list bitmap)))))))
468

469
(defun flymake--highlight-line (diagnostic)
470
  "Highlight buffer with info in DIAGNOSTIC."
471 472 473
  (when-let* ((ov (make-overlay
                   (flymake--diag-beg diagnostic)
                   (flymake--diag-end diagnostic))))
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
    ;; First set `category' in the overlay, then copy over every other
    ;; property.
    ;;
    (let ((alist (assoc-default (flymake--diag-type diagnostic)
                                flymake-diagnostic-types-alist)))
      (overlay-put ov 'category (assoc-default 'flymake-category alist))
      (cl-loop for (k . v) in alist
               unless (eq k 'category)
               do (overlay-put ov k v)))
    ;; Now ensure some essential defaults are set
    ;;
    (cl-flet ((default-maybe
                (prop value)
                (unless (or (plist-member (overlay-properties ov) prop)
                            (let ((cat (overlay-get ov
                                                    'flymake-category)))
                              (and cat
                                   (plist-member (symbol-plist cat) prop))))
                  (overlay-put ov prop value))))
493 494
      (default-maybe 'bitmap 'flymake-error-bitmap)
      (default-maybe 'face 'flymake-error)
495 496 497 498 499 500 501
      (default-maybe 'before-string
        (flymake--fringe-overlay-spec
         (overlay-get ov 'bitmap)))
      (default-maybe 'help-echo
        (lambda (_window _ov pos)
          (mapconcat
           (lambda (ov)
502
             (overlay-get ov 'flymake-text))
503 504 505 506
           (flymake--overlays :beg pos)
           "\n")))
      (default-maybe 'severity (warning-numeric-level :error))
      (default-maybe 'priority (+ 100 (overlay-get ov 'severity))))
Paul Eggert's avatar
Paul Eggert committed
507
    ;; Some properties can't be overridden.
508 509
    ;;
    (overlay-put ov 'evaporate t)
510
    (overlay-put ov 'flymake t)
511
    (overlay-put ov 'flymake-text (flymake--diag-text diagnostic))
512 513
    (overlay-put ov 'flymake--diagnostic diagnostic)))

514
;; Nothing in Flymake uses this at all any more, so this is just for
515 516 517
;; third-party compatibility.
(define-obsolete-function-alias 'flymake-display-warning 'message-box "26.1")

518 519 520 521
(defvar-local flymake--backend-state nil
  "Buffer-local hash table of a Flymake backend's state.
The keys to this hash table are functions as found in
`flymake-diagnostic-functions'. The values are structures
522
of the type `flymake--backend-state', with these slots:
523 524 525

`running', a symbol to keep track of a backend's replies via its
REPORT-FN argument. A backend is running if this key is
526 527
present. If nil, Flymake isn't expecting any replies from the
backend.
528

529 530
`diags', a (possibly empty) list of recent diagnostic objects
created by the backend with `flymake-make-diagnostic'.
531 532 533 534 535

`reported-p', a boolean indicating if the backend has replied
since it last was contacted.

`disabled', a string with the explanation for a previous
536 537
exceptional situation reported by the backend, nil if the
backend is operating normally.")
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552

(cl-defstruct (flymake--backend-state
               (:constructor flymake--make-backend-state))
  running reported-p disabled diags)

(defmacro flymake--with-backend-state (backend state-var &rest body)
  "Bind BACKEND's STATE-VAR to its state, run BODY."
  (declare (indent 2) (debug (sexp sexp &rest form)))
  (let ((b (make-symbol "b")))
    `(let* ((,b ,backend)
            (,state-var
             (or (gethash ,b flymake--backend-state)
                 (puthash ,b (flymake--make-backend-state)
                          flymake--backend-state))))
       ,@body)))
553

554
(defun flymake-is-running ()
555
  "Tell if Flymake has running backends in this buffer"
556 557
  (flymake-running-backends))

558 559 560
(cl-defun flymake--handle-report (backend token report-action
                                          &key explanation force
                                          &allow-other-keys)
561
  "Handle reports from BACKEND identified by TOKEN.
562
BACKEND, REPORT-ACTION and EXPLANATION, and FORCE conform to the calling
563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
convention described in `flymake-diagnostic-functions' (which
see). Optional FORCE says to handle a report even if TOKEN was
not expected."
  (let* ((state (gethash backend flymake--backend-state))
         (first-report (not (flymake--backend-state-reported-p state))))
    (setf (flymake--backend-state-reported-p state) t)
    (let (expected-token
          new-diags)
      (cond
       ((null state)
        (flymake-error
         "Unexpected report from unknown backend %s" backend))
       ((flymake--backend-state-disabled state)
        (flymake-error
         "Unexpected report from disabled backend %s" backend))
       ((progn
          (setq expected-token (flymake--backend-state-running state))
          (null expected-token))
        ;; should never happen
        (flymake-error "Unexpected report from stopped backend %s" backend))
       ((and (not (eq expected-token token))
             (not force))
        (flymake-error "Obsolete report from backend %s with explanation %s"
                       backend explanation))
587
       ((eq :panic report-action)
588
        (flymake--disable-backend backend explanation))
589
       ((not (listp report-action))
590
        (flymake--disable-backend backend
591 592
                                  (format "Unknown action %S" report-action))
        (flymake-error "Expected report, but got unknown key %s" report-action))
593
       (t
594
        (setq new-diags report-action)
595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613
        (save-restriction
          (widen)
          ;; only delete overlays if this is the first report
          (when first-report
            (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))
                new-diags)
          (setf (flymake--backend-state-diags state)
                (append new-diags (flymake--backend-state-diags state)))
          (when flymake-check-start-time
            (flymake-log :debug "backend %s reported %d diagnostics in %.2f second(s)"
                         backend
                         (length new-diags)
614 615 616 617 618 619
                         (- (float-time) flymake-check-start-time)))
          (when (and (get-buffer (flymake--diagnostics-buffer-name))
                     (get-buffer-window (flymake--diagnostics-buffer-name))
                     (null (cl-set-difference (flymake-running-backends)
                                              (flymake-reporting-backends))))
            (flymake-show-diagnostics-buffer))))))))
620 621

(defun flymake-make-report-fn (backend &optional token)
622
  "Make a suitable anonymous report function for BACKEND.
623 624
BACKEND is used to help Flymake distinguish different diagnostic
sources.  If provided, TOKEN helps Flymake distinguish between
625 626 627 628 629 630 631
different runs of the same backend."
  (let ((buffer (current-buffer)))
    (lambda (&rest args)
      (when (buffer-live-p buffer)
        (with-current-buffer buffer
          (apply #'flymake--handle-report backend token args))))))

632 633 634 635 636
(defun flymake--collect (fn &optional message-prefix)
  "Collect Flymake backends matching FN.
If MESSAGE-PREFIX, echo a message using that prefix"
  (unless flymake--backend-state
    (user-error "Flymake is not initialized"))
637 638 639 640
  (let (retval)
    (maphash (lambda (backend state)
               (when (funcall fn state) (push backend retval)))
             flymake--backend-state)
641 642 643 644 645
    (when message-prefix
      (message "%s%s"
               message-prefix
               (mapconcat (lambda (s) (format "%s" s))
                          retval ", ")))
646 647 648 649
    retval))

(defun flymake-running-backends ()
  "Compute running Flymake backends in current buffer."
650 651 652 653
  (interactive)
  (flymake--collect #'flymake--backend-state-running
                    (and (called-interactively-p 'interactive)
                         "Running backends: ")))
654 655 656

(defun flymake-disabled-backends ()
  "Compute disabled Flymake backends in current buffer."
657 658 659 660
  (interactive)
  (flymake--collect #'flymake--backend-state-disabled
                    (and (called-interactively-p 'interactive)
                         "Disabled backends: ")))
661 662 663

(defun flymake-reporting-backends ()
  "Compute reporting Flymake backends in current buffer."
664 665 666 667
  (interactive)
  (flymake--collect #'flymake--backend-state-reported-p
                    (and (called-interactively-p 'interactive)
                         "Reporting backends: ")))
668 669 670

(defun flymake--disable-backend (backend &optional explanation)
  "Disable BACKEND because EXPLANATION.
671
If it is running also stop it."
672 673 674 675 676
  (flymake-log :warning "Disabling backend %s because %s" backend explanation)
  (flymake--with-backend-state backend state
    (setf (flymake--backend-state-running state) nil
          (flymake--backend-state-disabled state) explanation
          (flymake--backend-state-reported-p state) t)))
677 678

(defun flymake--run-backend (backend)
679 680 681 682 683 684 685 686
  "Run the backend BACKEND, reenabling if necessary."
  (flymake-log :debug "Running backend %s" backend)
  (let ((run-token (cl-gensym "backend-token")))
    (flymake--with-backend-state backend state
      (setf (flymake--backend-state-running state) run-token
            (flymake--backend-state-disabled state) nil
            (flymake--backend-state-diags state) nil
            (flymake--backend-state-reported-p state) nil))
687
    ;; FIXME: Should use `condition-case-unless-debug' here, but don't
688 689 690 691 692 693 694 695 696 697 698 699 700 701
    ;; for two reasons: (1) that won't let me catch errors from inside
    ;; `ert-deftest' where `debug-on-error' appears to be always
    ;; t. (2) In cases where the user is debugging elisp somewhere
    ;; else, and using flymake, the presence of a frequently
    ;; misbehaving backend in the global hook (most likely the legacy
    ;; backend) will trigger an annoying backtrace.
    ;;
    (condition-case err
        (funcall backend
                 (flymake-make-report-fn backend run-token))
      (error
       (flymake--disable-backend backend err)))))

(defun flymake-start (&optional deferred force)
702 703 704 705 706 707 708 709 710 711
  "Start a syntax check for the current buffer.
DEFERRED is a list of symbols designating conditions to wait for
before actually starting the check.  If it is nil (the list is
empty), start it immediately, else defer the check to when those
conditions are met.  Currently recognized conditions are
`post-command', for waiting until the current command is over,
`on-display', for waiting until the buffer is actually displayed
in a window.  If DEFERRED is t, wait for all known conditions.

With optional FORCE run even disabled backends.
712 713 714

Interactively, with a prefix arg, FORCE is t."
  (interactive (list nil current-prefix-arg))
715 716 717 718 719 720 721 722 723 724
  (let ((deferred (if (eq t deferred)
                      '(post-command on-display)
                    deferred))
        (buffer (current-buffer)))
    (cl-labels
        ((start-post-command
          ()
          (remove-hook 'post-command-hook #'start-post-command
                       nil)
          (with-current-buffer buffer
725
            (flymake-start (remove 'post-command deferred) force)))
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740
         (start-on-display
          ()
          (remove-hook 'window-configuration-change-hook #'start-on-display
                       'local)
          (flymake-start (remove 'on-display deferred) force)))
      (cond ((and (memq 'post-command deferred)
                  this-command)
             (add-hook 'post-command-hook
                       #'start-post-command
                       'append nil))
            ((and (memq 'on-display deferred)
                  (not (get-buffer-window (current-buffer))))
             (add-hook 'window-configuration-change-hook
                       #'start-on-display
                       'append 'local))
741
            (t
742 743 744 745 746 747 748 749 750 751 752 753 754
             (setq flymake-check-start-time (float-time))
             (run-hook-wrapped
              'flymake-diagnostic-functions
              (lambda (backend)
                (cond
                 ((and (not force)
                       (flymake--with-backend-state backend state
                         (flymake--backend-state-disabled state)))
                  (flymake-log :debug "Backend %s is disabled, not starting"
                               backend))
                 (t
                  (flymake--run-backend backend)))
                nil)))))))
755

756
(defvar flymake-mode-map
757 758
  (let ((map (make-sparse-keymap))) map)
  "Keymap for `flymake-mode'")
759

760
;;;###autoload
761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796
(define-minor-mode flymake-mode
  "Toggle Flymake mode on or off.
With a prefix argument ARG, enable Flymake mode if ARG is
positive, and disable it otherwise.  If called from Lisp, enable
the mode if ARG is omitted or nil, and toggle it if ARG is `toggle'.

Flymake is an Emacs minor mode for on-the-fly syntax checking.
Flymake collects diagnostic information from multiple sources,
called backends, and visually annotates the buffer with the
results.

Flymake performs these checks while the user is editing.  The
customization variables `flymake-start-on-flymake-mode',
`flymake-no-changes-timeout' and
`flymake-start-syntax-check-on-newline' determine the exact
circumstances whereupon Flymake decides to initiate a check of
the buffer.

The commands `flymake-goto-next-error' and
`flymake-goto-prev-error' can be used to navigate among Flymake
diagnostics annotated in the buffer.

The visual appearance of each type of diagnostic can be changed
in the variable `flymake-diagnostic-types-alist'.

Activation or deactivation of backends used by Flymake in each
buffer happens via the special hook
`flymake-diagnostic-functions'.

Some backends may take longer than others to respond or complete,
and some may decide to disable themselves if they are not
suitable for the current buffer. The commands
`flymake-running-backends', `flymake-disabled-backends' and
`flymake-reporting-backends' summarize the situation, as does the
special *Flymake log* buffer."  :group 'flymake :lighter
  flymake--mode-line-format :keymap flymake-mode-map
797 798 799
  (cond
   ;; Turning the mode ON.
   (flymake-mode
800 801 802 803 804 805
    (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)

    (setq flymake--backend-state (make-hash-table))

806
    (when flymake-start-on-flymake-mode (flymake-start t)))
807 808 809 810 811 812 813 814 815 816 817 818

   ;; Turning the mode OFF.
   (t
    (remove-hook 'after-change-functions 'flymake-after-change-function t)
    (remove-hook 'after-save-hook 'flymake-after-save-hook t)
    (remove-hook 'kill-buffer-hook 'flymake-kill-buffer-hook t)
    ;;+(remove-hook 'find-file-hook (function flymake-find-file-hook) t)

    (flymake-delete-own-overlays)

    (when flymake-timer
      (cancel-timer flymake-timer)
819
      (setq flymake-timer nil)))))
820

821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838
(defun flymake--schedule-timer-maybe ()
  "(Re)schedule an idle timer for checking the buffer.
Do it only if `flymake-no-changes-timeout' is non-nil."
  (when flymake-timer (cancel-timer flymake-timer))
  (when flymake-no-changes-timeout
    (setq
     flymake-timer
     (run-with-idle-timer
      (seconds-to-time flymake-no-changes-timeout)
      nil
      (lambda (buffer)
        (when (buffer-live-p buffer)
          (with-current-buffer buffer
            (when (and flymake-mode
                       flymake-no-changes-timeout)
	      (flymake-log
               :debug "starting syntax check after idle for %s seconds"
               flymake-no-changes-timeout)
839
	      (flymake-start t))
840 841 842
            (setq flymake-timer nil))))
      (current-buffer)))))

843 844
;;;###autoload
(defun flymake-mode-on ()
845
  "Turn Flymake mode on."
846
  (flymake-mode 1))
847 848 849

;;;###autoload
(defun flymake-mode-off ()
850
  "Turn Flymake mode off."
851
  (flymake-mode 0))
852

853 854 855
(make-obsolete 'flymake-mode-on 'flymake-mode "26.1")
(make-obsolete 'flymake-mode-off 'flymake-mode "26.1")

856 857 858 859
(defun flymake-after-change-function (start stop _len)
  "Start syntax check for current buffer if it isn't already running."
  (let((new-text (buffer-substring start stop)))
    (when (and flymake-start-syntax-check-on-newline (equal new-text "\n"))
860
      (flymake-log :debug "starting syntax check as new-line has been seen")
861
      (flymake-start t))
862
    (flymake--schedule-timer-maybe)))
863 864

(defun flymake-after-save-hook ()
865 866
  (when flymake-mode
    (flymake-log :debug "starting syntax check as buffer was saved")
867
    (flymake-start t)))
868 869 870 871 872 873 874

(defun flymake-kill-buffer-hook ()
  (when flymake-timer
    (cancel-timer flymake-timer)
    (setq flymake-timer nil)))

(defun flymake-find-file-hook ()
875
  (unless (or flymake-mode
876
              (null flymake-diagnostic-functions))
877
    (flymake-mode)
878
    (flymake-log :warning "Turned on in `flymake-find-file-hook'")))
879

880
(defun flymake-goto-next-error (&optional n filter interactive)
881
  "Go to Nth next Flymake error in buffer matching FILTER.
882
Interactively, always move to the next error.  With a prefix arg,
883
skip any diagnostics with a severity less than `:warning'.
884

885
If `flymake-wrap-around' is non-nil and no more next errors,
886
resumes search from top.
887

888 889
FILTER is a list of diagnostic types found in
`flymake-diagnostic-types-alist', or nil, if no filter is to be
890 891 892
applied."
  ;; TODO: let filter be a number, a severity below which diags are
  ;; skipped.
893 894 895 896
  (interactive (list 1
                     (if current-prefix-arg
                         '(:error :warning))
                     t))
897
  (let* ((n (or n 1))
898 899 900 901 902 903 904 905 906
         (ovs (flymake--overlays :filter
                                 (lambda (ov)
                                   (let ((diag (overlay-get
                                                ov
                                                'flymake--diagnostic)))
                                     (and diag
                                          (or (not filter)
                                              (memq (flymake--diag-type diag)
                                                    filter)))))
907 908
                                 :compare (if (cl-plusp n) #'< #'>)
                                 :key #'overlay-start))
909 910 911 912 913 914 915 916 917 918 919 920
         (tail (cl-member-if (lambda (ov)
                               (if (cl-plusp n)
                                   (> (overlay-start ov)
                                      (point))
                                 (< (overlay-start ov)
                                    (point))))
                             ovs))
         (chain (if flymake-wrap-around
                    (if tail
                        (progn (setcdr (last tail) ovs) tail)
                      (and ovs (setcdr (last ovs) ovs)))
                  tail))
921 922 923 924 925
         (target (nth (1- n) chain)))
    (cond (target
           (goto-char (overlay-start target))
           (when interactive
             (message
926
              "%s"
927 928 929
              (funcall (overlay-get target 'help-echo)
                       nil nil (point)))))
          (interactive
930
           (user-error "No more Flymake errors%s"
931 932 933 934 935
                       (if filter
                           (format " of types %s" filter)
                         ""))))))

(defun flymake-goto-prev-error (&optional n filter interactive)
936
  "Go to Nth previous Flymake error in buffer matching FILTER.
937
Interactively, always move to the previous error.  With a prefix
938
arg, skip any diagnostics with a severity less than `:warning'.
939

940
If `flymake-wrap-around' is non-nil and no more previous errors,
941
resumes search from bottom.
942

943 944
FILTER is a list of diagnostic types found in
`flymake-diagnostic-types-alist', or nil, if no filter is to be
945
applied."
946 947 948 949
  (interactive (list 1 (if current-prefix-arg
                           '(:error :warning))
                     t))
  (flymake-goto-next-error (- (or n 1)) filter interactive))
950

951

952
;;; Mode-line and menu
953
;;;
954 955 956 957 958 959
(easy-menu-define flymake-menu flymake-mode-map "Flymake"
  `("Flymake"
    [ "Go to next error"      flymake-goto-next-error t ]
    [ "Go to previous error"  flymake-goto-prev-error t ]
    [ "Check now"             flymake-start t ]
    [ "Go to log buffer"      flymake-switch-to-log-buffer t ]
960
    [ "Show error buffer"     flymake-show-diagnostics-buffer t ]
961 962 963
    "--"
    [ "Turn off Flymake"      flymake-mode t ]))

964 965 966 967 968 969
(defvar flymake--mode-line-format `(:eval (flymake--mode-line-format)))

(put 'flymake--mode-line-format 'risky-local-variable t)

(defun flymake--mode-line-format ()
  "Produce a pretty minor mode indicator."
970 971 972 973 974 975 976 977 978 979 980 981 982 983
  (let* ((known (hash-table-keys flymake--backend-state))
         (running (flymake-running-backends))
         (disabled (flymake-disabled-backends))
         (reported (flymake-reporting-backends))
         (diags-by-type (make-hash-table))
         (all-disabled (and disabled (null running)))
         (some-waiting (cl-set-difference running reported)))
    (maphash (lambda (_b state)
               (mapc (lambda (diag)
                       (push diag
                             (gethash (flymake--diag-type diag)
                                      diags-by-type)))
                     (flymake--backend-state-diags state)))
             flymake--backend-state)
984 985 986
    `((:propertize " Flymake"
                   mouse-face mode-line-highlight
                   help-echo
987 988 989
                   ,(concat (format "%s known backends\n" (length known))
                            (format "%s running\n" (length running))
                            (format "%s disabled\n" (length disabled))
990 991
                            "mouse-1: Display minor mode menu\n"
                            "mouse-2: Show help for minor mode")
992 993
                   keymap
                   ,(let ((map (make-sparse-keymap)))
994 995
                      (define-key map [mode-line down-mouse-1]
                        flymake-menu)
996 997 998 999
                      (define-key map [mode-line mouse-2]
                        (lambda ()
                          (interactive)
                          (describe-function 'flymake-mode)))
1000
                      map))
1001 1002 1003 1004 1005
      ,@(pcase-let ((`(,ind ,face ,explain)
                     (cond ((null known)
                            `("?" mode-line "No known backends"))
                           (some-waiting
                            `("Wait" compilation-mode-line-run
1006 1007
                              ,(format "Waiting for %s running backend(s)"
                                       (length some-waiting))))
1008 1009 1010 1011 1012 1013 1014 1015 1016
                           (all-disabled
                            `("!" compilation-mode-line-run
                              "All backends disabled"))
                           (t
                            `(nil nil nil)))))
          (when ind
            `((":"
               (:propertize ,ind
                            face ,face
1017 1018 1019 1020 1021 1022
                            help-echo ,explain
                            keymap
                            ,(let ((map (make-sparse-keymap)))
                               (define-key map [mode-line mouse-1]
                                 'flymake-switch-to-log-buffer)
                               map))))))
1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049
      ,@(unless (or all-disabled
                    (null known))
          (cl-loop
           for (type . severity)
           in (cl-sort (mapcar (lambda (type)
                                 (cons type (flymake--lookup-type-property
                                             type
                                             'severity
                                             (warning-numeric-level :error))))
                               (cl-union (hash-table-keys diags-by-type)
                                         '(:error :warning)))
                       #'>
                       :key #'cdr)
           for diags = (gethash type diags-by-type)
           for face = (flymake--lookup-type-property type
                                                     'mode-line-face
                                                     'compilation-error)
           when (or diags
                    (>= severity (warning-numeric-level :warning)))
           collect `(:propertize
                     ,(format "%d" (length diags))
                     face ,face
                     mouse-face mode-line-highlight
                     keymap
                     ,(let ((map (make-sparse-keymap))
                            (type type))
                        (define-key map [mode-line mouse-4]
João Távora's avatar
João Távora committed
1050
                          (lambda (event)
1051
                            (interactive "e")
João Távora's avatar
João Távora committed
1052 1053
                            (with-selected-window (posn-window (event-start event))
                              (flymake-goto-prev-error 1 (list type) t))))
1054
                        (define-key map [mode-line mouse-5]
João Távora's avatar
João Távora committed
1055
                          (lambda (event)
1056
                            (interactive "e")
João Távora's avatar
João Távora committed
1057 1058
                            (with-selected-window (posn-window (event-start event))
                              (flymake-goto-next-error 1 (list type) t))))
1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074
                        map)
                     help-echo
                     ,(concat (format "%s diagnostics of type %s\n"
                                      (propertize (format "%d"
                                                          (length diags))
                                                  'face face)
                                      (propertize (format "%s" type)
                                                  'face face))
                              "mouse-4/mouse-5: previous/next of this type\n"))
           into forms
           finally return
           `((:propertize "[")
             ,@(cl-loop for (a . rest) on forms by #'cdr
                        collect a when rest collect
                        '(:propertize " "))
             (:propertize "]")))))))
1075 1076 1077 1078 1079

;;; Diagnostics buffer

(defvar-local flymake--diagnostics-buffer-source nil)

1080
(defvar flymake-diagnostics-buffer-mode-map
1081
  (let ((map (make-sparse-keymap)))
1082 1083
    (define-key map (kbd "RET") 'flymake-goto-diagnostic)
    (define-key map (kbd "SPC") 'flymake-show-diagnostic)
1084 1085
    map))

1086 1087 1088 1089
(defun flymake-show-diagnostic (pos &optional other-window)
  "Show location of diagnostic at POS."
  (interactive (list (point) t))
  (let* ((id (or (tabulated-list-get-id pos)
1090 1091
                 (user-error "Nothing at point")))
         (overlay (plist-get id :overlay)))
1092 1093
    (with-current-buffer (overlay-buffer overlay)
      (with-selected-window
1094
          (display-buffer (current-buffer) other-window)
1095 1096 1097 1098 1099 1100
        (goto-char (overlay-start overlay))
        (pulse-momentary-highlight-region (overlay-start overlay)
                                          (overlay-end overlay)
                                          'highlight))
      (current-buffer))))

1101 1102 1103 1104
(defun flymake-goto-diagnostic (pos)
  "Show location of diagnostic at POS.
POS can be a buffer position or a button"
  (interactive "d")
1105
  (pop-to-buffer
1106
   (flymake-show-diagnostic (if (button-type pos) (button-start pos) pos))))
1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125

(defun flymake--diagnostics-buffer-entries ()
  (with-current-buffer flymake--diagnostics-buffer-source
    (cl-loop for ov in (flymake--overlays)
             for diag = (overlay-get ov
                                     'flymake--diagnostic)
             for (line . col) =
             (save-excursion
               (goto-char (overlay-start ov))
               (cons (line-number-at-pos)
                     (- (point)
                        (line-beginning-position))))
             for type = (flymake--diag-type diag)
             collect
             (list (list :overlay ov
                         :line line
                         :severity (flymake--lookup-type-property
                                    type
                                    'severity (warning-numeric-level :error)))
1126
                   `[,(format "%s" line)
1127 1128 1129 1130
                     ,(format "%s" col)
                     ,(propertize (format "%s" type)
                                  'face (flymake--lookup-type-property
                                         type 'mode-line-face 'flymake-error))
1131 1132 1133 1134
                     (,(format "%s" (flymake--diag-text diag))
                      mouse-face highlight
                      help-echo "mouse-2: visit this diagnostic"
                      face nil
1135
                      action flymake-goto-diagnostic
1136
                      mouse-action flymake-goto-diagnostic)]))))
1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171

(define-derived-mode flymake-diagnostics-buffer-mode tabulated-list-mode
  "Flymake diagnostics"
  "A mode for listing Flymake diagnostics."
  (setq tabulated-list-format
        `[("Line" 5 (lambda (l1 l2)
                      (< (plist-get (car l1) :line)
                         (plist-get (car l2) :line)))
           :right-align t)
          ("Col" 3 nil :right-align t)
          ("Type" 8 (lambda (l1 l2)
                      (< (plist-get (car l1) :severity)
                         (plist-get (car l2) :severity))))
          ("Message" 0 t)])
  (setq tabulated-list-entries
        'flymake--diagnostics-buffer-entries)
  (tabulated-list-init-header))

(defun flymake--diagnostics-buffer-name ()
  (format "*Flymake diagnostics for %s*" (current-buffer)))

(defun flymake-show-diagnostics-buffer ()
  "Show a list of Flymake diagnostics for current buffer."
  (interactive)
  (let* ((name (flymake--diagnostics-buffer-name))
         (source (current-buffer))
         (target (or (get-buffer name)
                     (with-current-buffer (get-buffer-create name)
                       (flymake-diagnostics-buffer-mode)
                       (setq flymake--diagnostics-buffer-source source)
                       (current-buffer)))))
    (with-current-buffer target
      (revert-buffer)
      (display-buffer (current-buffer)))))

1172
(provide 'flymake)
1173 1174

(require 'flymake-proc)
1175

Eli Zaretskii's avatar
Eli Zaretskii committed
1176
;;; flymake.el ends here