aboutsummaryrefslogtreecommitdiffstats
path: root/flymake-rust.el
blob: f04051a22681f49f9923b90ca5df2376b0af5f0a (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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
;;; flymake-rust -- Flymake integration for the Rust programming language  -*- lexical-binding: t; -*-

;; Copyright © 2021 Brian Cully <bjc@kublai.com>

;; Author: Brian Cully <bjc@kublai.com>
;; URL: https://github.com/bjc/nspawn-tramp
;; Keywords: flymake, rust
;; Maintainer: Brian Cully <bjc@kublai.com>
;; Version: 0.1
;; Package-Requires: ((emacs "26.1"))

;;; License:

;; 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 <http://www.gnu.org/licenses/>.

;;; Commentary:

;;
;; Add support for the Rust programming language to Flymake.
;;
;; ## Usage
;;
;; Call ‘flymake-rust-setup’ in your Emacs initialization.
;;
;;     (add-hook ’rust-mode-hook ’flymake-rust-setup)
;;

;;; Code:

(defgroup flymake-rust nil
  "Flymake integration for the Rust programming language."
  :prefix "flymake-rust-"
  :group 'applications
  :link '(url-link :tag "Github" "https://github.com/bjc/flymake-rust")
  :link '(emacs-commentary-link :tag "Commentary" "flymake-rust"))

(defcustom flymake-rust-cargo-path "cargo"
  "Path to cargo executable."
  :type 'string
  :group 'flymake-rust)

(defvar-local flymake-rust--process nil
  "Flymake process associated with the current buffer.")

(defvar-local flymake-rust--parse-marker nil
  "Last parse position in the current buffer.")

(defun flymake-rust--buffer-name (suffix)
  "What to name the flymake buffer for ‘SUFFIX’."
  (format "*flymake-rust-cargo-check %s %s*" suffix buffer-file-name))

(defmacro flymake-rust--with-proc-buf (&rest body)
  "Run ‘BODY’ in the buffer for the current buffer’s Flymake process."
  (declare (indent defun))
  (let ((buf (gensym)))
    `(when-let ((,buf (and flymake-rust--process
                           (process-buffer flymake-rust--process))))
       (with-current-buffer ,buf
         ,@body))))

(defun flymake-rust--extract-error-location (hash)
  "Extract file location data from ‘HASH’.

Returns a sequence of data in the form of (BYTE-START . BYTE-END)
If any data are not available, nil will be used in its place."
  (let* ((spans (or (gethash "spans" hash) [])))
    (mapcar (lambda (span)
              (let ((byte-start (or (1+ (gethash "byte_start" span)) nil))
                    (byte-end (or (1+ (gethash "byte_end" span)) nil)))
                `(,byte-start . ,byte-end)))
            spans)))

(defun flymake-rust--level-to-action (level)
  "Convert ‘LEVEL’ into a Flymake action."
  (cond ((string= level "error") :error)
        ((string= level "warning") :warning)
        (t :note)))

(defun flymake-rust--report-errors (source-buffer report-fn hash)
  "Pull interesting things out of ‘HASH’ for ‘SOURCE-BUFFER ’and forward them to ‘REPORT-FN’."
  (let ((reason (gethash "reason" hash))
        (msghash (gethash "message" hash)))
    (when (and (string= reason "compiler-message") msghash)
      (let ((message (gethash "message" msghash))
            (level (gethash "level" msghash))
            (errlocs (flymake-rust--extract-error-location msghash)))
        (funcall report-fn
                 (mapcar (lambda (errloc)
                           (flymake-make-diagnostic source-buffer
                                                    (car errloc) (cdr errloc)
                                                    (flymake-rust--level-to-action level)
                                                    message))
                         errlocs))))))

(defun flymake-rust--parse-json (source-buffer report-fn)
  "Grab new JSON from ‘SOURCE-BUFFER’ and send it to ‘REPORT-FN’.

This function attempts to parse JSON data beginning at
  ‘flymake-rust--parse-marker’ and if successful, will update
  the marker to the end of the parsed data."
  (goto-char flymake-rust--parse-marker)
  (while (condition-case err
             (let ((parsed (json-parse-buffer)))
               (set-marker flymake-rust--parse-marker (point))
               (flymake-rust--report-errors source-buffer report-fn parsed)
               t)
           (json-parse-error nil)
           (json-end-of-file nil)
           (t (error err)))))

;; TODO: this blocks ‘C-g’, so it may be better to just do this in an
;; idle process.
(defun flymake-rust--filter (source-buffer report-fn proc string)
  "Process ‘STRING’ as JSON from ‘PROC’ and send it to ‘REPORT-FN’ for ‘SOURCE-BUFFER’."
  (flymake-rust--with-proc-buf
    (let ((moving (= (point) (process-mark proc))))

      (save-excursion
        ;; Append the latest output to the end of the previous.
        (setq buffer-read-only nil)
        (goto-char (process-mark proc))
        (insert string)
        (set-marker (process-mark proc) (point))
        (setq buffer-read-only t)
        (flymake-rust--parse-json source-buffer report-fn))

      (when moving
        (goto-char (process-mark proc))))))

(defun flymake-rust--make-filter (source-buffer report-fn)
  "Create a process filter for ‘SOURCE-BUFFER’ reporting to ‘REPORT-FN’."
  (lambda (proc string)
    (flymake-rust--filter source-buffer report-fn proc string)))

(defun flymake-rust--cleanup ()
  "Clean up and leftover processes and buffers for the current buffer."
  (when (process-live-p flymake-rust--process)
    (flymake-log :warning "Killing out-of-date checker process.")
    (delete-process flymake-rust--process))
  (mapc (lambda (suffix)
          (when-let ((buf (get-buffer (flymake-rust--buffer-name suffix))))
            (kill-buffer buf)))
        '("stdout" "stderr")))

(defun flymake-rust--setup-proc-buf ()
  "Set up stdout process buffer for scanning."
  ;; Save the position where output will begin so we can know where to
  ;; start scanning from when output is done.
  (flymake-rust--with-proc-buf
    (setq buffer-read-only t)
    (setq flymake-rust--parse-marker (make-marker))
    (set-marker flymake-rust--parse-marker (point))))

(defun flymake-rust--call (source-buffer report-fn)
  "Call out to the syntax checker for ‘SOURCE-BUFFER’ and report to ‘REPORT-FN’."
  (with-current-buffer source-buffer
    (flymake-rust--cleanup)
    (setq flymake-rust--process
          (make-process :name "flymake-rust-cargo-check"
                        :buffer (flymake-rust--buffer-name "stdout")
                        :command `(,flymake-rust-cargo-path "check" "--quiet" "--message-format" "json")
                        :noquery nil
                        :filter (flymake-rust--make-filter source-buffer report-fn)
                        :stderr (flymake-rust--buffer-name "stderr")
                        :stderr nil
                        :file-handler t))
    (flymake-rust--setup-proc-buf)))

(defun flymake-rust--backend (report-fn &rest args)
  "Send Flymake diagnostics to ‘REPORT-FN’.

For the value of ‘ARGS’, see the documentation for
‘flymake-diagnostic-functions’."
  (let ((rc (plist-member args :recent-changes)))
    (when (or (not rc) (cadr rc))
      (flymake-rust--call (current-buffer) report-fn))))

(defun flymake-rust-setup ()
  "Provide Flymake support for the Rust programming language."
  (add-hook 'flymake-diagnostic-functions 'flymake-rust--backend nil t))

(provide 'flymake-rust)
;;; flymake-rust.el ends here