diff options
-rw-r--r-- | flymake-rust.el | 218 |
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)) |