diff --git a/CHANGELOG.md b/CHANGELOG.md index a586868..4f29d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ All notable changes of the `phpstan.el` are documented in this file using the [K * Fix the `--tmp-file` copy being created in the system temporary directory when running containerized, where the container cannot see it. * Fix the JSON report being ignored when the container runtime prefixes it with progress output on STDERR, which made every check with `(phpstan-executable . container)` report no errors. * Fix `flycheck-phpstan` silently discarding the fallback warning when PHPStan produced no JSON report, hiding failures such as a broken configuration file behind a clean buffer. +* Fix editor mode detection asking the wrong program for its version. Only the first element of the command line was probed, which is the container runtime for `(phpstan-executable . docker)` / `container` and `php` for a PHAR without the executable bit — so `docker --version` and `php --version` were parsed as PHPStan versions (`d1c06ef`, `Technologies`) and editor mode was silently disabled for every setup except a directly executable `phpstan`. +* `phpstan-version` and `phpstan-editor-mode-available-p` now take the whole command line, as returned by `phpstan-get-executable-and-args`. A bare string is still accepted. `phpstan-version` no longer merges STDERR into the version string, which a container runtime pollutes with its progress report. ## [0.9.0] diff --git a/phpstan.el b/phpstan.el index aadc5f7..8269675 100644 --- a/phpstan.el +++ b/phpstan.el @@ -576,7 +576,7 @@ it returns the value of `SOURCE' as it is." (cond ((funcall (plist-get editor :analyze-original) original-file) (list "--" target-file)) - ((phpstan-editor-mode-available-p (car (phpstan-get-executable-and-args))) + ((phpstan-editor-mode-available-p executable-and-args) ;; A container only sees the project, so the temporary copy ;; has to be created inside it. `:temp-file' puts it in the ;; system temporary directory, which is not mounted. @@ -613,36 +613,71 @@ it returns the value of `SOURCE' as it is." collect (cons (plist-get message :line) (substring-no-properties msg (match-end 0)))))))) -(defun phpstan-version (executable) - "Return the PHPStan version of EXECUTABLE." - (if-let* ((cached-entry (assoc executable phpstan-executable-versions-alist))) - (cdr cached-entry) - (let* ((version (thread-first - (mapconcat #'shell-quote-argument (list executable "--version") " ") - (shell-command-to-string) - (string-trim-right) - (split-string " ") - (last) - (car-safe)))) - (prog1 version - (push (cons executable version) phpstan-executable-versions-alist))))) - -(defun phpstan-editor-mode-available-p (executable) - "Check if the specified PHPStan EXECUTABLE supports editor mode. - -If a cached result for EXECUTABLE exists, it is returned directly. +(defun phpstan--version-output (command) + "Run COMMAND with --version and return its standard output, or NIL. + +STDERR is discarded rather than merged: a container runtime reports its +progress there, and it would otherwise be read as part of the version." + (with-temp-buffer + (let ((status (apply #'process-file (car command) nil (list t nil) nil + (append (cdr command) (list "--version"))))) + (when (eq 0 status) + (buffer-string))))) + +(defun phpstan--version-from-output (output) + "Return the version number reported in OUTPUT, or NIL. + +OUTPUT looks like \"PHPStan - PHP Static Analysis Tool 1.12.33\"." + (when output + (let ((last-line (car (last (split-string (string-trim output) "\n" t))))) + (when last-line + (car (last (split-string last-line " " t))))))) + +(defun phpstan-version (command) + "Return the PHPStan version of COMMAND. + +COMMAND is the command line that runs PHPStan, as returned by +`phpstan-get-executable-and-args'. A bare string is also accepted, and +taken as the name of an executable. + +Passing the whole command line matters: PHPStan is not always the program +being executed. It is `docker' or `container' for a containerized PHPStan, +and `php' for a PHAR without the executable bit, and asking either of those +for its version answers a version that has nothing to do with PHPStan. + +The result is cached in `phpstan-executable-versions-alist', keyed by the +command line, because probing may have to start a container." + (let* ((command (if (listp command) command (list command))) + (cache-key (mapconcat #'identity command " "))) + (if-let* ((cached-entry (assoc cache-key phpstan-executable-versions-alist))) + (cdr cached-entry) + (let ((version (phpstan--version-from-output + (phpstan--version-output command)))) + (prog1 version + (push (cons cache-key version) phpstan-executable-versions-alist)))))) + +(defun phpstan-editor-mode-available-p (command) + "Check if the PHPStan invoked by COMMAND supports editor mode. + +COMMAND is the command line that runs PHPStan, as returned by +`phpstan-get-executable-and-args'. A bare string is also accepted, and +taken as the name of an executable. + +If a cached result for COMMAND exists, it is returned directly. Otherwise, this function attempts to determine support by retrieving -the PHPStan version using `phpstan --version' command." +the PHPStan version using `phpstan --version' command. Support is +assumed to be absent when the version cannot be determined." (pcase phpstan-activate-editor-mode ('enabled t) ('disabled nil) ('nil - (let* ((version (phpstan-version executable))) - (if (string-match-p (eval-when-compile (regexp-quote "-dev@")) version) - t - (pcase (elt version 0) - (?1 (version<= "1.12.27" version)) - (?2 (version<= "2.1.17" version)))))))) + (let ((version (phpstan-version command))) + (when (and version (not (string-empty-p version))) + (if (string-match-p (eval-when-compile (regexp-quote "-dev@")) version) + t + (pcase (elt version 0) + (?1 (version<= "1.12.27" version)) + (?2 (version<= "2.1.17" version))))))))) (defconst phpstan--re-ignore-tag (eval-when-compile