diff --git a/CHANGELOG.md b/CHANGELOG.md index 072c5d5..53ffd34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes of the `phpstan.el` are documented in this file using the [K +## [0.9.0] + +### Added + +* Add `phpstan-copy-dumped-type` command to copy the nearest dumped type from `PHPStan\dumpType()` or `PHPStan\dumpPhpDocType()` messages +* Add support for PHPStan [Editor Mode](https://phpstan.org/user-guide/editor-mode) + +### Changed + +* Improved error handling when no JSON response is returned + +### Fixed + +* Fixed erroneous dependency from `flymake-phpstan` to Flycheck functions + +### Removed + +* Drop support for Emacs 25.3 + ## [0.8.2] ### Fixed diff --git a/Eask b/Eask index bc9f916..dfec247 100644 --- a/Eask +++ b/Eask @@ -1,5 +1,7 @@ +;; -*- mode: eask; lexical-binding: t -*- + (package "phpstan" - "0.8.2" + "0.9.0" "Interface to PHPStan (PHP static analyzer)") (website-url "https://github.com/emacs-php/phpstan.el") diff --git a/README.org b/README.org index 9a6efb1..bbaf1e8 100644 --- a/README.org +++ b/README.org @@ -5,9 +5,16 @@ #+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 + +#+BEGIN_QUOTE +[!TIP] +This package provides support for the [[https://phpstan.org/user-guide/editor-mode][Editor Mode]] introduced in PHPStan [[https://github.com/phpstan/phpstan/releases/tag/2.1.17][2.1.17]] and [[https://github.com/phpstan/phpstan/releases/tag/1.12.27][1.12.27]].\\ +*We strongly recommend that you always update to the latest PHPStan.* +#+END_QUOTE + ** How to install *** Install from MELPA 1. If you have not set up MELPA, see [[https://melpa.org/#/getting-started][Getting Started - MELPA]]. @@ -105,6 +112,14 @@ Insert a ~@phpstan-ignore~ tag to suppress any PHPStan errors on the current lin By default it inserts the tag on the previous line, but if there is already a tag at the end of the current line or on the previous line, the identifiers will be appended there. If there is no existing tag and ~C-u~ is pressed before the command, it will be inserted at the end of the line. +*** Command ~phpstan-copy-dumped-type~ +Copy the nearest dumped type message from PHPStan's output. + +This command looks for messages like ~Dumped type: int|string|null~ reported by ~PHPStan\dumpType()~ or ~PHPStan\dumpPhpDocType()~, and copies the type string to the kill ring. + +If there are multiple dumped types in the buffer, it selects the one closest to the current line. + +If no dumped type messages are found, the command signals an error. ** API Most variables defined in this package are buffer local. If you want to set it for multiple projects, use [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Default-Value.html][setq-default]]. @@ -147,6 +162,13 @@ Use phpstan memory limit option when non-NIL. - ex) ~"1G"~ - ~nil~ :: Use memory limit in php.ini +*** Custom variable ~phpstan-activate-editor-mode~ +Determines whether PHPStan Editor Mode is available. + +- ~nil~ (default) :: Dynamically checks the PHPStan version by getting the path of the installed PHPStan executable. +- ~'enabled~ :: Always use Editor Mode (this will cause an error in older versions of PHPStan) +- ~'disabled~ :: Never use Editor Mode (no support for editors provided) + *** Custom variable ~phpstan-docker-image~ Docker image URL or Docker Hub image name or NIL. Default as ~"ghcr.io/phpstan/phpstan"~. See [[https://phpstan.org/user-guide/docker][Docker - PHPStan Documentation]] and [[https://github.com/orgs/phpstan/packages/container/package/phpstan][GitHub Container Registory - Package phpstan]]. diff --git a/flycheck-phpstan.el b/flycheck-phpstan.el index 4a6242b..5fd42b9 100644 --- a/flycheck-phpstan.el +++ b/flycheck-phpstan.el @@ -4,10 +4,10 @@ ;; Author: USAMI Kenta ;; Created: 15 Mar 2018 -;; Version: 0.8.2 +;; Version: 0.9.0 ;; Keywords: tools, php ;; Homepage: https://github.com/emacs-php/phpstan.el -;; Package-Requires: ((emacs "24.3") (flycheck "26") (phpstan "0.8.2")) +;; Package-Requires: ((emacs "25.1") (flycheck "26") (phpstan "0.8.2")) ;; License: GPL-3.0-or-later ;; This program is free software; you can redistribute it and/or modify @@ -44,6 +44,8 @@ ;; Usually it is defined dynamically by flycheck (defvar flycheck-phpstan-executable) (defvar flycheck-phpstan--temp-buffer-name "*Flycheck PHPStan*") +(defvar flycheck-phpstan--output-filter-added nil) +(defconst flycheck-phpstan--nofiles-message (eval-when-compile (regexp-quote "[ERROR] No files found to analyse."))) (defcustom flycheck-phpstan-ignore-metadata-list nil "Set of metadata items to ignore in PHPStan messages for Flycheck." @@ -57,10 +59,32 @@ :safe #'stringp :group 'phpstan) +(defun flycheck-phpstan--suppress-no-files-error (next checker exit-status files output callback cwd) + "Suppress Flycheck errors if PHPStan reports no files in a modified buffer. + +This function is intended to be used as an :around advice for +`flycheck-finish-checker-process'. + +It prevents Flycheck from displaying an error when: +- CHECKER is `phpstan', +- the current buffer is modified, +- and OUTPUT contains the message `flycheck-phpstan--nofiles-message'. + +NEXT, EXIT-STATUS, FILES, OUTPUT, CALLBACK, and CWD are the original arguments +passed to `flycheck-finish-checker-process'." + (unless (and (eq checker 'phpstan) + (buffer-modified-p) + (string-match-p flycheck-phpstan--nofiles-message output)) + (funcall next checker exit-status files output callback cwd))) + (defun flycheck-phpstan--enabled-and-set-variable () "Return path to phpstan configure file, and set buffer execute in side effect." (let ((enabled (phpstan-enabled))) (prog1 enabled + (unless flycheck-phpstan--output-filter-added + (advice-add 'flycheck-finish-checker-process + :around #'flycheck-phpstan--suppress-no-files-error) + (setq flycheck-phpstan--output-filter-added t)) (when (and enabled phpstan-flycheck-auto-set-executable (null (bound-and-true-p flycheck-phpstan-executable)) @@ -79,10 +103,13 @@ (erase-buffer) (insert output) (current-buffer))) - (data (phpstan--parse-json json-buffer)) + (data (if (string-prefix-p "{" output) + (phpstan--parse-json json-buffer) + (list (flycheck-error-new-at 1 1 'warning (string-trim output))))) (errors (phpstan--plist-to-alist (plist-get data :files)))) (unless phpstan-disable-buffer-errors (phpstan-update-ignorebale-errors-from-json-buffer errors)) + (phpstan-update-dumped-types errors) (flycheck-phpstan--build-errors errors))) (defun flycheck-phpstan--temp-buffer () @@ -107,18 +134,26 @@ (if (null lines) msg (concat msg flycheck-phpstan-metadata-separator - (mapconcat #'identity lines "\n")))) + (string-join lines "\n")))) collect (flycheck-error-new-at (plist-get messages :line) nil 'error text :filename file)))) +(defun flycheck-phpstan-analyze-original (original) + "Return non-NIL if ORIGINAL is non-NIL and buffer is not modified." + (and original (not (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..9e89c5f 100644 --- a/flymake-phpstan.el +++ b/flymake-phpstan.el @@ -4,7 +4,7 @@ ;; Author: USAMI Kenta ;; Created: 31 Mar 2020 -;; Version: 0.8.2 +;; Version: 0.9.0 ;; Keywords: tools, php ;; Homepage: https://github.com/emacs-php/phpstan.el ;; Package-Requires: ((emacs "26.1") (phpstan "0.8.2")) @@ -38,6 +38,7 @@ (require 'cl-lib) (require 'php-project) (require 'flymake) +(require 'flymake-proc) (require 'phpstan) (eval-when-compile (require 'pcase)) @@ -88,6 +89,15 @@ (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 non-NIL and buffer is not modified." + (and original (not (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 +105,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 +129,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 bb95533..b0135d8 100644 --- a/phpstan.el +++ b/phpstan.el @@ -4,7 +4,7 @@ ;; Author: USAMI Kenta ;; Created: 15 Mar 2018 -;; Version: 0.8.2 +;; Version: 0.9.0 ;; Keywords: tools, php ;; Homepage: https://github.com/emacs-php/phpstan.el ;; Package-Requires: ((emacs "25.1") (compat "30") (php-mode "1.22.3") (php-runtime "0.2")) @@ -77,13 +77,11 @@ (defcustom phpstan-flycheck-auto-set-executable t "Set flycheck phpstan-executable automatically." - :type 'boolean - :group 'phpstan) + :type 'boolean) (defcustom phpstan-enable-on-no-config-file t "If T, activate config from composer even when `phpstan.neon' is not found." - :type 'boolean - :group 'phpstan) + :type 'boolean) (defcustom phpstan-memory-limit nil "Set --memory-limit option." @@ -92,7 +90,7 @@ :link '(url-link :tag "PHP Manual" "https://www.php.net/manual/ini.core.php#ini.memory-limit") :safe (lambda (v) (or (null v) (stringp v))) - :group 'phpstan) + :local t) (defcustom phpstan-docker-image "ghcr.io/phpstan/phpstan" "Docker image URL or Docker Hub image name or NIL." @@ -104,7 +102,7 @@ :link '(url-link :tag "GitHub Container Registry" "https://github.com/orgs/phpstan/packages/container/package/phpstan") :safe (lambda (v) (or (null v) (stringp v))) - :group 'phpstan) + :local t) (defcustom phpstan-use-xdebug-option nil "Set --xdebug option." @@ -112,31 +110,31 @@ (const :tag "Add --xdebug option" t) (const :tag "No --xdebug option" nil)) :safe #'symbolp - :group 'phpstan) + :local t) (defcustom phpstan-generate-baseline-options '("--generate-baseline" "--allow-empty-baseline") "Command line options for generating PHPStan baseline." :type '(repeat string) :safe #'listp - :group 'phpstan) + :local t) (defcustom phpstan-baseline-file "phpstan-baseline.neon" "File name of PHPStan baseline file." :type 'string :safe #'stringp - :group 'phpstan) + :local t) (defcustom phpstan-tip-message-prefix "💡 " "Prefix of PHPStan tip message." :type 'string :safe #'stringp - :group 'phpstan) + :local t) (defcustom phpstan-identifier-prefix "🪪 " "Prefix of PHPStan error identifier." :type 'string :safe #'stringp - :group 'phpstan) + :local t) (defcustom phpstan-enable-remote-experimental nil "Enable PHPStan analysis remotely by TRAMP. @@ -146,7 +144,7 @@ This feature is experimental and should be used with caution as it may have unexpected behaviors or performance implications." :type 'boolean :safe #'booleanp - :group 'phpstan) + :local t) (defconst phpstan-template-dump-type "\\PHPStan\\dumpType();") (defconst phpstan-template-dump-phpdoc-type "\\PHPStan\\dumpPhpDocType();") @@ -154,25 +152,34 @@ have unexpected behaviors or performance implications." (defcustom phpstan-intert-dump-type-templates (cons phpstan-template-dump-type phpstan-template-dump-phpdoc-type) "Default template of PHPStan dumpType insertion." - :type '(cons string string) - :group 'phpstan) + :type '(cons string string)) (defcustom phpstan-disable-buffer-errors nil - "If T, don't keep errors per buffer to save memory." - :type 'boolean - :group 'phpstan) + "If non-NIL, don't keep errors per buffer to save memory." + :type 'boolean) (defcustom phpstan-not-ignorable-identifiers '("ignore.parseError") "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." + :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)) + :safe (lambda (v) (memq v '(nil enabled disabled))) + :local t) + (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 phpstan-working-dir nil + (defvar-local phpstan-working-dir nil "Path to working directory of PHPStan. *NOTICE*: This is different from the project root. @@ -185,7 +192,6 @@ STRING NIL Use (php-project-get-root-dir) as working directory.") - (make-variable-buffer-local 'phpstan-working-dir) (put 'phpstan-working-dir 'safe-local-variable #'(lambda (v) (if (consp v) (and (eq 'root (car v)) (stringp (cdr v))) @@ -193,7 +199,7 @@ NIL ;;;###autoload (progn - (defvar phpstan-config-file nil + (defvar-local phpstan-config-file nil "Path to project specific configuration file of PHPStan. STRING @@ -204,7 +210,6 @@ STRING NIL Search phpstan.neon(.dist) in (phpstan-get-working-dir).") - (make-variable-buffer-local 'phpstan-config-file) (put 'phpstan-config-file 'safe-local-variable #'(lambda (v) (if (consp v) (and (eq 'root (car v)) (stringp (cdr v))) @@ -243,25 +248,24 @@ max NIL Use rule level specified in `phpstan' configuration file.") (put 'phpstan-level 'safe-local-variable - #'(lambda (v) (or (null v) - (integerp v) - (eq 'max v) - (and (stringp v) - (string= "max" v) - (string-match-p "\\`[0-9]\\'" v)))))) + (lambda (v) (or (null v) + (integerp v) + (eq 'max v) + (and (stringp v) + (or (string= "max" v) + (string-match-p "\\`[0-9]\\'" v))))))) ;;;###autoload (progn - (defvar phpstan-replace-path-prefix) - (make-variable-buffer-local 'phpstan-replace-path-prefix) + (defvar-local phpstan-replace-path-prefix nil) (put 'phpstan-replace-path-prefix 'safe-local-variable - #'(lambda (v) (or (null v) (stringp v))))) + (lambda (v) (or (null v) (stringp v))))) (defconst phpstan-docker-executable "docker") ;;;###autoload (progn - (defvar phpstan-executable nil + (defvar-local phpstan-executable nil "PHPStan excutable file. STRING @@ -278,7 +282,6 @@ STRING NIL Auto detect `phpstan' executable file.") - (make-variable-buffer-local 'phpstan-executable) (put 'phpstan-executable 'safe-local-variable #'(lambda (v) (if (consp v) (or (and (eq 'root (car v)) (stringp (cdr v))) @@ -349,7 +352,8 @@ it returns the value of `SOURCE' as it is." (cond ((eq 'docker phpstan-executable) "/app") ((and (consp phpstan-executable) - (string= "docker" (car phpstan-executable))) "/app"))))) + (string= "docker" (car phpstan-executable))) + "/app"))))) (if prefix (expand-file-name (replace-regexp-in-string (concat "\\`" (regexp-quote root-directory)) @@ -481,7 +485,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)))) @@ -512,6 +516,16 @@ 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))) + (cond + ((funcall (plist-get editor :analyze-original) original-file) + (list "--" original-file)) + ((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)) + ((list "--" (funcall (plist-get editor :inplace))))))) options (and args (cons "--" args))))) @@ -526,6 +540,48 @@ it returns the value of `SOURCE' as it is." (setq phpstan--ignorable-errors (mapcar (lambda (v) (cons (car v) (mapcar #'cdr (cdr v)))) (seq-group-by #'car identifiers))))) +(defun phpstan-update-dumped-types (errors) + "Update `phpstan--dumped-types' variable by ERRORS." + (save-match-data + (setq phpstan--dumped-types + (cl-loop for (_ . entry) in errors + append (cl-loop for message in (plist-get entry :messages) + for msg = (plist-get message :message) + if (string-match (eval-when-compile (rx bos "Dumped type: ")) msg) + 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)) @@ -591,7 +647,20 @@ POSITION determines where to insert the comment and can be either `this-line' or (goto-char new-point)) (insert (concat padding (if new-position (if append ", " " ") "// @phpstan-ignore ") - (mapconcat #'identity identifiers ", "))))))) + (string-join identifiers ", "))))))) + +;;;###autoload +(defun phpstan-copy-dumped-type () + "Copy a dumped PHPStan type." + (interactive) + (if phpstan--dumped-types + (let ((type (if (eq 1 (length phpstan--dumped-types)) + (cdar phpstan--dumped-types) + (let ((linum (line-number-at-pos))) + (cdar (seq-sort-by (lambda (elm) (abs (- linum (car elm)))) #'< phpstan--dumped-types)))))) + (kill-new type) + (message "Copied %s" type)) + (user-error "No dumped PHPStan types"))) ;;;###autoload (defun phpstan-insert-dumptype (&optional expression prefix-num)