branch: elpa/flycheck
commit cc1f6037a92b317f13769bfe1e5dd4c88bfc3433
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>
Localize remaining checker paths and helpers for TRAMP
A second review pass found the same remote-path class in more checkers
and in several local-only helpers. Complete the coverage:
- Add flycheck-buffer-file-local-name, the local name of the visited
file (with a fallback for buffers without one), and route the checkers
that pass the file name to the tool through it: rubocop/cookstyle
--stdin, ruff/stylelint --stdin-filename, flake8 --stdin-display-name,
phpcs --stdin-path, phpcs-changed, and the eslint/stylelint
--print-config probes. They previously handed the raw TRAMP path to the
remote process, which broke config/ruleset resolution.
- Add flycheck--process-file-lines, a process-file-based process-lines,
and use it for the helpers reachable on remote buffers:
flycheck-rust-cargo-has-command-p (rust-clippy :enabled),
flycheck-haskell--find-stack-default-directory (:working-directory,
whose result is now named on the remote host), and go-vet :verify.
These ran the local toolchain regardless of where the check executes.
- Key the ghc -outputdir cache by host and create it with
make-nearby-temp-file, so remote ghc gets a directory it can write to.
- Guard flycheck-start-command-checker against a nil executable so a
checker whose program has vanished no longer errors in file-local-name.
Update the flycheck-substitute-argument docstring to recommend the new
helper for stdin-filename flags.
---
flycheck.el | 122 +++++++++++++++++++++++++++++++++++-------------
test/specs/test-util.el | 33 +++++++++++++
2 files changed, 123 insertions(+), 32 deletions(-)
diff --git a/flycheck.el b/flycheck.el
index 78b7397ec8..2632086d95 100644
--- a/flycheck.el
+++ b/flycheck.el
@@ -1609,6 +1609,15 @@ opens the right file when jumped to."
(concat remote (expand-file-name filename (file-local-name directory)))
(expand-file-name filename directory)))
+(defun flycheck-buffer-file-local-name (&optional fallback)
+ "Return the visited file's name as a plain local name.
+
+Strip any remote (TRAMP) prefix with `file-local-name', so a
+checker running on the host of the buffer's file receives a path
+that is valid there. Return FALLBACK when the buffer has no
+backing file."
+ (if buffer-file-name (file-local-name buffer-file-name) fallback))
+
(defun flycheck-same-files-p (file-a file-b)
"Determine whether FILE-A and FILE-B refer to the same file.
@@ -6623,10 +6632,12 @@ of command checkers is `flycheck-sanitize-errors'.
Some checkers that support reading from standard input have
a separate flag to indicate the name of the file whose
contents are being passed on standard input (typically
- `stdin-filename'). In that case, use the `(option)' form in
- `:command' to pass the value of variable `buffer-file-name'
- when the current buffer has a file name (that is,
- use `option \"--stdin-file-name\" buffer-file-name').
+ `stdin-filename'). In that case, use an `(eval)' form in
+ `:command' to pass `flycheck-buffer-file-local-name', which
+ yields the file name the checker's host understands even when
+ the buffer visits a remote file over TRAMP (that is, use
+ `eval (when buffer-file-name (list \"--stdin-file-name\"
+ (flycheck-buffer-file-local-name)))').
For buffers not backed by files, checkers that support input
on stdin typically report a file name like `-' or `<stdin>'.
@@ -6786,6 +6797,27 @@ the error checking automatically."
checker exit-code output))))
(kill-buffer temp))))
+(defun flycheck--process-file-lines (program &rest args)
+ "Execute PROGRAM with ARGS, returning its output as a list of lines.
+
+Like `process-lines', but runs PROGRAM through `process-file', so
+it executes on the host of `default-directory' (a remote host
+over TRAMP) instead of always locally. PROGRAM must be the plain
+local name on that host. Signal an error if PROGRAM cannot be
+found or exits with a non-zero status."
+ (with-temp-buffer
+ (let ((status (apply #'process-file program nil (current-buffer) nil
args)))
+ (unless (eq status 0)
+ (error "%s exited with status %s" program status))
+ (goto-char (point-min))
+ (let (lines)
+ (while (not (eobp))
+ (push (buffer-substring-no-properties
+ (line-beginning-position) (line-end-position))
+ lines)
+ (forward-line 1))
+ (nreverse lines)))))
+
(defun flycheck-checker-arguments (checker)
"Get the command arguments of CHECKER."
(cdr (flycheck-checker-get checker 'command)))
@@ -7032,9 +7064,12 @@ PROCESS, and terminates standard input with EOF."
(condition-case err
(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)))
+ ;; local name on the remote host. It may also return nil (a
+ ;; cached-enabled checker whose executable later vanished), in
+ ;; which case leave it nil, as before, rather than erroring in
+ ;; `file-local-name'.
+ (executable (flycheck-find-checker-executable checker))
+ (program (and executable (file-local-name executable)))
(args (flycheck-checker-substituted-arguments checker))
(command (flycheck--wrap-command program args))
(sentinel-events nil)
@@ -9163,7 +9198,7 @@ about the JSON format of stylelint."
"Whether there is a valid stylelint CHECKER config for the current buffer."
(zerop (flycheck-call-checker-process
checker nil nil nil
- "--print-config" (or buffer-file-name "index.js"))))
+ "--print-config" (flycheck-buffer-file-local-name "index.js"))))
(defun flycheck--stylelint-get-major-version (checker)
"Return major version of stylelint CHECKER."
@@ -9196,7 +9231,8 @@ See URL `https://stylelint.io/'."
(eval flycheck-stylelint-args)
(option-flag "--quiet" flycheck-stylelint-quiet)
(config-file "--config" flycheck-stylelintrc)
- "--stdin-filename" (eval (or (buffer-file-name) "style.css")))
+ "--stdin-filename" (eval (flycheck-buffer-file-local-name
+ "style.css")))
:standard-input t
:verify (lambda (_) (flycheck--stylelint-verify 'css-stylelint))
:error-parser flycheck-parse-stylelint
@@ -10127,7 +10163,8 @@ See URL `https://go.dev/cmd/go/' and URL
:verify (lambda (_)
(let* ((go (flycheck-checker-executable 'go-vet))
(have-vet (member "vet" (ignore-errors
- (process-lines go "tool")))))
+ (flycheck--process-file-lines
+ (file-local-name go) "tool")))))
(list
(flycheck-verification-result-new
:label "go tool vet"
@@ -10408,17 +10445,25 @@ pragma. Each extension is enabled via `-X'."
:safe #'flycheck-string-list-p
:package-version '(flycheck . "0.19"))
-(defvar flycheck-haskell-ghc-cache-directory nil
- "The cache directory for `ghc' output.")
+(defvar flycheck-haskell-ghc-cache-directory (make-hash-table :test 'equal)
+ "The cache directory for `ghc' output, keyed by host.
+The key is the remote identifier of `default-directory' (see
+`file-remote-p'), or nil for the local host, since `ghc' runs on
+the host of the checked buffer and needs a cache directory there.")
(defun flycheck-haskell-ghc-cache-directory ()
"Get the cache location for `ghc' output.
-If no cache directory exists yet, create one and return it.
-Otherwise return the previously used cache directory."
- (setq flycheck-haskell-ghc-cache-directory
- (or flycheck-haskell-ghc-cache-directory
- (make-temp-file "flycheck-haskell-ghc-cache" 'directory))))
+If no cache directory exists yet for the current host, create one
+on that host and return it. Otherwise return the previously used
+cache directory."
+ (let ((host (file-remote-p default-directory)))
+ (or (gethash host flycheck-haskell-ghc-cache-directory)
+ ;; `make-nearby-temp-file' creates the directory on the host of
+ ;; `default-directory', so `ghc' running there can write to it.
+ (puthash host
+ (make-nearby-temp-file "flycheck-haskell-ghc-cache"
'directory)
+ flycheck-haskell-ghc-cache-directory))))
(defun flycheck--locate-dominating-file-matching (directory regexp)
"Search for a file in directory hierarchy starting at DIRECTORY.
@@ -10442,10 +10487,15 @@ directory returned by \"stack path --project-root\"."
(rx "stack" (* any) "." (or "yml" "yaml") eos)))
(when-let* ((stack (funcall flycheck-executable-find "stack"))
(output (ignore-errors
- (process-lines stack
- "--no-install-ghc"
- "path" "--project-root")))
- (stack-dir (car output)))
+ (flycheck--process-file-lines
+ (file-local-name stack)
+ "--no-install-ghc"
+ "path" "--project-root")))
+ (root (car output))
+ ;; `stack' reports a host-local path; name it on the host of
+ ;; `default-directory' so it is usable as the remote working
+ ;; directory (and checked on the right host below).
+ (stack-dir (flycheck--expand-file-name root default-directory)))
(and (file-directory-p stack-dir) stack-dir))))
(defun flycheck-haskell--ghc-find-default-directory (_checker)
@@ -10464,7 +10514,8 @@ See URL `https://github.com/commercialhaskell/stack'."
(option "--stack-yaml" flycheck-ghc-stack-project-file)
(option-flag "--nix" flycheck-ghc-stack-use-nix)
"ghc" "--" "-Wall" "-no-link"
- "-outputdir" (eval (flycheck-haskell-ghc-cache-directory))
+ "-outputdir" (eval (file-local-name
+ (flycheck-haskell-ghc-cache-directory)))
(option-list "-X" flycheck-ghc-language-extensions concat)
(option-list "-i" flycheck-ghc-search-path concat)
(eval (concat
@@ -10519,7 +10570,8 @@ See URL `https://github.com/commercialhaskell/stack'."
See URL `https://www.haskell.org/ghc/'."
:command ("ghc" "-Wall" "-no-link"
- "-outputdir" (eval (flycheck-haskell-ghc-cache-directory))
+ "-outputdir" (eval (file-local-name
+ (flycheck-haskell-ghc-cache-directory)))
(option-flag "-no-user-package-db"
flycheck-ghc-no-user-package-database)
(option-list "-package-db" flycheck-ghc-package-databases)
@@ -10679,7 +10731,7 @@ for more information about the custom directories."
"Whether there is a valid eslint config for the current buffer."
(zerop (flycheck-call-checker-process
'javascript-eslint nil nil nil
- "--print-config" (or buffer-file-name "index.js"))))
+ "--print-config" (flycheck-buffer-file-local-name "index.js"))))
(defun flycheck--eslint-handle-suspicious (_checker exit-status output)
"Disable the checker when eslint cannot lint at all.
@@ -11225,8 +11277,8 @@ See URL
`https://pear.php.net/package/PHP_CodeSniffer/'."
;; Pass original file name to phpcs. We need to concat explicitly
;; here, because phpcs really insists to get option and argument as
;; a single command line argument :|
- (eval (when (buffer-file-name)
- (concat "--stdin-path=" (buffer-file-name))))
+ (eval (when buffer-file-name
+ (concat "--stdin-path="
(flycheck-buffer-file-local-name))))
;; Read from standard input
"-")
:standard-input t
@@ -11259,7 +11311,7 @@ See URL `https://github.com/sirbrillig/phpcs-changed'."
"--git-base" (eval flycheck-phpcs-changed-git-base)
"--git-unstaged"
(option "--standard=" flycheck-phpcs-standard concat)
- (eval (buffer-file-name)))
+ (eval (flycheck-buffer-file-local-name)))
:standard-input t
:error-parser flycheck-parse-checkstyle
:error-filter
@@ -11688,7 +11740,8 @@ Requires Flake8 3.0 or newer. See URL
(option "--max-line-length" flycheck-flake8-maximum-line-length nil
flycheck-option-int)
(eval (when buffer-file-name
- (concat "--stdin-display-name=" buffer-file-name)))
+ (concat "--stdin-display-name="
+ (flycheck-buffer-file-local-name))))
"-")
:standard-input t
:working-directory flycheck-python-find-project-root
@@ -11735,7 +11788,8 @@ See URL `https://docs.astral.sh/ruff/'."
(config-file "--config" flycheck-python-ruff-config)
;; older versions of ruff (before 0.2) used "text" instead of
"concise"
"--output-format=concise"
- (option "--stdin-filename" buffer-file-name)
+ (eval (when buffer-file-name
+ (list "--stdin-filename"
(flycheck-buffer-file-local-name))))
"-")
:standard-input t
:error-filter (lambda (errors)
@@ -12460,7 +12514,7 @@ See URL `https://rubocop.org/'."
;; from standard input, but it chokes when that name is the empty
;; string, so fall back to "stdin" in order to handle buffers with
;; no backing file (e.g. org-mode snippet buffers)
- "--stdin" (eval (or (buffer-file-name) "stdin")))
+ "--stdin" (eval (flycheck-buffer-file-local-name "stdin")))
:standard-input t
:working-directory #'flycheck-ruby--find-project-root
:error-patterns flycheck-ruby-rubocop-error-patterns
@@ -12486,7 +12540,7 @@ See URL `https://github.com/chef/cookstyle'."
;; from standard input, but it chokes when that name is the empty
;; string, so fall back to "stdin" in order to handle buffers with
;; no backing file (e.g. org-mode snippet buffers)
- "--stdin" (eval (or (buffer-file-name) "stdin")))
+ "--stdin" (eval (flycheck-buffer-file-local-name "stdin")))
:standard-input t
:working-directory #'flycheck-ruby--find-project-root
:error-patterns flycheck-ruby-rubocop-error-patterns
@@ -12739,7 +12793,11 @@ Execute `cargo --list' to find out whether COMMAND is
present."
(member command
(mapcar (lambda (line)
(car (split-string (string-trim line))))
- (ignore-errors (process-lines cargo "--list"))))))
+ ;; Probe on the host the check will run on (remote over
+ ;; TRAMP), where cargo was resolved.
+ (ignore-errors
+ (flycheck--process-file-lines
+ (file-local-name cargo) "--list"))))))
(defun flycheck-rust-valid-crate-type-p (crate-type)
"Whether CRATE-TYPE is a valid target type for Cargo.
diff --git a/test/specs/test-util.el b/test/specs/test-util.el
index 28db5ccd40..f20c4bcc88 100644
--- a/test/specs/test-util.el
+++ b/test/specs/test-util.el
@@ -241,6 +241,39 @@
"/ssh:host:/tmp/foo.py" "/ssh:host:/home/user/")
:to-equal "/ssh:host:/tmp/foo.py")))
+ (describe "flycheck-buffer-file-local-name"
+
+ (it "returns a local file name unchanged"
+ (flycheck-buttercup-with-temp-buffer
+ (setq buffer-file-name "/home/user/foo.py")
+ (expect (flycheck-buffer-file-local-name) :to-equal
"/home/user/foo.py")))
+
+ (it "strips the remote prefix from a TRAMP file name"
+ (flycheck-buttercup-with-temp-buffer
+ (setq buffer-file-name "/ssh:host:/home/user/foo.py")
+ (expect (flycheck-buffer-file-local-name)
+ :to-equal "/home/user/foo.py")))
+
+ (it "returns the fallback when there is no backing file"
+ (flycheck-buttercup-with-temp-buffer
+ (setq buffer-file-name nil)
+ (expect (flycheck-buffer-file-local-name "stdin") :to-equal "stdin")
+ (expect (flycheck-buffer-file-local-name) :to-be nil))))
+
+ (describe "flycheck--process-file-lines"
+
+ (it "returns the program output as a list of lines"
+ (spy-on 'process-file :and-call-fake
+ (lambda (_program _infile buffer &rest _)
+ (with-current-buffer buffer (insert "line1\nline2\n"))
+ 0))
+ (expect (flycheck--process-file-lines "prog" "arg")
+ :to-equal '("line1" "line2")))
+
+ (it "signals an error on a non-zero exit status"
+ (spy-on 'process-file :and-return-value 1)
+ (expect (flycheck--process-file-lines "prog") :to-throw)))
+
(describe "flycheck-same-files-p"
(it "returns true for same files"