aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrian Cully <bjc@kublai.com>2021-02-25 09:04:29 -0500
committerGitHub <noreply@github.com>2021-02-25 09:04:29 -0500
commit1ec08d927975949c2ba8e73c792e3d8b6b52948f (patch)
tree0b241a51392476e2ea08ee9f71d70c1383ea6275
parent0d400b729c510e8abeca07ce811255239f7d0e65 (diff)
parentcf412740aabae885d5bd735b4874a3456d6cbdeb (diff)
downloadflymake-rust-main.tar.gz
flymake-rust-main.zip
Merge pull request #2 from bjc/nextHEADmain
Next
-rw-r--r--flymake-rust.el218
1 files changed, 128 insertions, 90 deletions
diff --git a/flymake-rust.el b/flymake-rust.el
index 8092061..bc2fec1 100644
--- a/flymake-rust.el
+++ b/flymake-rust.el
@@ -1,10 +1,10 @@
-;;; flymake-rust -- Flymake integration for the Rust programming language -*- lexical-binding: t; -*-
+;;; flymake-rust.el -- 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
+;; URL: https://github.com/bjc/flymake-rust
+;; Keywords: tools, flymake, rust
;; Maintainer: Brian Cully <bjc@kublai.com>
;; Version: 0.1
;; Package-Requires: ((emacs "27.1"))
@@ -38,6 +38,8 @@
;;; Code:
+(require 'generator)
+
(defgroup flymake-rust nil
"Flymake integration for the Rust programming language."
:prefix "flymake-rust-"
@@ -47,22 +49,26 @@
(defcustom flymake-rust-cargo-path "cargo"
"Path to cargo executable."
+ :package-version '(flymake-rust . "0.1")
:type 'string
:group 'flymake-rust)
(defcustom flymake-rust-rustc-path "rustc"
"Path to rustc executable."
+ :package-version '(flymake-rust . "0.1")
: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
+ * cargo (default) can only check on save, but is suitable
for all projects.
- * rustc can check between saves, but isn’t project aware, and
+ * rustc can check between saves, but isn’t project aware, can’t
+figure out your edition (or anything else from Cargo.toml) and
will complain about things like missing main functions."
+ :package-version '(flymake-rust . "0.1")
:type '(choice (const cargo)
(const rustc))
:options '(cargo rustc)
@@ -74,27 +80,18 @@ will complain about things like missing main functions."
(defvar flymake-rust--cargo-flags '("check" "--quiet" "--message-format=json")
"Additional flags to pass to cargo.")
-(defvar flymake-rust--rustc-flags '("--error-format=json" "-")
+(defvar flymake-rust--rustc-flags '("--error-format=json" "-o" "/dev/null" "-")
"Additional flags to pass to rustc.")
(defun flymake-rust--buffer-name (suffix)
"What to name the flymake buffer for ‘SUFFIX’."
(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."
- (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 (FILE-NAME (BYTE-START
-. BYTE-END)) If any data are not available, nil will be used in
+. BYTE-END)). If any datum is not available, nil will be used in
its place."
(let* ((spans (or (gethash "spans" hash) [])))
(mapcar (lambda (span)
@@ -106,43 +103,68 @@ its place."
(defun flymake-rust--level-to-action (level)
"Convert ‘LEVEL’ into a Flymake action."
- (cond ((string= level "error") :error)
- ((string= level "warning") :warning)
- (t :note)))
+ (pcase level
+ ("error" :error)
+ ("warning" :warning)
+ (_ :note)))
(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)))
+Cargo and rustc have slightly different formats for this, which
+this function attempts to account for."
+ (pcase flymake-rust-checker
+ ('cargo (when (string= (gethash "reason" hash) "compiler-message")
+ (gethash "message" hash)))
+ ('rustc hash)))
+
+(defun flymake-rust--crate-local-path (crate)
+ "Return the local path for ‘CRATE’."
+ (string-match (rx "(path+file://" (group (1+ (not ")"))) ")")
+ crate)
+ (match-string-no-properties 1 crate))
+
+(flymake-rust--crate-local-path "std-async 0.1.0 (path+file:///home/bjc/src/std-async)")
+
(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)))
+path to the workspace from ‘HASH’ to get the full path.
+Unfortunately, cargo doesn’t include this information directly,
+but instead appears to expect you to call it with the ‘metadata’
+option to extract it. Which would be fine, if everything were
+local, but over TRAMP it may cause undue delay.
+
+So, to avoid calling cargo twice every check, we make
+the (probably safe) assumption that the thing we’re working on is
+local, and thus the local crate will have a ‘path+file’ URL in
+‘package_id’ section, from which we can extract the project
+root."
+ (pcase flymake-rust-checker
+ ('cargo (expand-file-name file-name
+ (flymake-rust--crate-local-path
+ (gethash "package_id" hash))))
+ ('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’."
+ "Extract diagnostic messages for Flymake from ‘HASH’.
+
+‘HASH’ is the hash-table representation of the JSON output by the
+checker 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)))
+ (pcase-let ((`(,file-name (,start . ,end)) 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))
+ (when (or (string= file-name "<anon>")
+ (string= (flymake-rust--normalize-path hash file-name)
(file-local-name (buffer-file-name source-buffer))))
(flymake-make-diagnostic source-buffer
start end
@@ -150,38 +172,51 @@ path to the workspace from ‘HASH’ to get the full path."
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 ((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"))))))
+(iter-defun flymake-rust--json-generator ()
+ "Return an iterator for JSON data from the current buffer.
+
+The entire buffer is treated as JSON data, with the exception of
+newlines, since that’s the only non-JSON data that the Rust
+compiler suite currently emits.
+
+This function parses the entire buffer from beginning to end, and
+tramples all over point, so save that if you need to."
+ (goto-char (point-min))
+ (while (not (eobp))
+ (iter-yield (json-parse-buffer))
+ (search-forward "\n" nil t)))
+
+(defun flymake-rust--parse-buffer (diag-buffer source-buffer report-fn)
+ "Parse ‘DIAG-BUFFER’ as JSON compiler output for ‘REPORT-FN’.
+
+‘DIAG-BUFFER’ is the process buffer associated with the checker
+process for ‘SOURCE-BUFFER’, and is expected to contain JSON
+output from the Rust compiler."
+ (with-current-buffer diag-buffer
+ (js-mode)
+ (let ((diags))
+ (iter-do (diag (flymake-rust--json-generator))
+ (push (flymake-rust--make-diagnostics source-buffer diag) diags))
+
+ (if diags
+ (funcall report-fn (flatten-list diags))
+ (funcall report-fn nil :explanation "no errors")))))
;; 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.
+ "Handle events for ‘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."
+The only event currently being handled is the process finishing,
+at which point a short-delay idle timer is set up to handle the
+processing of compiler output for ‘SOURCE-BUFFER’ which is
+reported to ‘REPORT-FN’."
(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))))
+ (let ((timer (run-with-idle-timer 0.1 nil
+ 'flymake-rust--parse-buffer
+ (process-buffer proc) source-buffer report-fn)))
+ (process-put proc 'timer timer))))
(defun flymake-rust--make-sentinel (source-buffer report-fn)
"Create a sentinel for ‘SOURCE-BUFFER’ reporting to ‘REPORT-FN’."
@@ -189,20 +224,21 @@ processes inhibits Emacs handling of events like quit."
(flymake-rust--sentinel source-buffer report-fn proc event)))
(defun flymake-rust--cleanup ()
- "Clean up and leftover processes and buffers for the current buffer."
+ "Clean up processes and buffers associated with 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))
- (mapc (lambda (suffix)
- (when-let ((buf (get-buffer (flymake-rust--buffer-name suffix))))
- (kill-buffer buf)))
- '("stdout" "stderr")))
+
+ (dolist (suffix '("stdout" "stderr"))
+ (when-let ((buf (get-buffer (flymake-rust--buffer-name suffix))))
+ (kill-buffer buf))))
(defun flymake-rust--cargo-command ()
- "Return an appropriate cargo check command line."
+ "Return a command line for cargo check."
(cons (executable-find flymake-rust-cargo-path t)
flymake-rust--cargo-flags))
@@ -210,37 +246,35 @@ processes inhibits Emacs handling of events like quit."
"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’.
+ "Return a command line used to check the current buffer’s file.
-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))))
+This uses the value of ‘flymake-rust-checker’ to determine the
+specific command line."
+ (pcase flymake-rust-checker
+ ('cargo (flymake-rust--cargo-command))
+ ('rustc (flymake-rust--rustc-command))))
(defun flymake-rust--call (source-buffer report-fn)
- "Begin checking ‘SOURCE-BUFFER’ and report to ‘REPORT-FN’."
+ "Check ‘SOURCE-BUFFER’ for errors and report them to ‘REPORT-FN’.
+
+‘REPORT-FN’ is a function normally created by Flymake which
+expects a list of diagnostics created by
+‘flymake-make-diagnostic’. For further information, see the Info
+node ‘(flymake)Backend functions’."
(with-current-buffer source-buffer
(flymake-rust--cleanup)
- (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)))
+
+ (setq flymake-rust--process
+ (make-process :name "flymake-rust"
+ :buffer (get-buffer-create (flymake-rust--buffer-name "stdout"))
+ :command (flymake-rust--checker-command)
+ :connection-type 'pipe
+ :noquery nil
+ :sentinel (flymake-rust--make-sentinel source-buffer report-fn)
+ :stderr (when (eq flymake-rust-checker 'cargo)
+ (get-buffer-create (flymake-rust--buffer-name "stderr")))
+ :file-handler t))
(when (eq flymake-rust-checker 'rustc)
(process-send-region flymake-rust--process
@@ -252,10 +286,14 @@ This will use the value configured in ‘flymake-rust-checker’ to what to run.
For the value of ‘ARGS’, see the documentation for
‘flymake-diagnostic-functions’."
+ ;; It seems like ‘:recent-changes’ is set when a temporary buffer
+ ;; change happens between saves, but is nil on file save (or initial
+ ;; check).
(let ((rc (plist-member args :recent-changes)))
- (when (or (not rc) (cadr rc))
+ (when (or (not rc) (and (eq flymake-rust-checker 'rustc) (cadr rc)))
(flymake-rust--call (current-buffer) report-fn))))
+;;;###autoload
(defun flymake-rust-setup ()
"Provide Flymake support for the Rust programming language."
(add-hook 'flymake-diagnostic-functions 'flymake-rust--backend nil t))