diff --git a/README.org b/README.org index f75efff..0c09935 100644 --- a/README.org +++ b/README.org @@ -5,9 +5,14 @@ #+END_HTML Emacs interface to [[https://github.com/phpstan/phpstan][PHPStan]], includes checker for [[http://www.flycheck.org/en/latest/][Flycheck]]. ** Support version -- Emacs 25+ +- Emacs 26+ - PHPStan latest/dev-master (NOT support 0.9 seriese) - PHP 7.1+ or Docker runtime + +> [!TIP] +> This package provides support for the [editor mode](https://phpstan.org/user-guide/editor-mode) that will be added in PHPStan 2.1.17 and 1.12.27. +> **We strongly recommend that you always update to the latest PHPStan.** + ** How to install *** Install from MELPA 1. If you have not set up MELPA, see [[https://melpa.org/#/getting-started][Getting Started - MELPA]]. diff --git a/flycheck-phpstan.el b/flycheck-phpstan.el index c8ba4ab..c96837e 100644 --- a/flycheck-phpstan.el +++ b/flycheck-phpstan.el @@ -77,6 +77,11 @@ passed to `flycheck-finish-checker-process'." (string-match-p flycheck-phpstan--nofiles-message output)) (funcall next checker exit-status files output callback cwd))) +(defcustom flycheck-phpstan-fallback-to-original-analysis-if-editor-mode-unavailable t + "If non-NIL, analyze the original file when PHPStan editor mode is unavailable." + :type 'boolean + :safe #'booleanp) + (defun flycheck-phpstan--enabled-and-set-variable () "Return path to phpstan configure file, and set buffer execute in side effect." (let ((enabled (phpstan-enabled))) @@ -139,13 +144,23 @@ passed to `flycheck-finish-checker-process'." nil 'error text :filename file)))) +(defun flycheck-phpstan-analyze-original (original) + "Return non-NIL if ORIGINAL is NIL, fallback is enabled, and buffer is modified." + (and (null original) + flycheck-phpstan-fallback-to-original-analysis-if-editor-mode-unavailable + (buffer-modified-p))) + (flycheck-define-checker phpstan "PHP static analyzer based on PHPStan." - :command ("php" (eval (phpstan-get-command-args :format "json")) - (eval (if (or (buffer-modified-p) (not buffer-file-name)) - (phpstan-normalize-path - (flycheck-save-buffer-to-temp #'flycheck-temp-file-inplace)) - buffer-file-name))) + :command ("php" + (eval + (phpstan-get-command-args + :format "json" + :editor (list + :analyze-original #'flycheck-phpstan-analyze-original + :original-file buffer-file-name + :temp-file (lambda () (flycheck-save-buffer-to-temp #'flycheck-temp-file-system)) + :inplace (lambda () (flycheck-save-buffer-to-temp #'flycheck-temp-file-inplace)))))) :working-directory (lambda (_) (phpstan-get-working-dir)) :enabled (lambda () (flycheck-phpstan--enabled-and-set-variable)) :error-parser flycheck-phpstan-parse-output diff --git a/flymake-phpstan.el b/flymake-phpstan.el index 3f1c5d2..efa0cf1 100644 --- a/flymake-phpstan.el +++ b/flymake-phpstan.el @@ -52,6 +52,11 @@ :type 'boolean :group 'flymake-phpstan) +(defcustom flycheck-phpstan-fallback-to-original-analysis-if-editor-mode-unavailable t + "If non-NIL, analyze the original file when PHPStan editor mode is unavailable." + :type 'boolean + :safe #'booleanp) + (defvar-local flymake-phpstan--proc nil) (defun flymake-phpstan-make-process (root command-args report-fn source) @@ -88,6 +93,17 @@ (kill-buffer (process-buffer proc)))) (code (user-error "PHPStan error (exit status: %s)" code))))))) +(defun flymake-phpstan-analyze-original (original) + "Return non-NIL if ORIGINAL is NIL, fallback is enabled, and buffer is modified." + (and (null original) + flymake-phpstan-fallback-to-original-analysis-if-editor-mode-unavailable + (buffer-modified-p))) + +(defun flymake-phpstan--create-temp-file () + "Create temp file and return the path." + (phpstan-normalize-path + (flymake-proc-init-create-temp-buffer-copy 'flymake-proc-create-temp-inplace))) + (defun flymake-phpstan (report-fn &rest _ignored-args) "Flymake backend for PHPStan report using REPORT-FN." (let ((command-args (phpstan-get-command-args :include-executable t))) @@ -95,14 +111,18 @@ (user-error "Cannot find a phpstan executable command")) (when (process-live-p flymake-phpstan--proc) (kill-process flymake-phpstan--proc)) - (let ((source (current-buffer)) - (target-path (if (or (buffer-modified-p) (not buffer-file-name)) - (phpstan-normalize-path - (flycheck-save-buffer-to-temp #'flycheck-temp-file-inplace)) - buffer-file-name))) + (let* ((source (current-buffer)) + (args (phpstan-get-command-args + :include-executable t + :format "raw" + :editor (list + :analyze-original #'flymake-phpstan-analyze-original + :original-file buffer-file-name + :temp-file #'flymake-phpstan--create-temp-file + :inplace #'flymake-phpstan--create-temp-file)))) (save-restriction (widen) - (setq flymake-phpstan--proc (flymake-phpstan-make-process (php-project-get-root-dir) (append command-args (list "--" target-path)) report-fn source)) + (setq flymake-phpstan--proc (flymake-phpstan-make-process (php-project-get-root-dir) args report-fn source)) (process-send-region flymake-phpstan--proc (point-min) (point-max)) (process-send-eof flymake-phpstan--proc))))) @@ -115,7 +135,7 @@ (flymake-mode 1) (when flymake-phpstan-disable-c-mode-hooks (remove-hook 'flymake-diagnostic-functions #'flymake-cc t)) - (add-hook 'flymake-diagnostic-functions #'flymake-phpstan nil t)))) + (add-hook 'flymake-diagnostic-functions #'flymake-phpstan nil 'local)))) (provide 'flymake-phpstan) ;;; flymake-phpstan.el ends here diff --git a/phpstan.el b/phpstan.el index d080adb..f514121 100644 --- a/phpstan.el +++ b/phpstan.el @@ -166,11 +166,20 @@ have unexpected behaviors or performance implications." "Lists identifiers prohibited from being added to @phpstan-ignore tags." :type '(repeat string)) +(defcustom phpstan-activate-editor-mode nil + "Controls how PHPStan's editor mode is activated." + :local t + :type '(choice (const :tag "Automatic (based on version)" nil) + (const :tag "Editor mode will be actively enabled, regardless of the PHPStan version." 'enabled) + (const :tag "Editor mode will be explicitly disabled." 'disabled))) + (defvar-local phpstan--use-xdebug-option nil) (defvar-local phpstan--ignorable-errors '()) (defvar-local phpstan--dumped-types '()) +(defvar phpstan-executable-versions-alist '()) + ;;;###autoload (progn (defvar-local phpstan-working-dir nil @@ -479,7 +488,7 @@ it returns the value of `SOURCE' as it is." ((executable-find "phpstan") (list (executable-find "phpstan"))) (t (error "PHPStan executable not found"))))))) -(cl-defun phpstan-get-command-args (&key include-executable use-pro args format options config verbose) +(cl-defun phpstan-get-command-args (&key include-executable use-pro args format options config verbose editor) "Return command line argument for PHPStan." (let ((executable-and-args (phpstan-get-executable-and-args)) (config (or config (phpstan-normalize-path (phpstan-get-config-file)))) @@ -510,6 +519,15 @@ it returns the value of `SOURCE' as it is." "--xdebug")) (list phpstan--use-xdebug-option)) (phpstan-use-xdebug-option (list "--xdebug"))) + (when editor + (let ((original-file (plist-get editor :original-file))) + (if (phpstan-editor-mode-available-p (car (phpstan-get-executable-and-args))) + (list "--tmp-file" (funcall (plist-get editor :temp-file)) + "--instead-of" original-file + "--" original-file) + (if (funcall (plist-get editor :analyze-original) original-file) + (list "--" original-file) + (list "--" (funcall (plist-get editor :inplace))))))) options (and args (cons "--" args))))) @@ -535,6 +553,37 @@ 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. +Otherwise, this function attempts to determine support by retrieving +the PHPStan version using 'phpstan --version' command." + (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)))))))) + (defconst phpstan--re-ignore-tag (eval-when-compile (rx (* (syntax whitespace)) "//" (* (syntax whitespace))