branch: elpa/flycheck
commit f56c1227b3fe8db3d75feb100ac37e85f534753c
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>

    Check remote buffers by default with a narrower cadence
    
    global-flycheck-mode now enables Flycheck in buffers visiting remote
    files; the old refusal in flycheck-may-enable-mode is gone. Since each
    remote check spawns a process over the network, the new
    flycheck-check-syntax-automatically-remote option narrows the automatic
    triggers for remote buffers to save and mode-enabled by default,
    avoiding a remote process on every keystroke. Set it to t to check
    remote buffers on the same events as local ones.
    
    Part of the TRAMP support tracked in [#1816].
---
 flycheck.el                           |  48 +++++++++++++---
 test/specs/test-automatic-checking.el |  33 ++++++++++-
 test/specs/test-tramp.el              | 101 ++++++++++++++++++++++++++++++++++
 3 files changed, 173 insertions(+), 9 deletions(-)

diff --git a/flycheck.el b/flycheck.el
index 8d53e1bacf..333419edfd 100644
--- a/flycheck.el
+++ b/flycheck.el
@@ -874,6 +874,32 @@ If nil, never check syntax automatically.  In this case, 
use
   :package-version '(flycheck . "0.12")
   :safe #'flycheck-symbol-list-p)
 
+(defcustom flycheck-check-syntax-automatically-remote '(save mode-enabled)
+  "When Flycheck should check syntax automatically in remote buffers.
+
+Like `flycheck-check-syntax-automatically', but used for buffers
+visiting remote files (see `file-remote-p').  Checking a remote
+buffer spawns a process on the remote host over TRAMP, which is
+slow, so the change-driven triggers (`idle-change', `new-line',
+`idle-buffer-switch') are excluded by default and remote buffers
+are only checked on `save' and `mode-enabled'.
+
+Set to the symbol t to check remote buffers on the same events as
+local ones, i.e. to use `flycheck-check-syntax-automatically'
+unchanged.  A manual \\[flycheck-buffer] always works regardless
+of this option."
+  :group 'flycheck
+  :type '(choice
+          (const :tag "Same as local buffers" t)
+          (set (const :tag "After the buffer was saved" save)
+               (const :tag "After the buffer was changed and idle" idle-change)
+               (const
+                :tag "After switching the current buffer" idle-buffer-switch)
+               (const :tag "After a new line was inserted" new-line)
+               (const :tag "After `flycheck-mode' was enabled" mode-enabled)))
+  :package-version '(flycheck . "38")
+  :safe (lambda (value) (or (eq value t) (flycheck-symbol-list-p value))))
+
 (defcustom flycheck-idle-change-delay 0.5
   "How many seconds to wait after a change before checking syntax.
 
@@ -3792,10 +3818,16 @@ under at least one of them, according to
   (and (not (or buffer-read-only (flycheck-ephemeral-buffer-p)))
        (file-exists-p default-directory)
        (or (not conditions)
-           (seq-some
-            (lambda (condition)
-              (memq condition flycheck-check-syntax-automatically))
-            conditions))))
+           (let ((allowed
+                  ;; Remote buffers use a narrower set of trigger events by
+                  ;; default, since each check spawns a slow remote process
+                  (if (and (file-remote-p default-directory)
+                           (not (eq flycheck-check-syntax-automatically-remote
+                                    t)))
+                      flycheck-check-syntax-automatically-remote
+                    flycheck-check-syntax-automatically)))
+             (seq-some (lambda (condition) (memq condition allowed))
+                       conditions)))))
 
 (defvar-local flycheck--idle-trigger-timer nil
   "Timer used to trigger a syntax check after an idle delay.")
@@ -4004,9 +4036,11 @@ Flycheck mode is not enabled for
 - Flycheck's own error message buffer,
 - ephemeral buffers (see `flycheck-ephemeral-buffer-p'),
 - encrypted buffers (see `flycheck-encrypted-buffer-p'),
-- remote files (see `file-remote-p'),
 - and major modes excluded by `flycheck-global-modes'.
 
+Remote files (see `file-remote-p') are checked like local ones;
+command checkers run on the remote host over TRAMP.
+
 Return non-nil if Flycheck mode may be enabled, and nil
 otherwise."
   (and (pcase flycheck-global-modes
@@ -4019,9 +4053,7 @@ otherwise."
                 (eq (get major-mode 'mode-class) 'special)
                 (derived-mode-p 'flycheck-error-message-mode)
                 (flycheck-ephemeral-buffer-p)
-                (flycheck-encrypted-buffer-p)
-                (and (buffer-file-name)
-                     (file-remote-p (buffer-file-name) 'method))))))
+                (flycheck-encrypted-buffer-p)))))
 
 (defun flycheck-mode-on-safe ()
   "Enable command `flycheck-mode' if it is safe to do so.
diff --git a/test/specs/test-automatic-checking.el 
b/test/specs/test-automatic-checking.el
index 44d614f8fb..132becdda9 100644
--- a/test/specs/test-automatic-checking.el
+++ b/test/specs/test-automatic-checking.el
@@ -42,7 +42,38 @@
             (expect (seq-every-p #'flycheck-may-check-automatically
                                  flycheck-check-syntax-automatically)
                     :to-be-truthy)
-            (expect (flycheck-may-check-automatically) :to-be-truthy))))))
+            (expect (flycheck-may-check-automatically) :to-be-truthy)))))
+
+    (describe "in remote buffers"
+      ;; `file-remote-p' parses the prefix without connecting, so faking
+      ;; it is enough to exercise the remote cadence without a live host.
+
+      (it "narrows to the remote trigger set by default"
+        (flycheck-buttercup-with-resource-buffer "automatic-check-dummy.el"
+          (spy-on 'file-remote-p :and-return-value "/ssh:host:")
+          (expect (flycheck-may-check-automatically 'save) :to-be-truthy)
+          (expect (flycheck-may-check-automatically 'mode-enabled) 
:to-be-truthy)
+          (expect (flycheck-may-check-automatically 'idle-change)
+                  :not :to-be-truthy)
+          (expect (flycheck-may-check-automatically 'new-line)
+                  :not :to-be-truthy)))
+
+      (it "honors flycheck-check-syntax-automatically-remote"
+        (flycheck-buttercup-with-resource-buffer "automatic-check-dummy.el"
+          (spy-on 'file-remote-p :and-return-value "/ssh:host:")
+          (let ((flycheck-check-syntax-automatically-remote '(idle-change)))
+            (expect (flycheck-may-check-automatically 'idle-change)
+                    :to-be-truthy)
+            (expect (flycheck-may-check-automatically 'save)
+                    :not :to-be-truthy))))
+
+      (it "uses the local trigger set when set to t"
+        (flycheck-buttercup-with-resource-buffer "automatic-check-dummy.el"
+          (spy-on 'file-remote-p :and-return-value "/ssh:host:")
+          (let ((flycheck-check-syntax-automatically-remote t)
+                (flycheck-check-syntax-automatically '(idle-change)))
+            (expect (flycheck-may-check-automatically 'idle-change)
+                    :to-be-truthy))))))
 
   (describe "flycheck-check-syntax-automatically"
 
diff --git a/test/specs/test-tramp.el b/test/specs/test-tramp.el
new file mode 100644
index 0000000000..e2cbcdcc58
--- /dev/null
+++ b/test/specs/test-tramp.el
@@ -0,0 +1,101 @@
+;;; test-tramp.el --- Flycheck Specs: Remote checking over TRAMP -*- 
lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Flycheck contributors
+
+;; This file is not part of GNU Emacs.
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Specs exercising Flycheck over a TRAMP connection.  They use TRAMP's
+;; "mock" method, which runs a local shell through the full remote
+;; file-name handler stack, so `process-file', `start-file-process',
+;; `make-nearby-temp-file' and remote `executable-find' all take their
+;; remote code paths against localhost -- no real remote host needed.
+
+;;; Code:
+
+(require 'flycheck-buttercup)
+(require 'tramp)
+(require 'python)
+
+(defconst flycheck-test-tramp-remote-prefix "/mock:localhost:"
+  "TRAMP prefix of the local mock connection used by these specs.")
+
+(defun flycheck-test-tramp-setup-method ()
+  "Register TRAMP's mock method, running a local shell as the remote."
+  (add-to-list 'tramp-methods
+               '("mock"
+                 (tramp-login-program "sh")
+                 (tramp-login-args (("-i")))
+                 (tramp-remote-shell "/bin/sh")
+                 (tramp-remote-shell-args ("-c"))
+                 (tramp-connection-timeout 10)))
+  (add-to-list 'tramp-default-host-alist '("\\`mock\\'" nil "localhost")))
+
+(defun flycheck-test-tramp-connectable-p ()
+  "Return non-nil if a mock TRAMP connection can be established."
+  (ignore-errors
+    (let ((default-directory flycheck-test-tramp-remote-prefix))
+      (file-exists-p (concat flycheck-test-tramp-remote-prefix "/")))))
+
+(describe "Remote syntax checking over TRAMP"
+  (before-all
+    (flycheck-test-tramp-setup-method))
+
+  (after-each
+    (ignore-errors (tramp-cleanup-all-connections)))
+
+  (it "checks a remote buffer end to end and maps error filenames back"
+    ;; Skip (rather than fail) when the mock connection can't be brought
+    ;; up or python3 is unavailable -- these are environment issues, not
+    ;; regressions.  Everything past the assumes is a hard assertion.
+    (assume (flycheck-test-tramp-connectable-p) "no mock TRAMP connection")
+    (let ((default-directory flycheck-test-tramp-remote-prefix))
+      (assume (executable-find "python3" t) "python3 not on remote"))
+    (let* ((local (make-temp-file "flycheck-tramp-" nil ".py"
+                                  "import os\n\ndef broken(:\n    pass\n"))
+           (remote (concat flycheck-test-tramp-remote-prefix local))
+           (buf (find-file-noselect remote)))
+      (unwind-protect
+          (with-current-buffer buf
+            (expect (file-remote-p default-directory) :to-be-truthy)
+            ;; Flycheck must be willing to run here now.
+            (expect (flycheck-may-enable-mode) :to-be-truthy)
+            (python-mode)
+            (let ((flycheck-checkers '(python-pycompile))
+                  (flycheck-check-syntax-automatically nil)
+                  (done nil)
+                  (deadline (+ 30 (float-time))))
+              (add-hook 'flycheck-after-syntax-check-hook
+                        (lambda () (setq done t)) nil t)
+              (flycheck-mode)
+              (flycheck-buffer)
+              (while (and (not done) (< (float-time) deadline))
+                (accept-process-output nil 0.2))
+              (expect done :to-be-truthy)
+              (expect flycheck-current-errors :to-be-truthy)
+              (let ((err (car flycheck-current-errors)))
+                (expect (flycheck-error-level err) :to-be 'error)
+                ;; The filename reported by the remote checker is mapped
+                ;; back to the full remote name, not a bare local path.
+                (expect (flycheck-error-filename err)
+                        :to-equal remote))))
+        (kill-buffer buf)
+        (ignore-errors (delete-file local))))))
+
+(provide 'test-tramp)
+
+;;; test-tramp.el ends here

Reply via email to