aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrian Cully <bjc@kublai.com>2021-02-21 11:19:11 -0500
committerBrian Cully <bjc@kublai.com>2021-02-22 09:09:33 -0500
commitcfec7d35afa31164ca50156c472b98f6be961111 (patch)
treea2fc1273fe319f60e0154fbeda5214ae72c9f44d
parent85b96b90393c39ca62b1f0b3ab0ed769d97973c9 (diff)
downloadflymake-rust-cfec7d35afa31164ca50156c472b98f6be961111.tar.gz
flymake-rust-cfec7d35afa31164ca50156c472b98f6be961111.zip
Add support for checking with rustc.
Using the rust compiler directly is, in general, weaker than using cargo because it’s not workspace-aware, and thus can’t tell that the missing ‘main’ function is actually in a different file somewhere else. However, rustc does allow me to pass rust code on standard input, which means I can run checks between file saves, which is at least somewhat useful. Unfortunately, while both rustc and cargo can be told to use JSON, and the objects roughly match, it has enough deviation to require slight code rework all over the place, to the point that it honestly probably would have been easier, and had less code duplication, to just use regexp on the human readable stuff. In addition to rustc, this patch also makes the following changes: * Emacs 27 is required, because I’m using ‘executable-find’ with the option second argument. This is needed so remote paths can be found for the checkers if the file being checked is also remote. * Removed the process filter. Now I’m only checking when everything is done. This simplified the code quite a lot, and shouldn’t be any slower. * Checker output is now processed on the idle timer with a very short delay. This retains something very close to previous speed and should prevent issues where a bug in the processing causes Emacs to appear to lock up.
-rw-r--r--flymake-rust.el224
1 files changed, 147 insertions, 77 deletions
diff --git a/flymake-rust.el b/flymake-rust.el
index f04051a..8092061 100644
--- a/flymake-rust.el
+++ b/flymake-rust.el
@@ -7,7 +7,7 @@
;; Keywords: flymake, rust
;; Maintainer: Brian Cully <bjc@kublai.com>
;; Version: 0.1
-;; Package-Requires: ((emacs "26.1"))
+;; Package-Requires: ((emacs "27.1"))
;;; License:
@@ -50,15 +50,36 @@
:type 'string
:group 'flymake-rust)
+(defcustom flymake-rust-rustc-path "rustc"
+ "Path to rustc executable."
+ :type 'string
+ :group 'flymake-rust)
+
+(defcustom flymake-rust-checker 'cargo
+ "Which checker to use.
+
+ * cargo (the default) can only check on save, but is suitable
+for all projects.
+
+ * rustc can check between saves, but isn’t project aware, and
+will complain about things like missing main functions."
+ :type '(choice (const cargo)
+ (const rustc))
+ :options '(cargo rustc)
+ :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.")
+(defvar flymake-rust--cargo-flags '("check" "--quiet" "--message-format=json")
+ "Additional flags to pass to cargo.")
+
+(defvar flymake-rust--rustc-flags '("--error-format=json" "-")
+ "Additional flags to pass to rustc.")
(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))
+ (format "*flymake-rust[%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."
@@ -72,13 +93,15 @@
(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."
+Returns a sequence of data in the form of (FILE-NAME (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))
+ (let ((file-name (or (gethash "file_name" span) nil))
+ (byte-start (or (1+ (gethash "byte_start" span)) nil))
(byte-end (or (1+ (gethash "byte_end" span)) nil)))
- `(,byte-start . ,byte-end)))
+ `(,file-name (,byte-start . ,byte-end))))
spans)))
(defun flymake-rust--level-to-action (level)
@@ -87,64 +110,89 @@ If any data are not available, nil will be used in its place."
((string= level "warning") :warning)
(t :note)))
-(defun flymake-rust--report-errors (source-buffer report-fn hash)
+(defun flymake-rust--extract-msghash (hash)
+ "Return the message hash from ‘HASH’.
+
+Cargo and rustc have slightly different formats for this."
+ (cond ((eq flymake-rust-checker 'cargo)
+ (when (string= (gethash "reason" hash) "compiler-message")
+ (gethash "message" hash)))
+ ((eq flymake-rust-checker 'rustc) hash)))
+
+(defun flymake-rust--normalize-path (hash file-name)
+ "Return full path to ‘FILE-NAME’.
+
+Cargo only puts the relative path in there, so we need to add the
+path to the workspace from ‘HASH’ to get the full path."
+ (cond ((eq flymake-rust-checker 'cargo)
+ (gethash "src_path" (gethash "target" hash)))
+ ((eq flymake-rust-checker 'rustc) file-name)))
+
+;; See https://doc.rust-lang.org/rustc/json.html for a description of
+;; the JSON format.
+(defun flymake-rust--make-diagnostics (source-buffer 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’."
+ (when-let ((msghash (flymake-rust--extract-msghash hash)))
+ (let ((message (gethash "message" msghash))
+ (level (gethash "level" msghash))
+ (errlocs (flymake-rust--extract-error-location msghash)))
+ (mapcar (lambda (errloc)
+ (when-let ((start (caadr errloc))
+ (end (cdadr errloc)))
+ ;; Filter out errors that don’t relate to
+ ;; ‘source-buffer’.
+ (when (or (string= (car errloc) "<anon>")
+ (string= (flymake-rust--normalize-path hash (car errloc))
+ (file-local-name (buffer-file-name source-buffer))))
+ (flymake-make-diagnostic source-buffer
+ start end
+ (flymake-rust--level-to-action level)
+ message))))
+ errlocs))))
+
+(defun flymake-rust--parse-buffer (source-buffer report-fn)
+ "Parse the diagnostics for ‘SOURCE-BUFFER’ and send to ‘REPORT-FN’."
(flymake-rust--with-proc-buf
- (let ((moving (= (point) (process-mark proc))))
+ (let ((continue t)
+ (diags nil))
+ (goto-char (point-min))
+ (while continue
+ (if-let ((diag (condition-case err
+ (json-parse-buffer)
+ (json-end-of-file nil)
+ (t (error err)))))
+ (progn
+ (push (flymake-rust--make-diagnostics source-buffer diag) diags))
+ (setq continue nil)))
+ (let ((diags (flatten-list diags)))
+ (if diags
+ (funcall report-fn (flatten-list diags))
+ (funcall report-fn nil :explanation "no errors"))))))
- (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))
+;; TODO: verify that idle timers don’t actually break anything, and
+;; are, in fact, an improvement over just calling stuff in the
+;; sentinel directly.
+(defun flymake-rust--sentinel (source-buffer report-fn proc _event)
+ "Call ‘REPORT-FN’ for ‘SOURCE-BUFFER’ with diagnostics when ‘PROC’ finishes.
- (when moving
- (goto-char (process-mark proc))))))
+This function does not directly call ‘REPORT-FN’, but instead
+sets up a short timer to do so. This is done because sentinel
+processes inhibits Emacs handling of events like quit."
+ (when (eq 'exit (process-status proc))
+ (process-put proc 'timer
+ (run-with-idle-timer 0.1 nil
+ 'flymake-rust--parse-buffer source-buffer report-fn))))
-(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--make-sentinel (source-buffer report-fn)
+ "Create a sentinel for ‘SOURCE-BUFFER’ reporting to ‘REPORT-FN’."
+ (lambda (proc event)
+ (flymake-rust--sentinel source-buffer report-fn proc event)))
(defun flymake-rust--cleanup ()
"Clean up and leftover processes and buffers for the current buffer."
+ (when-let ((timer (and flymake-rust--process
+ (process-get flymake-rust--process 'timer))))
+ (cancel-timer timer))
(when (process-live-p flymake-rust--process)
(flymake-log :warning "Killing out-of-date checker process.")
(delete-process flymake-rust--process))
@@ -153,29 +201,51 @@ This function attempts to parse JSON data beginning at
(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--cargo-command ()
+ "Return an appropriate cargo check command line."
+ (cons (executable-find flymake-rust-cargo-path t)
+ flymake-rust--cargo-flags))
+
+(defun flymake-rust--rustc-command ()
+ "Return a command line for rustc reading from standard input."
+ (cons (executable-find flymake-rust-rustc-path t) flymake-rust--rustc-flags))
+
+(defun flymake-rust--ignore-stderr-p ()
+ "Return t if the stderr output of ‘flymake-rust-checker’ should be ignored.
+
+Cargo puts non-JSON data on stderr as it runs, so it has to be
+shunted elsewhere or it breaks parsing."
+ (eq flymake-rust-checker 'cargo))
+
+(defun flymake-rust--checker-command ()
+ "Return a command line to check ‘PATH’.
+
+This will use the value configured in ‘flymake-rust-checker’ to what to run."
+ (cond ((eq flymake-rust-checker 'cargo) (flymake-rust--cargo-command))
+ ((eq flymake-rust-checker 'rustc) (flymake-rust--rustc-command))))
(defun flymake-rust--call (source-buffer report-fn)
- "Call out to the syntax checker for ‘SOURCE-BUFFER’ and report to ‘REPORT-FN’."
+ "Begin checking ‘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)))
+ (let ((buf (get-buffer-create (flymake-rust--buffer-name "stdout"))))
+ (with-current-buffer buf
+ (js-mode))
+ (setq flymake-rust--process
+ (make-process :name "flymake-rust"
+ :buffer buf
+ :command (flymake-rust--checker-command)
+ :connection-type 'pipe
+ :noquery nil
+ :sentinel (flymake-rust--make-sentinel source-buffer report-fn)
+ :stderr (and (flymake-rust--ignore-stderr-p)
+ (flymake-rust--buffer-name "stderr"))
+ :file-handler t)))
+
+ (when (eq flymake-rust-checker 'rustc)
+ (process-send-region flymake-rust--process
+ (point-min) (point-max))
+ (process-send-eof flymake-rust--process))))
(defun flymake-rust--backend (report-fn &rest args)
"Send Flymake diagnostics to ‘REPORT-FN’.