diff --git a/.gitignore b/.gitignore index 176df11..52cfd9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /.cask +/composer.lock +/vendor diff --git a/Cask b/Cask index 267ffdf..bac0818 100644 --- a/Cask +++ b/Cask @@ -3,4 +3,5 @@ (package-file "phpstan.el") (development - (depends-on "flycheck")) + (depends-on "flycheck") + (depends-on "php-mode")) diff --git a/README.org b/README.org index e2bddd9..0ba6b45 100644 --- a/README.org +++ b/README.org @@ -1,2 +1,98 @@ * phpstan.el Emacs interface to [[https://github.com/phpstan/phpstan][PHPStan]], includes checker for [[http://www.flycheck.org/en/latest/][Flycheck]]. +** Support version +- Emacs 24+ +- PHPStan latest/dev-master (NOT support 0.9 seriese) +** How to install +*** Install from MELPA +/TBD/ +** How to use +*** For Flycheck user +/TBD/ +*** For Flymake user +The function for flymake will be implemented soon. You do not have to depend on flycheck. +*** Using Docker (phpstan/docker-image) +Install [[https://www.docker.com/community-edition][Docker CE]] and [[https://github.com/phpstan/docker-image][phpstan/docker-image]](latest). + +If you always use Docker for PHPStan, add the following into your ~.emacs~ file (~~/.emacs.d/init.el~) +#+BEGIN_SRC emacs-lisp +(setq-default phpstan-executable 'docker) +#+END_SRC + +Put the following into ~.dir-locals.el~ files on the root directory of project. +#+BEGIN_SRC emacs-lisp +((nil . ((php-project-root . git) + (phpstan-executable . docker) + (phpstan-working-dir . (root . "path/to/dir")) + (phpstan-config-file . (root . "path/to/dir/phpstan-docker.neon")) + (phpstan-level . 7)))) +#+END_SRC + +*** Using composer (project specific) +If your project Composer relies on phpstan, you do not need to set anything. +#+BEGIN_SRC emacs-lisp +((nil . ((php-project-root . git) + (phpstan-executable . docker) + (phpstan-working-dir . (root . "path/to/dir")) + (phpstan-config-file . (root . "path/to/dir/phpstan-docker.neon")) + (phpstan-level . 7)))) +#+END_SRC +*** Using PHAR archive +*NOTICE*: ~phpstan.el~ is incompatible with the [[https://github.com/phpstan/phpstan/releases][released versions]] of PHPStan. It will probably be distributed in the form of the Phar archive when the current development version is officially released in the near future. + +If you want to use the Phar archive you built yourself, set the Phar archive path to phpstan-executable. + +** Settings +Variables for phpstan are mainly controlled by [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html][directory variables]] (~.dir-locals.el~). + +Frequently ~(root. "path/to/file")~ notation appears in these variables. It is relative to the top level directory of the project. In general, the directory containing one of ~.projectile~, ~composer.json~, ~.git~ file (or directory) is at the top level. + +Please be aware that the root directory of the PHP project may *NOT* match either of PHPStan's ~%rootDir%~ and/or ~%currentWorkingDirectory%~. + +Typically, you would set the following ~.dir-locals.el~. + +#+BEGIN_SRC emacs-lisp +((nil . ((php-project-root . auto) + (phpstan-executable . docker) + (phpstan-working-dir . (root . "path/to/dir/")) + (phpstan-config-file . (root . "path/to/dir/phpstan-custom.neon")) + (phpstan-level . max)))) +#+END_SRC + +If there is a ~phpstan.neon~ file in the root directory of the project, you do not need to set both ~phpstan-working-dir~ and ~phpstan-config-file~. + +** 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]]. + +*** Local variable ~phpstan-working-dir~ +Path to working directory of PHPStan. + +- STRING :: Absolute path to `phpstan' working directory. + - ex) ~"/path/to/phpstan.phar"~ +- ~(root . STRING)~ :: Relative path to `phpstan' working directory from project root directory. + - ex) ~(root . "path/to/dir")~ +- ~nil~ :: Use ~(php-project-get-root-dir)~ as working directory. + +*** Local variable ~phpstan-config-file~ +Path to project specific configuration file of PHPStan. + +- STRING :: Absolute path to ~phpstan~ configuration file. +- ~(root . STRING)~ :: Relative path to ~phpstan~ configuration file from project root directory. +- NIL :: Search ~phpstan.neon(.dist)~ in ~(phpstan-get-working-dir)~. + +*** Local variable ~phpstan-level~ +Rule level of PHPStan analysis. Please see [[https://github.com/phpstan/phpstan/blob/master/README.md#rule-levels][README #Rule levels of PHPStan]]. +~0~ is the loosest and you can also use ~max~ as an alias for the highest level. Default level is ~0~. + +*** Local variable ~phpstan-executable~ +- STRING :: Absolute path to `phpstan' executable file. + - ex) ~"/path/to/phpstan.phar"~ +- SYMBOL ~docker~ :: Use Docker using phpstan/docker-image. +- ~(root . STRING)~ :: Relative path to `phpstan' executable file from project root directory. + - ex) ~(root . "script/phpstan")~ +- ~(STRING . (ARGUMENTS ...))~ :: Command name and arguments. + - ex) ~("docker" "run" "--rm" "-v" "/path/to/project-dir/:/app" "your/docker-image")~ +- ~nil~ :: Auto detect ~phpstan~ executable file by composer dependencies of the project or executable command in ~PATH~ environment variable. + +*** Custom variable ~phpstan-flycheck-auto-set-executable~ +Set flycheck phpstan-executable automatically when non-NIL. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6bca841 --- /dev/null +++ b/composer.json @@ -0,0 +1,8 @@ +{ + "name": "emacs-php/phpstan.el", + "description": "Emacs interface to PHPStan", + "license": "GPL-3.0-or-later", + "require-dev": { + "phpstan/phpstan": "dev-master" + } +} diff --git a/phpstan.el b/phpstan.el index c761731..a08b41f 100644 --- a/phpstan.el +++ b/phpstan.el @@ -28,19 +28,233 @@ ;; https://github.com/phpstan/phpstan ;;; Code: +(require 'php-project) (require 'flycheck nil) + +;; Variables: + +(defgroup phpstan nil + "Interaface to PHPStan" + :tag "PHPStan" + :prefix "phpstan-" + :group 'tools + :group 'php + :link '(url-link :tag "PHPStan" "https://github.com/phpstan/phpstan") + :link '(url-link :tag "phpstan.el" "https://github.com/emacs-php/phpstan.el")) + +(defcustom phpstan-flycheck-auto-set-executable t + "Set flycheck phpstan-executable automatically." + :type 'boolean + :group 'phpstan) + +;;;###autoload +(progn + (defvar phpstan-working-dir nil + "Path to working directory of PHPStan. + +*NOTICE*: This is different from the project root. + +STRING + Absolute path to `phpstan' working directory. + +`(root . STRING)' + Relative path to `phpstan' working directory from project root directory. + +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))) + (null v) (stringp v))))) + +;;;###autoload +(progn + (defvar phpstan-config-file nil + "Path to project specific configuration file of PHPStan. + +STRING + Absolute path to `phpstan' configuration file. + +`(root . STRING)' + Relative path to `phpstan' configuration file from project root directory. + +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))) + (null v) (stringp v))))) + +;;;###autoload +(progn + (defvar phpstan-level "0" + "Rule level of PHPStan. + +INTEGER or STRING + Number of PHPStan rule level. + +max + The highest of PHPStan rule level.") + (make-variable-buffer-local 'phpstan-level) + (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)))))) + +;;;###autoload +(progn + (defvar phpstan-replace-path-prefix) + (make-variable-buffer-local 'phpstan-replace-path-prefix) + (put 'phpstan-replace-path-prefix 'safe-local-variable + #'(lambda (v) (or (null v) (stringp v))))) + +(defconst phpstan-docker-executable "docker") + +;; Usually it is defined dynamically by flycheck +(defvar flycheck-phpstan-executable) + +;;;###autoload +(progn + (defvar phpstan-executable nil + "PHPStan excutable file. + +STRING + Absolute path to `phpstan' executable file. + +`docker' + Use Docker using phpstan/docker-image. + +`(root . STRING)' + Relative path to `phpstan' executable file. + +`(STRING . (ARGUMENTS ...))' + Command name and arguments. + +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))) + (and (stringp (car v)) (listp (cdr v)))) + (or (eq 'docker v) (null v) (stringp v)))))) + +;; Functions: +(defun phpstan-get-working-dir () + "Return path to working directory of PHPStan." + (if (and phpstan-working-dir (consp phpstan-working-dir) (eq 'root (car phpstan-working-dir))) + (expand-file-name (cdr phpstan-working-dir) (php-project-get-root-dir)) + (php-project-get-root-dir))) + +(defun phpstan-get-config-file () + "Return path to phpstan configure file or `NIL'." + (if phpstan-config-file + (if (and (consp phpstan-config-file) + (eq 'root (car phpstan-config-file))) + ;; Use (php-project-get-root-dir), not phpstan-working-dir. + (expand-file-name (cdr phpstan-config-file) (php-project-get-root-dir)) + phpstan-config-file) + (let ((working-directory (phpstan-get-working-dir))) + (cl-loop for name in '("phpstan.neon" "phpstan.neon.dist") + for dir = (locate-dominating-file working-directory name) + if dir + return (expand-file-name name dir))))) + +(defun phpstan-enabled-and-set-flycheck-variable () + "Return path to phpstan configure file, and set buffer execute in side effect." + (let ((enabled (not (null (or phpstan-working-dir (phpstan-get-config-file)))))) + (prog1 enabled + (when (and phpstan-flycheck-auto-set-executable + (not (and (boundp 'flycheck-phpstan-executable) + (symbol-value 'flycheck-phpstan-executable))) + (or (eq 'docker phpstan-executable) + (and (consp phpstan-executable) + (stringp (car phpstan-executable)) + (listp (cdr phpstan-executable))))) + (set (make-local-variable 'flycheck-phpstan-executable) + (if (eq 'docker phpstan-executable) + phpstan-docker-executable + (car phpstan-executable))))))) + +(defun phpstan-normalize-path (source-original &optional source) + "Return normalized source file path to pass by `SOURCE-ORIGINAL' OR `SOURCE'. + +If neither `phpstan-replace-path-prefix' nor executable docker is set, +it returns the value of `SOURCE' as it is." + (let ((root-directory (expand-file-name (php-project-get-root-dir))) + (prefix + (cond + ((not (null phpstan-replace-path-prefix)) phpstan-replace-path-prefix) + ((eq 'docker phpstan-executable) "/app") + ((and (consp phpstan-executable) + (string= "docker" (car phpstan-executable))) "/app")))) + (if prefix + (expand-file-name + (replace-regexp-in-string (concat "\\`" (regexp-quote root-directory)) + "" + source-original t t) + prefix) + (or source source-original)))) + +(defun phpstan-get-level () + "Return path to phpstan configure file or `NIL'." + (cond + ((null phpstan-level) "0") + ((integerp phpstan-level) (int-to-string phpstan-level)) + ((symbolp phpstan-level) (symbol-name phpstan-level)) + (t phpstan-level))) + +(defun phpstan-get-executable () + "Return PHPStan excutable file and arguments." + (cond + ((eq 'docker phpstan-executable) + (list "run" "--rm" "-v" + (concat (expand-file-name (php-project-get-root-dir)) ":/app") + "phpstan/phpstan")) + ((and (consp phpstan-executable) + (eq 'root (car phpstan-executable))) + (expand-file-name (cdr phpstan-executable) (php-project-get-root-dir))) + ((and phpstan-flycheck-auto-set-executable + (listp phpstan-executable) + (stringp (car phpstan-executable)) + (listp (cdr phpstan-executable))) + (cdr phpstan-executable)) + ((null phpstan-executable) + (let ((vendor-phpstan (expand-file-name "vendor/bin/phpstan" + (php-project-get-root-dir)))) + (cond + ((file-exists-p vendor-phpstan) (list vendor-phpstan)) + ((executable-find "phpstan") (list (executable-find "phpstan"))) + (t (error "PHPStan executable not found"))))))) + +(defun phpstan-get-command-args () + "Return command line argument for PHPStan." + (let ((executable (phpstan-get-executable)) + (args (list "analyze" "--errorFormat=raw" "--no-progress" "--no-interaction")) + (path (phpstan-normalize-path (phpstan-get-config-file))) + (level (phpstan-get-level))) + (when path + (setq args (append args (list "-c" path)))) + (when level + (setq args (append args (list "-l" level)))) + (append executable args))) + ;;;###autoload (when (featurep 'flycheck) - (flycheck-define-checker phpstan-checker + (flycheck-define-checker phpstan "PHP static analyzer based on PHPStan." - :command ("phpstan" - "analyze" - "--no-progress" - "--errorFormat=raw" - source) - :working-directory (lambda (_) (php-project-get-root-dir)) - :enabled (lambda () (locate-dominating-file "phpstan.neon" default-directory)) + :command ("php" (eval (phpstan-get-command-args)) + (eval (phpstan-normalize-path + (flycheck-save-buffer-to-temp #'flycheck-temp-file-inplace) + (flycheck-save-buffer-to-temp #'flycheck-temp-file-system)))) + :working-directory (lambda (_) (phpstan-get-working-dir)) + :enabled (lambda () (phpstan-enabled-and-set-flycheck-variable)) :error-patterns ((error line-start (1+ (not (any ":"))) ":" line ":" (message) line-end)) :modes (php-mode) diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..9ce06a8 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1 @@ +# dummy diff --git a/test-docker.php b/test-docker.php new file mode 100644 index 0000000..2fa9634 --- /dev/null +++ b/test-docker.php @@ -0,0 +1,15 @@ +