From c735494681f71bb0b64cae865b554cd056045f62 Mon Sep 17 00:00:00 2001 From: Brian Cully Date: Sun, 21 Feb 2021 22:50:17 -0500 Subject: Minor code changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * mapc -> dolist * cond -> pcase I find pcase to be easier to read when it can be used, which, in this case, is everywhere cond was being used. * use some thread-last. * Fix URL in comment header. * Add package version to defcustom strings. * Have rustc dump build results to /dev/null. * Fix URL in header comments. * Tell rustc to send output files to /dev/null * Add ‘tools’ keyword to package. * Add autoload for setup, use filename in comment header. * Get rid of single use macro. * Calculate flymake proc buffer names from source buffer name, not file. * Update comments. --- flymake-rust.el | 120 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 70 insertions(+), 50 deletions(-) diff --git a/flymake-rust.el b/flymake-rust.el index 8092061..9bbb0e3 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 ;; Author: Brian Cully -;; URL: https://github.com/bjc/nspawn-tramp -;; Keywords: flymake, rust +;; URL: https://github.com/bjc/flymake-rust +;; Keywords: tools, flymake, rust ;; Maintainer: Brian Cully ;; Version: 0.1 ;; Package-Requires: ((emacs "27.1")) @@ -38,6 +38,9 @@ ;;; Code: +(eval-when-compile + (require 'subr-x)) + (defgroup flymake-rust nil "Flymake integration for the Rust programming language." :prefix "flymake-rust-" @@ -47,11 +50,13 @@ (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) @@ -63,6 +68,7 @@ for all projects. * rustc can check between saves, but isn’t project aware, 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,49 @@ 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--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))) + (pcase flymake-rust-checker + ('cargo (thread-last hash + (gethash "target") + (gethash "src_path"))) + ('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) "") - (string= (flymake-rust--normalize-path hash (car errloc)) + (when (or (string= file-name "") + (string= (flymake-rust--normalize-path hash file-name) (file-local-name (buffer-file-name source-buffer)))) (flymake-make-diagnostic source-buffer start end @@ -150,9 +153,13 @@ 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 +(defun flymake-rust--parse-buffer (proc-buffer source-buffer report-fn) + "Parse ‘PROC-BUFFER’ as JSON compiler output for ‘REPORT-FN’. + +‘PROC-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 proc-buffer (let ((continue t) (diags nil)) (goto-char (point-min)) @@ -161,9 +168,9 @@ path to the workspace from ‘HASH’ to get the full path." (json-parse-buffer) (json-end-of-file nil) (t (error err))))) - (progn - (push (flymake-rust--make-diagnostics source-buffer diag) diags)) + (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)) @@ -173,15 +180,17 @@ path to the workspace from ‘HASH’ to get the full path." ;; 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 will be +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)))) + 'flymake-rust--parse-buffer + (process-buffer flymake-rust--process) source-buffer report-fn)))) (defun flymake-rust--make-sentinel (source-buffer report-fn) "Create a sentinel for ‘SOURCE-BUFFER’ reporting to ‘REPORT-FN’." @@ -189,20 +198,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)) @@ -220,17 +230,26 @@ shunted elsewhere or it breaks parsing." (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)))) +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 @@ -256,6 +275,7 @@ For the value of ‘ARGS’, see the documentation for (when (or (not rc) (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)) -- cgit v1.2.3 From c5b12f62aa415a1183c399c7a842d90cf82841dd Mon Sep 17 00:00:00 2001 From: Brian Cully Date: Mon, 22 Feb 2021 10:56:18 -0500 Subject: Allow the rustc checker to run between saves. --- flymake-rust.el | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flymake-rust.el b/flymake-rust.el index 9bbb0e3..bc160f6 100644 --- a/flymake-rust.el +++ b/flymake-rust.el @@ -271,8 +271,11 @@ node ‘(flymake)Backend functions’." 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 -- cgit v1.2.3 From cf412740aabae885d5bd735b4874a3456d6cbdeb Mon Sep 17 00:00:00 2001 From: Brian Cully Date: Wed, 24 Feb 2021 21:22:09 -0500 Subject: A bunch of fixes. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Get cargo checker working with non-main source files by making some assumptions about the crate path we’re given in cargo’s check output. * Attempt to fix TRAMP issues in Emacs 28.0.50. These may be due to bugs in TRAMP, in nativecomp, or maybe I just wrote some broken code that happens to work in 27.1. - Simplify output buffer names so they don’t ever have TRAMP paths * Use a generator for JSON parsing. This makes the code much easier to read, IMHO. * Skip past newlines when processing JSON. Now I no longer need to catch errors from the parser, since end-of-file shouldn’t be encountered anymore by the parser. --- flymake-rust.el | 123 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 69 insertions(+), 54 deletions(-) diff --git a/flymake-rust.el b/flymake-rust.el index bc160f6..bc2fec1 100644 --- a/flymake-rust.el +++ b/flymake-rust.el @@ -38,8 +38,7 @@ ;;; Code: -(eval-when-compile - (require 'subr-x)) +(require 'generator) (defgroup flymake-rust nil "Flymake integration for the Rust programming language." @@ -63,10 +62,11 @@ (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) @@ -118,15 +118,34 @@ this function attempts to account for." (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." +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 (thread-last hash - (gethash "target") - (gethash "src_path"))) + ('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 @@ -153,44 +172,51 @@ checker for ‘SOURCE-BUFFER’" message)))) errlocs)))) -(defun flymake-rust--parse-buffer (proc-buffer source-buffer report-fn) - "Parse ‘PROC-BUFFER’ as JSON compiler output for ‘REPORT-FN’. +(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. -‘PROC-BUFFER’ is the process buffer associated with the checker +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 proc-buffer - (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))))) - (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")))))) + (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) - "Handle events for ‘PROC‘. + "Handle events for ‘PROC’. 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 will be +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 + (let ((timer (run-with-idle-timer 0.1 nil 'flymake-rust--parse-buffer - (process-buffer flymake-rust--process) source-buffer report-fn)))) + (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’." @@ -220,15 +246,8 @@ reported to ‘REPORT-FN’." "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 uses the value of ‘flymake-rust-checker’ to determine the specific command line." @@ -246,20 +265,16 @@ 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 -- cgit v1.2.3