You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

302 lines
11 KiB

;;; php-project.el --- Project support for PHP application -*- lexical-binding: t; -*-
;; Copyright (C) 2020 Friends of Emacs-PHP development
;; Author: USAMI Kenta <tadsan@zonu.me>
;; Keywords: tools, files
;; URL: https://github.com/emacs-php/php-mode
;; Version: 1.24.0
;; License: GPL-3.0-or-later
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Define project specific functions and variables for PHP application.
;;
;; ## API
;;
;; ### `php-project-get-root-dir()'
;;
;; Return root directory of current buffer file. The root directory is
;; determined by several marker file or directory.
;;
;; ### `php-project-get-bootstrap-scripts()'
;;
;; Return list of path to bootstrap script file.
;;
;; ### `php-project-get-php-executable()'
;;
;; Return path to PHP executable file with the project settings overriding.
;;
;; ### `php-project-get-phan-executable()'
;;
;; Return path to Phan executable file with the project settings overriding.
;; Phan is a static analyzer and LSP server implementation for PHP.
;; See https://github.com/phan/phan
;;
;; ## `.dir-locals.el' support
;;
;; - `php-project-coding-style'
;; - Symbol value of the coding style. (ex. `pear', `psr2')
;; - `php-project-root'
;; - Symbol of marker file of project root. (ex. `git', `composer')
;; - Full path to project root directory. (ex. "/path/to/your-project")
;; - `php-project-bootstrap-scripts'
;; - List of path to bootstrap file of project.
;; (ex. (((root . "vendor/autoload.php") (root . "inc/bootstrap.php")))
;; - `php-project-php-executable'
;; - Path to project specific PHP executable file.
;; - If you want to use a file different from the system wide `php' command.
;; - `php-project-phan-executable'
;; - Path to project specific Phan executable file.
;; - When not specified explicitly, it is automatically searched from
;; Composer's dependency of the project and `exec-path'.
;;
;;; Code:
(eval-when-compile
(require 'cl-lib))
(require 'projectile nil t)
;; Constants
(defconst php-project-composer-autoloader "vendor/autoload.php")
;; Custom variables
(defgroup php-project nil
"Major mode for editing PHP code."
:tag "PHP Project"
:prefix "php-project-"
:group 'php)
(defcustom php-project-auto-detect-etags-file nil
"If `T', automatically detect etags file when file is opened."
:tag "PHP Project Auto Detect Etags File"
:group 'php-project
:type 'boolean)
(defcustom php-project-use-projectile-to-detect-root nil
"If `T' and projectile-mode is activated, use Projectile for root detection."
:tag "PHP Project Use Projectile To Detect Root"
:group 'php-project
:type 'boolean)
;; Variables
(defvar php-project-available-root-files
'((projectile ".projectile")
(composer "composer.json" "composer.lock")
(git ".git")
(mercurial ".hg")
(subversion ".svn")
;; NOTICE: This method does not detect the top level of .editorconfig
;; However, we can integrate it by adding the editorconfig.el's API.
;;(editorconfig . ".editorconfig")
))
;; Buffer local variables
;;;###autoload
(progn
(defvar-local php-project-root 'auto
"Method of searching for the top level directory.
`auto' (default)
Try to search file in order of `php-project-available-root-files'.
SYMBOL
Key of `php-project-available-root-files'.
STRING
A file/directory name of top level marker.
If the string is an actual directory path, it is set as the absolute path
of the root directory, not the marker.")
(put 'php-project-root 'safe-local-variable
#'(lambda (v) (or (stringp v) (assq v php-project-available-root-files))))
(defvar-local php-project-etags-file nil)
(put 'php-project-etags-file 'safe-local-variable
#'(lambda (v) (or (functionp v)
(eq v t)
(php-project--eval-bootstrap-scripts v))))
(defvar-local php-project-bootstrap-scripts nil
"List of path to bootstrap php script file.
The ideal bootstrap file is silent, it only includes dependent files,
defines constants, and sets the class loaders.")
(put 'php-project-bootstrap-scripts 'safe-local-variable #'php-project--eval-bootstrap-scripts)
(defvar-local php-project-php-executable nil
"Path to php executable file.")
(put 'php-project-php-executable 'safe-local-variable
#'(lambda (v) (and (stringp v) (file-executable-p v))))
(defvar-local php-project-phan-executable nil
"Path to phan executable file.")
(put 'php-project-phan-executable 'safe-local-variable #'php-project--eval-bootstrap-scripts)
(defvar-local php-project-coding-style nil
"Symbol value of the coding style of the project that PHP major mode refers to.
Typically it is `pear', `drupal', `wordpress', `symfony2' and `psr2'.")
(put 'php-project-coding-style 'safe-local-variable #'symbolp)
(defvar-local php-project-align-lines t
"If T, automatically turn on `php-align-mode' by `php-align-setup'.")
(put 'php-project-align-lines 'safe-local-variable #'booleanp)
(defvar-local php-project-php-file-as-template 'auto
"
`auto' (default)
Automatically switch to mode for template when HTML tag detected in file.
`t'
Switch all PHP files in that directory to mode for HTML template.
`nil'
Any .php in that directory is just a PHP script.
\(\(PATTERN . SYMBOL))
Alist of file name pattern regular expressions and the above symbol pairs.
PATTERN is regexp pattern.
")
(put 'php-project-php-file-as-template 'safe-local-variable #'php-project--validate-php-file-as-template)
(defvar-local php-project-repl nil
"Function name or path to REPL (interactive shell) script.")
(put 'php-project-repl 'safe-local-variable
#'(lambda (v) (or (functionp v)
(php-project--eval-bootstrap-scripts v))))
(defvar-local php-project-unit-test nil
"Function name or path to unit test script.")
(put 'php-project-unit-test 'safe-local-variable
#'(lambda (v) (or (functionp v)
(php-project--eval-bootstrap-scripts v))))
(defvar-local php-project-deploy nil
"Function name or path to deploy script.")
(put 'php-project-deploy 'safe-local-variable
#'(lambda (v) (or (functionp v)
(php-project--eval-bootstrap-scripts v))))
(defvar-local php-project-build nil
"Function name or path to build script.")
(put 'php-project-build 'safe-local-variable
#'(lambda (v) (or (functionp v)
(php-project--eval-bootstrap-scripts v))))
(defvar-local php-project-server-start nil
"Function name or path to server-start script.")
(put 'php-project-server-start 'safe-local-variable
#'(lambda (v) (or (functionp v)
(php-project--eval-bootstrap-scripts v)))))
;; Functions
(defun php-project--validate-php-file-as-template (val)
"Return T when `VAL' is valid list of safe ."
(cond
((null val) t)
((memq val '(t auto)) t)
((listp val)
(cl-loop for v in val
always (and (consp v)
(stringp (car v))
(php-project--validate-php-file-as-template (cdr v)))))
(t nil)))
(defun php-project--eval-bootstrap-scripts (val)
"Return T when `VAL' is valid list of safe bootstrap php script."
(cond
((stringp val) (and (file-exists-p val) val))
((eq 'composer val)
(let ((path (expand-file-name php-project-composer-autoloader (php-project-get-root-dir))))
(and (file-exists-p path) path)))
((and (consp val) (eq 'root (car val)) (stringp (cdr val)))
(let ((path (expand-file-name (cdr val) (php-project-get-root-dir))))
(and (file-exists-p path) path)))
((null val) nil)
((listp val)
(cl-loop for v in val collect (php-project--eval-bootstrap-scripts v)))
(t nil)))
(defun php-project-get-php-executable ()
"Return path to PHP executable file."
(cond
((and (stringp php-project-php-executable)
(file-executable-p php-project-php-executable))
php-project-php-executable)
((boundp 'php-executable) php-executable)
(t (executable-find "php"))))
(defun php-project-get-phan-executable ()
"Return path to phan executable file."
(or (car-safe (php-project--eval-bootstrap-scripts
(list php-project-phan-executable
(cons 'root "vendor/bin/phan"))))
(executable-find "phan")))
(defun php-project-get-file-html-template-type (filename)
"Return symbol T, NIL or `auto' by `FILENAME'."
(cond
((not php-project-php-file-as-template) nil)
((eq t php-project-php-file-as-template) t)
((eq 'auto php-project-php-file-as-template) 'auto)
((listp php-project-php-file-as-template)
(assoc-default filename php-project-php-file-as-template #'string-match-p))
(t (prog1 nil
(warn "php-project-php-file-as-template is unexpected format")))))
(defun php-project-apply-local-variables ()
"Apply php-project variables to local variables."
(when (null tags-file-name)
(when (or (and php-project-auto-detect-etags-file
(null php-project-etags-file))
(eq php-project-etags-file t))
(let ((tags-file (expand-file-name "TAGS" (php-project-get-root-dir))))
(when (file-exists-p tags-file)
(setq-local php-project-etags-file tags-file))))
(when php-project-etags-file
(setq-local tags-file-name (php-project--eval-bootstrap-scripts php-project-etags-file)))))
;;;###autoload
(defun php-project-get-bootstrap-scripts ()
"Return list of bootstrap script."
(let ((scripts (php-project--eval-bootstrap-scripts php-project-bootstrap-scripts)))
(if (stringp scripts) (list scripts) scripts)))
;;;###autoload
(defun php-project-get-root-dir ()
"Return path to current PHP project."
(if (and (stringp php-project-root) (file-directory-p php-project-root))
php-project-root
(php-project--detect-root-dir)))
(defun php-project--detect-root-dir ()
"Return detected project root."
(if (and php-project-use-projectile-to-detect-root
(bound-and-true-p projectile-mode)
(fboundp 'projectile-project-root))
(projectile-project-root default-directory)
(let ((detect-method
(cond
((stringp php-project-root) (list php-project-root))
((eq php-project-root 'auto)
(cl-loop for m in php-project-available-root-files
append (cdr m)))
(t (cdr-safe (assq php-project-root php-project-available-root-files))))))
(cl-loop for m in detect-method
thereis (locate-dominating-file default-directory m)))))
(provide 'php-project)
;;; php-project.el ends here