branch: elpa/flycheck
commit 23f1a270b393b5e30799ae187437779e4034bc80
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>
Run syntax checkers over TRAMP connections
Route Flycheck's process layer through TRAMP-aware APIs so command
checkers work in buffers visiting remote files. Spawn checks with
process-file/start-file-process instead of call-process/start-process,
resolve executables on the remote host via executable-find's REMOTE
argument, create temporary files with make-nearby-temp-file, and pass
host-local paths to the checker while mapping reported filenames back to
their remote names. A few checkers building output paths get the same
file-local-name treatment.
Part of the TRAMP support tracked in [#1816].
---
flycheck.el | 133 +++++++++++++++++++++++++++++++++++-------------
test/specs/test-util.el | 34 ++++++++++++-
2 files changed, 130 insertions(+), 37 deletions(-)
diff --git a/flycheck.el b/flycheck.el
index 9ed8e5f617..8d53e1bacf 100644
--- a/flycheck.el
+++ b/flycheck.el
@@ -85,6 +85,7 @@
;; Declare a bunch of dynamic variables that we need from other modes
(defvar sh-shell) ; For shell script checker predicates
(defvar ess-language) ; For r-lintr predicate
+(defvar tramp-remote-process-environment) ; For remote checker env, see below
(defvar markdown-hide-markup) ;
(defvar markdown-fontify-code-block-default-mode) ; For rust-error-explainer
(defvar markdown-fontify-code-blocks-natively) ;
@@ -566,17 +567,20 @@ sandboxes."
(defun flycheck-default-executable-find (executable)
"Resolve EXECUTABLE to a full path.
-Like `executable-find', but supports relative paths.
+Like `executable-find', but supports relative paths, and resolves
+EXECUTABLE on the remote host when `default-directory' is remote,
+so that checkers can run over TRAMP.
Attempts invoking `executable-find' first; if that returns nil,
and EXECUTABLE contains a directory component, expands to a full
path and tries invoking `executable-find' again."
- ;; file-name-directory returns non-nil iff the given path has a
- ;; directory component.
- (or
- (executable-find executable)
- (when (file-name-directory executable)
- (executable-find (expand-file-name executable)))))
+ (let ((remote (file-remote-p default-directory)))
+ ;; file-name-directory returns non-nil iff the given path has a
+ ;; directory component.
+ (or
+ (executable-find executable remote)
+ (when (file-name-directory executable)
+ (executable-find (expand-file-name executable) remote)))))
(defcustom flycheck-indication-mode 'auto
"The indication mode for Flycheck errors.
@@ -1566,6 +1570,19 @@ to a number and return it. Otherwise return nil."
(puthash file (file-truename (directory-file-name file))
flycheck--file-truename-cache)))
+(defun flycheck--expand-file-name (filename directory)
+ "Expand FILENAME against DIRECTORY, honoring a remote DIRECTORY.
+
+Like `expand-file-name', but when DIRECTORY is remote and
+FILENAME is a host-local path -- as a checker running on the
+remote host over TRAMP reports -- the result names the file on
+that host, so it compares against the remote temporary files and
+opens the right file when jumped to."
+ (if-let* ((remote (and (not (file-remote-p filename))
+ (file-remote-p directory))))
+ (concat remote (expand-file-name filename (file-local-name directory)))
+ (expand-file-name filename directory)))
+
(defun flycheck-same-files-p (file-a file-b)
"Determine whether FILE-A and FILE-B refer to the same file.
@@ -1584,8 +1601,12 @@ if they resolve to the same canonical paths."
Use `flycheck-temp-prefix' as prefix, and add the directory to
`flycheck-temporaries'.
-Return the path of the directory"
- (let* ((tempdir (make-temp-file flycheck-temp-prefix 'directory)))
+Return the path of the directory.
+
+The directory is created on the remote host when
+`default-directory' is remote, so that checkers running over
+TRAMP can access it."
+ (let* ((tempdir (make-nearby-temp-file flycheck-temp-prefix 'directory)))
(push tempdir flycheck-temporaries)
tempdir))
@@ -1606,7 +1627,7 @@ Return the path of the file."
(if filename
(expand-file-name (file-name-nondirectory filename)
(flycheck-temp-dir-system))
- (make-temp-file flycheck-temp-prefix nil suffix)))))
+ (make-nearby-temp-file flycheck-temp-prefix nil
suffix)))))
(push tempfile flycheck-temporaries)
tempfile))
@@ -4430,7 +4451,7 @@ Return ERRORS, modified in-place."
(seq-do (lambda (err)
(setf (flycheck-error-filename err)
(if-let (filename (flycheck-error-filename err))
- (expand-file-name filename directory)
+ (flycheck--expand-file-name filename directory)
(buffer-file-name))))
errors)
errors)
@@ -6692,7 +6713,12 @@ from INFILE, and its output is sent to DESTINATION, as in
`call-process'."
(if-let (executable (flycheck-find-checker-executable checker))
(condition-case err
- (apply #'call-process executable infile destination nil args)
+ ;; `process-file' runs EXECUTABLE on the remote host when
+ ;; `default-directory' is remote, and behaves like
+ ;; `call-process' otherwise. The program must be the plain
+ ;; local name on that host.
+ (apply #'process-file (file-local-name executable)
+ infile destination nil args)
(error (when error (signal (car err) (cdr err)))))
(when error
(user-error "Cannot find `%s' using `flycheck-executable-find'"
@@ -6851,30 +6877,44 @@ Note that substitution is *not* recursive. No symbols
or cells
are substituted within the body of cells!"
(pcase arg
((pred stringp) (list arg))
+ ;; File names below are reduced with `file-local-name': the checker
+ ;; process runs on the host of `default-directory' (a remote host
+ ;; over TRAMP) and must receive a plain local name, not a TRAMP file
+ ;; name. The temporary files are created and later deleted through
+ ;; their full (possibly remote) names.
(`source
- (list (flycheck-save-buffer-to-temp #'flycheck-temp-file-system)))
+ (list (file-local-name
+ (flycheck-save-buffer-to-temp #'flycheck-temp-file-system))))
(`source-inplace
- (list (flycheck-save-buffer-to-temp #'flycheck-temp-file-inplace)))
+ (list (file-local-name
+ (flycheck-save-buffer-to-temp #'flycheck-temp-file-inplace))))
(`(source ,suffix)
- (list (flycheck-save-buffer-to-temp
- (lambda (filename) (flycheck-temp-file-system filename suffix)))))
+ (list (file-local-name
+ (flycheck-save-buffer-to-temp
+ (lambda (filename) (flycheck-temp-file-system filename
suffix))))))
(`(source-inplace ,suffix)
- (list (flycheck-save-buffer-to-temp
- (lambda (filename) (flycheck-temp-file-inplace filename suffix)))))
- (`source-original (list (or (buffer-file-name) "")))
- (`temporary-directory (list (flycheck-temp-dir-system)))
+ (list (file-local-name
+ (flycheck-save-buffer-to-temp
+ (lambda (filename) (flycheck-temp-file-inplace filename
suffix))))))
+ (`source-original (list (if-let* ((f (buffer-file-name)))
+ (file-local-name f)
+ "")))
+ (`temporary-directory (list (file-local-name (flycheck-temp-dir-system))))
(`temporary-file-name
(let ((directory (flycheck-temp-dir-system)))
- (list (make-temp-name (expand-file-name "flycheck" directory)))))
+ (list (file-local-name
+ (make-temp-name (expand-file-name "flycheck" directory))))))
(`null-device (list null-device))
(`(config-file ,option-name ,file-name-var)
(when-let* ((value (symbol-value file-name-var))
(file-name (flycheck-locate-config-file value checker)))
- (flycheck-prepend-with-option option-name (list file-name))))
+ (flycheck-prepend-with-option
+ option-name (list (file-local-name file-name)))))
(`(config-file ,option-name ,file-name-var ,prepend-fn)
(when-let* ((value (symbol-value file-name-var))
(file-name (flycheck-locate-config-file value checker)))
- (flycheck-prepend-with-option option-name (list file-name) prepend-fn)))
+ (flycheck-prepend-with-option
+ option-name (list (file-local-name file-name)) prepend-fn)))
(`(option ,option-name ,variable)
(when-let (value (symbol-value variable))
(unless (stringp value)
@@ -6953,7 +6993,11 @@ PROCESS, and terminates standard input with EOF."
"Start a command CHECKER with CALLBACK."
(let (process)
(condition-case err
- (let* ((program (flycheck-find-checker-executable checker))
+ (let* (;; `flycheck-find-checker-executable' may return a remote
+ ;; (TRAMP) file name; the process program must be the plain
+ ;; local name on the remote host
+ (program (file-local-name
+ (flycheck-find-checker-executable checker)))
(args (flycheck-checker-substituted-arguments checker))
(command (flycheck--wrap-command program args))
(sentinel-events nil)
@@ -6967,10 +7011,15 @@ PROCESS, and terminates standard input with EOF."
;; rather than LC_ALL so that the character encoding
;; (LC_CTYPE) is left untouched; using LC_ALL=C forces an
;; ASCII locale that breaks checkers reading UTF-8 input,
- ;; such as hledger (see #2170).
- (process-environment (cons "LC_MESSAGES=C"
process-environment)))
+ ;; such as hledger (see #2170). For remote checks the
+ ;; environment reaches the process through
+ ;; `tramp-remote-process-environment'.
+ (process-environment (cons "LC_MESSAGES=C" process-environment))
+ (tramp-remote-process-environment
+ (when (boundp 'tramp-remote-process-environment)
+ (cons "LC_MESSAGES=C" tramp-remote-process-environment))))
;; We do not associate the process with any buffer, by
- ;; passing nil for the BUFFER argument of `start-process'.
+ ;; passing nil for the BUFFER argument of `start-file-process'.
;; Instead, we just remember the buffer being checked in a
;; process property (see below). This neatly avoids all
;; side-effects implied by attaching a process to a buffer, which
@@ -6978,7 +7027,12 @@ PROCESS, and terminates standard input with EOF."
;;
;; See https://github.com/flycheck/flycheck/issues/298 for an
;; example for such a conflict.
- (setq process (apply 'start-process (format "flycheck-%s" checker)
+ ;;
+ ;; We use `start-file-process' rather than `start-process' so
+ ;; the checker runs on the remote host when `default-directory'
+ ;; is remote; it behaves exactly like `start-process' otherwise.
+ (setq process (apply 'start-file-process
+ (format "flycheck-%s" checker)
nil command))
;; Process sentinels can be called while sending input to the
process.
;; We want to record errors raised by process-send before calling
@@ -7341,8 +7395,11 @@ _CHECKER is ignored."
"Locate a configuration FILENAME in the home directory.
Return the absolute path, if FILENAME exists in the user's home
-directory, or nil otherwise."
- (let ((path (expand-file-name filename "~")))
+directory, or nil otherwise. For a remote buffer, the remote
+user's home directory is searched."
+ (let* ((remote (file-remote-p default-directory))
+ (home (if remote (concat remote "~") "~"))
+ (path (expand-file-name filename home)))
(when (file-exists-p path)
path)))
@@ -7582,7 +7639,7 @@ ERR is in BUFFER-FILES, replace it with the value of
variable
(flycheck-error-with-buffer err
(when-let (filename (flycheck-error-filename err))
(when (seq-some (apply-partially #'flycheck-same-files-p
- (expand-file-name filename cwd))
+ (flycheck--expand-file-name filename
cwd))
buffer-files)
(setf (flycheck-error-filename err) buffer-file-name)
(when (and buffer-file-name (flycheck-error-message err))
@@ -11180,8 +11237,10 @@ See
https://github.com/processing/processing/wiki/Command-Line"
:command ("processing-java" "--force"
;; Don't change the order of these arguments, processing is pretty
;; picky
- (eval (concat "--sketch=" (file-name-directory
(buffer-file-name))))
- (eval (concat "--output=" (flycheck-temp-dir-system)))
+ (eval (concat "--sketch=" (file-local-name
+ (file-name-directory
(buffer-file-name)))))
+ (eval (concat "--output=" (file-local-name
+ (flycheck-temp-dir-system))))
"--build")
:error-patterns
((error line-start (file-name) ":" line ":" column
@@ -11243,7 +11302,8 @@ or `unknown' if not yet detected.")
"Return command arguments for proselint, detecting the version once."
(when (eq flycheck--proselint-use-old-args 'unknown)
(setq flycheck--proselint-use-old-args
- (zerop (call-process
+ ;; Probe on the host the check will run on (remote over TRAMP)
+ (zerop (process-file
(or flycheck-proselint-executable "proselint")
nil nil nil "--version"))))
(if flycheck--proselint-use-old-args
@@ -11278,7 +11338,8 @@ are relative to the file being checked."
See URL `https://developers.google.com/protocol-buffers/'."
:command ("protoc" "--error_format" "gcc"
- (eval (concat "--java_out=" (flycheck-temp-dir-system)))
+ (eval (concat "--java_out=" (file-local-name
+ (flycheck-temp-dir-system))))
;; Add the current directory to resolve imports
(eval (concat "--proto_path="
(file-name-directory (buffer-file-name))))
@@ -12719,7 +12780,7 @@ This syntax checker needs Rust 1.18 or newer. See URL
:command ("rustc"
(option "--crate-type" flycheck-rust-crate-type)
"--emit=metadata"
- "--out-dir" (eval (flycheck-temp-dir-system)) ; avoid creating
binaries
+ "--out-dir" (eval (file-local-name (flycheck-temp-dir-system))) ;
avoid creating binaries
"--error-format=json"
(option-flag "--test" flycheck-rust-check-tests)
(option-list "-L" flycheck-rust-library-path concat)
diff --git a/test/specs/test-util.el b/test/specs/test-util.el
index 03d7841066..28db5ccd40 100644
--- a/test/specs/test-util.el
+++ b/test/specs/test-util.el
@@ -132,7 +132,18 @@
(exec-suffixes '(".program" ".bat"))
(result (flycheck-default-executable-find
(expand-file-name "dir/flycheck-testprog"
temp-dir))))
- (expect result :to-equal program-path))))))
+ (expect result :to-equal program-path)))))
+
+ (describe "on a remote host"
+ (it "resolves the executable on the host of a remote default-directory"
+ ;; `file-remote-p' parses the prefix without connecting, so no
+ ;; live remote is needed to verify the REMOTE argument is passed.
+ (spy-on 'executable-find :and-return-value "/ssh:host:/usr/bin/prog")
+ (let ((default-directory "/ssh:host:/home/user/"))
+ (expect (flycheck-default-executable-find "prog")
+ :to-equal "/ssh:host:/usr/bin/prog")
+ (expect 'executable-find
+ :to-have-been-called-with "prog" "/ssh:host:")))))
(describe "flycheck-string-to-number-safe"
@@ -209,6 +220,27 @@
(it "returns true for an empty list"
(expect (flycheck-symbol-list-p '()) :to-be-truthy)))
+ (describe "flycheck--expand-file-name"
+
+ (it "expands a relative name against a local directory"
+ (expect (flycheck--expand-file-name "foo.py" "/home/user/")
+ :to-equal "/home/user/foo.py"))
+
+ (it "expands a relative name against a remote directory"
+ (expect (flycheck--expand-file-name "foo.py" "/ssh:host:/home/user/")
+ :to-equal "/ssh:host:/home/user/foo.py"))
+
+ (it "grafts the remote prefix onto a host-local absolute path"
+ ;; A checker running on the remote host reports host-local paths;
+ ;; the result must name the file on that host, not locally.
+ (expect (flycheck--expand-file-name "/tmp/foo.py"
"/ssh:host:/home/user/")
+ :to-equal "/ssh:host:/tmp/foo.py"))
+
+ (it "leaves an already-remote filename untouched"
+ (expect (flycheck--expand-file-name
+ "/ssh:host:/tmp/foo.py" "/ssh:host:/home/user/")
+ :to-equal "/ssh:host:/tmp/foo.py")))
+
(describe "flycheck-same-files-p"
(it "returns true for same files"