branch: elpa/flycheck
commit ed0f9b546130fa367dcef8b3be396dc9f62d8b94
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>
[Fix #1787] Add :handle-suspicious property for command checkers
When a checker exits with a non-zero code without producing any
parsable errors, Flycheck reports a cryptic "suspicious state"
warning and eventually disables the checker. Tool-wrapper checkers
like flycheck-eldev know how to make sense of such failures (missing
dependencies, broken -pkg.el files), so give them a hook to translate
the output into regular errors shown in the buffer.
Based on the patch from #1795.
Co-authored-by: Paul Pogonyshev <[email protected]>
---
CHANGES.rst | 5 ++++
flycheck.el | 59 +++++++++++++++++++++++++++++++-------
test/specs/test-command-checker.el | 43 +++++++++++++++++++++++++++
test/specs/test-helpers.el | 18 ++++++++++++
4 files changed, 114 insertions(+), 11 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index a5a3d50744..ff83cca0bd 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,6 +2,11 @@
=======================
- Drop support for Emacs 27; Flycheck now requires Emacs 28.1 or newer.
+- [#1787]: Add the ``:handle-suspicious`` property to command checkers.
+ It lets a checker translate a "suspicious state" (a non-zero exit
+ status with no parsable errors, e.g. a missing dependency) into
+ regular errors shown in the buffer, instead of the generic
+ suspicious-state warning.
- [#2161]: Fix the ``org-lint`` checker erroring out on Emacs 31, where
``org-lint`` reports line numbers as strings.
- [#2174]: Fix the ``haskell-ghc`` and ``haskell-stack-ghc`` checkers
diff --git a/flycheck.el b/flycheck.el
index 7b2c55f45e..168b2d5f0f 100644
--- a/flycheck.el
+++ b/flycheck.el
@@ -5893,6 +5893,23 @@ of command checkers is `flycheck-sanitize-errors'.
`flycheck-parse-with-patterns'. In this case,
`:error-patterns' is mandatory.
+`:handle-suspicious FUNCTION'
+ A function to handle suspicious state: when the process
+ returns non-zero code, but no standard errors (i.e. using
+ `:error-patterns' or `:error-parser') are found.
+
+ The function is called with three arguments: CHECKER,
+ EXIT-STATUS and OUTPUT (as string) with the checked buffer
+ as current. It should process the output and return a list
+ of non-standard errors that best describe what exactly has
+ failed. The returned errors go through `:error-filter' just
+ like regular parsed errors. If the function cannot make
+ sense of the output, it should return symbol `suspicious' to
+ indicate that what has happened is really not expected.
+
+ This property is optional. If omitted, such state is always
+ treated as suspicious.
+
`:standard-input t'
Whether to send the buffer contents on standard input.
@@ -5943,6 +5960,7 @@ default `:verify' function of command checkers."
(patterns (plist-get properties :error-patterns))
(parser (or (plist-get properties :error-parser)
#'flycheck-parse-with-patterns))
+ (handle-suspicious (plist-get properties :handle-suspicious))
(enabled (plist-get properties :enabled))
(standard-input (plist-get properties :standard-input)))
(unless command
@@ -5980,10 +5998,11 @@ default `:verify' function of command checkers."
(car p)))
patterns)))
(pcase-dolist (`(,prop . ,value)
- `((command . ,command)
- (error-parser . ,parser)
- (error-patterns . ,patterns)
- (standard-input . ,standard-input)))
+ `((command . ,command)
+ (error-parser . ,parser)
+ (error-patterns . ,patterns)
+ (handle-suspicious . ,handle-suspicious)
+ (standard-input . ,standard-input)))
(setf (flycheck-checker-get symbol prop) value)))))
(eval-and-compile
@@ -6479,16 +6498,22 @@ Parse the OUTPUT and report an appropriate error status.
Resolve all errors in OUTPUT using CWD as working directory."
(let ((errors (flycheck-parse-output output checker (current-buffer))))
(when (and (not (equal exit-status 0)) (null errors))
- ;; Warn about a suspicious result from the syntax checker. We do right
- ;; after parsing the errors, before filtering, because a syntax checker
- ;; might report errors from other files (e.g. includes) even if there
- ;; are no errors in the file being checked.
- (funcall callback 'suspicious
- (format "Flycheck checker %S returned %S, but \
+ ;; Give the checker a chance to recover from suspicious state:
+ ;; exit status is nonzero, but there are no errors.
+ (let ((recovered (flycheck-handle-suspicious-state checker exit-status
+ output)))
+ (if (listp recovered)
+ (setf errors recovered)
+ ;; Warn about a suspicious result from the syntax checker. We do
+ ;; right after parsing the errors, before filtering, because a syntax
+ ;; checker might report errors from other files (e.g. includes) even
+ ;; if there are no errors in the file being checked.
+ (funcall callback 'suspicious
+ (format "Flycheck checker %S returned %S, but \
its output contained no errors: %s\nTry installing a more \
recent version of %S, and please open a bug report if the issue \
persists in the latest release. Thanks!" checker exit-status
-output checker)))
+output checker)))))
(funcall callback 'finished
;; Fix error file names, by substituting them backwards from the
;; temporaries.
@@ -6877,6 +6902,15 @@ CHECKER. BUFFER is the buffer which was checked.
Return the errors parsed with the error patterns of CHECKER."
(funcall (flycheck-checker-get checker 'error-parser) output checker buffer))
+(defun flycheck-handle-suspicious-state (checker exit-status output)
+ "Handle suspicious state of given CHECKER.
+EXIT-STATUS and OUTPUT are passed to `:handle-suspicious'
+function of the CHECKER, if any."
+ (if-let* ((handle-suspicious
+ (flycheck-checker-get checker 'handle-suspicious)))
+ (funcall handle-suspicious checker exit-status output)
+ 'suspicious))
+
(defun flycheck-fix-error-filename (err buffer-files cwd)
"Fix the file name of ERR from BUFFER-FILES.
@@ -7448,6 +7482,7 @@ SYMBOL with `flycheck-def-executable-var'."
(let ((command (plist-get properties :command))
(parser (plist-get properties :error-parser))
(filter (plist-get properties :error-filter))
+ (handle-suspicious (plist-get properties :handle-suspicious))
(explainer (plist-get properties :error-explainer))
(predicate (plist-get properties :predicate))
(enabled-fn (plist-get properties :enabled))
@@ -7464,6 +7499,8 @@ SYMBOL with `flycheck-def-executable-var'."
:error-patterns ',(plist-get properties :error-patterns)
,@(when filter
`(:error-filter #',filter))
+ ,@(when handle-suspicious
+ `(:handle-suspicious #',handle-suspicious))
,@(when explainer
`(:error-explainer #',explainer))
:modes ',(plist-get properties :modes)
diff --git a/test/specs/test-command-checker.el
b/test/specs/test-command-checker.el
index da48c10c25..cba1fc89cd 100644
--- a/test/specs/test-command-checker.el
+++ b/test/specs/test-command-checker.el
@@ -128,6 +128,49 @@
(expect (shut-up (flycheck-buttercup-should-syntax-check-in-buffer))
:to-throw 'flycheck-buttercup-suspicious-checker))))
+ (it "reports errors returned by the :handle-suspicious function"
+ (assume (or (executable-find "python3") (executable-find "python")))
+ (cl-letf* ((flycheck-checker 'suspicious-exit)
+ ((symbol-plist 'suspicious-exit)
+ flycheck-test--suspicious-exit)
+ ((flycheck-checker-get 'suspicious-exit 'handle-suspicious)
+ (lambda (checker exit-status output)
+ (list (flycheck-error-new-at
+ 1 1 'error
+ (format "exited with %d: %s" exit-status
+ (string-trim output))
+ :checker checker)))))
+ (flycheck-buttercup-with-temp-buffer
+ (suspicious-exit-mode)
+ (insert "hello\n")
+ (flycheck-buttercup-should-syntax-check-in-buffer
+ '(1 1 error "exited with 1: tool exploded"
+ :checker suspicious-exit)))))
+
+ (it "stays suspicious when the :handle-suspicious function declines"
+ (assume (or (executable-find "python3") (executable-find "python")))
+ (cl-letf* ((flycheck-checker 'suspicious-exit)
+ ((symbol-plist 'suspicious-exit)
+ flycheck-test--suspicious-exit)
+ ((flycheck-checker-get 'suspicious-exit 'handle-suspicious)
+ (lambda (_checker _exit-status _output) 'suspicious)))
+ (flycheck-buttercup-with-temp-buffer
+ (suspicious-exit-mode)
+ (insert "hello\n")
+ (expect (shut-up (flycheck-buttercup-should-syntax-check-in-buffer))
+ :to-throw 'flycheck-buttercup-suspicious-checker))))
+
+ (it "treats a non-zero exit without errors as suspicious by default"
+ (assume (or (executable-find "python3") (executable-find "python")))
+ (cl-letf* ((flycheck-checker 'suspicious-exit)
+ ((symbol-plist 'suspicious-exit)
+ flycheck-test--suspicious-exit))
+ (flycheck-buttercup-with-temp-buffer
+ (suspicious-exit-mode)
+ (insert "hello\n")
+ (expect (shut-up (flycheck-buttercup-should-syntax-check-in-buffer))
+ :to-throw 'flycheck-buttercup-suspicious-checker))))
+
(it "forces English messages without overriding the character set"
;; LC_MESSAGES=C gives English output, while LC_ALL is left alone so
;; that the subprocess keeps the user's LC_CTYPE (see #2170).
diff --git a/test/specs/test-helpers.el b/test/specs/test-helpers.el
index 9ac1424247..a9ac73768d 100644
--- a/test/specs/test-helpers.el
+++ b/test/specs/test-helpers.el
@@ -181,6 +181,24 @@ The manifest path is relative to
(setf (symbol-plist 'print-locale) nil)
+;;; Suspicious-state test helpers (for command-checker tests)
+
+(define-derived-mode suspicious-exit-mode prog-mode "suspicious")
+
+(flycheck-define-command-checker 'suspicious-exit
+ "Exit with a non-zero code without producing parsable errors."
+ :command `(,(or (executable-find "python3") "python")
+ "-c" "import sys; print('tool exploded'); sys.exit(1)")
+ :error-patterns '((error bol "never-matches:" line ":" (message)))
+ :modes '(suspicious-exit-mode))
+
+(defconst flycheck-test--suspicious-exit
+ (symbol-plist 'suspicious-exit)
+ "Saved plist for the suspicious-exit checker.")
+
+(setf (symbol-plist 'suspicious-exit) nil)
+
+
;;; Custom error level for error-level tests
(flycheck-define-error-level 'test-level