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

    [Fix #1129] Drop eslint's blocking config probe
    
    The :enabled test of javascript-eslint ran eslint --print-config
    synchronously before the first check in every buffer, freezing Emacs
    for its duration - the single biggest complaint in #1129.  Diagnose a
    missing configuration from the check's own asynchronous output instead:
    :handle-suspicious learns a new return value, disable, which disables
    the checker in the buffer quietly, like a failing :enabled test.  On
    projects with a config the probe cost disappears entirely.
---
 CHANGES.rst                             |  10 +++
 flycheck.el                             | 133 +++++++++++++++++++++++++-------
 test/specs/languages/test-javascript.el |  19 +++++
 test/specs/test-command-checker.el      |  88 +++++++++++++++++++++
 4 files changed, 224 insertions(+), 26 deletions(-)

diff --git a/CHANGES.rst b/CHANGES.rst
index ae2df861e2..b53880ff66 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,6 +2,16 @@
 =======================
 
 - Drop support for Emacs 27; Flycheck now requires Emacs 28.1 or newer.
+- [#1129]: ``javascript-eslint`` no longer probes for a configuration
+  file with a blocking ``--print-config`` call before the first check in
+  every buffer, which used to freeze Emacs for its duration (the
+  interactive ``flycheck-verify-setup`` still probes).  A fatal eslint
+  failure (a missing or broken configuration) is now diagnosed from the
+  check's own exit status: the checker disables itself in the buffer
+  with an echo-area notice naming the reason, and a fallback checker
+  takes over on the next automatic check.  Checker authors can use the
+  same mechanism by returning ``disable`` or ``(disable . reason)`` from
+  ``:handle-suspicious``.
 - A new syntax check now interrupts a still-running one and starts
   immediately, instead of waiting for it to finish and displaying its
   stale results in the meantime.  This makes slow checkers (cargo, mypy
diff --git a/flycheck.el b/flycheck.el
index 9cfed98509..67943f401f 100644
--- a/flycheck.el
+++ b/flycheck.el
@@ -3520,6 +3520,16 @@ optional DATA.  STATUS may be one of the following 
symbols:
      user needs to be informed about.  DATA is an optional
      message.
 
+`self-disabled'
+     The syntax checker diagnosed itself as inapplicable to the
+     buffer, e.g. a linter without a configuration file.  The
+     checker is disabled in the buffer like a failing `:enabled'
+     test, and checker selection is re-run so that another
+     checker can take over.  DATA is an optional reason string
+     for the echo-area notice.
+
+     This report finishes the current syntax check.
+
 A syntax checker _must_ report a status at least once with any
 symbol that finishes the current syntax checker.  Otherwise
 Flycheck gets stuck with the current syntax check.
@@ -3550,6 +3560,26 @@ discarded."
                (message "Suspicious state from syntax checker %s: %s"
                         checker (or data "UNKNOWN!")))
              (flycheck-report-status 'suspicious))
+            (`self-disabled
+             (when flycheck-mode
+               ;; Disable the checker like a failing `:enabled' test.  A
+               ;; fallback checker is selected on the next automatic
+               ;; check, uniformly for all buffers.  We deliberately don't
+               ;; force a fallback in this very cycle: doing so has to
+               ;; route through the automatic-check gates (which refuse
+               ;; e.g. read-only buffers) and re-runs earlier chain
+               ;; members, for a marginal gain over the next idle tick.
+               (cl-pushnew checker flycheck--automatically-disabled-checkers)
+               (message
+                (substitute-command-keys
+                 "Flycheck: %s disabled itself in this buffer%s; \
+\\[universal-argument] \\[flycheck-disable-checker] re-enables it")
+                checker (if data (format " (%s)" data) ""))
+               ;; Complete the check without running the disabled
+               ;; checker's own `:next-checkers'
+               (flycheck-finish-current-syntax-check
+                nil (flycheck-syntax-check-working-directory syntax-check)
+                'no-next)))
             (`finished
              (when flycheck-mode
                ;; Only report errors from the checker if Flycheck Mode is
@@ -3561,13 +3591,14 @@ discarded."
              (error "Unknown status %s from syntax checker %s"
                     status checker))))))))
 
-(defun flycheck-finish-current-syntax-check (errors working-dir)
+(defun flycheck-finish-current-syntax-check (errors working-dir &optional 
no-next)
   "Finish the current syntax-check in the current buffer with ERRORS.
 
 ERRORS is a list of `flycheck-error' objects reported by the
 current syntax check in `flycheck-current-syntax-check'.
 
-Report all ERRORS and potentially start any next syntax checkers.
+Report all ERRORS and, unless NO-NEXT is non-nil, potentially
+start any next syntax checkers.
 
 If the current syntax checker reported excessive errors, they are
 truncated or discarded via `flycheck--handle-excessive-errors',
@@ -3584,7 +3615,8 @@ WORKING-DIR."
                    working-dir))))
     (flycheck-report-current-errors
      (flycheck--handle-excessive-errors checker errors))
-    (let ((next-checker (flycheck-get-next-checker-for-buffer checker)))
+    (let ((next-checker (unless no-next
+                          (flycheck-get-next-checker-for-buffer checker))))
       (if next-checker
           (flycheck-start-current-syntax-check next-checker)
         (setq flycheck-current-syntax-check nil)
@@ -6496,9 +6528,22 @@ of command checkers is `flycheck-sanitize-errors'.
      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.
+     like regular parsed errors.
+
+     The function may also return symbol `disable', or a cons
+     cell `(disable . REASON)' with a reason string, when the
+     output shows that the checker doesn't apply to this buffer
+     at all, e.g. a linter reporting that it has no configuration
+     file: the checker is then disabled in the buffer like a
+     failing `:enabled' test, with an echo-area notice including
+     REASON, and checker selection re-runs so a fallback checker
+     can take over.  This avoids probing for applicability with a
+     blocking process call in `:enabled'; the asynchronous check
+     itself serves as the probe.
+
+     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.
@@ -7090,28 +7135,42 @@ 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))
-      ;; 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 \
+    (let ((self-disabled nil))
+      (when (and (not (equal exit-status 0)) (null errors))
+        ;; 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)))
+          (cond
+           ((or (eq recovered 'disable)
+                (and (consp recovered) (eq (car recovered) 'disable)))
+            ;; The checker diagnosed itself as inapplicable to this
+            ;; buffer, e.g. a linter without a configuration file.  The
+            ;; status report performs the disabling, subject to the usual
+            ;; staleness checks, and finishes the syntax check.
+            (setq self-disabled t)
+            (funcall callback 'self-disabled
+                     (and (consp recovered) (cdr recovered))))
+           ((listp recovered)
+            (setf errors recovered))
+           (t
+            ;; 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)))))
-    (funcall callback 'finished
-             ;; Fix error file names, by substituting them backwards from the
-             ;; temporaries.
-             (mapcar (lambda (e) (flycheck-fix-error-filename e files cwd))
-                      errors))))
+output checker))))))
+      (unless self-disabled
+        (funcall callback 'finished
+                 ;; Fix error file names, by substituting them backwards
+                 ;; from the temporaries.
+                 (mapcar (lambda (e) (flycheck-fix-error-filename e files cwd))
+                         errors))))))
 
 
 ;;; Executables of command checkers.
@@ -10417,6 +10476,24 @@ for more information about the custom directories."
           'javascript-eslint nil nil nil
           "--print-config" (or buffer-file-name "index.js"))))
 
+(defun flycheck--eslint-handle-suspicious (_checker exit-status output)
+  "Disable the checker when eslint cannot lint at all.
+
+Eslint exits with status 2 on any fatal failure -- a missing or
+broken configuration, a crashing plugin -- rather than lint
+findings, so the checker cannot be used in this buffer.  This
+matches the semantics of the blocking `--print-config' probe that
+previous versions ran in `:enabled' (see URL
+`https://github.com/flycheck/flycheck/issues/1129'), and doesn't
+depend on the wording of any particular eslint version.  The
+first line of OUTPUT is included in the disable notice.
+
+Any other exit status without parsable errors is suspicious: it
+suggests an output format Flycheck fails to parse."
+  (if (eq exit-status 2)
+      (cons 'disable (car (split-string output "\n" t)))
+    'suspicious))
+
 (defun flycheck-parse-eslint (output checker buffer)
   "Parse ESLint errors/warnings from JSON OUTPUT.
 
@@ -10470,7 +10547,11 @@ See URL `https://eslint.org/'."
             "--stdin" "--stdin-filename" source-original)
   :standard-input t
   :error-parser flycheck-parse-eslint
-  :enabled (lambda () (flycheck-eslint-config-exists-p))
+  ;; A missing eslint config is diagnosed from the check's own output
+  ;; (see `flycheck--eslint-handle-suspicious') instead of a blocking
+  ;; `--print-config' probe in `:enabled', which used to freeze Emacs on
+  ;; the first check in every buffer
+  :handle-suspicious flycheck--eslint-handle-suspicious
   :modes (js-mode js-jsx-mode js2-mode js2-jsx-mode js3-mode rjsx-mode
                   typescript-mode js-ts-mode typescript-ts-mode tsx-ts-mode)
   :working-directory flycheck-eslint--find-working-directory
diff --git a/test/specs/languages/test-javascript.el 
b/test/specs/languages/test-javascript.el
index 72d5c27e7f..0bc52ad04f 100644
--- a/test/specs/languages/test-javascript.el
+++ b/test/specs/languages/test-javascript.el
@@ -73,6 +73,25 @@
                                         :end-line 5
                                         :end-column 2))))))
 
+  (describe "Fatal-failure handling"
+    (it "disables the checker on a fatal eslint exit"
+      ;; Exit status 2 is eslint's fatal-error status, used for missing
+      ;; and broken configurations alike, in every eslint version
+      (expect (flycheck--eslint-handle-suspicious
+               'javascript-eslint 2
+               "ESLint couldn't find an eslint.config.(js|mjs|cjs) file.
+Some more explanation here.")
+              :to-equal
+              '(disable
+                . "ESLint couldn't find an eslint.config.(js|mjs|cjs) file.")))
+
+    (it "stays suspicious on unparsable lint results"
+      ;; Exit status 1 means eslint found lint problems; reaching the
+      ;; suspicious handler then means we failed to parse them
+      (expect (flycheck--eslint-handle-suspicious
+               'javascript-eslint 1 "[{\"unexpected\": \"format\"}]")
+              :to-be 'suspicious)))
+
   (describe "Checker tests"
     (flycheck-buttercup-def-checker-test javascript-eslint javascript error
       (let ((inhibit-message t))
diff --git a/test/specs/test-command-checker.el 
b/test/specs/test-command-checker.el
index 3ace0e88ba..e8a8cdd504 100644
--- a/test/specs/test-command-checker.el
+++ b/test/specs/test-command-checker.el
@@ -184,6 +184,94 @@ afterwards."
           (expect (shut-up (flycheck-buttercup-should-syntax-check-in-buffer))
                   :to-throw 'flycheck-buttercup-suspicious-checker))))
 
+    (it "disables the checker when the :handle-suspicious function says so"
+      (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) 'disable)))
+        (flycheck-buttercup-with-temp-buffer
+          (suspicious-exit-mode)
+          (insert "hello\n")
+          ;; The check finishes cleanly with no errors and no suspicious
+          ;; warning, and the checker is disabled like a failed :enabled
+          (flycheck-buttercup-should-syntax-check-in-buffer)
+          (expect (flycheck-automatically-disabled-checker-p 'suspicious-exit)
+                  :to-be-truthy))))
+
+    (it "selects the fallback checker after a self-disable"
+      (assume (or (executable-find "python3") (executable-find "python")))
+      (flycheck-buttercup-with-temp-buffer
+        (rename-buffer "flycheck-self-disable-test")
+        (cl-letf* (((symbol-plist 'suspicious-exit)
+                    flycheck-test--suspicious-exit)
+                   ((flycheck-checker-get 'suspicious-exit
+                                          'handle-suspicious)
+                    (lambda (_checker _exit-status _output) 'disable))
+                   ((symbol-function 'get-buffer-window)
+                    (lambda (&rest _) (selected-window))))
+          (flycheck-define-generic-checker 'fallback-checker
+            "Report a fixed warning synchronously."
+            :start (lambda (checker callback)
+                     (funcall callback 'finished
+                              (list (flycheck-error-new-at
+                                     1 1 'warning "fallback ran"
+                                     :checker checker))))
+            :modes '(suspicious-exit-mode))
+          (unwind-protect
+              (let ((flycheck-checkers '(suspicious-exit fallback-checker)))
+                (suspicious-exit-mode)
+                (insert "hello\n")
+                (flycheck-mode)
+                (shut-up (flycheck-buttercup-buffer-sync))
+                ;; The self-disabled checker is out of the running...
+                (expect (flycheck-automatically-disabled-checker-p
+                         'suspicious-exit)
+                        :to-be-truthy)
+                ;; ...so the next check picks the fallback checker
+                (expect (flycheck-get-checker-for-buffer)
+                        :to-be 'fallback-checker)
+                (shut-up (flycheck-buttercup-buffer-sync))
+                (expect (mapcar #'flycheck-error-message
+                                flycheck-current-errors)
+                        :to-equal '("fallback ran")))
+            (setf (symbol-plist 'fallback-checker) nil)))))
+
+    (it "does not run the chain of a self-disabled checker"
+      (assume (or (executable-find "python3") (executable-find "python")))
+      (flycheck-buttercup-with-temp-buffer
+        (rename-buffer "flycheck-chain-skip-test")
+        (cl-letf* (((symbol-plist 'suspicious-exit)
+                    flycheck-test--suspicious-exit)
+                   ((flycheck-checker-get 'suspicious-exit
+                                          'handle-suspicious)
+                    (lambda (_checker _exit-status _output) 'disable))
+                   ((flycheck-checker-get 'suspicious-exit 'next-checkers)
+                    '(chain-target))
+                   ((symbol-function 'get-buffer-window)
+                    (lambda (&rest _) (selected-window))))
+          (flycheck-define-generic-checker 'chain-target
+            "Report a fixed error synchronously."
+            :start (lambda (checker callback)
+                     (funcall callback 'finished
+                              (list (flycheck-error-new-at
+                                     1 1 'error "chain ran"
+                                     :checker checker))))
+            :modes '(suspicious-exit-mode))
+          (unwind-protect
+              (let ((flycheck-checkers '(suspicious-exit)))
+                (suspicious-exit-mode)
+                (insert "hello\n")
+                (flycheck-mode)
+                (shut-up (flycheck-buttercup-buffer-sync))
+                ;; The disabled checker's own chain must not run
+                (expect (flycheck-automatically-disabled-checker-p
+                         'suspicious-exit)
+                        :to-be-truthy)
+                (expect flycheck-current-errors :not :to-be-truthy))
+            (setf (symbol-plist 'chain-target) nil)))))
+
     (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)

Reply via email to