Skip to content

Support PHPStan editor-mode #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.org
Original file line number Diff line number Diff line change
Expand Up @@ -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]].
Expand Down
25 changes: 20 additions & 5 deletions flycheck-phpstan.el
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down Expand Up @@ -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
Expand Down
34 changes: 27 additions & 7 deletions flymake-phpstan.el
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -88,21 +93,36 @@
(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)))
(unless (car command-args)
(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)))))

Expand All @@ -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
51 changes: 50 additions & 1 deletion phpstan.el
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))))
Expand Down Expand Up @@ -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)))))

Expand All @@ -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))
Expand Down