|
|
|
|
# phpinspect.el
|
|
|
|
|
PHPInspect is a minor mode that provides code intelligence for PHP in Emacs. At
|
|
|
|
|
its core is a PHP parser implemented in Emacs Lisp. PHPInspect comes with
|
|
|
|
|
backends for `completion-at-point`, `company-mode` and `eldoc`. A backend for
|
|
|
|
|
`xref` (which provides go-to-definition functionality) is planned to be
|
|
|
|
|
implemented at a later date. The main documentation of the mode is in the
|
|
|
|
|
docstring of the mode itself (`C-h f phpinspect-mode RET` to view, or read it in
|
|
|
|
|
the source code of [phpinspect.el](phpinspect.el)).
|
|
|
|
|
|
|
|
|
|
## Projects and Finding Types
|
|
|
|
|
By default, phpinspect will recognize composer projects and read their
|
|
|
|
|
composer.json files for autoload information which is used to find files in
|
|
|
|
|
which the types/classes/functions you use in your code are defined. It is also
|
|
|
|
|
possible to add an "include directory" of files that should always be read and
|
|
|
|
|
indexed for a certain project. To do this, open a file in a project and run `M-x
|
|
|
|
|
phpinspect-project-add-include-dir`. You can also edit the list of include
|
|
|
|
|
directories via `M-x customize-goup RET phpinspect RET`.
|
|
|
|
|
|
|
|
|
|
## Example Configuration
|
|
|
|
|
If you already have a completion UI setup that is able to use
|
|
|
|
|
`completion-at-point-functions` as completion source, you can basically just
|
|
|
|
|
enable phpinspect-mode and you'll be good to go. An example of a basic mode hook
|
|
|
|
|
configuration to get the most out of phpinspect is the following:
|
|
|
|
|
|
|
|
|
|
```elisp
|
|
|
|
|
(defun my-php-personal-hook ()
|
|
|
|
|
;; Shortcut to add use statements for classes you use.
|
|
|
|
|
(define-key php-mode-map (kbd \"C-c u\") 'phpinspect-fix-imports)
|
|
|
|
|
|
|
|
|
|
;; Shortcuts to quickly search/open files of PHP classes.
|
|
|
|
|
;; You can make these local to php-mode, but making them global
|
|
|
|
|
;; like this makes them work in other modes/filetypes as well, which
|
|
|
|
|
;; can be handy when jumping between templates, config files and PHP code.
|
|
|
|
|
(global-set-key (kbd \"C-c a\") 'phpinspect-find-class-file)
|
|
|
|
|
(global-set-key (kbd \"C-c c\") 'phpinspect-find-own-class-file)
|
|
|
|
|
|
|
|
|
|
;; Enable phpinspect-mode
|
|
|
|
|
(phpinspect-mode))
|
|
|
|
|
|
|
|
|
|
(add-hook 'php-mode-hook #'my-php-personal-hook)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Example config with company mode setup
|
|
|
|
|
|
|
|
|
|
```elisp
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun my-php-personal-hook ()
|
|
|
|
|
;; It is important to enable `company-mode' before setting
|
|
|
|
|
;; the variables below.
|
|
|
|
|
(company-mode)
|
|
|
|
|
(setq-local company-minimum-prefix-length 0)
|
|
|
|
|
(setq-local company-tooltip-align-annotations t)
|
|
|
|
|
(setq-local company-idle-delay 0.1)
|
|
|
|
|
(setq-local company-backends '(phpinspect-company-backend))
|
|
|
|
|
|
|
|
|
|
;; Shortcut to add use statements for classes you use.
|
|
|
|
|
(define-key php-mode-map (kbd "C-c u") 'phpinspect-fix-imports)
|
|
|
|
|
|
|
|
|
|
;; Shortcuts to quickly search/open files of PHP classes.
|
|
|
|
|
(global-set-key (kbd "C-c a") 'phpinspect-find-class-file)
|
|
|
|
|
(global-set-key (kbd "C-c c") 'phpinspect-find-own-class-file)
|
|
|
|
|
|
|
|
|
|
(phpinspect-mode))
|
|
|
|
|
|
|
|
|
|
(add-hook 'php-mode-hook #'my-php-personal-hook)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Install from git
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
git clone https://git.snorba.art/hugo/phpinspect.el ~/projects/phpinspect.el
|
|
|
|
|
cd ~/projects/phpinspect.el
|
|
|
|
|
make
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```elisp
|
|
|
|
|
(add-to-list 'load-path "~/projects/phpinspect.el")
|
|
|
|
|
(require 'phpinspect)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Compilation
|
|
|
|
|
It is highly recommended to byte- or native compile phpinspect. Aside from the
|
|
|
|
|
normal performance boost that this brings to most packages, it can reduce
|
|
|
|
|
phpinspect's parsing time by up to 90%. It especially makes a difference when
|
|
|
|
|
incrementally parsing edited buffers. For example:
|
|
|
|
|
|
|
|
|
|
### benchmarks/parse-file.el uncompiled on Ryzen 5 3600 (time in seconds):
|
|
|
|
|
```
|
|
|
|
|
Incremental parse (warmup):
|
|
|
|
|
Elapsed time: 0.168390 (0.019751 in 1 GC’s)
|
|
|
|
|
Incremental parse:
|
|
|
|
|
Elapsed time: 0.143811 (0.000000 in 0 GC’s)
|
|
|
|
|
Incremental parse (no edits):
|
|
|
|
|
Elapsed time: 0.000284 (0.000000 in 0 GC’s)
|
|
|
|
|
Incremental parse repeat (no edits):
|
|
|
|
|
Elapsed time: 0.000241 (0.000000 in 0 GC’s)
|
|
|
|
|
Incremental parse after buffer edit:
|
|
|
|
|
Elapsed time: 0.012449 (0.000000 in 0 GC’s)
|
|
|
|
|
Incremental parse after 2 more edits:
|
|
|
|
|
Elapsed time: 0.015839 (0.000000 in 0 GC’s)
|
|
|
|
|
Bare (no token reuse) parse (warmup):
|
|
|
|
|
Elapsed time: 0.048996 (0.000000 in 0 GC’s)
|
|
|
|
|
Bare (no token reuse) parse:
|
|
|
|
|
Elapsed time: 0.052495 (0.000000 in 0 GC’s)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### benchmarks/parse-file.el with native compilation on Ryzen 5 3600 (time in seconds):
|
|
|
|
|
```
|
|
|
|
|
Incremental parse (warmup):
|
|
|
|
|
Elapsed time: 0.023432 (0.000000 in 0 GC’s)
|
|
|
|
|
Incremental parse:
|
|
|
|
|
Elapsed time: 0.018350 (0.000000 in 0 GC’s)
|
|
|
|
|
Incremental parse (no edits):
|
|
|
|
|
Elapsed time: 0.000076 (0.000000 in 0 GC’s)
|
|
|
|
|
Incremental parse repeat (no edits):
|
|
|
|
|
Elapsed time: 0.000058 (0.000000 in 0 GC’s)
|
|
|
|
|
Incremental parse after buffer edit:
|
|
|
|
|
Elapsed time: 0.001212 (0.000000 in 0 GC’s)
|
|
|
|
|
Incremental parse after 2 more edits:
|
|
|
|
|
Elapsed time: 0.001381 (0.000000 in 0 GC’s)
|
|
|
|
|
Bare (no token reuse) parse (warmup):
|
|
|
|
|
Elapsed time: 0.013874 (0.000000 in 0 GC’s)
|
|
|
|
|
Bare (no token reuse) parse:
|
|
|
|
|
Elapsed time: 0.013878 (0.000000 in 0 GC’s)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Development
|
|
|
|
|
The codebase of phpinspect is relatively large. As a new contributor, you'll
|
|
|
|
|
probably feel a bit lost in the many files, functions and datatypes of the
|
|
|
|
|
package. Let me tell you a bit about phpinspect's architecture.
|
|
|
|
|
|
|
|
|
|
### Domains
|
|
|
|
|
There are roughly four main domains under which code in phpinspect can fall:
|
|
|
|
|
parsing, indexation, interpetation and user-facing functions. Below is a short
|
|
|
|
|
explanation of each domain.
|
|
|
|
|
|
|
|
|
|
#### Parsing
|
|
|
|
|
The parser provides the underpinnings for almost all of phpinspect's
|
|
|
|
|
functionalities. It is implemented with specialized macros and supports two main
|
|
|
|
|
parsing modes: bare and incremental. The parser code is located in
|
|
|
|
|
`phpinspect-parser.el`.
|
|
|
|
|
|
|
|
|
|
##### Bare Parsing
|
|
|
|
|
The parser produces a parse result in the form of a basic list, where each
|
|
|
|
|
parsed token is a list itself. The car of each list contains a keyword,
|
|
|
|
|
describing its meaning in the parsed code. Below is a lisp form and its
|
|
|
|
|
resulting insertion to demonstrate the structure of a syntax tree produced by
|
|
|
|
|
the bare parser.
|
|
|
|
|
|
|
|
|
|
`(insert (pp-to-string (phpinspect-parse-string "function foo(Bar $bar) { return $bar->baz; }")))`
|
|
|
|
|
|
|
|
|
|
```elisp
|
|
|
|
|
(:root
|
|
|
|
|
(:function
|
|
|
|
|
(:declaration
|
|
|
|
|
(:word "function")
|
|
|
|
|
(:word "foo")
|
|
|
|
|
(:list
|
|
|
|
|
(:word "Bar")
|
|
|
|
|
(:variable "bar")))
|
|
|
|
|
(:block
|
|
|
|
|
(:word "return")
|
|
|
|
|
(:variable "bar")
|
|
|
|
|
(:object-attrib
|
|
|
|
|
(:word "baz"))
|
|
|
|
|
(:terminator ";"))))
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
##### Incremental Parsing
|
|
|
|
|
Bare parsing is fast, because it makes use of simple lisp datastructures and
|
|
|
|
|
keeps the parser logic simple. A downside of this parsing mode is that the
|
|
|
|
|
result lacks any extra metadata about the tokens, like what their start- and
|
|
|
|
|
endpoints are in a buffer. It also makes it hard to partially update the tree
|
|
|
|
|
after a buffer has been edited. As both are essential for a program that needs
|
|
|
|
|
to work efficiently in live-edited buffers, the incremental parser was
|
|
|
|
|
implemented. When parsing incrementally, the parser still produces the same
|
|
|
|
|
structure of nested lists as a result. But it also stores metadata about each
|
|
|
|
|
parsed token in an n-ary tree of `phpinspect-meta` objects (see
|
|
|
|
|
`phpinspect-meta.el`). The incremental parser is able to partially update an
|
|
|
|
|
existing tree after code has been edited, making it efficient for live buffers.
|
|
|
|
|
|
|
|
|
|
##### Parsers Summarised
|
|
|
|
|
The incremental parsing is efficient for use with live edited buffers. Due to
|
|
|
|
|
its higher complexity it is not as fast at parsing entire files as the bare
|
|
|
|
|
parser. For this reason, both the bare and the incremental parser are used for
|
|
|
|
|
what they are best at.
|
|
|
|
|
|
|
|
|
|
#### Indexation
|
|
|
|
|
Within phpinspect, indexation is referred to as the process of extracting
|
|
|
|
|
information from parsed code and project configuration files.
|
|
|
|
|
|
|
|
|
|
##### Code Indexation
|
|
|
|
|
The code in
|
|
|
|
|
`phpinspect-index.el` consumes bare syntax trees and extracts information about
|
|
|
|
|
PHP functions and types from them. Code in `phpinspect-buffer.el` implements the
|
|
|
|
|
same functionality, but for live buffers based on the data produced by the
|
|
|
|
|
incremental parser.
|
|
|
|
|
|
|
|
|
|
##### Project Indexation
|
|
|
|
|
Code in `phpinspect-autoload.el` implements logic for `psr-4`, `psr-0` and
|
|
|
|
|
`files` autoload strategies. It is able to read a composer.json file and
|
|
|
|
|
generate autoload information based on which a project's types and functions can
|
|
|
|
|
be found.
|
|
|
|
|
|
|
|
|
|
##### Caching
|
|
|
|
|
Code in `phpinspect-cache.el` and `phpinspect-class.el` allows phpinspect to
|
|
|
|
|
store the result of code indexations in memory, so that they can be accessed
|
|
|
|
|
instantly when a user requests information about their code.
|
|
|
|
|
|
|
|
|
|
##### Threading
|
|
|
|
|
Indexation takes time. Because emacs is single-threaded, you might think that
|
|
|
|
|
indexing a project would lock up your emacs for a while. Do not despair!
|
|
|
|
|
`phpinspect-worker.el` contains code for a worker with a job queue that uses
|
|
|
|
|
collaborative threads to do work when emacs is idle. When you start interacting
|
|
|
|
|
with emacs, it will back off and let you do your thing! `phpinspect-pipeline.el`
|
|
|
|
|
contains code for something similar to generators in PHP, combined with
|
|
|
|
|
collaborative threading. Pipelines are used for more intense processes that
|
|
|
|
|
should be completed with a bit more of a hurry. Emacs should remain responsive
|
|
|
|
|
while a pipeline is running, but there may be a slightly noticeable sluggishness
|
|
|
|
|
while they run. At the moment of writing, pipelines are mainly used to refresh a
|
|
|
|
|
projects autoloader.
|
|
|
|
|
|
|
|
|
|
#### Interpretation
|
|
|
|
|
Having parsing, indexation and caching infrastructure is all well and good, but
|
|
|
|
|
how to we determine what information is actually useful to show to a user? This
|
|
|
|
|
is where code interpretation plays a big role. The main goal of interpretation
|
|
|
|
|
in phpinspect is the determination of the PHP type to display information
|
|
|
|
|
about. Determining this type is referred to as "resolving" in phpinspect's
|
|
|
|
|
codebase. Resolving a type is built around the concept of a "resolvecontext". A
|
|
|
|
|
resolvecontext is an object that contains information about a location in a
|
|
|
|
|
buffer and its surroundings. Based on the informatoin in the resolvecontext, the
|
|
|
|
|
type that a statement is expected to evaluate to can be determined. The code for
|
|
|
|
|
resolving types is mostly contained in `phpinspect-type.el`,
|
|
|
|
|
`phpinspect-resolve.el` and `phpinspect-resolvecontext.el`. The main entrypoints
|
|
|
|
|
are the functions `phpinspect-get-resolvecontext` and
|
|
|
|
|
`phpinspect-resolve-type-from-context`.
|
|
|
|
|
|
|
|
|
|
#### User-facing Functions
|
|
|
|
|
User-facing functions in phpinspect are mostly integrations with existing
|
|
|
|
|
infrastructure within emacs. For completion there is
|
|
|
|
|
`completion-at-point-functions`, for tooltips there is `eldoc` and for
|
|
|
|
|
go-to-definition there is `xref`. Aside from these integrations, phpinspect aims
|
|
|
|
|
to provide functionalities for code formatting.
|
|
|
|
|
|
|
|
|
|
##### Completion
|
|
|
|
|
Completion is implemented in `phpinspect-completion.el` as a strategy pattern. A
|
|
|
|
|
completion strategy can be added by implementing the methods
|
|
|
|
|
`phpinspect-comp-strategy-supports` and
|
|
|
|
|
`phpinspect-comp-strategy-execute`. Completion strategies are high level
|
|
|
|
|
abstractions that build on top of the type resolving code and the code in
|
|
|
|
|
`phpinspect-suggest.el`.
|
|
|
|
|
|
|
|
|
|
##### Eldoc
|
|
|
|
|
Eldoc support is implemented in a strategy pattern similar to that of
|
|
|
|
|
completion. An eldoc strategy can be added by implementing the methods
|
|
|
|
|
`phpinspect-eld-strategy-supports` and `phpinspect-eld-strategy-execute`.
|
|
|
|
|
|
|
|
|
|
##### Code Formatting
|
|
|
|
|
`phpinspect-fix-imports` adds, removes and sorts use statements. At the time of
|
|
|
|
|
writing, this is the only code formatting functionality that phpinspect
|
|
|
|
|
provides. See `phpinspect-imports.el` for the implementation.
|
|
|
|
|
|
|
|
|
|
##### Really Want To But Not Implemented: Xref
|
|
|
|
|
A thing that would be possible to implement on top of phpinspect's
|
|
|
|
|
infrastructure, but not much time has been spent on yet, is an integration with
|
|
|
|
|
`xref`. Xref integration would enable functionalities like go-to-definition.
|
|
|
|
|
|
|
|
|
|
### Building
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
make
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Running tests
|
|
|
|
|
Tests are implemented using `ert`. You can run them in batch mode with the following
|
|
|
|
|
command:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
make test
|
|
|
|
|
|
|
|
|
|
# or:
|
|
|
|
|
emacs -L ./ -batch -l ert -l ./test/phpinspect-test.el -f ert-run-tests-batch-and-exit
|
|
|
|
|
```
|