branch: externals/flymake-clippy
commit ea2ed74df57fdaba097c7f131584f58a24440df7
Author: Michael Kirkland <michael@siberzk>
Commit: Michael Kirkland <michael@siberzk>
Flymake backend for displaying Rust lint diagnostics from Clippy
---
clippy-flymake.el | 159 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 159 insertions(+)
diff --git a/clippy-flymake.el b/clippy-flymake.el
new file mode 100644
index 0000000000..a67c2c3305
--- /dev/null
+++ b/clippy-flymake.el
@@ -0,0 +1,159 @@
+;;; clippy-flymake.el --- Flymake backend for Clippy -*- lexical-binding: t;
-*-
+
+;; Copyright (C) 2025 Michael Kirkland
+
+;; Author: Michael Kirkland <mak.kirkl...@proton.me>
+;; Keywords: languages tools
+;; Version: 1.0.0
+;; Package-Requires: ((emacs "26.1"))
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Flymake backend for Clippy.
+;;
+;; Based on "An annotated example backend" in the Flymake docs:
+;; https://www.gnu.org/software/emacs/manual/html_mono/flymake.html
+
+;;; Code:
+
+(require 'cl-lib)
+
+(defgroup clippy-flymake nil
+ "Flymake backend for Clippy."
+ :group 'programming)
+
+(defvar-local clippy-flymake--proc nil
+ "Bound to cargo clippy process during it's execution.")
+
+(defun clippy-flymake-setup ()
+ (add-hook 'flymake-diagnostic-functions #'clippy-flymake-backend nil t))
+
+(defun clippy-flymake-backend (report-fn &rest _args)
+ "See `flymake-diagnostic-functions'."
+ (unless (executable-find "cargo")
+ (error "Cannot find cargo"))
+ ;; If process is still running from the last check, kill it
+ (when (process-live-p clippy-flymake--proc)
+ (kill-process clippy-flymake--proc))
+ (let* ((source-buffer (current-buffer))
+ (filename (buffer-file-name source-buffer)))
+ (save-restriction
+ (widen)
+ (setq
+ clippy-flymake--proc
+ (make-process
+ :name "clippy-flymake" :noquery t :connection-type 'pipe
+ :buffer (generate-new-buffer "*clippy-flymake*")
+ :command '("cargo" "clippy" "--message-format=json")
+ :sentinel
+ (lambda (proc _event)
+ ;; Check the process has indeed exited, as it might be
+ ;; simply suspended
+ (when (memq (process-status proc) '(exit signal))
+ (unwind-protect
+ ;; Only proceed if registered process is current process
+ ;; (maybe a new call has been made since)
+ (if (eq proc clippy-flymake--proc)
+ (with-current-buffer (process-buffer proc)
+ (goto-char (point-min))
+ (let (diagnostics)
+ ;; Parse the JSON output and process diagnostics
+ (while (re-search-forward "^{.*}$" nil t)
+ (let* ((json (json-parse-string (match-string 0)
+ :object-type 'alist))
+ (diagnostic (clippy-flymake--parse-diagnostic
+ json
+ source-buffer)))
+ (when diagnostic
+ (cl-destructuring-bind (beg end type text)
+ diagnostic
+ (push (flymake-make-diagnostic source-buffer
+ beg
+ end
+ type
+ text)
+ diagnostics)))))
+ (funcall report-fn diagnostics)))
+ (flymake-log :warning "Cancelling obsolete check %s" proc))
+ ;; Cleanup temporary buffer
+ (kill-buffer (process-buffer proc))))))))))
+
+(defun clippy-flymake--parse-diagnostic (json source-buffer)
+ "Parse JSON diagnostic and return a LIST.
+
+LIST contains ordered args required by FLYMAKE-MAKE-DIAGNOSTIC.
+
+SOURCE-BUFFER is needed to find the buffer points corresponding
+to the reported line and column numbers"
+ (let* ((diagnostic (cdr (assq 'message json)))
+ (message (cdr (assq 'message diagnostic)))
+ (level (cdr (assq 'level diagnostic)))
+ (spans (cdr (assq 'spans diagnostic)))
+ (spans (when (> (length spans) 0)
+ (aref spans 0))))
+ (when (and message spans (not (string= level "note")))
+ (let* ((start-line (alist-get 'line_start spans))
+ (start-col (alist-get 'column_start spans))
+ (end-line (alist-get 'line_end spans))
+ (end-col (alist-get 'column_end spans))
+ (message (concat level ": " message))
+ (message (clippy-flymake--include-help diagnostic message))
+ (type (pcase level
+ ("error" :error)
+ ("warning" :warning)))
+ (beg (with-current-buffer source-buffer
+ (clippy-flymake-line-col-buffer-position start-line
+ start-col)))
+ (end (with-current-buffer source-buffer
+ (clippy-flymake-line-col-buffer-position end-line
+ end-col))))
+ (list beg end type message)))))
+
+(defun clippy-flymake--include-help (diagnostic message)
+ "Concatenate MESSAGE with help tips extracted from DIAGNOSTIC."
+ (cl-loop
+ for child across (cdr (assq 'children diagnostic))
+ for msg = (cdr (assq 'message child))
+ for level = (cdr (assq 'level child))
+ for spans = (cdr (assq 'spans child))
+ for spans = (when (> (length spans) 0)
+ (aref spans 0))
+ for replace = (cdr (assq 'suggested_replacement spans))
+ ;; When help messages don't span text, there's nothing to overlay
+ when (and spans (string= level "help"))
+ do (setq message
+ ;; Include the help message
+ (concat message
+ "\nhelp: "
+ msg
+ ;; Include suggested replacement
+ (when replace
+ (format ": %s" replace)))))
+ message)
+
+(defun clippy-flymake-line-col-buffer-position (line column)
+ "Return the position in the current buffer at LINE and COLUMN."
+ (save-excursion
+ (save-restriction
+ (widen)
+ (goto-char (point-min))
+ (forward-line (1- line))
+ (move-to-column (1- column))
+ (point))))
+
+(provide 'clippy-flymake)
+
+;;; clippy-flymake.el ends here