Skip to content
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

Variable configure file #4

Merged
merged 25 commits into from
Apr 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7bf0f23
composer require --dev phpstan/phpstan:dev-master
zonuexe Apr 11, 2018
ba7dfc8
Add dependency php-mode in development
zonuexe Apr 11, 2018
526f0b7
Add file local variables and getter functions
zonuexe Apr 11, 2018
968b8fe
Consider the existence of "phpstan.neon.dist"
zonuexe Apr 11, 2018
c48251b
Refactor using cl-loop
zonuexe Apr 11, 2018
a5f1711
Modify flycheck checker
zonuexe Apr 11, 2018
45ba779
Rename flycheck checker
zonuexe Apr 11, 2018
bc56031
Add test files
zonuexe Apr 11, 2018
0825ac8
Use config-file instead of configure-file
zonuexe Apr 11, 2018
6fa077b
Remove unused variable
zonuexe Apr 12, 2018
6840f6c
Fix phpstan executable
zonuexe Apr 12, 2018
a198897
Fix pattern of level (accepts 1 digit number)
zonuexe Apr 12, 2018
63fd9eb
Add defgroup phpstan and custom variable phpstan-flycheck-auto-set-ex…
zonuexe Apr 12, 2018
4555592
Modify phpstan-executable specification
zonuexe Apr 12, 2018
d559ee1
Add phpstan-get-config-file-and-set-flycheck-variable as side effect
zonuexe Apr 12, 2018
05c9f76
Add phpstan-normalize-path and support Docker
zonuexe Apr 12, 2018
70cb256
Add 'docker keyword
zonuexe Apr 12, 2018
23d1860
Normalize phpstan-config-file for Docker
zonuexe Apr 12, 2018
dffcad7
Add 'max keyword to phpstan-level
zonuexe Apr 12, 2018
3748533
Add phpstan-working-dir for working directory of PHPStan
zonuexe Apr 12, 2018
4d53e60
Rename root-directory local variable
zonuexe Apr 12, 2018
56355b0
Search phpstan.neon from (phpstan-get-working-dir)
zonuexe Apr 12, 2018
21cf970
Modify doc comment and README
zonuexe Apr 12, 2018
8d63798
Modify phpstan-enabled-and-set-flycheck-variable
zonuexe Apr 13, 2018
0508372
phpstan-get-executable return always executable with arguments
zonuexe Apr 13, 2018
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
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)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not clear what the structure of phpstan-config-file should be. I think user options should at least be defined as defcustoms with a type annotation.

;; 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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this work when this starts "php" but I set 'docker as executable? Then the arguments are going to be run --rm -v..., that won't work with php would it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Fuco1 This is an insane hack.

phpstan-enabled-and-set-flycheck-variable invoked as :enabled function sets flycheck-phpstan-executable as its side effect when flycheck-phpstan-executable is not set. Since it uses the fact that :enabled is called beforehand, there is a possibility that it will be destroyed in the future with low probability.

It was possible to explicitly write flycheck-phpstan-executable in the user setting, but it was handled within this function as it is somewhat complicated. I am looking for ways to solve this problem besides this hack.

(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:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is not automated, but we can check flycheck's behavior manually.

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