|
28 | 28 | ;; https://github.com/phpstan/phpstan |
29 | 29 |
|
30 | 30 | ;;; Code: |
| 31 | +(require 'php-project) |
31 | 32 | (require 'flycheck nil) |
32 | 33 |
|
| 34 | + |
| 35 | +;; Variables: |
| 36 | + |
| 37 | +(defgroup phpstan nil |
| 38 | + "Interaface to PHPStan" |
| 39 | + :tag "PHPStan" |
| 40 | + :prefix "phpstan-" |
| 41 | + :group 'tools |
| 42 | + :group 'php |
| 43 | + :link '(url-link :tag "PHPStan" "https://github.com/phpstan/phpstan") |
| 44 | + :link '(url-link :tag "phpstan.el" "https://github.com/emacs-php/phpstan.el")) |
| 45 | + |
| 46 | +(defcustom phpstan-flycheck-auto-set-executable t |
| 47 | + "Set flycheck phpstan-executable automatically." |
| 48 | + :type 'boolean |
| 49 | + :group 'phpstan) |
| 50 | + |
| 51 | +;;;###autoload |
| 52 | +(progn |
| 53 | + (defvar phpstan-working-dir nil |
| 54 | + "Path to working directory of PHPStan. |
| 55 | +
|
| 56 | +*NOTICE*: This is different from the project root. |
| 57 | +
|
| 58 | +STRING |
| 59 | + Absolute path to `phpstan' working directory. |
| 60 | +
|
| 61 | +`(root . STRING)' |
| 62 | + Relative path to `phpstan' working directory from project root directory. |
| 63 | +
|
| 64 | +NIL |
| 65 | + Use (php-project-get-root-dir) as working directory.") |
| 66 | + (make-variable-buffer-local 'phpstan-working-dir) |
| 67 | + (put 'phpstan-working-dir 'safe-local-variable |
| 68 | + #'(lambda (v) (if (consp v) |
| 69 | + (and (eq 'root (car v)) (stringp (cdr v))) |
| 70 | + (null v) (stringp v))))) |
| 71 | + |
| 72 | +;;;###autoload |
| 73 | +(progn |
| 74 | + (defvar phpstan-config-file nil |
| 75 | + "Path to project specific configuration file of PHPStan. |
| 76 | +
|
| 77 | +STRING |
| 78 | + Absolute path to `phpstan' configuration file. |
| 79 | +
|
| 80 | +`(root . STRING)' |
| 81 | + Relative path to `phpstan' configuration file from project root directory. |
| 82 | +
|
| 83 | +NIL |
| 84 | + Search phpstan.neon(.dist) in (phpstan-get-working-dir).") |
| 85 | + (make-variable-buffer-local 'phpstan-config-file) |
| 86 | + (put 'phpstan-config-file 'safe-local-variable |
| 87 | + #'(lambda (v) (if (consp v) |
| 88 | + (and (eq 'root (car v)) (stringp (cdr v))) |
| 89 | + (null v) (stringp v))))) |
| 90 | + |
| 91 | +;;;###autoload |
| 92 | +(progn |
| 93 | + (defvar phpstan-level "0" |
| 94 | + "Rule level of PHPStan. |
| 95 | +
|
| 96 | +INTEGER or STRING |
| 97 | + Number of PHPStan rule level. |
| 98 | +
|
| 99 | +max |
| 100 | + The highest of PHPStan rule level.") |
| 101 | + (make-variable-buffer-local 'phpstan-level) |
| 102 | + (put 'phpstan-level 'safe-local-variable |
| 103 | + #'(lambda (v) (or (null v) |
| 104 | + (integerp v) |
| 105 | + (eq 'max v) |
| 106 | + (and (stringp v) |
| 107 | + (string= "max" v) |
| 108 | + (string-match-p "\\`[0-9]\\'" v)))))) |
| 109 | + |
| 110 | +;;;###autoload |
| 111 | +(progn |
| 112 | + (defvar phpstan-replace-path-prefix) |
| 113 | + (make-variable-buffer-local 'phpstan-replace-path-prefix) |
| 114 | + (put 'phpstan-replace-path-prefix 'safe-local-variable |
| 115 | + #'(lambda (v) (or (null v) (stringp v))))) |
| 116 | + |
| 117 | +(defconst phpstan-docker-executable "docker") |
| 118 | + |
| 119 | +;; Usually it is defined dynamically by flycheck |
| 120 | +(defvar flycheck-phpstan-executable) |
| 121 | + |
| 122 | +;;;###autoload |
| 123 | +(progn |
| 124 | + (defvar phpstan-executable nil |
| 125 | + "PHPStan excutable file. |
| 126 | +
|
| 127 | +STRING |
| 128 | + Absolute path to `phpstan' executable file. |
| 129 | +
|
| 130 | +`docker' |
| 131 | + Use Docker using phpstan/docker-image. |
| 132 | +
|
| 133 | +`(root . STRING)' |
| 134 | + Relative path to `phpstan' executable file. |
| 135 | +
|
| 136 | +`(STRING . (ARGUMENTS ...))' |
| 137 | + Command name and arguments. |
| 138 | +
|
| 139 | +NIL |
| 140 | + Auto detect `phpstan' executable file.") |
| 141 | + (make-variable-buffer-local 'phpstan-executable) |
| 142 | + (put 'phpstan-executable 'safe-local-variable |
| 143 | + #'(lambda (v) (if (consp v) |
| 144 | + (or (and (eq 'root (car v)) (stringp (cdr v))) |
| 145 | + (and (stringp (car v)) (listp (cdr v)))) |
| 146 | + (or (eq 'docker v) (null v) (stringp v)))))) |
| 147 | + |
| 148 | +;; Functions: |
| 149 | +(defun phpstan-get-working-dir () |
| 150 | + "Return path to working directory of PHPStan." |
| 151 | + (if (and phpstan-working-dir (consp phpstan-working-dir) (eq 'root (car phpstan-working-dir))) |
| 152 | + (expand-file-name (cdr phpstan-working-dir) (php-project-get-root-dir)) |
| 153 | + (php-project-get-root-dir))) |
| 154 | + |
| 155 | +(defun phpstan-get-config-file () |
| 156 | + "Return path to phpstan configure file or `NIL'." |
| 157 | + (if phpstan-config-file |
| 158 | + (if (and (consp phpstan-config-file) |
| 159 | + (eq 'root (car phpstan-config-file))) |
| 160 | + ;; Use (php-project-get-root-dir), not phpstan-working-dir. |
| 161 | + (expand-file-name (cdr phpstan-config-file) (php-project-get-root-dir)) |
| 162 | + phpstan-config-file) |
| 163 | + (let ((working-directory (phpstan-get-working-dir))) |
| 164 | + (cl-loop for name in '("phpstan.neon" "phpstan.neon.dist") |
| 165 | + for dir = (locate-dominating-file working-directory name) |
| 166 | + if dir |
| 167 | + return (expand-file-name name dir))))) |
| 168 | + |
| 169 | +(defun phpstan-enabled-and-set-flycheck-variable () |
| 170 | + "Return path to phpstan configure file, and set buffer execute in side effect." |
| 171 | + (let ((enabled (not (null (or phpstan-working-dir (phpstan-get-config-file)))))) |
| 172 | + (prog1 enabled |
| 173 | + (when (and phpstan-flycheck-auto-set-executable |
| 174 | + (not (and (boundp 'flycheck-phpstan-executable) |
| 175 | + (symbol-value 'flycheck-phpstan-executable))) |
| 176 | + (or (eq 'docker phpstan-executable) |
| 177 | + (and (consp phpstan-executable) |
| 178 | + (stringp (car phpstan-executable)) |
| 179 | + (listp (cdr phpstan-executable))))) |
| 180 | + (set (make-local-variable 'flycheck-phpstan-executable) |
| 181 | + (if (eq 'docker phpstan-executable) |
| 182 | + phpstan-docker-executable |
| 183 | + (car phpstan-executable))))))) |
| 184 | + |
| 185 | +(defun phpstan-normalize-path (source-original &optional source) |
| 186 | + "Return normalized source file path to pass by `SOURCE-ORIGINAL' OR `SOURCE'. |
| 187 | +
|
| 188 | +If neither `phpstan-replace-path-prefix' nor executable docker is set, |
| 189 | +it returns the value of `SOURCE' as it is." |
| 190 | + (let ((root-directory (expand-file-name (php-project-get-root-dir))) |
| 191 | + (prefix |
| 192 | + (cond |
| 193 | + ((not (null phpstan-replace-path-prefix)) phpstan-replace-path-prefix) |
| 194 | + ((eq 'docker phpstan-executable) "/app") |
| 195 | + ((and (consp phpstan-executable) |
| 196 | + (string= "docker" (car phpstan-executable))) "/app")))) |
| 197 | + (if prefix |
| 198 | + (expand-file-name |
| 199 | + (replace-regexp-in-string (concat "\\`" (regexp-quote root-directory)) |
| 200 | + "" |
| 201 | + source-original t t) |
| 202 | + prefix) |
| 203 | + (or source source-original)))) |
| 204 | + |
| 205 | +(defun phpstan-get-level () |
| 206 | + "Return path to phpstan configure file or `NIL'." |
| 207 | + (cond |
| 208 | + ((null phpstan-level) "0") |
| 209 | + ((integerp phpstan-level) (int-to-string phpstan-level)) |
| 210 | + ((symbolp phpstan-level) (symbol-name phpstan-level)) |
| 211 | + (t phpstan-level))) |
| 212 | + |
| 213 | +(defun phpstan-get-executable () |
| 214 | + "Return PHPStan excutable file and arguments." |
| 215 | + (cond |
| 216 | + ((eq 'docker phpstan-executable) |
| 217 | + (list "run" "--rm" "-v" |
| 218 | + (concat (expand-file-name (php-project-get-root-dir)) ":/app") |
| 219 | + "phpstan/phpstan")) |
| 220 | + ((and (consp phpstan-executable) |
| 221 | + (eq 'root (car phpstan-executable))) |
| 222 | + (expand-file-name (cdr phpstan-executable) (php-project-get-root-dir))) |
| 223 | + ((and phpstan-flycheck-auto-set-executable |
| 224 | + (listp phpstan-executable) |
| 225 | + (stringp (car phpstan-executable)) |
| 226 | + (listp (cdr phpstan-executable))) |
| 227 | + (cdr phpstan-executable)) |
| 228 | + ((null phpstan-executable) |
| 229 | + (let ((vendor-phpstan (expand-file-name "vendor/bin/phpstan" |
| 230 | + (php-project-get-root-dir)))) |
| 231 | + (cond |
| 232 | + ((file-exists-p vendor-phpstan) (list vendor-phpstan)) |
| 233 | + ((executable-find "phpstan") (list (executable-find "phpstan"))) |
| 234 | + (t (error "PHPStan executable not found"))))))) |
| 235 | + |
| 236 | +(defun phpstan-get-command-args () |
| 237 | + "Return command line argument for PHPStan." |
| 238 | + (let ((executable (phpstan-get-executable)) |
| 239 | + (args (list "analyze" "--errorFormat=raw" "--no-progress" "--no-interaction")) |
| 240 | + (path (phpstan-normalize-path (phpstan-get-config-file))) |
| 241 | + (level (phpstan-get-level))) |
| 242 | + (when path |
| 243 | + (setq args (append args (list "-c" path)))) |
| 244 | + (when level |
| 245 | + (setq args (append args (list "-l" level)))) |
| 246 | + (append executable args))) |
| 247 | + |
33 | 248 | ;;;###autoload |
34 | 249 | (when (featurep 'flycheck) |
35 | | - (flycheck-define-checker phpstan-checker |
| 250 | + (flycheck-define-checker phpstan |
36 | 251 | "PHP static analyzer based on PHPStan." |
37 | | - :command ("phpstan" |
38 | | - "analyze" |
39 | | - "--no-progress" |
40 | | - "--errorFormat=raw" |
41 | | - source) |
42 | | - :working-directory (lambda (_) (php-project-get-root-dir)) |
43 | | - :enabled (lambda () (locate-dominating-file "phpstan.neon" default-directory)) |
| 252 | + :command ("php" (eval (phpstan-get-command-args)) |
| 253 | + (eval (phpstan-normalize-path |
| 254 | + (flycheck-save-buffer-to-temp #'flycheck-temp-file-inplace) |
| 255 | + (flycheck-save-buffer-to-temp #'flycheck-temp-file-system)))) |
| 256 | + :working-directory (lambda (_) (phpstan-get-working-dir)) |
| 257 | + :enabled (lambda () (phpstan-enabled-and-set-flycheck-variable)) |
44 | 258 | :error-patterns |
45 | 259 | ((error line-start (1+ (not (any ":"))) ":" line ":" (message) line-end)) |
46 | 260 | :modes (php-mode) |
|
0 commit comments