Skip to content

Commit

Permalink
Merge pull request #4 from emacs-php/feature/variable-configure-file
Browse files Browse the repository at this point in the history
Variable configure file
  • Loading branch information
zonuexe authored Apr 15, 2018
2 parents 3131d00 + 0508372 commit 8a95d94
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 9 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
/.cask
/composer.lock
/vendor
3 changes: 2 additions & 1 deletion Cask
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

(package-file "phpstan.el")
(development
(depends-on "flycheck"))
(depends-on "flycheck")
(depends-on "php-mode"))
96 changes: 96 additions & 0 deletions README.org
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 8 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
230 changes: 222 additions & 8 deletions phpstan.el
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# dummy
15 changes: 15 additions & 0 deletions test-docker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

foo();
f();
foo();

echo Fooo;
echo FOO;

// Local Variables:
// phpstan-executable: docker
// phpstan-working-dir: (root . "tests/")
// phpstan-config-file: (root . "tests/phpstan-docker.neon")
// phpstan-level: 7
// End:
13 changes: 13 additions & 0 deletions test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

foo();
f();
foo();

echo Fooo;
echo FOO;

// Local Variables:
// phpstan-config-file: (root . "tests/phpstan.neon")
// phpstan-level: 7
// End:
5 changes: 5 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

const FOO = 'Foo';

function foo() {}
Loading

0 comments on commit 8a95d94

Please sign in to comment.