branch: externals/phpinspect commit dfdef3e3823a07ce43229c258c652fd9afe673b4 Author: Hugo Thunnissen <de...@hugot.nl> Commit: Hugo Thunnissen <de...@hugot.nl>
Implement support for PHP8.1 property typehints --- Makefile | 2 +- phpinspect-buffer.el | 25 ++++++++++++++++-- phpinspect-index.el | 42 +++++++++++++++++++++--------- phpinspect-meta.el | 16 ++++++++++++ phpinspect-parser.el | 6 ++--- phpinspect-resolve.el | 12 ++++----- phpinspect-token-predicates.el | 5 ++++ test/fixtures/IndexClass1.eld | 2 +- test/fixtures/IndexClass2.eld | 2 +- test/test-buffer.el | 58 ++++++++++++++++++++++++++++++++++++++++++ test/test-index.el | 54 +++++++++++++++++++++++++++++++++++++++ 11 files changed, 197 insertions(+), 27 deletions(-) diff --git a/Makefile b/Makefile index 4dce40a255..1dd88afd19 100644 --- a/Makefile +++ b/Makefile @@ -49,4 +49,4 @@ compile-native: ./.deps .PHONY: test test: deps - $(RUN_EMACS) -L ./test -l ./test/phpinspect-test e -f ert-run-tests-batch + $(RUN_EMACS) -L ./test -l ./test/phpinspect-test -f ert-run-tests-batch diff --git a/phpinspect-buffer.el b/phpinspect-buffer.el index 3bdaa5c95d..354ec5e0b2 100644 --- a/phpinspect-buffer.el +++ b/phpinspect-buffer.el @@ -339,27 +339,38 @@ linked with." (setf (phpinspect-buffer-class-variables buffer) (phpinspect-make-toc class-variables))) (dolist (var to-be-indexed) + ;; We have left the class we were previously indexing properties + ;; for. (when (and class (> (phpinspect-meta-start var) (phpinspect-meta-end class))) (setq class nil)) + ;; Retrieve the class that the property belongs to. In a lot of cases only + ;; one class will be in the current buffer. But PHP allows the definition + ;; of multiple classes in the same file so this should be supported. (unless class (setq class (phpinspect-toc-token-at-point classes (phpinspect-meta-start var)))) + ;; Fetch the index for the current class (setq class-obj (phpinspect-buffer-get-index-for-token buffer (phpinspect-meta-token class))) (let (scope static indexed index-env comment-before) (if (phpinspect-static-p (phpinspect-meta-token (phpinspect-meta-parent var))) + ;; Variable is defined as [scope?] static [type?] $variable (progn (setq static (phpinspect-meta-parent var)) (when (phpinspect-scope-p (phpinspect-meta-token (phpinspect-meta-parent static))) + ;; Variable is defined as scope static [type?] $variable (setq scope `(,(car (phpinspect-meta-token (phpinspect-meta-parent static))) - ,(phpinspect-meta-token var)) + ,@(phpinspect-meta-token-with-left-siblings var)) comment-before (phpinspect-meta-find-left-sibling (phpinspect-meta-parent static))))) (when (phpinspect-scope-p (phpinspect-meta-token (phpinspect-meta-parent var))) + ;; Variable is defined as scope [type?] $variable (setq scope (phpinspect-meta-token (phpinspect-meta-parent var)) comment-before (phpinspect-meta-find-left-sibling (phpinspect-meta-parent var))))) - (unless scope (setq scope `(:public ,(phpinspect-meta-token var)))) + (unless scope + (setq scope `(:public ,@(mapcar #'phpinspect-meta-token (phpinspect-meta-left-siblings var)) + ,(phpinspect-meta-token var)))) (unless (setq index-env (alist-get class class-environments nil nil #'eq)) (setq index-env (phpinspect-get-token-index-context namespaces imports class)) @@ -419,6 +430,16 @@ linked with." (phpinspect-buffer-parse buffer 'no-interrupt)) (cl-defmethod phpinspect-buffer-update-project-index ((buffer phpinspect-buffer)) + "Update project index using the last parsed token map of this +buffer. When `phpinspect-buffer-parse' has been executed before +and a map is available from the previous parse, this is used. If +none is available `phpinspect-buffer-parse' is called before +continuing execution." + + ;; Parse buffer if no map is available. + (unless (phpinspect-buffer-map buffer) + (phpinspect-buffer-parse buffer)) + ;; Use inhibit-quit to prevent index corruption though partial index ;; application. (let ((inhibit-quit t)) diff --git a/phpinspect-index.el b/phpinspect-index.el index 6c999d0fed..a786861399 100644 --- a/phpinspect-index.el +++ b/phpinspect-index.el @@ -174,10 +174,29 @@ function (think \"new\" statements, return types etc.)." (when (stringp type) type))) (defun phpinspect--index-variable-from-scope (type-resolver scope comment-before &optional static) - "Index the variable inside `scope`." - (let* ((variable-name (cadr (cadr scope))) - (type - (phpinspect--variable-type-string-from-comment comment-before variable-name))) + "Index the variable inside SCOPE, use doc in COMMENT-BEFORE if missing typehint. + +Provide STATIC if the variable was defined as static. + +SCOPE should be a scope token (`phpinspect-scope-p')." + (setq scope (take 5 (seq-filter #'phpinspect-not-comment-p scope))) + (let (variable-name type) + (cond + ((phpinspect--match-sequence (take 3 scope) + ;; Sequence: scope-type, typehint, variable [ = value ] + :m * :f #'phpinspect-word-p :f #'phpinspect-variable-p) + (setq variable-name (cadr (nth 2 scope))) + (setq type (cadr (nth 1 scope)))) + ((phpinspect--match-sequence (take 2 scope) + ;; Sequence: variable [ = value ] + :m * :f #'phpinspect-variable-p) + (setq variable-name (cadr (nth 1 scope)) + ;; Since no typehint is available, attempt extracting one from a + ;; docstring. + type (phpinspect--variable-type-string-from-comment + comment-before variable-name))) + (t (error (format "Failed to determine variable from scope %s" scope)))) + (phpinspect--log "calling resolver from index-variable-from-scope") (phpinspect--make-variable ;; Static class variables are always prefixed with dollar signs when @@ -260,7 +279,7 @@ function (think \"new\" statements, return types etc.)." (cond ((phpinspect-const-p (cadr token)) (push (phpinspect--index-const-from-scope token) constants)) - ((phpinspect-variable-p (cadr token)) + ((seq-find #'phpinspect-variable-p token) (push (phpinspect--index-variable-from-scope type-resolver token comment-before) @@ -275,10 +294,9 @@ function (think \"new\" statements, return types etc.)." add-used-types) static-methods)) - ((phpinspect-variable-p (cadadr token)) + ((seq-find #'phpinspect-variable-p (cdadr token)) (push (phpinspect--index-variable-from-scope type-resolver - (list (car token) - (cadadr token)) + `(,(car token) ,@(cdadr token)) comment-before 'static) static-variables)))) @@ -298,11 +316,11 @@ function (think \"new\" statements, return types etc.)." add-used-types) static-methods)) - ((phpinspect-variable-p (cadr token)) + ((seq-find #'phpinspect-variable-p (cdr token)) (push (phpinspect--index-variable-from-scope type-resolver - `(:public - ,(cadr token)) - comment-before) + `(:public ,@(cdr token)) + comment-before + 'static) static-variables)))) ((phpinspect-const-p token) ;; Bare constants are always public diff --git a/phpinspect-meta.el b/phpinspect-meta.el index 098b9ed7a5..d6590af02a 100644 --- a/phpinspect-meta.el +++ b/phpinspect-meta.el @@ -149,6 +149,22 @@ (phpinspect-meta-children (phpinspect-meta-parent meta)) (phpinspect-meta-parent-offset meta)) #'phpinspect-meta-sort-start)) +(defun phpinspect-meta-left-sibling-tokens (meta) + (let* ((tokens (cons nil nil)) + (rear tokens)) + (dolist (sibling (phpinspect-meta-left-siblings meta)) + (setq rear (setcdr rear (cons (phpinspect-meta-token sibling) nil)))) + (cdr tokens))) + +(defun phpinspect-meta-token-with-left-siblings (meta) + (nconc (phpinspect-meta-left-sibling-tokens meta) (list (phpinspect-meta-token meta)))) + +(defun phpinspect-meta-left-siblings (meta) + (sort + (phpinspect-splayt-find-all-before + (phpinspect-meta-children (phpinspect-meta-parent meta)) (phpinspect-meta-parent-offset meta)) + #'phpinspect-meta-sort-start)) + (defun phpinspect-meta-wrap-token-pred (predicate) (lambda (meta) (funcall predicate (phpinspect-meta-token meta)))) diff --git a/phpinspect-parser.el b/phpinspect-parser.el index 61f41f69fc..26cc97c7d7 100644 --- a/phpinspect-parser.el +++ b/phpinspect-parser.el @@ -760,19 +760,19 @@ Returns the consumed text string without face properties." (phpinspect-defparser scope-public :tree-keyword "public" :handlers '(function-keyword static-keyword const-keyword class-variable here-doc - string terminator tag comment) + string terminator tag comment word) :delimiter-predicate #'phpinspect--scope-terminator-p) (phpinspect-defparser scope-private :tree-keyword "private" :handlers '(function-keyword static-keyword const-keyword class-variable here-doc - string terminator tag comment) + string terminator tag comment word) :delimiter-predicate #'phpinspect--scope-terminator-p) (phpinspect-defparser scope-protected :tree-keyword "protected" :handlers '(function-keyword static-keyword const-keyword class-variable here-doc - string terminator tag comment) + string terminator tag comment word) :delimiter-predicate #'phpinspect--scope-terminator-p) (phpinspect-defhandler scope-keyword (start-token max-point) diff --git a/phpinspect-resolve.el b/phpinspect-resolve.el index cae5fa8185..9dfdb87291 100644 --- a/phpinspect-resolve.el +++ b/phpinspect-resolve.el @@ -46,6 +46,11 @@ (or (phpinspect-assignment-p token) (equal '(:word "as") token))) +(define-inline phpinspect-not-assignment-p (token) + "Inverse of applying `phpinspect-assignment-p to TOKEN." + (inline-quote + (not (phpinspect-maybe-assignment-p ,token)))) + (cl-defgeneric phpinspect--find-assignments-in-token (token) "Find any assignments that are in TOKEN, at top level or nested in blocks" (when (keywordp (car token)) @@ -71,13 +76,6 @@ (phpinspect--log "Found statements in token: %s" statements) assignments)) -(defsubst phpinspect-not-assignment-p (token) - "Inverse of applying `phpinspect-assignment-p to TOKEN." - (not (phpinspect-maybe-assignment-p token))) - -(defsubst phpinspect-not-comment-p (token) - (not (phpinspect-comment-p token))) - (defun phpinspect--find-assignments-by-predicate (token predicate) (let ((variable-assignments) (all-assignments (phpinspect--find-assignments-in-token token))) diff --git a/phpinspect-token-predicates.el b/phpinspect-token-predicates.el index 27dce56988..4724c39e6f 100644 --- a/phpinspect-token-predicates.el +++ b/phpinspect-token-predicates.el @@ -251,4 +251,9 @@ Type can be any of the token types returned by (and (listp token) (keywordp (car token)))) +(define-inline phpinspect-not-comment-p (token) + (inline-quote + (not (phpinspect-comment-p ,token)))) + + (provide 'phpinspect-token-predicates) diff --git a/test/fixtures/IndexClass1.eld b/test/fixtures/IndexClass1.eld index 8ccf155b4c..88f6f3f9d2 100644 --- a/test/fixtures/IndexClass1.eld +++ b/test/fixtures/IndexClass1.eld @@ -1 +1 @@ -(:root (:word "declare") (:list (:word "strict_types") (:assignment "=")) (:terminator ";") (:namespace (:word "App\\Entity") (:terminator ";") (:use (:word "Doctrine\\ORM\\Mapping") (:word "as") (:word "ORM") (:terminator ";")) (:doc-block (:annotation "ORM\\Entity")) (:class (:declaration (:word "class") (:word "AuthToken")) (:block (:private (:class-variable "token") (:terminator ";")) (:doc-block (:var-annotation (:word "App\\\\Entity\\\\User"))) (:private (:class-variable "user") (: [...] +(:root (:word "declare") (:list (:word "strict_types") (:assignment "=")) (:terminator ";") (:namespace (:word "App\\Entity") (:terminator ";") (:use (:word "Doctrine\\ORM\\Mapping") (:word "as") (:word "ORM") (:terminator ";")) (:doc-block (:annotation "ORM\\Entity")) (:class (:declaration (:word "class") (:word "AuthToken")) (:block (:private (:class-variable "token") (:terminator ";")) (:doc-block (:var-annotation (:word "App\\\\Entity\\\\User"))) (:private (:class-variable "user") (: [...] diff --git a/test/fixtures/IndexClass2.eld b/test/fixtures/IndexClass2.eld index 691272b1e9..ce6fcde686 100644 --- a/test/fixtures/IndexClass2.eld +++ b/test/fixtures/IndexClass2.eld @@ -1 +1 @@ -(:root (:word "declare") (:list (:word "strict_types") (:assignment "=")) (:terminator ";") (:namespace (:word "App\\Entity") (:terminator ";") (:use (:word "Doctrine\\ORM\\Mapping") (:word "as") (:word "ORM") (:terminator ";")) (:doc-block (:annotation "ORM\\Entity")) (:class (:declaration (:word "class") (:word "AuthToken")) (:block (:private (:class-variable "token") (:terminator ";")) (:private (:class-variable "extra") (:terminator ";")) (:doc-block (:var-annotation (:word "App\\\\E [...] +(:root (:word "declare") (:list (:word "strict_types") (:assignment "=")) (:terminator ";") (:namespace (:word "App\\Entity") (:terminator ";") (:use (:word "Doctrine\\ORM\\Mapping") (:word "as") (:word "ORM") (:terminator ";")) (:doc-block (:annotation "ORM\\Entity")) (:class (:declaration (:word "class") (:word "AuthToken")) (:block (:private (:class-variable "token") (:terminator ";")) (:private (:class-variable "extra") (:terminator ";")) (:doc-block (:var-annotation (:word "App\\\\E [...] diff --git a/test/test-buffer.el b/test/test-buffer.el index 9df7b2a476..689e7cb2d4 100644 --- a/test/test-buffer.el +++ b/test/test-buffer.el @@ -491,3 +491,61 @@ class AccountStatisticsController { (:use (:word "Illuminate\\Support\\Facades\\Auth") (:terminator ";"))) (mapcar #'phpinspect-meta-token (phpinspect-splayt-to-list (phpinspect-bmap-imports bmap))))))))) + + +(ert-deftest phpinspect-buffer-index-typehinted-class-variables () + (with-temp-buffer + (let ((buffer (phpinspect-make-buffer + :buffer (current-buffer) + :-project (phpinspect--make-project :autoload (phpinspect-make-autoloader))))) + (insert "<?php +declare(strict_types=1); + +namespace App\\Controller\\Api\\V1; + +use Illuminate\\Database\\Eloquent\\Model; +use Illuminate\\Database\\Eloquent\\Relations\\Relation; +use Illuminate\\Support\\Facades\\Auth; + +class AccountStatisticsController { + + public Model $model; + private Model $privModel; + static Relation $relation; + public static Relation $staticRelation; +}") + + (phpinspect-buffer-update-project-index buffer) + + (let ((class (phpinspect-project-get-class + (phpinspect-buffer-project buffer) + (phpinspect--make-type + :name "\\App\\Controller\\Api\\V1\\AccountStatisticsController" + :fully-qualified t)))) + (should class) + + (let ((model (phpinspect--class-get-variable class "model")) + (priv-model (phpinspect--class-get-variable class "privModel")) + ;; Static variables are stored with "$" prefix + (relation (phpinspect--class-get-variable class "$relation")) + (static-relation (phpinspect--class-get-variable class "$staticRelation"))) + (should model) + (should priv-model) + (should relation) + (should static-relation) + + (let ((model-type (phpinspect--make-type + :name "\\Illuminate\\Database\\Eloquent\\Model" + :fully-qualified t)) + (relation-type (phpinspect--make-type + :name "\\Illuminate\\Database\\Eloquent\\Relations\\Relation" + :fully-qualified t))) + + (should (phpinspect--variable-type model)) + (should (phpinspect--type= model-type (phpinspect--variable-type model))) + (should (phpinspect--variable-type priv-model)) + (should (phpinspect--type= model-type (phpinspect--variable-type priv-model))) + (should (phpinspect--variable-type relation)) + (should (phpinspect--type= relation-type (phpinspect--variable-type relation))) + (should (phpinspect--variable-type static-relation)) + (should (phpinspect--type= relation-type (phpinspect--variable-type static-relation))))))))) diff --git a/test/test-index.el b/test/test-index.el index f984b6e76b..d1621bc8ae 100644 --- a/test/test-index.el +++ b/test/test-index.el @@ -291,3 +291,57 @@ function example(Firewall $wall): Thing {}") (should (member (phpinspect-intern-name "Thing") (alist-get 'used-types index))) (should (alist-get 'used-types index)))) + +(ert-deftest phpinspect-index-typehinted-variables () + (with-temp-buffer + (let ((project (phpinspect--make-project :autoload (phpinspect-make-autoloader)))) + (insert "<?php +declare(strict_types=1); + +namespace App\\Controller\\Api\\V1; + +use Illuminate\\Database\\Eloquent\\Model; +use Illuminate\\Database\\Eloquent\\Relations\\Relation; +use Illuminate\\Support\\Facades\\Auth; + +class AccountStatisticsController { + + public Model $model; + private Model $privModel; + static Relation $relation; + public static Relation $staticRelation; +}") + (phpinspect-project-add-index project (phpinspect-index-current-buffer)) + + (let ((class (phpinspect-project-get-class + project + (phpinspect--make-type + :name "\\App\\Controller\\Api\\V1\\AccountStatisticsController" + :fully-qualified t)))) + (should class) + + (let ((model (phpinspect--class-get-variable class "model")) + (priv-model (phpinspect--class-get-variable class "privModel")) + ;; Static variables are stored with "$" prefix + (relation (phpinspect--class-get-variable class "$relation")) + (static-relation (phpinspect--class-get-variable class "$staticRelation"))) + (should model) + (should priv-model) + (should relation) + (should static-relation) + + (let ((model-type (phpinspect--make-type + :name "\\Illuminate\\Database\\Eloquent\\Model" + :fully-qualified t)) + (relation-type (phpinspect--make-type + :name "\\Illuminate\\Database\\Eloquent\\Relations\\Relation" + :fully-qualified t))) + + (should (phpinspect--variable-type model)) + (should (phpinspect--type= model-type (phpinspect--variable-type model))) + (should (phpinspect--variable-type priv-model)) + (should (phpinspect--type= model-type (phpinspect--variable-type priv-model))) + (should (phpinspect--variable-type relation)) + (should (phpinspect--type= relation-type (phpinspect--variable-type relation))) + (should (phpinspect--variable-type static-relation)) + (should (phpinspect--type= relation-type (phpinspect--variable-type static-relation)))))))))