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

285 lines
12 KiB
Markdown

# phpinspect.el
1 year ago
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)).
1 year ago
## 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`.
1 year ago
## 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 GCs)
Incremental parse:
Elapsed time: 0.143811 (0.000000 in 0 GCs)
Incremental parse (no edits):
Elapsed time: 0.000284 (0.000000 in 0 GCs)
Incremental parse repeat (no edits):
Elapsed time: 0.000241 (0.000000 in 0 GCs)
Incremental parse after buffer edit:
Elapsed time: 0.012449 (0.000000 in 0 GCs)
Incremental parse after 2 more edits:
Elapsed time: 0.015839 (0.000000 in 0 GCs)
Bare (no token reuse) parse (warmup):
Elapsed time: 0.048996 (0.000000 in 0 GCs)
Bare (no token reuse) parse:
Elapsed time: 0.052495 (0.000000 in 0 GCs)
```
### 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 GCs)
Incremental parse:
Elapsed time: 0.018350 (0.000000 in 0 GCs)
Incremental parse (no edits):
Elapsed time: 0.000076 (0.000000 in 0 GCs)
Incremental parse repeat (no edits):
Elapsed time: 0.000058 (0.000000 in 0 GCs)
Incremental parse after buffer edit:
Elapsed time: 0.001212 (0.000000 in 0 GCs)
Incremental parse after 2 more edits:
Elapsed time: 0.001381 (0.000000 in 0 GCs)
Bare (no token reuse) parse (warmup):
Elapsed time: 0.013874 (0.000000 in 0 GCs)
Bare (no token reuse) parse:
Elapsed time: 0.013878 (0.000000 in 0 GCs)
```
## 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
```