aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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’.