From 86a567af426db6e75976cfe29979f59425cb8da7 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Fri, 17 Jul 2026 22:01:19 +0900 Subject: [PATCH] Ask PHPStan for its version, not the program that launches it `phpstan-get-command-args' probed editor mode support with (phpstan-editor-mode-available-p (car (phpstan-get-executable-and-args))) but the car of that command line is only PHPStan itself when PHPStan is directly executable. Everywhere else it is whatever launches PHPStan, and that program answered for its own version: * `(phpstan-executable . docker)' and `container' probed the runtime, so `Docker version 29.5.3, build d1c06ef' parsed as version "d1c06ef"; * a PHAR without the executable bit runs as ("php" "..."), so PHP's `--version' banner parsed as version "Technologies". Neither starts with "1" or "2", so `phpstan-editor-mode-available-p' returned nil and editor mode was silently off for every setup but one. Pass the whole command line down and run `COMMAND... --version', which reaches the real PHPStan in each case. Both `phpstan-version' and `phpstan-editor-mode-available-p' still accept a bare string. `phpstan-version' also captured STDERR, because `shell-command-to-string' merges it. Apple container prints its progress there, so it would have been read as part of the version; capture only STDOUT. Probing may now have to start a container (measured at ~0.4s warm for Docker), so the cache is keyed by the whole command line rather than the executable. For a plain executable that key is unchanged. Support is now treated as absent when the version cannot be determined, rather than signalling from `elt' on an empty string. --- CHANGELOG.md | 2 ++ phpstan.el | 87 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 63 insertions(+), 26 deletions(-) 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