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