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