summaryrefslogtreecommitdiff
path: root/flymake-clippy.el
blob: 7925362d64088866a7ce30401d9fef50c60eed2a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
;;; flymake-clippy.el --- Flymake backend for Clippy  -*- lexical-binding: t; -*-

;; Copyright (C) 2023  Graham Marlow

;; Author: Graham Marlow <info@mgmarlow.com>
;; Keywords: tools
;; URL: https://sr.ht/~mgmarlow/flymake-clippy/
;; Version: 1.0.0
;; Package-Requires: ((emacs "26.1"))

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

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

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Flymake backend for Clippy, the Rust linter.
;;
;; (use-package flymake-clippy
;;   :hook (rust-mode . flymake-clippy-setup-backend))

;;; Code:

(require 'cl-lib)

;; Capture group source example:
;; "warning: ..."
;;    --> src/filename.rs
;; 98 | ...
(defun flymake-clippy--build-regexp ()
  "Regexp for Clippy output."
  (rx (seq line-start
           ;; Message
           (group (or "warning:" "error:")
                  (zero-or-more nonl))
           "\n"
           (zero-or-more nonl)
           "--> "
           ;; File
           (group
            (zero-or-more nonl))
           ":"
           ;; Line
           (group
            (one-or-more
             (any "0-9")))
           ":"
           ;; Col
           (group
            (one-or-more
             (any "0-9")))
           line-end)))

(defvar-local flymake-clippy--proc nil
  "Clippy subprocess object, used to ensure obsolete processes aren't reused.")

(defun flymake-clippy-backend (report-fn &rest _args)
  "Flymake backend for Clippy, the Rust linter.

Calls REPORT-FN with a list of Flymake diagnostics for the
current buffer.

Use `flymake-clippy-setup-backend' to register the backend
with the appropriate Flymake hook."
  (unless (executable-find "cargo")
    (error "Cannot find cargo"))

  (let* ((source (current-buffer))
         (filename (buffer-file-name source)))
    (save-restriction
      (widen)
      (setq flymake-clippy--proc
            (make-process
             :name "flymake-clippy" :noquery t :connection-type 'pipe
             :buffer (generate-new-buffer "*flymake-clippy*")
             :command '("cargo" "clippy")
             :sentinel
             (lambda (proc _event)
               (when (memq (process-status proc) '(exit signal))
                 (unwind-protect
                     (if (with-current-buffer source (eq proc flymake-clippy--proc))
                         (with-current-buffer (process-buffer proc)
                           (goto-char (point-min))
                           ;; Collect output buffer into diagnostic messages/locations,
                           ;; exposing them via `report-fn'.
                           (cl-loop
                            while (search-forward-regexp
                                   (flymake-clippy--build-regexp)
                                   nil t)
                            for msg = (match-string 1)
                            for sourcefile = (match-string 2)
                            for (beg . end) = (flymake-diag-region
                                               source
                                               (string-to-number (match-string 3)))
                            for type = (if (string-match "^warning" msg)
                                           :warning
                                         :error)
                            when (and sourcefile (string-match-p sourcefile filename))
                            collect (flymake-make-diagnostic source beg end type msg)
                            into diags
                            finally (funcall report-fn diags)))
                       (flymake-log :warning "Canceling obsolete check %s" proc))
                   ;; Cleanup temp buffer.
                   (kill-buffer (process-buffer proc)))))))
      (process-send-region flymake-clippy--proc (point-min) (point-max))
      (process-send-eof flymake-clippy--proc))))

(defun flymake-clippy-setup-backend ()
  "Add `flymake-clippy' to `flymake-diagnostic-functions' hook."
  (add-hook 'flymake-diagnostic-functions #'flymake-clippy-backend nil t))

(provide 'flymake-clippy)

;;; flymake-clippy.el ends here