summaryrefslogtreecommitdiff
path: root/phpinspect.el
blob: 3025f6df3ad4c0ad6b71baebf157ac70c8620938 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
;;; phpinspect.el --- PHP parsing and code intelligence package  -*- lexical-binding: t; -*-

;; Copyright (C) 2021-2025  Free Software Foundation, Inc

;; Author: Hugo Thunnissen <devel@hugot.nl>
;; Keywords: php, languages, tools, convenience
;; Version: 3.0.1
;; Package-Requires: ((compat "29"))
;; URL: https://github.com/hugot/phpinspect.el

;; 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:

;; 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` and `eldoc`.  A backend
;; for `xref` (which provides go-to-definition functionality) is planned to be
;; implemented at a later date.
;;
;; See docstrings for elaborate documentation on how to use the mode, starting
;; with `phpinspect-mode'. Also see M-x customize-group RET phpinspect RET.

;;; News:

;; Version 3.0.1

;; - Reworked collaborative threading approach which should result in better
;;   overall responsiveness.
;; - Made parsing and indexation of buffer fully async and pro-active
;;   using collaborative threads.
;; - Removed company-mode backend (use capf instead)
;; - Added tests, fixed various bugs and made incremental indexation
;;   of buffer code more robust.
;; - Implemented "classmap" autoload directive

;; Version 3.0.1

;; - Implemented basic support for completion of keywords and type names
;; - Made completion of function names, type names and keywords context-aware so
;;   that the provided completions are relevant at point.
;; - Implemented interactive function to insert type names and automatically
;;   import them when necessary, called `phpinspect-insert-type'.

;; - Fixed end-of-buffer error bugs in parsing of comments
;; - Fixed bug in `phpinspect-suggest-variables-at-point' which caused local
;;   "foreach" variables to not be suggested.
;; - Fixed infinite recursion bug caused by typedef dependent on itself (only
;;   occured when the type definition could not be found via the autoloader)

;; - Made incremental parser more reliable by not adopting incomplete tokens
;;   into new tree.
;; - Increased overall test coverage of phpinspect-suggest.el and
;;   phpinspect-completion.el

;; Version 2.0.1

;; - Fixed bug in `phpinspect-fix-imports' that caused it to not function
;;   properly for types used in within classes.

;; Version 2.0.0

;; - Implemented support for traits
;; - Implemented more accurate/nuanced in-memory representation of types and
;;   inherited properties/methods.  `phpinspect--class' has been removed and
;;   replaced with `phpinspect-typedef', the the function prefix of which is
;;   "phpi-typedef-".  This change is backwards-incompatible due to various name
;;   changes, refactorings and function/type removals.
;; - Improved completion performance by re-using completion lists when possible.
;; - Introduced new customizable variable: `phpinspect-imports-remove-unused',
;;   which enables/disables this behaviour for `phpinspect-fix-imports'.
;;   The default is disabled (nil).
;; - Increased test coverage for the parser and fixed various bugs discovered in
;;   the process.

;; Version 1.2.0

;; - Fixed bug in the resolving of function call return types.
;; - Implemented parsing of string concatenation tokens (.), allowing a more
;;   accurate determination of the statement to provide information about.
;;   (read: "\'a string\' . $foo->" now actually yields completion results
;;   instead of failing).

;;; Code:

(require 'cl-lib)
(require 'json)

;; internal dependencies
(require 'phpinspect-cache)
(require 'phpinspect-parser)
(require 'phpinspect-project)
(require 'phpinspect-util)
(require 'phpinspect-type)
(require 'phpinspect-index)
(require 'phpinspect-typedef)
(require 'phpinspect-worker)
(require 'phpinspect-autoload)
(require 'phpinspect-imports)
(require 'phpinspect-buffer)
(require 'phpinspect-resolvecontext)
(require 'phpinspect-eldoc)
(require 'phpinspect-suggest)
(require 'phpinspect-completion)
(require 'phpinspect-name)

(defvar phpinspect-insert-file-contents-function #'insert-file-contents-literally
  "Function that phpinspect uses to insert file contents into a buffer.")

(defvar phpinspect-type-filepath-function #'phpinspect-get-class-filepath
  "Function that phpinspect uses to find the filepath of a class by its FQN.")

(define-inline phpinspect-type-filepath (fqn)
  "Call `phpinspect-type-filepath-function' with FQN as argument."
  (inline-quote
   (funcall phpinspect-type-filepath-function ,fqn)))

(defun phpinspect-parse-string-to-bmap (string)
  (with-temp-buffer
    (insert string)
    (let ((context (phpinspect-make-pctx :incremental t
                                         :bmap (phpinspect-make-bmap))))
      (phpinspect-with-parse-context context
        (phpinspect-parse-current-buffer))

      (phpinspect-pctx-bmap context))))

(defun phpinspect--init-mode ()
  "Initialize the phpinspect minor mode for the current buffer."
  (phpinspect-ensure-worker)
  (when (and phpinspect-load-stubs (not phpinspect-stub-cache))
    (phpinspect-load-stub-index))

  (phpinspect-claim-buffer (current-buffer))

  (phpinspect-register-current-buffer
   (lambda () (phpinspect-buffer-reset phpinspect-current-buffer)))
  (add-hook 'kill-buffer-hook #'phpinspect-unregister-current-buffer)

  (add-hook 'completion-at-point-functions #'phpinspect-complete-at-point nil 'local)


  (add-to-list 'eldoc-documentation-functions #'phpinspect-eldoc-function)

  (make-local-variable 'eldoc-message-commands)
  (eldoc-add-command 'c-electric-paren)
  (eldoc-add-command 'c-electric-backspace)

  (phpinspect--after-save-action)

  (add-hook 'after-save-hook #'phpinspect--after-save-action nil 'local))

(defun phpinspect--after-save-action ()
  "This is intended to be run every time a phpinspect buffer is saved.

Reparses the entire buffer without token reuse."
  (when (and (boundp 'phpinspect-mode) phpinspect-mode)
    ;; Make sure that the project's autoloader is aware of the file
    (when-let ((file-name (buffer-file-name))
               (project (phpinspect-buffer-project phpinspect-current-buffer))
               (autoloader (phpinspect-project-autoload project)))
      (phpinspect-autoloader-ensure-file-indexed autoloader file-name))))

(defun phpinspect--disable-mode ()
  "Clean up the buffer environment for the mode to be disabled."
  (setq phpinspect-current-buffer nil)
  (kill-local-variable 'phpinspect--buffer-project)
  (kill-local-variable 'eldoc-documentation-function)
  (kill-local-variable 'eldoc-message-commands)
  (phpinspect-unregister-current-buffer))

(defun phpinspect--mode-function ()
  (if (and (boundp 'phpinspect-mode) phpinspect-mode)
      (phpinspect--init-mode)
    (phpinspect--disable-mode)))

(defgroup phpinspect '((phpinspect-projects custom-variable))
  "PHPInspect, PHP code intelligence and completion"
  :group 'programming
  :group 'php
  :link '(function-link phpinspect-mode))

(define-minor-mode phpinspect-mode
  "A minor mode for intelligent completion for and interaction
with PHP files.

See also: \\[customize-group] RET phpinspect RET .

To initially index a project, use M-x `phpinspect-index-current-project'
in a buffer of one of the project files. Project root is detected with
`phpinspect-project-root-file-list'.

For completion see `phpinspect-complete-at-point' which is
automatically added to `completion-at-point-functions' when
phpinspect-mode is activated.

For eldoc see `phpinspect-eldoc-function'.

For finding/opening class files see
 `phpinspect-find-own-class-file' (bound to \\[phpinspect-find-own-class-file]) and
 `phpinspect-find-class-file' (bound to \\[phpinspect-find-class-file]).

To automatically add missing use statements for used classes to a
visited file, use `phpinspect-fix-imports'
(bound to \\[phpinspect-fix-imports]].)

By default, phpinspect loads code like PHP does: via standards
compliant autoloading. Upon opening a file and activating
phpinspect-mode, phpinspect will look for a composer.json file to
extract autoload-information from. Supported autoload directives
are:
  - files: list of files to parse/index wholesale
  - PSR-0: directory with nested subdirectories structured according to
           the namespacing scheme.
  - PSR-4: PSR-0 directory with namespace prefix
  - classmap: Directories/files to parse and index wholesale.
              (not enabled by default, see additional note)

Note on classmap directive: As of [2024-09-28], the classmap
autoload directive has been implemented but is not enabled by
default. It can be enabled by setting
`phpinspect-autoload-classmaps' to `t'.

It is also possible to wholesale index an entire directory by
adding it as an include dir. To do this, use
\\[phpinspect-project-add-include-dir]. Include directories can
be edited at all times using \\[customize-group] RET phpinspect.
Include dirs do not depend on the project using composer.

Because of limitations in the current autoloader implementation,
you will have to run \\[phpinspect-index-current-project] when
you delete a file, for it to be removed from the autoloader.

Example configuration if you already have a completion
UI (Company, Corfu) setup that can take advantage of completion
at point (capf) functions:

With `use-package':
    (use-package phpinspect
      :ensure nil
      :commands (phpinspect-mode)
      :bind ((\"C-c c\" . phpinspect-find-own-class-file)
             (\"C-c u\" . phpinspect-fix-imports)
             :map phpinspect-mode-map
             (\"C-c a\" . phpinspect-find-class-file))
      ;; Automatically add missing imports before saving a file
      :hook ((before-save . phpinspect-fix-imports))
      :custom (phpinspect-autoload-classmaps t
               \"Enable classmap autoload directive\"))

With a classic hook function:
    (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)

;; end example configuration."

  :after-hook (phpinspect--mode-function)
  :keymap  (list (cons (kbd "C-c u") #'phpinspect-fix-imports)))

(defun phpinspect--suggest-at-point ()
  (phpinspect--log "Entering suggest at point. Point: %d" (point))
  (phpinspect-completion-query-execute
   (phpinspect-make-completion-query
    :buffer phpinspect-current-buffer
    :completion-point (phpinspect--determine-completion-point)
    :point (point))))

(defsubst phpinspect-insert-file-contents (&rest args)
  "Call `phpinspect-insert-file-contents-function' with ARGS as arguments."
  (apply phpinspect-insert-file-contents-function args))

(defun phpinspect-get-all-fqns (&optional filter)
  "Return a list of all FQNS congruent with FILTER in the currently active project.

FILTER must be nil or the symbol `own' if FILTER is `own', only
fully qualified names from the project's source, and not its
dependencies, are returned."
  (let* ((project (phpinspect--cache-get-project-create
                   (phpinspect--get-or-create-global-cache)
                   (phpinspect-current-project-root)))
         (autoloader (phpinspect-project-autoload project)))
    (let ((fqns))
      (maphash (lambda (type _) (push (phpinspect-name-string type) fqns))
               (if (eq 'own filter)
                   (phpinspect-autoloader-own-types autoloader)
                 (phpinspect-autoloader-types autoloader)))
      fqns)))

;;;###autoload
(defun phpinspect-find-class-file (fqn)
  "`find-file', but for FQNs of PHP classes.

When called interactively, presents the the user with a list of
available FQNs in a project.  This may require
`phpinspect-index-current-project' to have run once for the
project directory before it can be used."
  (interactive (list (phpinspect--make-type
                      :name (completing-read "Class: " (phpinspect-get-all-fqns)))))
  (find-file (phpinspect-type-filepath fqn)))

(defun phpinspect-find-own-class-file (fqn)
  "`phpinspect-find-class-file', but for non-vendored classes.

When called interactively, presents the user with a list of
available FQNs for classes in the current project, which aren't
located in \"vendor\" folder."
  (interactive (list (phpinspect--make-type
                      :name
                      (completing-read "Class: " (phpinspect-get-all-fqns 'own)))))
  (find-file (phpinspect-type-filepath fqn)))

(defun phpinspect-get-class-filepath (class &optional index-new)
  "Retrieve filepath to CLASS definition file.

when INDEX-NEW is non-nil, new files are added to the index
before the search is executed."
  (let* ((project (phpinspect--cache-get-project-create
                   (phpinspect--get-or-create-global-cache)
                   (phpinspect-current-project-root))))
    (phpinspect-project-get-type-filepath project class index-new)))

(defun phpinspect-project-refresh-autoloader (project)
  (interactive (list (phpinspect--cache-get-project-create
                      (phpinspect--get-or-create-global-cache)
                      (phpinspect-current-project-root))))
  (let* ((autoloader (phpinspect-project-autoload project)))
    ;; Update display so that it is clear to the user that emacs is
    ;; responsive. Otherwise the autoloader refresh thread hogging the cpu will
    ;; make it look like emacs is not responsive, especially when M-x uses some
    ;; kind of completion framework, in which case the completion popup will
    ;; appear frozen while the thread is executing.
    (redisplay)

    (phpinspect-autoloader-refresh autoloader nil 'report-progress)))

(defun phpinspect-index-current-project ()
  "Index all available FQNs in the current project."
  (interactive)
  (let* ((project (phpinspect--cache-get-project-create
                  (phpinspect--get-or-create-global-cache)
                  (phpinspect-current-project-root))))
    (phpinspect-project-refresh-autoloader project)
    (phpinspect-project-enqueue-include-dirs project)))

(defun phpinspect-insert-skeleton ()
  "Insert a PHP opening tag and a namespace at the top of the buffer.

Only works when in a PSR0 or PSR4 autoload-able directory."
  (interactive)
  (when (and phpinspect-mode buffer-file-name)
    (when-let ((type-name (phpinspect-autoloader-request-type-name
                           (phpinspect-project-autoload
                            (phpinspect-current-project))
                           buffer-file-name)))
      (save-excursion
        (goto-char (point-min))

        (insert (format "<?php

namespace %s;

" (phpinspect-name-non-fqn-string
   (phpinspect-name-namespace type-name))))))))

(when (featurep 'autoinsert)
  (define-auto-insert "\\.php$" #'phpinspect-insert-skeleton))

(provide 'phpinspect)
;;; phpinspect.el ends here