Subject: Re: bug#78862: 31.0.50; Eglot couldn't refresh Flymake diagnostics on Rust project

From: [email-masked]
Date: Wed, 23 Jul 2025 12:53:27 -0400

I ran a git bisect and found that commit
0ff82eb48725e15bb87a75d4f937b75c2482c59b appears to be the first commit
to exhibit this behavior, although I'm not entirely sure why.  It was a
fix for bug #69809.

My understanding is this: every time flymake recomputes diagnostics
(e.g. on file save), it queries all of the backends in
`flymake-diagnostic-functions' to get new diagnostics.  When it gets new
diagnostics from a backend, it goes through the buffer, removes all the
existing diagnostics produced by that backend, and inserts the new
diagnostics that it just got.

Before running eglot, `flymake-diagnostic-functions' is equal to
(rust-ts-flymake t) (I'm not sure what the significance of the t is).

When you start eglot, this line in `eglot--managed-mode' replaces the
value of flymake-diagnostic-functions:
(eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend))
Then it has flymake refresh its diagnostics.  Since rust-ts-flymake is
no longer in flymake-diagnostic-functions, flymake doesn't request any
new diagnostics from it.  But since flymake only deletes the existing
overlays for a diagnostics backend when that backend provides new
diagnostics, those diagnostics never get removed.

The attached patch fixes this by adding code to flymake-start which goes
through flymake--state and removes all diagnostics and their overlays
from any backend which is no longer in flymake-diagnostic-functions.

I also noticed a related (but less serious) bug here: after opening a
rust buffer, running rust-ts-mode, then eglot, if you stop eglot with
eglot-shutdown or something, all the eglot diagnostics in the buffer are
removed, but the rust-ts-flymake diagnostics don't come back until you
do something to trigger flymake-start (like save the buffer).

Eason Huang <[email-masked]> writes:

> I find out that it is related to `rust-ts-flymake-command`, when
> customize it's value to `'("cargo" "clippy")`, it works as expected.
>
> It works with below rust config:
>
> ```
> (use-package rust-mode
>   :defer t
>   :custom
>   (rust-mode-treesitter-derive (and (fboundp 'treesit-available-p)
>                                     (treesit-available-p)))
>   (rust-ts-flymake-command '("cargo" "clippy")))
>
> ```
>
> May be this information help.

This gets rid of the initial errors from rust-ts-flymake, which may be
spurrious.  I'm not sure.  Maybe this should be the default for
rust-ts-flymake-command?  Either way, I think a patch like what I've
written is necessary to solve this general problem.

>From a2d10fc6d17e1ba3517868a3bb70c162b73bf9f8 Mon Sep 17 00:00:00 2001
From: Matthew Tromp <[email-masked]>
Date: Wed, 23 Jul 2025 12:44:46 -0400
Subject: [PATCH] Delete overlays from backends that no longer exist

Fix for bug#78862: previously, when starting eglot, existing flymake
diagnostics in the buffer would persist until flymake was completely
restarted (like by running flymake-mode twice).  This change adds logic
to flymake-start to delete all diagnostics and their overlays from any
backend that is no longer in flymake-diagnostic-functions.

* lisp/progmodes/flymake.el (flymake-start): remove overlays from
backends that no longer exist
---
 lisp/progmodes/flymake.el | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/lisp/progmodes/flymake.el b/lisp/progmodes/flymake.el
index e911faf603d..bdfcf51a5ff 100644
--- a/lisp/progmodes/flymake.el
+++ b/lisp/progmodes/flymake.el
@@ -1406,6 +1406,26 @@ flymake-start
                            (cl-reduce
                             #'max (mapcar #'cadr flymake--recent-changes))))))
                (setq flymake--recent-changes nil)
+               ;; Delete all overlays that didn't come from one of the
+               ;; current diagnostic functions.
+               ;; Sometimes diagnostic functions are removed from
+               ;; `flymake-diagnostic-functions' (e.g. by eglot).  This
+               ;; leaves overlays in the buffer which otherwise won't be
+               ;; cleaned up until `flymake-mode' is restarted.
+               ;; See bug#78862
+               (maphash (lambda (backend state)
+                          (unless (memq backend flymake-diagnostic-functions)
+                            ;; Delete all overlays
+                            (dolist (diag (flymake--state-diags state))
+                              (let ((ov (flymake--diag-overlay diag)))
+                                (flymake--delete-overlay ov)))
+                            ;; Set the list of diagnostics to nil to
+                            ;; avoid trying to delete them again.
+                            ;; We keep the state object itself around in
+                            ;; case there's still diagnostics in flight,
+                            ;; so we don't break things.
+                            (setf (flymake--state-diags state) nil)))
+                        flymake--state)
                (run-hook-wrapped
                 'flymake-diagnostic-functions
                 (lambda (backend)
-- 
2.31.1