Skip to content

Commit 3b59a5b

Browse files
committed
Support PHPStan editor-mode
1 parent 715fef5 commit 3b59a5b

File tree

4 files changed

+103
-14
lines changed

4 files changed

+103
-14
lines changed

README.org

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
#+END_HTML
66
Emacs interface to [[https://github.com/phpstan/phpstan][PHPStan]], includes checker for [[http://www.flycheck.org/en/latest/][Flycheck]].
77
** Support version
8-
- Emacs 25+
8+
- Emacs 26+
99
- PHPStan latest/dev-master (NOT support 0.9 seriese)
1010
- PHP 7.1+ or Docker runtime
11+
12+
> [!TIP]
13+
> 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.
14+
> **We strongly recommend that you always update to the latest PHPStan.**
15+
1116
** How to install
1217
*** Install from MELPA
1318
1. If you have not set up MELPA, see [[https://melpa.org/#/getting-started][Getting Started - MELPA]].

flycheck-phpstan.el

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ passed to `flycheck-finish-checker-process'."
7777
(string-match-p flycheck-phpstan--nofiles-message output))
7878
(funcall next checker exit-status files output callback cwd)))
7979

80+
(defcustom flycheck-phpstan-fallback-to-original-analysis-if-editor-mode-unavailable t
81+
"If non-NIL, analyze the original file when PHPStan editor mode is unavailable."
82+
:type 'boolean
83+
:safe #'booleanp)
84+
8085
(defun flycheck-phpstan--enabled-and-set-variable ()
8186
"Return path to phpstan configure file, and set buffer execute in side effect."
8287
(let ((enabled (phpstan-enabled)))
@@ -139,13 +144,23 @@ passed to `flycheck-finish-checker-process'."
139144
nil 'error text
140145
:filename file))))
141146

147+
(defun flycheck-phpstan-analyze-original (original)
148+
"Return non-NIL if ORIGINAL is NIL, fallback is enabled, and buffer is modified."
149+
(and (null original)
150+
flycheck-phpstan-fallback-to-original-analysis-if-editor-mode-unavailable
151+
(buffer-modified-p)))
152+
142153
(flycheck-define-checker phpstan
143154
"PHP static analyzer based on PHPStan."
144-
:command ("php" (eval (phpstan-get-command-args :format "json"))
145-
(eval (if (or (buffer-modified-p) (not buffer-file-name))
146-
(phpstan-normalize-path
147-
(flycheck-save-buffer-to-temp #'flycheck-temp-file-inplace))
148-
buffer-file-name)))
155+
:command ("php"
156+
(eval
157+
(phpstan-get-command-args
158+
:format "json"
159+
:editor (list
160+
:analyze-original #'flycheck-phpstan-analyze-original
161+
:original-file buffer-file-name
162+
:temp-file (lambda () (flycheck-save-buffer-to-temp #'flycheck-temp-file-system))
163+
:inplace (lambda () (flycheck-save-buffer-to-temp #'flycheck-temp-file-inplace))))))
149164
:working-directory (lambda (_) (phpstan-get-working-dir))
150165
:enabled (lambda () (flycheck-phpstan--enabled-and-set-variable))
151166
:error-parser flycheck-phpstan-parse-output

flymake-phpstan.el

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
:type 'boolean
5353
:group 'flymake-phpstan)
5454

55+
(defcustom flycheck-phpstan-fallback-to-original-analysis-if-editor-mode-unavailable t
56+
"If non-NIL, analyze the original file when PHPStan editor mode is unavailable."
57+
:type 'boolean
58+
:safe #'booleanp)
59+
5560
(defvar-local flymake-phpstan--proc nil)
5661

5762
(defun flymake-phpstan-make-process (root command-args report-fn source)
@@ -88,21 +93,36 @@
8893
(kill-buffer (process-buffer proc))))
8994
(code (user-error "PHPStan error (exit status: %s)" code)))))))
9095

96+
(defun flymake-phpstan-analyze-original (original)
97+
"Return non-NIL if ORIGINAL is NIL, fallback is enabled, and buffer is modified."
98+
(and (null original)
99+
flymake-phpstan-fallback-to-original-analysis-if-editor-mode-unavailable
100+
(buffer-modified-p)))
101+
102+
(defun flymake-phpstan--create-temp-file ()
103+
"Create temp file and return the path."
104+
(phpstan-normalize-path
105+
(flymake-proc-init-create-temp-buffer-copy 'flymake-proc-create-temp-inplace)))
106+
91107
(defun flymake-phpstan (report-fn &rest _ignored-args)
92108
"Flymake backend for PHPStan report using REPORT-FN."
93109
(let ((command-args (phpstan-get-command-args :include-executable t)))
94110
(unless (car command-args)
95111
(user-error "Cannot find a phpstan executable command"))
96112
(when (process-live-p flymake-phpstan--proc)
97113
(kill-process flymake-phpstan--proc))
98-
(let ((source (current-buffer))
99-
(target-path (if (or (buffer-modified-p) (not buffer-file-name))
100-
(phpstan-normalize-path
101-
(flycheck-save-buffer-to-temp #'flycheck-temp-file-inplace))
102-
buffer-file-name)))
114+
(let* ((source (current-buffer))
115+
(args (phpstan-get-command-args
116+
:include-executable t
117+
:format "raw"
118+
:editor (list
119+
:analyze-original #'flymake-phpstan-analyze-original
120+
:original-file buffer-file-name
121+
:temp-file #'flymake-phpstan--create-temp-file
122+
:inplace #'flymake-phpstan--create-temp-file))))
103123
(save-restriction
104124
(widen)
105-
(setq flymake-phpstan--proc (flymake-phpstan-make-process (php-project-get-root-dir) (append command-args (list "--" target-path)) report-fn source))
125+
(setq flymake-phpstan--proc (flymake-phpstan-make-process (php-project-get-root-dir) args report-fn source))
106126
(process-send-region flymake-phpstan--proc (point-min) (point-max))
107127
(process-send-eof flymake-phpstan--proc)))))
108128

@@ -115,7 +135,7 @@
115135
(flymake-mode 1)
116136
(when flymake-phpstan-disable-c-mode-hooks
117137
(remove-hook 'flymake-diagnostic-functions #'flymake-cc t))
118-
(add-hook 'flymake-diagnostic-functions #'flymake-phpstan nil t))))
138+
(add-hook 'flymake-diagnostic-functions #'flymake-phpstan nil 'local))))
119139

120140
(provide 'flymake-phpstan)
121141
;;; flymake-phpstan.el ends here

phpstan.el

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,20 @@ have unexpected behaviors or performance implications."
166166
"Lists identifiers prohibited from being added to @phpstan-ignore tags."
167167
:type '(repeat string))
168168

169+
(defcustom phpstan-activate-editor-mode nil
170+
"Controls how PHPStan's editor mode is activated."
171+
:local t
172+
:type '(choice (const :tag "Automatic (based on version)" nil)
173+
(const :tag "Editor mode will be actively enabled, regardless of the PHPStan version." 'enabled)
174+
(const :tag "Editor mode will be explicitly disabled." 'disabled)))
175+
169176
(defvar-local phpstan--use-xdebug-option nil)
170177

171178
(defvar-local phpstan--ignorable-errors '())
172179
(defvar-local phpstan--dumped-types '())
173180

181+
(defvar phpstan-executable-versions-alist '())
182+
174183
;;;###autoload
175184
(progn
176185
(defvar-local phpstan-working-dir nil
@@ -479,7 +488,7 @@ it returns the value of `SOURCE' as it is."
479488
((executable-find "phpstan") (list (executable-find "phpstan")))
480489
(t (error "PHPStan executable not found")))))))
481490

482-
(cl-defun phpstan-get-command-args (&key include-executable use-pro args format options config verbose)
491+
(cl-defun phpstan-get-command-args (&key include-executable use-pro args format options config verbose editor)
483492
"Return command line argument for PHPStan."
484493
(let ((executable-and-args (phpstan-get-executable-and-args))
485494
(config (or config (phpstan-normalize-path (phpstan-get-config-file))))
@@ -510,6 +519,15 @@ it returns the value of `SOURCE' as it is."
510519
"--xdebug"))
511520
(list phpstan--use-xdebug-option))
512521
(phpstan-use-xdebug-option (list "--xdebug")))
522+
(when editor
523+
(let ((original-file (plist-get editor :original-file)))
524+
(if (phpstan-editor-mode-available-p (car (phpstan-get-executable-and-args)))
525+
(list "--tmp-file" (funcall (plist-get editor :temp-file))
526+
"--instead-of" original-file
527+
"--" original-file)
528+
(if (funcall (plist-get editor :analyze-original) original-file)
529+
(list "--" original-file)
530+
(list "--" (funcall (plist-get editor :inplace)))))))
513531
options
514532
(and args (cons "--" args)))))
515533

@@ -535,6 +553,37 @@ it returns the value of `SOURCE' as it is."
535553
collect (cons (plist-get message :line)
536554
(substring-no-properties msg (match-end 0))))))))
537555

556+
(defun phpstan-version (executable)
557+
"Return the PHPStan version of EXECUTABLE."
558+
(if-let* ((cached-entry (assoc executable phpstan-executable-versions-alist)))
559+
(cdr cached-entry)
560+
(let* ((version (thread-first
561+
(mapconcat #'shell-quote-argument (list executable "--version") " ")
562+
(shell-command-to-string)
563+
(string-trim-right)
564+
(split-string " ")
565+
(last)
566+
(car-safe))))
567+
(prog1 version
568+
(push (cons executable version) phpstan-executable-versions-alist)))))
569+
570+
(defun phpstan-editor-mode-available-p (executable)
571+
"Check if the specified PHPStan EXECUTABLE supports editor mode.
572+
573+
If a cached result for EXECUTABLE exists, it is returned directly.
574+
Otherwise, this function attempts to determine support by retrieving
575+
the PHPStan version using 'phpstan --version' command."
576+
(pcase phpstan-activate-editor-mode
577+
('enabled t)
578+
('disabled nil)
579+
('nil
580+
(let* ((version (phpstan-version executable)))
581+
(if (string-match-p (eval-when-compile (regexp-quote "-dev@")) version)
582+
t
583+
(pcase (elt version 0)
584+
(?1 (version<= "1.12.27" version))
585+
(?2 (version<= "2.1.17" version))))))))
586+
538587
(defconst phpstan--re-ignore-tag
539588
(eval-when-compile
540589
(rx (* (syntax whitespace)) "//" (* (syntax whitespace))

0 commit comments

Comments
 (0)