|
36 | 36 | ;; |
37 | 37 | ;; (add-hook 'php-mode-hook 'my-php-mode-setup) |
38 | 38 | ;; |
| 39 | +;; ## For Lisp maintainers |
| 40 | +;; |
| 41 | +;; This is a generic checker (`flycheck-define-generic-checker'), not a command |
| 42 | +;; checker (`flycheck-define-checker'). A command checker takes its executable |
| 43 | +;; from the car of `:command', which must be a literal string, overridable only |
| 44 | +;; through the single string variable `flycheck-CHECKER-executable'. PHPStan |
| 45 | +;; does not fit that shape: `phpstan-executable' may expand to a whole command |
| 46 | +;; line such as `docker run --rm -v ...', and it is chosen per project. So we |
| 47 | +;; drive the process ourselves and build the command from `phpstan-executable'. |
39 | 48 |
|
40 | 49 | ;;; Code: |
| 50 | +(require 'cl-lib) |
41 | 51 | (require 'flycheck) |
42 | 52 | (require 'phpstan) |
43 | 53 |
|
44 | | -;; Usually it is defined dynamically by flycheck |
45 | | -(defvar flycheck-phpstan-executable) |
46 | 54 | (defvar flycheck-phpstan--temp-buffer-name "*Flycheck PHPStan*") |
47 | | -(defvar flycheck-phpstan--output-filter-added nil) |
48 | 55 | (defconst flycheck-phpstan--nofiles-message (eval-when-compile (regexp-quote "[ERROR] No files found to analyse."))) |
49 | 56 |
|
50 | 57 | (defcustom flycheck-phpstan-ignore-metadata-list nil |
|
59 | 66 | :safe #'stringp |
60 | 67 | :group 'phpstan) |
61 | 68 |
|
62 | | -(defun flycheck-phpstan--suppress-no-files-error (next checker exit-status files output callback cwd) |
63 | | - "Suppress Flycheck errors if PHPStan reports no files in a modified buffer. |
64 | | -
|
65 | | -This function is intended to be used as an :around advice for |
66 | | -`flycheck-finish-checker-process'. |
67 | | -
|
68 | | -It prevents Flycheck from displaying an error when: |
69 | | -- CHECKER is `phpstan', |
70 | | -- the current buffer is modified, |
71 | | -- and OUTPUT contains the message `flycheck-phpstan--nofiles-message'. |
72 | | -
|
73 | | -NEXT, EXIT-STATUS, FILES, OUTPUT, CALLBACK, and CWD are the original arguments |
74 | | -passed to `flycheck-finish-checker-process'." |
75 | | - (unless (and (eq checker 'phpstan) |
76 | | - (buffer-modified-p) |
77 | | - (string-match-p flycheck-phpstan--nofiles-message output)) |
78 | | - (funcall next checker exit-status files output callback cwd))) |
79 | | - |
80 | | -(defun flycheck-phpstan--enabled-and-set-variable () |
81 | | - "Return path to phpstan configure file, and set buffer execute in side effect." |
82 | | - (let ((enabled (phpstan-enabled))) |
83 | | - (prog1 enabled |
84 | | - (unless flycheck-phpstan--output-filter-added |
85 | | - (advice-add 'flycheck-finish-checker-process |
86 | | - :around #'flycheck-phpstan--suppress-no-files-error) |
87 | | - (setq flycheck-phpstan--output-filter-added t)) |
88 | | - (when (and enabled |
89 | | - phpstan-flycheck-auto-set-executable |
90 | | - (null (bound-and-true-p flycheck-phpstan-executable)) |
91 | | - (or (stringp phpstan-executable) |
92 | | - (eq 'docker phpstan-executable) |
93 | | - (and (eq 'root (car-safe phpstan-executable)) |
94 | | - (stringp (cdr-safe phpstan-executable))) |
95 | | - (and (stringp (car-safe phpstan-executable)) |
96 | | - (listp (cdr-safe phpstan-executable))) |
97 | | - (null phpstan-executable))) |
98 | | - (setq-local flycheck-phpstan-executable (car (phpstan-get-executable-and-args))))))) |
99 | | - |
| 69 | +;; Parsing PHPStan output: |
100 | 70 | (defun flycheck-phpstan-parse-output (output &optional _checker _buffer) |
101 | 71 | "Parse PHPStan errors from OUTPUT." |
102 | 72 | (let* ((json-buffer (with-current-buffer (flycheck-phpstan--temp-buffer) |
@@ -143,21 +113,171 @@ passed to `flycheck-finish-checker-process'." |
143 | 113 | "Return non-NIL if ORIGINAL is non-NIL and buffer is not modified." |
144 | 114 | (and original (not (buffer-modified-p)))) |
145 | 115 |
|
146 | | -(flycheck-define-checker phpstan |
| 116 | +;; Running PHPStan: |
| 117 | +(defun flycheck-phpstan--command () |
| 118 | + "Return the whole PHPStan command line to check the current buffer. |
| 119 | +
|
| 120 | +This has the side effect of saving the buffer to a temporary file, which is |
| 121 | +registered in `flycheck-temporaries' for later deletion." |
| 122 | + (phpstan-get-command-args |
| 123 | + :include-executable t |
| 124 | + :format "json" |
| 125 | + :editor (list |
| 126 | + :analyze-original #'flycheck-phpstan-analyze-original |
| 127 | + :original-file buffer-file-name |
| 128 | + :temp-file (lambda () (flycheck-save-buffer-to-temp #'flycheck-temp-file-system)) |
| 129 | + :inplace (lambda () (flycheck-save-buffer-to-temp #'flycheck-temp-file-inplace))))) |
| 130 | + |
| 131 | +(defun flycheck-phpstan--start (checker callback) |
| 132 | + "Start CHECKER, reporting the result to CALLBACK. |
| 133 | +
|
| 134 | +Return the process, which Flycheck hands back to `:interrupt'." |
| 135 | + (let (process) |
| 136 | + (condition-case err |
| 137 | + (let ((command (flycheck-phpstan--command)) |
| 138 | + ;; `flycheck-phpstan--nofiles-message' matches PHPStan's English |
| 139 | + ;; output, so keep the checker process in the C locale. Only |
| 140 | + ;; LC_MESSAGES is set, to leave the encoding of the source alone. |
| 141 | + (process-environment (cons "LC_MESSAGES=C" process-environment))) |
| 142 | + (setq process |
| 143 | + (make-process |
| 144 | + :name (format "flycheck-%s" checker) |
| 145 | + ;; Do not associate a buffer, to avoid the side effects of |
| 146 | + ;; attaching a process to the buffer being checked. |
| 147 | + :buffer nil |
| 148 | + :command command |
| 149 | + :noquery t |
| 150 | + :connection-type 'pipe |
| 151 | + :filter #'flycheck-phpstan--receive-output |
| 152 | + :sentinel #'flycheck-phpstan--handle-signal)) |
| 153 | + (process-put process 'flycheck-phpstan-checker checker) |
| 154 | + (process-put process 'flycheck-phpstan-callback callback) |
| 155 | + (process-put process 'flycheck-phpstan-buffer (current-buffer)) |
| 156 | + ;; Flycheck binds `default-directory' to `:working-directory' around |
| 157 | + ;; this function, so remember it for resolving relative file names. |
| 158 | + (process-put process 'flycheck-phpstan-cwd default-directory) |
| 159 | + ;; Track the temporaries in the process itself, to get rid of the |
| 160 | + ;; buffer-local state as soon as possible. |
| 161 | + (process-put process 'flycheck-phpstan-temporaries flycheck-temporaries) |
| 162 | + (setq flycheck-temporaries nil) |
| 163 | + process) |
| 164 | + (error |
| 165 | + (flycheck-safe-delete-temporaries) |
| 166 | + ;; Deleting the process triggers the sentinel, which deletes the |
| 167 | + ;; temporary files of the process anyway. |
| 168 | + (when process |
| 169 | + (delete-process process)) |
| 170 | + (signal (car err) (cdr err)))))) |
| 171 | + |
| 172 | +(defun flycheck-phpstan--interrupt (_checker process) |
| 173 | + "Interrupt PROCESS." |
| 174 | + ;; Deleting the process always triggers the sentinel, which does the cleanup. |
| 175 | + (when process |
| 176 | + (delete-process process))) |
| 177 | + |
| 178 | +(defun flycheck-phpstan--receive-output (process output) |
| 179 | + "Accumulate OUTPUT of the PHPStan PROCESS for later parsing." |
| 180 | + (process-put process 'flycheck-phpstan-pending-output |
| 181 | + (cons output (process-get process 'flycheck-phpstan-pending-output)))) |
| 182 | + |
| 183 | +(defun flycheck-phpstan--get-output (process) |
| 184 | + "Return the complete output of the PHPStan PROCESS." |
| 185 | + (with-demoted-errors "Error while retrieving process output: %S" |
| 186 | + (apply #'concat (nreverse (process-get process 'flycheck-phpstan-pending-output))))) |
| 187 | + |
| 188 | +(defun flycheck-phpstan--handle-signal (process _event) |
| 189 | + "Handle a signal from the PHPStan PROCESS. |
| 190 | +
|
| 191 | +_EVENT is ignored." |
| 192 | + (when (memq (process-status process) '(signal exit)) |
| 193 | + (let ((files (process-get process 'flycheck-phpstan-temporaries)) |
| 194 | + (buffer (process-get process 'flycheck-phpstan-buffer)) |
| 195 | + (callback (process-get process 'flycheck-phpstan-callback)) |
| 196 | + (cwd (process-get process 'flycheck-phpstan-cwd))) |
| 197 | + (mapc #'flycheck-safe-delete files) |
| 198 | + (when (buffer-live-p buffer) |
| 199 | + (with-current-buffer buffer |
| 200 | + (condition-case err |
| 201 | + (pcase (process-status process) |
| 202 | + (`signal |
| 203 | + (funcall callback 'interrupted)) |
| 204 | + (`exit |
| 205 | + (flycheck-phpstan--finish |
| 206 | + (process-get process 'flycheck-phpstan-checker) |
| 207 | + (process-exit-status process) |
| 208 | + files |
| 209 | + (flycheck-phpstan--get-output process) |
| 210 | + callback cwd))) |
| 211 | + ((debug error) |
| 212 | + (funcall callback 'errored (error-message-string err))))))))) |
| 213 | + |
| 214 | +(defun flycheck-phpstan--finish (checker exit-status files output callback cwd) |
| 215 | + "Parse OUTPUT of CHECKER and report the result to CALLBACK. |
| 216 | +
|
| 217 | +EXIT-STATUS is the exit status of the PHPStan process. FILES is the list of |
| 218 | +temporary files given to PHPStan, used to map reported file names back onto |
| 219 | +the buffer. Relative file names are resolved against CWD. |
| 220 | +
|
| 221 | +CALLBACK is always invoked with a status that finishes the syntax check, |
| 222 | +because Flycheck gets stuck on the current check otherwise." |
| 223 | + (if (and (buffer-modified-p) |
| 224 | + (string-match-p flycheck-phpstan--nofiles-message output)) |
| 225 | + ;; PHPStan found nothing to analyse because the buffer is being edited. |
| 226 | + ;; That is not a result worth showing, but the check must still finish. |
| 227 | + (funcall callback 'finished nil) |
| 228 | + (let ((errors (flycheck-phpstan-parse-output output checker (current-buffer)))) |
| 229 | + (when (and (not (equal exit-status 0)) (null errors)) |
| 230 | + ;; Warn about a suspicious result, but keep going: `suspicious' does |
| 231 | + ;; not finish the syntax check on its own. |
| 232 | + (funcall callback 'suspicious |
| 233 | + (format "Flycheck checker %S returned %S, but its output \ |
| 234 | +contained no errors: %s\nTry installing a more recent version of PHPStan, and \ |
| 235 | +please open a bug report if the issue persists in the latest release. Thanks!" |
| 236 | + checker exit-status output))) |
| 237 | + (funcall callback 'finished |
| 238 | + ;; Fix error file names, by substituting them backwards from the |
| 239 | + ;; temporaries. |
| 240 | + (mapcar (lambda (e) (flycheck-fix-error-filename e files cwd)) |
| 241 | + errors))))) |
| 242 | + |
| 243 | +(defun flycheck-phpstan--verify (_checker) |
| 244 | + "Verify the PHPStan setup of the current buffer." |
| 245 | + (let* ((executable-and-args (ignore-errors (phpstan-get-executable-and-args))) |
| 246 | + (program (car executable-and-args)) |
| 247 | + (found (and program |
| 248 | + (if (file-name-absolute-p program) |
| 249 | + (and (file-executable-p program) program) |
| 250 | + (executable-find program)))) |
| 251 | + (config-file (phpstan-get-config-file))) |
| 252 | + (list |
| 253 | + (flycheck-verification-result-new |
| 254 | + :label "executable" |
| 255 | + :message (cond (found (format "Found at %s" found)) |
| 256 | + (program (format "%s not found" program)) |
| 257 | + (t "Not found")) |
| 258 | + :face (if found 'success '(bold error))) |
| 259 | + (flycheck-verification-result-new |
| 260 | + :label "command" |
| 261 | + :message (if executable-and-args |
| 262 | + (mapconcat #'shell-quote-argument executable-and-args " ") |
| 263 | + "Not available") |
| 264 | + :face (if executable-and-args 'success 'warning)) |
| 265 | + (flycheck-verification-result-new |
| 266 | + :label "configuration file" |
| 267 | + :message (if config-file (format "Found at %S" config-file) "Not found") |
| 268 | + :face (if config-file 'success 'warning))))) |
| 269 | + |
| 270 | +(flycheck-define-generic-checker 'phpstan |
147 | 271 | "PHP static analyzer based on PHPStan." |
148 | | - :command ("php" |
149 | | - (eval |
150 | | - (phpstan-get-command-args |
151 | | - :format "json" |
152 | | - :editor (list |
153 | | - :analyze-original #'flycheck-phpstan-analyze-original |
154 | | - :original-file buffer-file-name |
155 | | - :temp-file (lambda () (flycheck-save-buffer-to-temp #'flycheck-temp-file-system)) |
156 | | - :inplace (lambda () (flycheck-save-buffer-to-temp #'flycheck-temp-file-inplace)))))) |
| 272 | + :start #'flycheck-phpstan--start |
| 273 | + :interrupt #'flycheck-phpstan--interrupt |
| 274 | + :verify #'flycheck-phpstan--verify |
| 275 | + ;; `flycheck-define-checker' installs this filter for command checkers, but |
| 276 | + ;; generic checkers default to `identity'. |
| 277 | + :error-filter #'flycheck-sanitize-errors |
157 | 278 | :working-directory (lambda (_) (phpstan-get-working-dir)) |
158 | | - :enabled (lambda () (flycheck-phpstan--enabled-and-set-variable)) |
159 | | - :error-parser flycheck-phpstan-parse-output |
160 | | - :modes (php-mode php-ts-mode phps-mode)) |
| 279 | + :enabled (lambda () (phpstan-enabled)) |
| 280 | + :modes '(php-mode php-ts-mode phps-mode)) |
161 | 281 |
|
162 | 282 | (add-to-list 'flycheck-checkers 'phpstan t) |
163 | 283 | (flycheck-add-next-checker 'php 'phpstan) |
|
0 commit comments