From 224bbd791602f724db8b81bb860b8444142b11e3 Mon Sep 17 00:00:00 2001 From: Hugo Thunnissen Date: Sat, 18 Feb 2023 21:27:42 +0100 Subject: [PATCH] Implement array member type inference phpinspect now understands typed arrays! --- phpinspect-cache.el | 9 +- phpinspect-index.el | 25 +- phpinspect-parser.el | 21 +- phpinspect-type.el | 12 +- phpinspect-util.el | 135 ++++++- phpinspect.el | 334 ++++++++++++------ .../IncompleteClassBlockedNamespace.eld | 2 +- .../IncompleteClassMultipleNamespaces.eld | 2 +- test/fixtures/IndexClass1-indexed.eld | 2 +- test/fixtures/IndexClass1.eld | 2 +- test/fixtures/IndexClass1.php | 8 + test/phpinspect-test.el | 124 ++++++- test/test-index.el | 1 + test/test-util.el | 47 +++ 14 files changed, 592 insertions(+), 132 deletions(-) create mode 100644 test/test-util.el diff --git a/phpinspect-cache.el b/phpinspect-cache.el index a401b50..b6f25e5 100644 --- a/phpinspect-cache.el +++ b/phpinspect-cache.el @@ -41,14 +41,11 @@ as keys and project caches as values.")) ((cache phpinspect--cache) (project-root string)) (gethash project-root (phpinspect--cache-projects cache))) -(cl-defgeneric phpinspect--cache-get-project-create - ((cache phpinspect--cache) (project-root string)) - "Get a project that is located in PROJECT-ROOT from CACHE. -If no such project exists in the cache yet, it is created and -then returned.") - (cl-defmethod phpinspect--cache-get-project-create ((cache phpinspect--cache) (project-root string)) + "Get a project that is located in PROJECT-ROOT from CACHE. +If no such project exists in the cache yet, it is created and +then returned." (let ((project (phpinspect--cache-getproject cache project-root))) (unless project (setq project (puthash project-root diff --git a/phpinspect-index.el b/phpinspect-index.el index 31e802f..ba23303 100644 --- a/phpinspect-index.el +++ b/phpinspect-index.el @@ -72,17 +72,17 @@ function (think \"new\" statements, return types etc.)." ;; @return annotation. When dealing with a collection, we want to store the ;; type of its members. - (let* ((is-collection - (when type - (member (phpinspect--type-name type) phpinspect-collection-types))) - (return-annotation-type - (when (or (phpinspect--should-prefer-return-annotation type) is-collection) - (cadadr - (seq-find #'phpinspect-return-annotation-p - comment-before))))) - (phpinspect--log "found return annotation %s when type is %s" - return-annotation-type - type) + (let* ((return-annotation-type + (cadadr (seq-find #'phpinspect-return-annotation-p comment-before))) + (is-collection + (and type + (phpinspect--type-is-collection type)))) + (phpinspect--log "found return annotation %s in %s when type is %s" + return-annotation-type comment-before type) + + (when (string-suffix-p "[]" return-annotation-type) + (setq is-collection t) + (setq return-annotation-type (string-trim-right return-annotation-type "\\[\\]"))) (when return-annotation-type (cond ((phpinspect--should-prefer-return-annotation type) @@ -104,8 +104,7 @@ function (think \"new\" statements, return types etc.)." (phpinspect--make-function :scope `(,(car scope)) :name (cadadr (cdr declaration)) - :return-type (if type (funcall type-resolver type) - phpinspect--null-type) + :return-type (or type phpinspect--null-type) :arguments (phpinspect--index-function-arg-list type-resolver (phpinspect-function-argument-list php-func) diff --git a/phpinspect-parser.el b/phpinspect-parser.el index 1265a76..514721c 100644 --- a/phpinspect-parser.el +++ b/phpinspect-parser.el @@ -173,6 +173,9 @@ Type can be any of the token types returned by "Get the argument list of a function" (seq-find #'phpinspect-list-p (seq-find #'phpinspect-declaration-p php-func nil) nil)) +(defun phpinspect-function-block (token) + (cadr token)) + (defun phpinspect-annotation-p (token) (phpinspect-token-type-p token :annotation)) @@ -236,7 +239,8 @@ Type can be any of the token types returned by (phpinspect-token-type-p object :use)) (defun phpinspect-comment-p (token) - (phpinspect-token-type-p token :comment)) + (or (phpinspect-token-type-p token :comment) + (phpinspect-token-type-p token :doc-block))) (defsubst phpinspect-class-block (class) (caddr class)) @@ -444,7 +448,8 @@ token is \";\", which marks the end of a statement in PHP." (list-handler (phpinspect-handler 'list)) (list-regexp (phpinspect-handler-regexp 'list)) (word-handler (phpinspect-handler 'word)) - (word-regexp (phpinspect-handler-regexp 'word)) + ;; Return annotations may end with "[]" for collections. + (word-regexp (concat (phpinspect-handler-regexp 'word) "\\(\\[\\]\\)?")) (variable-handler (phpinspect-handler 'variable)) (variable-regexp (phpinspect-handler-regexp 'variable)) (annotation-regexp (phpinspect-handler-regexp 'annotation))) @@ -740,9 +745,15 @@ nature like argument lists" ;; TODO: Look into using different names for function and class :declaration tokens. They ;; don't necessarily require the same handlers to parse. (defsubst phpinspect-get-or-create-declaration-parser () - (phpinspect-get-parser-create :declaration - '(comment word list terminator tag) - #'phpinspect-end-of-token-p)) + (let ((parser (phpinspect-get-parser-create + :declaration + '(comment word list terminator tag) + #'phpinspect-end-of-token-p))) + (lambda (&rest arguments) + (let ((result (apply parser arguments))) + (if (phpinspect-terminator-p (car (last result))) + (butlast result) + result))))) (phpinspect-defhandler function-keyword (start-token max-point) diff --git a/phpinspect-type.el b/phpinspect-type.el index 550d22d..dce5805 100644 --- a/phpinspect-type.el +++ b/phpinspect-type.el @@ -26,7 +26,8 @@ (require 'phpinspect-util) (cl-defstruct (phpinspect--type - (:constructor phpinspect--make-type-generated)) + (:constructor phpinspect--make-type-generated) + (:copier phpinspect--copy-type)) "Represents an instance of a PHP type in the phpinspect syntax tree." (name-symbol nil :type symbol @@ -97,6 +98,13 @@ See https://wiki.php.net/rfc/static_return_type ." (when (phpinspect--type= type native) (throw 'found t))))) +(defsubst phpinspect--type-is-collection (type) + (catch 'found + (dolist (collection phpinspect-collection-types) + (when (phpinspect--type= type collection) + (throw 'found t))))) + + (cl-defmethod phpinspect--type-name ((type phpinspect--type)) (symbol-name (phpinspect--type-name-symbol type))) @@ -142,6 +150,8 @@ NAMESPACE may be nil, or a string with a namespace FQN." type (phpinspect--resolve-type-name types namespace (phpinspect--type-name type))) (setf (phpinspect--type-fully-qualified type) t)) + (when (phpinspect--type-is-collection type) + (setf (phpinspect--type-collection type) t)) type) (defun phpinspect--make-type-resolver (types &optional token-tree namespace) diff --git a/phpinspect-util.el b/phpinspect-util.el index 56920f5..458f95f 100644 --- a/phpinspect-util.el +++ b/phpinspect-util.el @@ -52,7 +52,6 @@ PHP. Used to optimize string comparison.") (message "Enabled phpinspect logging.") (message "Disabled phpinspect logging."))) - (defsubst phpinspect--log (&rest args) (when phpinspect--debug (with-current-buffer (get-buffer-create "**phpinspect-logs**") @@ -60,7 +59,139 @@ PHP. Used to optimize string comparison.") (set (make-local-variable 'window-point-insertion-type) t)) (goto-char (buffer-end 1)) (insert (concat "[" (format-time-string "%H:%M:%S") "]: " - (apply #'format args) "\n"))))) + (apply #'format args) "\n"))))) + +(cl-defstruct (phpinspect--pattern + (:constructor phpinspect--make-pattern-generated)) + "An object that can be used to match lists to a given +pattern. See `phpinspect--match-sequence'." + (matcher nil + :type lambda + :documentation "The function used to match sequences") + (code nil + :type list + :documentation "The original code list used to create this pattern")) + +(defsubst phpinspect--make-pattern (&rest pattern) + (phpinspect--make-pattern-generated + :matcher (apply #'phpinspect--match-sequence-lambda pattern) + :code pattern)) + +(defun phpinspect--match-sequence-lambda (&rest pattern) + (lambda (sequence) + (apply #'phpinspect--match-sequence sequence pattern))) + +(cl-defmethod phpinspect--pattern-match ((pattern phpinspect--pattern) sequence) + "Match SEQUENCE to PATTERN." + (funcall (phpinspect--pattern-matcher pattern) sequence)) + +;; (defmacro phpinspect--match-sequence (sequence &rest pattern) +;; "Match SEQUENCE to positional matchers defined in PATTERN. + +;; PATTERN is a plist with the allowed keys being :m and :f. Each +;; key-value pair in the plist defines a match operation that is +;; applied to the corresponding index of SEQUENCE (so for ex.: key 0 +;; is applied to pos. 0 of SEQUENCE, key 1 to pos. 1, and so on). + +;; Possible match operations: + +;; :m - This key can be used to match a list element to the literal +;; value supplied for it, using the `equal' comparison function. For +;; example, providing `(\"foobar\") as value will result in the +;; comparison (equal (elt SEQUENCE pos) `(\"foobar\")). There is one +;; exception to this rule: using the symbol * as value for the :m +;; key will match anything, essentially skipping comparison for the +;; element at this position in SEQUENCE. + +;; :f - This key can be used to match a list element by executing +;; the function provided as value. The function is executed with the +;; list element as argument, and will be considered as matching if +;; it evaluates to a non-nil value." +;; (let ((pattern-length (length pattern)) +;; (count 0) +;; (sequence-pos 0) +;; (and-statement)) +;; (while (< count pattern-length) +;; (let ((key (elt pattern count)) +;; (value (elt pattern (+ count 1)))) +;; (unless (keywordp key) +;; (error (format "Invalid, expected keyword, got %s" key))) + +;; (cond ((eq key :m) +;; (unless (eq value '*) +;; (push `(equal ,value (elt ,sequence ,sequence-pos)) and-statement))) +;; ((eq key :f) +;; (push `(,value (elt ,sequence ,sequence-pos)) and-statement)) +;; (t (error (format "Invalid keyword: %s" key)))) +;; (setq count (+ count 2) +;; sequence-pos (+ sequence-pos 1)))) + +;; `(when (= ,sequence-pos (length ,sequence)) (and ,@and-statement)))) + +(defun phpinspect--match-sequence (sequence &rest pattern) + "Match SEQUENCE to positional matchers defined in PATTERN. + +PATTERN is a plist with the allowed keys being :m and :f. Each +key-value pair in the plist defines a match operation that is +applied to the corresponding index of SEQUENCE (so for ex.: key 0 +is applied to pos. 0 of SEQUENCE, key 1 to pos. 1, and so on). + +Possible match operations: + +:m - This key can be used to match a list element to the literal +value supplied for it, using the `equal' comparison function. For +example, providing `(\"foobar\") as value will result in the +comparison (equal (elt SEQUENCE pos) `(\"foobar\")). There is one +exception to this rule: using the symbol * as value for the :m +key will match anything, essentially skipping comparison for the +element at this position in SEQUENCE. + +:f - This key can be used to match a list element by executing +the function provided as value. The function is executed with the +list element as argument, and will be considered as matching if +it evaluates to a non-nil value." + (let* ((pattern-length (length pattern)) + (count 0) + (sequence-pos 0) + (sequence-length (/ pattern-length 2))) + + (and (= sequence-length (length sequence)) + (catch 'found + (while (< count pattern-length) + (let ((key (elt pattern count)) + (value (elt pattern (+ count 1)))) + (unless (keywordp key) + (error (format "Invalid, expected keyword, got %s" key))) + + (cond ((eq key :m) + (unless (eq value '*) + (unless (equal value (elt sequence sequence-pos)) + (throw 'found nil)))) + ((eq key :f) + (unless (funcall value (elt sequence sequence-pos)) + (throw 'found nil))) + (t (error (format "Invalid keyword: %s" key)))) + (setq count (+ count 2) + sequence-pos (+ sequence-pos 1)))) + (throw 'found t))))) + + + +(defun phpinspect--pattern-concat (pattern1 pattern2) + (let* ((pattern1-sequence-length (/ (length (phpinspect--pattern-code pattern1)) 2))) + (phpinspect--make-pattern-generated + :matcher (lambda (sequence) + (unless (< (length sequence) pattern1-sequence-length) + (and (phpinspect--pattern-match + pattern1 + (butlast sequence (- (length sequence) pattern1-sequence-length))) + (phpinspect--pattern-match + pattern2 + (last sequence (- (length sequence) pattern1-sequence-length)))))) + :code (append (phpinspect--pattern-code pattern1) + (phpinspect--pattern-code pattern2))))) + + (provide 'phpinspect-util) ;;; phpinspect-util.el ends here diff --git a/phpinspect.el b/phpinspect.el index fadc7a0..2b02c83 100644 --- a/phpinspect.el +++ b/phpinspect.el @@ -142,12 +142,11 @@ accompanied by all of its enclosing tokens." (last-encountered-token (car (phpinspect--resolvecontext-enclosing-tokens resolvecontext)))) - (if (and (or (phpinspect-function-p last-encountered-token) + (unless (and (or (phpinspect-function-p last-encountered-token) (phpinspect-class-p last-encountered-token)) (phpinspect-block-p token)) ;; When a class or function has been inserted already, its block ;; doesn't need to be added on top. - (phpinspect--resolvecontext-push-enclosing-token resolvecontext nil) (phpinspect--resolvecontext-push-enclosing-token resolvecontext token)) (if (phpinspect-incomplete-token-p last-token) @@ -156,9 +155,6 @@ accompanied by all of its enclosing tokens." (setf (phpinspect--resolvecontext-subject resolvecontext) (phpinspect--get-last-statement-in-token token)) - ;; Delete all occurences of nil caused higher up in the function. - (cl-delete nil (phpinspect--resolvecontext-enclosing-tokens - resolvecontext)) resolvecontext))) @@ -229,7 +225,6 @@ accompanied by all of its enclosing tokens." (when method (phpinspect--function-return-type method)))))) - (defsubst phpinspect-get-cached-project-class-variable-type (project-root class-fqn variable-name) (phpinspect--log "Getting cached project class variable type for %s (%s::%s)" @@ -283,12 +278,19 @@ accompanied by all of its enclosing tokens." (insert string) (phpinspect-parse-current-buffer))) -(defun phpinspect--split-list (predicate list) +(defun phpinspect--split-statements (tokens &optional predicate) + "Split TOKENS into separate statements. + +If PREDICATE is provided, it is used as additional predicate to +determine whether a token delimits a statement." (let ((sublists) (current-sublist)) - (dolist (thing list) - (if (funcall predicate thing) + (dolist (thing tokens) + (if (or (phpinspect-end-of-statement-p thing) + (when predicate (funcall predicate thing))) (when current-sublist + (when (phpinspect-block-p thing) + (push thing current-sublist)) (push (nreverse current-sublist) sublists) (setq current-sublist nil)) (push thing current-sublist))) @@ -394,6 +396,15 @@ TODO: (phpinspect--format-type-name (phpinspect--function-return-type method))))))))) +(cl-defstruct (phpinspect--assignment + (:constructor phpinspect--make-assignment)) + (to nil + :type phpinspect-variable + :documentation "The variable that is assigned to") + (from nil + :type phpinspect-token + :documentation "The token that is assigned from")) + (defsubst phpinspect-block-or-list-p (token) (or (phpinspect-block-p token) (phpinspect-list-p token))) @@ -405,74 +416,72 @@ TODO: (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)) + (setq token (cdr token))) + (let ((assignments) - (block-or-list) - (statements (phpinspect--split-list #'phpinspect-end-of-statement-p token))) + (blocks-or-lists) + (statements (phpinspect--split-statements token))) (dolist (statement statements) - (cond ((seq-find #'phpinspect-assignment-p statement) - (phpinspect--log "Found assignment statement") - (push statement assignments)) - ((setq block-or-list (seq-find #'phpinspect-block-or-list-p statement)) - (phpinspect--log "Found block or list %s" block-or-list) - (setq assignments - (append - (phpinspect--find-assignments-in-token block-or-list) - assignments))))) + (when (seq-find #'phpinspect-maybe-assignment-p statement) + (phpinspect--log "Found assignment statement") + (push statement assignments)) + + (when (setq blocks-or-lists (seq-filter #'phpinspect-block-or-list-p statement)) + (dolist (block-or-list blocks-or-lists) + (phpinspect--log "Found block or list %s" block-or-list) + (let ((local-assignments (phpinspect--find-assignments-in-token block-or-list))) + (dolist (local-assignment (nreverse local-assignments)) + (push local-assignment assignments)))))) + ;; return (phpinspect--log "Found assignments in token: %s" assignments) (phpinspect--log "Found statements in token: %s" statements) assignments)) -(cl-defmethod phpinspect--find-assignments-in-token ((token (head :list))) - "Find assignments that are in a list token." - (phpinspect--log "looking for assignments in list %s" token) - (seq-filter - (lambda (statement) - (phpinspect--log "checking statement %s" statement) - (seq-find #'phpinspect-maybe-assignment-p statement)) - (phpinspect--split-list #'phpinspect-end-of-statement-p (cdr token)))) - (defsubst phpinspect-not-assignment-p (token) "Inverse of applying `phpinspect-assignment-p to TOKEN." (not (phpinspect-maybe-assignment-p token))) -(defun phpinspect--find-assignment-values-for-variable-in-token (variable-name token) - "Find all assignments of variable VARIABLE-NAME in 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))) (dolist (assignment all-assignments) (let* ((is-loop-assignment nil) (left-of-assignment - (seq-take-while #'phpinspect-not-assignment-p assignment)) + (seq-filter #'phpinspect-not-comment-p + (seq-take-while #'phpinspect-not-assignment-p assignment))) (right-of-assignment - (cdr (seq-drop-while (lambda (elt) - (if (phpinspect-maybe-assignment-p elt) - (progn - (when (equal '(:word "as") elt) - (phpinspect--log "It's a loop assignment %s" elt) - (setq is-loop-assignment t)) - nil) - t)) - assignment)))) + (seq-filter + #'phpinspect-not-comment-p + (cdr (seq-drop-while + (lambda (elt) + (if (phpinspect-maybe-assignment-p elt) + (progn + (when (equal '(:word "as") elt) + (phpinspect--log "It's a loop assignment %s" elt) + (setq is-loop-assignment t)) + nil) + t)) + assignment))))) + (if is-loop-assignment - (when (member `(:variable ,variable-name) right-of-assignment) - (push left-of-assignment variable-assignments)) - (when (member `(:variable ,variable-name) left-of-assignment) - (push right-of-assignment variable-assignments))))) + (when (funcall predicate right-of-assignment) + ;; Masquerade as an array access assignment + (setq left-of-assignment (append left-of-assignment '((:array)))) + (push (phpinspect--make-assignment :to right-of-assignment + :from left-of-assignment) + variable-assignments)) + (when (funcall predicate left-of-assignment) + (push (phpinspect--make-assignment :from right-of-assignment + :to left-of-assignment) + variable-assignments))))) + (phpinspect--log "Returning the thing %s" variable-assignments) (nreverse variable-assignments))) - ;; (if (or (member `(:variable ,variable-name) - ;; (seq-take-while #'phpinspect-not-assignment-p - ;; assignment))5 - ;; (and (phpinspect-list-p (car assignment)) - ;; (member `(:variable ,variable-name) (car assignment))) - ;; (and (member '(:word "as") assignment) - ;; (member `(:variable ,variable-name) - ;; (seq-drop-while (lambda (elt) - ;; (not (equal '(:word "as") elt))))))) - ;; (push assignment variable-assignments))) - ;; (nreverse variable-assignments))) - (defsubst phpinspect-drop-preceding-barewords (statement) (while (and statement (phpinspect-word-p (cadr statement))) (pop statement)) @@ -566,7 +575,11 @@ $variable = $variable->method();" resolvecontext) (funcall type-resolver previous-attribute-type) (cadr attribute-word)) - previous-attribute-type))))))))) + previous-attribute-type))))))) + ((and previous-attribute-type (phpinspect-array-p current-token)) + (setq previous-attribute-type + (or (phpinspect--type-contains previous-attribute-type) + previous-attribute-type))))) (phpinspect--log "Found derived type: %s" previous-attribute-type) ;; Make sure to always return a FQN (funcall type-resolver previous-attribute-type)))) @@ -588,48 +601,168 @@ resolve types of function argument variables." (phpinspect--log "Looking for assignments of variable %s in php block" variable-name) (if (string= variable-name "this") (funcall type-resolver (phpinspect--make-type :name "self")) + (phpinspect-get-pattern-type-in-block + resolvecontext (phpinspect--make-pattern :m `(:variable ,variable-name)) + php-block type-resolver function-arg-list))) ;; else - (let* ((assignments - (phpinspect--find-assignment-values-for-variable-in-token variable-name php-block)) - (last-assignment-value (when assignments (car (last assignments))))) - - (phpinspect--log "Last assignment: %s" last-assignment-value) - (phpinspect--log "Current block: %s" php-block) - ;; When the right of an assignment is more than $variable; or "string";(so - ;; (:variable "variable") (:terminator ";") or (:string "string") (:terminator ";") - ;; in tokens), we're likely working with a derived assignment like $object->method() - ;; or $object->attribute - (cond ((and (phpinspect-word-p (car last-assignment-value)) - (string= (cadar last-assignment-value) "new")) - (funcall type-resolver (phpinspect--make-type :name (cadadr last-assignment-value)))) - ((and (> (length last-assignment-value) 1) - (seq-find #'phpinspect-attrib-p last-assignment-value)) - (phpinspect--log "Variable was assigned with a derived statement") - (phpinspect-get-derived-statement-type-in-block resolvecontext - last-assignment-value - php-block - type-resolver - function-arg-list)) - ;; If the right of an assignment is just $variable;, we can check if it is a - ;; function argument and otherwise recurse to find the type of that variable. - ((phpinspect-variable-p (car last-assignment-value)) - (phpinspect--log "Variable was assigned with the value of another variable: %s" - last-assignment-value) - (or (when function-arg-list - (phpinspect-get-variable-type-in-function-arg-list (cadar last-assignment-value) - type-resolver - function-arg-list)) - (phpinspect-get-variable-type-in-block resolvecontext - (cadar last-assignment-value) - php-block - type-resolver - function-arg-list))) - ((not assignments) - (phpinspect--log "No assignments found for variable %s, checking function arguments" - variable-name) - (phpinspect-get-variable-type-in-function-arg-list variable-name - type-resolver - function-arg-list)))))) + ;; (let* ((assignments + ;; (phpinspect--find-assignments-by-predicate + ;; php-block (phpinspect--match-sequence-lambda + ;; :m `(:variable ,variable-name)))) + ;; (last-assignment (when assignments (car (last assignments)))) + ;; (last-assignment-value (when last-assignment + ;; (phpinspect--assignment-from last-assignment))) + ;; (result)) + + ;; (if (not assignments) + ;; (progn + ;; (phpinspect--log "No assignments found for variable %s, checking function arguments" + ;; variable-name) + ;; (setq result (phpinspect-get-variable-type-in-function-arg-list + ;; variable-name type-resolver function-arg-list))) + ;; (setq result + ;; (phpinspect--interpret-expression-type-in-context + ;; resolvecontext php-block type-resolver + ;; last-assignment-value function-arg-list))) + + ;; (phpinspect--log "Type interpreted from last assignment expression of variable %s: %s" + ;; variable-name result) + + ;; ;; Detect array access + ;; (if (and last-assignment-value result + ;; (< 1 (length last-assignment-value)) + ;; (phpinspect-array-p (car (last last-assignment-value)))) + ;; (progn + ;; (phpinspect--log (concat + ;; "Detected array access in last assignment of variable %s" + ;; ", collection type: %s") + ;; variable-name result) + ;; (phpinspect--type-contains result)) + ;; result)))) + +(defun phpinspect-get-pattern-type-in-block + (resolvecontext pattern php-block type-resolver &optional function-arg-list) + "Find the type of PATTERN in PHP-BLOCK using TYPE-RESOLVER. + +PATTERN must be an object of the type `phpinspect--pattern'. + +Returns either a FQN or a relative type name, depending on +whether or not the root variable of the assignment value (right +side of assignment) needs to be extracted from FUNCTION-ARG-LIST. + +When PHP-BLOCK belongs to a function, supply FUNCTION-ARG-LIST to +resolve types of function argument variables." + (let* ((assignments + (phpinspect--find-assignments-by-predicate + php-block (phpinspect--pattern-matcher pattern))) + (last-assignment (when assignments (car (last assignments)))) + (last-assignment-value (when last-assignment + (phpinspect--assignment-from last-assignment))) + (pattern-code (phpinspect--pattern-code pattern)) + (result)) + (phpinspect--log "Looking for assignments of pattern %s in php block" pattern-code) + + (if (not assignments) + (when (and (= (length pattern-code) 2) (phpinspect-variable-p (cadr pattern-code))) + (let ((variable-name (cadadr pattern-code))) + (progn + (phpinspect--log "No assignments found for variable %s, checking function arguments: %s" + variable-name function-arg-list) + (setq result (phpinspect-get-variable-type-in-function-arg-list + variable-name type-resolver function-arg-list))))) + (setq result + (phpinspect--interpret-expression-type-in-context + resolvecontext php-block type-resolver + last-assignment-value function-arg-list))) + + (phpinspect--log "Type interpreted from last assignment expression of pattern %s: %s" + pattern-code result) + + (when (and result (phpinspect--type-collection result) (not (phpinspect--type-contains result))) + (phpinspect--log (concat + "Interpreted type %s is a collection type, but 'contains'" + "attribute is not set. Attempting to infer type from context") + result) + (setq result (phpinspect--copy-type result)) + (let ((concat-pattern + (phpinspect--pattern-concat + pattern (phpinspect--make-pattern :f #'phpinspect-array-p)))) + (phpinspect--log "Inferring type of concatenated pattern %s" + (phpinspect--pattern-code concat-pattern)) + (setf (phpinspect--type-contains result) + (phpinspect-get-pattern-type-in-block + resolvecontext concat-pattern php-block + type-resolver function-arg-list)))) + + ;; Detect array access + (if (and last-assignment-value result + (< 1 (length last-assignment-value)) + (phpinspect-array-p (car (last last-assignment-value)))) + (progn + (phpinspect--log (concat + "Detected array access in last assignment of pattern %s" + ", collection type: %s") + pattern-code result) + (phpinspect--type-contains result)) + result))) + + +(defun phpinspect--interpret-expression-type-in-context + (resolvecontext php-block type-resolver expression &optional function-arg-list) + "Infer EXPRESSION's type from provided context. + +Use RESOLVECONTEXT, PHP-BLOCK, TYPE-RESOLVER and +FUNCTION-ARG-LIST as contextual information to infer type of +EXPRESSION." + + ;; When the right of an assignment is more than $variable; or "string";(so + ;; (:variable "variable") (:terminator ";") or (:string "string") (:terminator ";") + ;; in tokens), we're likely working with a derived assignment like $object->method() + ;; or $object->attributen + (cond ((phpinspect-array-p (car expression)) + (let ((collection-contains) + (collection-items (phpinspect--split-statements (cdr (car expression)))) + (count 0)) + (phpinspect--log "Checking collection items: %s" collection-items) + (while (and (< count (length collection-items)) + (not collection-contains)) + (setq collection-contains + (phpinspect--interpret-expression-type-in-context + resolvecontext php-block type-resolver + (elt collection-items count) function-arg-list) + count (+ count 1))) + + (phpinspect--log "Collection contained: %s" collection-contains) + + (phpinspect--make-type :name "\\array" + :fully-qualified t + :collection t + :contains collection-contains))) + ((and (phpinspect-word-p (car expression)) + (string= (cadar expression) "new")) + (funcall + type-resolver (phpinspect--make-type :name (cadadr expression)))) + ((and (> (length expression) 1) + (seq-find #'phpinspect-attrib-p expression)) + (phpinspect--log "Variable was assigned with a derived statement") + (phpinspect-get-derived-statement-type-in-block + resolvecontext expression php-block + type-resolver function-arg-list)) + + ;; If the right of an assignment is just $variable;, we can check if it is a + ;; function argument and otherwise recurse to find the type of that variable. + ((phpinspect-variable-p (car expression)) + (phpinspect--log "Variable was assigned with the value of another variable: %s" + expression) + (or (when function-arg-list + (phpinspect-get-variable-type-in-function-arg-list + (cadar expression) + type-resolver function-arg-list)) + (phpinspect-get-variable-type-in-block resolvecontext + (cadar expression) + php-block + type-resolver + function-arg-list))))) (defun phpinspect-resolve-type-from-context (resolvecontext type-resolver) @@ -1211,7 +1344,8 @@ before the search is executed." (autoloader (phpinspect--project-autoload project))) (when (eq index-new 'index-new) (phpinspect-autoloader-refresh autoloader)) - (let* ((result (phpinspect-autoloader-resolve autoloader (phpinspect--type-name-symbol class)))) + (let* ((result (phpinspect-autoloader-resolve + autoloader (phpinspect--type-name-symbol class)))) (if (not result) ;; Index new files and try again if not done already. (if (eq index-new 'index-new) diff --git a/test/fixtures/IncompleteClassBlockedNamespace.eld b/test/fixtures/IncompleteClassBlockedNamespace.eld index 2bf9eaa..bb7e112 100644 --- a/test/fixtures/IncompleteClassBlockedNamespace.eld +++ b/test/fixtures/IncompleteClassBlockedNamespace.eld @@ -1 +1 @@ -(:root (:word "declare") (:list (:word "strict_types") (:assignment "=")) (:terminator ";") (:namespace (:word "App\\Controller") (:incomplete-block (:use (:word "Symfony\\Component\\HttpFoundation\\Response") (:terminator ";")) (:use (:word "App\\Entity\\Address") (:terminator ";")) (:use (:word "Symfony\\Component\\HttpFoundation\\RedirectResponse") (:terminator ";")) (:use (:word "App\\Repository\\AddressRepository") (:terminator ";")) (:use (:word "App\\Repository\\UserRepository") (:terminator ";")) (:use (:word "Doctrine\\ORM\\EntityManagerInterface") (:terminator ";")) (:use (:word "Twig\\Environment") (:terminator ";")) (:use (:word "Symfony\\Component\\HttpFoundation\\Request") (:terminator ";")) (:use (:word "Symfony\\Component\\Routing\\Annotation\\Route") (:terminator ";")) (:class (:declaration (:word "class") (:word "AddressController")) (:incomplete-block (:const (:word "A_CONSTANT_FOR_THE_SAKE_OF_HAVING_ONE") (:assignment "=") (:string "a value") (:terminator ";")) (:public (:const (:word "ARRAY_CONSTANT") (:assignment "=") (:array (:string "key") (:fat-arrow "=>") (:string "value") (:comma ",") (:string "key") (:fat-arrow "=>")) (:terminator ";"))) (:private (:variable "repo") (:terminator ";")) (:private (:variable "user_repo") (:terminator ";")) (:private (:variable "twig") (:terminator ";")) (:private (:variable "em") (:terminator ";")) (:public (:function (:declaration (:word "function") (:word "__construct") (:list (:word "AddressRepository") (:variable "repo") (:comma ",") (:word "UserRepository") (:variable "user_repo") (:comma ",") (:word "Environment") (:variable "twig") (:comma ",") (:word "EntityManagerInterface") (:variable "em"))) (:block (:variable "this") (:object-attrib (:word "repo")) (:assignment "=") (:variable "repo") (:terminator ";") (:variable "this") (:object-attrib (:word "user_repo")) (:assignment "=") (:variable "user_repo") (:terminator ";") (:variable "this") (:object-attrib (:word "twig")) (:assignment "=") (:variable "twig") (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:assignment "=") (:variable "em") (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "addAddressPage") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "user") (:assignment "=") (:variable "this") (:object-attrib (:word "user_repo")) (:object-attrib (:word "findOne")) (:list (:variable "req") (:object-attrib (:word "get")) (:list (:string "user"))) (:terminator ";") (:word "return") (:word "new") (:word "Response") (:list (:variable "this") (:object-attrib (:word "twig")) (:object-attrib (:word "render")) (:list (:string "address/create.html.twig") (:comma ",") (:array (:string "user") (:fat-arrow "=>") (:variable "user") (:comma ",")))) (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "addAddressAction") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "user") (:assignment "=") (:variable "this") (:object-attrib (:word "user_repo")) (:object-attrib (:word "findOne")) (:list (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "user"))) (:terminator ";") (:variable "address_string") (:assignment "=") (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "address")) (:terminator ";") (:variable "address") (:assignment "=") (:word "new") (:word "Address") (:list (:variable "user") (:comma ",") (:variable "address_string")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "persist")) (:list (:variable "address")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "flush")) (:list) (:terminator ";") (:word "return") (:word "new") (:word "RedirectResponse") (:list (:string "/user/") (:variable "user") (:object-attrib (:word "getLoginName")) (:list) (:string "/manage")) (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "deleteAddressAction") (:list (:word "Request") (:variable "req")) (:word "Response")) (:incomplete-block (:variable "address") (:assignment "=") (:variable "this") (:object-attrib (:word "repo")) (:object-attrib (:word "find")) (:list (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "address"))) (:terminator ";") (:comment) (:comment) (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "remove")) (:incomplete-list (:variable "this") (:object-attrib (:word "em")) (:object-attrib nil)))))))))) \ No newline at end of file +(:root (:word "declare") (:list (:word "strict_types") (:assignment "=")) (:terminator ";") (:namespace (:word "App\\Controller") (:incomplete-block (:use (:word "Symfony\\Component\\HttpFoundation\\Response") (:terminator ";")) (:use (:word "App\\Entity\\Address") (:terminator ";")) (:use (:word "Symfony\\Component\\HttpFoundation\\RedirectResponse") (:terminator ";")) (:use (:word "App\\Repository\\AddressRepository") (:terminator ";")) (:use (:word "App\\Repository\\UserRepository") (:terminator ";")) (:use (:word "Doctrine\\ORM\\EntityManagerInterface") (:terminator ";")) (:use (:word "Twig\\Environment") (:terminator ";")) (:use (:word "Symfony\\Component\\HttpFoundation\\Request") (:terminator ";")) (:use (:word "Symfony\\Component\\Routing\\Annotation\\Route") (:terminator ";")) (:word "class") (:word "AddressController") (:incomplete-block (:const (:word "A_CONSTANT_FOR_THE_SAKE_OF_HAVING_ONE") (:assignment "=") (:string "a value") (:terminator ";")) (:public (:const (:word "ARRAY_CONSTANT") (:assignment "=") (:array (:string "key") (:fat-arrow "=>") (:string "value") (:comma ",") (:string "key") (:fat-arrow "=>")) (:terminator ";"))) (:private (:variable "repo") (:terminator ";")) (:private (:variable "user_repo") (:terminator ";")) (:private (:variable "twig") (:terminator ";")) (:private (:variable "em") (:terminator ";")) (:public (:function (:declaration (:word "function") (:word "__construct") (:list (:word "AddressRepository") (:variable "repo") (:comma ",") (:word "UserRepository") (:variable "user_repo") (:comma ",") (:word "Environment") (:variable "twig") (:comma ",") (:word "EntityManagerInterface") (:variable "em"))) (:block (:variable "this") (:object-attrib (:word "repo")) (:assignment "=") (:variable "repo") (:terminator ";") (:variable "this") (:object-attrib (:word "user_repo")) (:assignment "=") (:variable "user_repo") (:terminator ";") (:variable "this") (:object-attrib (:word "twig")) (:assignment "=") (:variable "twig") (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:assignment "=") (:variable "em") (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "addAddressPage") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "user") (:assignment "=") (:variable "this") (:object-attrib (:word "user_repo")) (:object-attrib (:word "findOne")) (:list (:variable "req") (:object-attrib (:word "get")) (:list (:string "user"))) (:terminator ";") (:word "return") (:word "new") (:word "Response") (:list (:variable "this") (:object-attrib (:word "twig")) (:object-attrib (:word "render")) (:list (:string "address/create.html.twig") (:comma ",") (:array (:string "user") (:fat-arrow "=>") (:variable "user") (:comma ",")))) (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "addAddressAction") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "user") (:assignment "=") (:variable "this") (:object-attrib (:word "user_repo")) (:object-attrib (:word "findOne")) (:list (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "user"))) (:terminator ";") (:variable "address_string") (:assignment "=") (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "address")) (:terminator ";") (:variable "address") (:assignment "=") (:word "new") (:word "Address") (:list (:variable "user") (:comma ",") (:variable "address_string")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "persist")) (:list (:variable "address")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "flush")) (:list) (:terminator ";") (:word "return") (:word "new") (:word "RedirectResponse") (:list (:string "/user/") (:variable "user") (:object-attrib (:word "getLoginName")) (:list) (:string "/manage")) (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "deleteAddressAction") (:list (:word "Request") (:variable "req")) (:word "Response")) (:incomplete-block (:variable "address") (:assignment "=") (:variable "this") (:object-attrib (:word "repo")) (:object-attrib (:word "find")) (:list (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "address"))) (:terminator ";") (:comment) (:comment) (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "remove")) (:incomplete-list (:variable "this") (:object-attrib (:word "em")) (:object-attrib nil))))))))) \ No newline at end of file diff --git a/test/fixtures/IncompleteClassMultipleNamespaces.eld b/test/fixtures/IncompleteClassMultipleNamespaces.eld index f978752..7d33315 100644 --- a/test/fixtures/IncompleteClassMultipleNamespaces.eld +++ b/test/fixtures/IncompleteClassMultipleNamespaces.eld @@ -1 +1 @@ -(:root (:word "declare") (:list (:word "strict_types") (:assignment "=")) (:terminator ";") (:namespace (:word "Circus\\Artist") (:block (:use (:word "Symfony\\Component\\HttpFoundation\\Response") (:terminator ";")) (:use (:word "App\\Entity\\Address") (:terminator ";")) (:use (:word "Symfony\\Component\\HttpFoundation\\RedirectResponse") (:terminator ";")) (:use (:word "App\\Repository\\AddressRepository") (:terminator ";")) (:use (:word "App\\Repository\\UserRepository") (:terminator ";")) (:use (:word "Doctrine\\ORM\\EntityManagerInterface") (:terminator ";")) (:use (:word "Twig\\Environment") (:terminator ";")) (:use (:word "Symfony\\Component\\HttpFoundation\\Request") (:terminator ";")) (:use (:word "Symfony\\Component\\Routing\\Annotation\\Route") (:terminator ";")) (:class (:declaration (:word "class") (:word "AddressController")) (:block (:const (:word "A_CONSTANT_FOR_THE_SAKE_OF_HAVING_ONE") (:assignment "=") (:string "a value") (:terminator ";")) (:public (:const (:word "ARRAY_CONSTANT") (:assignment "=") (:array (:string "key") (:fat-arrow "=>") (:string "value") (:comma ",") (:string "key") (:fat-arrow "=>")) (:terminator ";"))) (:private (:variable "repo") (:terminator ";")) (:private (:variable "user_repo") (:terminator ";")) (:private (:variable "twig") (:terminator ";")) (:private (:variable "em") (:terminator ";")) (:public (:function (:declaration (:word "function") (:word "__construct") (:list (:word "AddressRepository") (:variable "repo") (:comma ",") (:word "UserRepository") (:variable "user_repo") (:comma ",") (:word "Environment") (:variable "twig") (:comma ",") (:word "EntityManagerInterface") (:variable "em"))) (:block (:variable "this") (:object-attrib (:word "repo")) (:assignment "=") (:variable "repo") (:terminator ";") (:variable "this") (:object-attrib (:word "user_repo")) (:assignment "=") (:variable "user_repo") (:terminator ";") (:variable "this") (:object-attrib (:word "twig")) (:assignment "=") (:variable "twig") (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:assignment "=") (:variable "em") (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "addAddressPage") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "user") (:assignment "=") (:variable "this") (:object-attrib (:word "user_repo")) (:object-attrib (:word "findOne")) (:list (:variable "req") (:object-attrib (:word "get")) (:list (:string "user"))) (:terminator ";") (:word "return") (:word "new") (:word "Response") (:list (:variable "this") (:object-attrib (:word "twig")) (:object-attrib (:word "render")) (:list (:string "address/create.html.twig") (:comma ",") (:array (:string "user") (:fat-arrow "=>") (:variable "user") (:comma ",")))) (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "addAddressAction") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "user") (:assignment "=") (:variable "this") (:object-attrib (:word "user_repo")) (:object-attrib (:word "findOne")) (:list (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "user"))) (:terminator ";") (:variable "address_string") (:assignment "=") (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "address")) (:terminator ";") (:variable "address") (:assignment "=") (:word "new") (:word "Address") (:list (:variable "user") (:comma ",") (:variable "address_string")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "persist")) (:list (:variable "address")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "flush")) (:list) (:terminator ";") (:word "return") (:word "new") (:word "RedirectResponse") (:list (:string "/user/") (:variable "user") (:object-attrib (:word "getLoginName")) (:list) (:string "/manage")) (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "deleteAddressAction") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "address") (:assignment "=") (:variable "this") (:object-attrib (:word "repo")) (:object-attrib (:word "find")) (:list (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "address"))) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "remove")) (:list (:variable "address")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "flush")) (:list) (:terminator ";") (:word "return") (:word "new") (:word "RedirectResponse") (:list (:string "/user/") (:variable "address") (:object-attrib (:word "getUser")) (:list) (:object-attrib (:word "getLoginName")) (:list) (:string "/manage")) (:terminator ";")))))))) (:namespace (:word "App\\Controller") (:incomplete-block (:use (:word "Symfony\\Component\\HttpFoundation\\Response") (:terminator ";")) (:use (:word "App\\Entity\\Address") (:terminator ";")) (:use (:word "Symfony\\Component\\HttpFoundation\\RedirectResponse") (:terminator ";")) (:use (:word "App\\Repository\\AddressRepository") (:terminator ";")) (:use (:word "App\\Repository\\UserRepository") (:terminator ";")) (:use (:word "Doctrine\\ORM\\EntityManagerInterface") (:terminator ";")) (:use (:word "Twig\\Environment") (:terminator ";")) (:use (:word "Symfony\\Component\\HttpFoundation\\Request") (:terminator ";")) (:use (:word "Symfony\\Component\\Routing\\Annotation\\Route") (:terminator ";")) (:class (:declaration (:word "class") (:word "AddressController")) (:incomplete-block (:const (:word "A_CONSTANT_FOR_THE_SAKE_OF_HAVING_ONE") (:assignment "=") (:string "a value") (:terminator ";")) (:public (:const (:word "ARRAY_CONSTANT") (:assignment "=") (:array (:string "key") (:fat-arrow "=>") (:string "value") (:comma ",") (:string "key") (:fat-arrow "=>")) (:terminator ";"))) (:private (:variable "repo") (:terminator ";")) (:private (:variable "user_repo") (:terminator ";")) (:private (:variable "twig") (:terminator ";")) (:private (:variable "em") (:terminator ";")) (:public (:function (:declaration (:word "function") (:word "__construct") (:list (:word "AddressRepository") (:variable "repo") (:comma ",") (:word "UserRepository") (:variable "user_repo") (:comma ",") (:word "Environment") (:variable "twig") (:comma ",") (:word "EntityManagerInterface") (:variable "em"))) (:block (:variable "this") (:object-attrib (:word "repo")) (:assignment "=") (:variable "repo") (:terminator ";") (:variable "this") (:object-attrib (:word "user_repo")) (:assignment "=") (:variable "user_repo") (:terminator ";") (:variable "this") (:object-attrib (:word "twig")) (:assignment "=") (:variable "twig") (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:assignment "=") (:variable "em") (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "addAddressPage") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "user") (:assignment "=") (:variable "this") (:object-attrib (:word "user_repo")) (:object-attrib (:word "findOne")) (:list (:variable "req") (:object-attrib (:word "get")) (:list (:string "user"))) (:terminator ";") (:word "return") (:word "new") (:word "Response") (:list (:variable "this") (:object-attrib (:word "twig")) (:object-attrib (:word "render")) (:list (:string "address/create.html.twig") (:comma ",") (:array (:string "user") (:fat-arrow "=>") (:variable "user") (:comma ",")))) (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "addAddressAction") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "user") (:assignment "=") (:variable "this") (:object-attrib (:word "user_repo")) (:object-attrib (:word "findOne")) (:list (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "user"))) (:terminator ";") (:variable "address_string") (:assignment "=") (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "address")) (:terminator ";") (:variable "address") (:assignment "=") (:word "new") (:word "Address") (:list (:variable "user") (:comma ",") (:variable "address_string")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "persist")) (:list (:variable "address")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "flush")) (:list) (:terminator ";") (:word "return") (:word "new") (:word "RedirectResponse") (:list (:string "/user/") (:variable "user") (:object-attrib (:word "getLoginName")) (:list) (:string "/manage")) (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "deleteAddressAction") (:list (:word "Request") (:variable "req")) (:word "Response")) (:incomplete-block (:variable "address") (:assignment "=") (:variable "this") (:object-attrib (:word "repo")) (:object-attrib (:word "find")) (:list (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "address"))) (:terminator ";") (:comment) (:comment) (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "remove")) (:incomplete-list (:variable "this") (:object-attrib (:word "em")) (:object-attrib nil)))))))))) \ No newline at end of file +(:root (:word "declare") (:list (:word "strict_types") (:assignment "=")) (:terminator ";") (:namespace (:word "Circus\\Artist") (:block (:use (:word "Symfony\\Component\\HttpFoundation\\Response") (:terminator ";")) (:use (:word "App\\Entity\\Address") (:terminator ";")) (:use (:word "Symfony\\Component\\HttpFoundation\\RedirectResponse") (:terminator ";")) (:use (:word "App\\Repository\\AddressRepository") (:terminator ";")) (:use (:word "App\\Repository\\UserRepository") (:terminator ";")) (:use (:word "Doctrine\\ORM\\EntityManagerInterface") (:terminator ";")) (:use (:word "Twig\\Environment") (:terminator ";")) (:use (:word "Symfony\\Component\\HttpFoundation\\Request") (:terminator ";")) (:use (:word "Symfony\\Component\\Routing\\Annotation\\Route") (:terminator ";")) (:word "class") (:word "AddressController") (:block (:const (:word "A_CONSTANT_FOR_THE_SAKE_OF_HAVING_ONE") (:assignment "=") (:string "a value") (:terminator ";")) (:public (:const (:word "ARRAY_CONSTANT") (:assignment "=") (:array (:string "key") (:fat-arrow "=>") (:string "value") (:comma ",") (:string "key") (:fat-arrow "=>")) (:terminator ";"))) (:private (:variable "repo") (:terminator ";")) (:private (:variable "user_repo") (:terminator ";")) (:private (:variable "twig") (:terminator ";")) (:private (:variable "em") (:terminator ";")) (:public (:function (:declaration (:word "function") (:word "__construct") (:list (:word "AddressRepository") (:variable "repo") (:comma ",") (:word "UserRepository") (:variable "user_repo") (:comma ",") (:word "Environment") (:variable "twig") (:comma ",") (:word "EntityManagerInterface") (:variable "em"))) (:block (:variable "this") (:object-attrib (:word "repo")) (:assignment "=") (:variable "repo") (:terminator ";") (:variable "this") (:object-attrib (:word "user_repo")) (:assignment "=") (:variable "user_repo") (:terminator ";") (:variable "this") (:object-attrib (:word "twig")) (:assignment "=") (:variable "twig") (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:assignment "=") (:variable "em") (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "addAddressPage") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "user") (:assignment "=") (:variable "this") (:object-attrib (:word "user_repo")) (:object-attrib (:word "findOne")) (:list (:variable "req") (:object-attrib (:word "get")) (:list (:string "user"))) (:terminator ";") (:word "return") (:word "new") (:word "Response") (:list (:variable "this") (:object-attrib (:word "twig")) (:object-attrib (:word "render")) (:list (:string "address/create.html.twig") (:comma ",") (:array (:string "user") (:fat-arrow "=>") (:variable "user") (:comma ",")))) (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "addAddressAction") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "user") (:assignment "=") (:variable "this") (:object-attrib (:word "user_repo")) (:object-attrib (:word "findOne")) (:list (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "user"))) (:terminator ";") (:variable "address_string") (:assignment "=") (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "address")) (:terminator ";") (:variable "address") (:assignment "=") (:word "new") (:word "Address") (:list (:variable "user") (:comma ",") (:variable "address_string")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "persist")) (:list (:variable "address")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "flush")) (:list) (:terminator ";") (:word "return") (:word "new") (:word "RedirectResponse") (:list (:string "/user/") (:variable "user") (:object-attrib (:word "getLoginName")) (:list) (:string "/manage")) (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "deleteAddressAction") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "address") (:assignment "=") (:variable "this") (:object-attrib (:word "repo")) (:object-attrib (:word "find")) (:list (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "address"))) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "remove")) (:list (:variable "address")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "flush")) (:list) (:terminator ";") (:word "return") (:word "new") (:word "RedirectResponse") (:list (:string "/user/") (:variable "address") (:object-attrib (:word "getUser")) (:list) (:object-attrib (:word "getLoginName")) (:list) (:string "/manage")) (:terminator ";"))))))) (:namespace (:word "App\\Controller") (:incomplete-block (:use (:word "Symfony\\Component\\HttpFoundation\\Response") (:terminator ";")) (:use (:word "App\\Entity\\Address") (:terminator ";")) (:use (:word "Symfony\\Component\\HttpFoundation\\RedirectResponse") (:terminator ";")) (:use (:word "App\\Repository\\AddressRepository") (:terminator ";")) (:use (:word "App\\Repository\\UserRepository") (:terminator ";")) (:use (:word "Doctrine\\ORM\\EntityManagerInterface") (:terminator ";")) (:use (:word "Twig\\Environment") (:terminator ";")) (:use (:word "Symfony\\Component\\HttpFoundation\\Request") (:terminator ";")) (:use (:word "Symfony\\Component\\Routing\\Annotation\\Route") (:terminator ";")) (:word "class") (:word "AddressController") (:incomplete-block (:const (:word "A_CONSTANT_FOR_THE_SAKE_OF_HAVING_ONE") (:assignment "=") (:string "a value") (:terminator ";")) (:public (:const (:word "ARRAY_CONSTANT") (:assignment "=") (:array (:string "key") (:fat-arrow "=>") (:string "value") (:comma ",") (:string "key") (:fat-arrow "=>")) (:terminator ";"))) (:private (:variable "repo") (:terminator ";")) (:private (:variable "user_repo") (:terminator ";")) (:private (:variable "twig") (:terminator ";")) (:private (:variable "em") (:terminator ";")) (:public (:function (:declaration (:word "function") (:word "__construct") (:list (:word "AddressRepository") (:variable "repo") (:comma ",") (:word "UserRepository") (:variable "user_repo") (:comma ",") (:word "Environment") (:variable "twig") (:comma ",") (:word "EntityManagerInterface") (:variable "em"))) (:block (:variable "this") (:object-attrib (:word "repo")) (:assignment "=") (:variable "repo") (:terminator ";") (:variable "this") (:object-attrib (:word "user_repo")) (:assignment "=") (:variable "user_repo") (:terminator ";") (:variable "this") (:object-attrib (:word "twig")) (:assignment "=") (:variable "twig") (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:assignment "=") (:variable "em") (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "addAddressPage") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "user") (:assignment "=") (:variable "this") (:object-attrib (:word "user_repo")) (:object-attrib (:word "findOne")) (:list (:variable "req") (:object-attrib (:word "get")) (:list (:string "user"))) (:terminator ";") (:word "return") (:word "new") (:word "Response") (:list (:variable "this") (:object-attrib (:word "twig")) (:object-attrib (:word "render")) (:list (:string "address/create.html.twig") (:comma ",") (:array (:string "user") (:fat-arrow "=>") (:variable "user") (:comma ",")))) (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "addAddressAction") (:list (:word "Request") (:variable "req")) (:word "Response")) (:block (:variable "user") (:assignment "=") (:variable "this") (:object-attrib (:word "user_repo")) (:object-attrib (:word "findOne")) (:list (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "user"))) (:terminator ";") (:variable "address_string") (:assignment "=") (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "address")) (:terminator ";") (:variable "address") (:assignment "=") (:word "new") (:word "Address") (:list (:variable "user") (:comma ",") (:variable "address_string")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "persist")) (:list (:variable "address")) (:terminator ";") (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "flush")) (:list) (:terminator ";") (:word "return") (:word "new") (:word "RedirectResponse") (:list (:string "/user/") (:variable "user") (:object-attrib (:word "getLoginName")) (:list) (:string "/manage")) (:terminator ";")))) (:doc-block (:annotation "Route")) (:public (:function (:declaration (:word "function") (:word "deleteAddressAction") (:list (:word "Request") (:variable "req")) (:word "Response")) (:incomplete-block (:variable "address") (:assignment "=") (:variable "this") (:object-attrib (:word "repo")) (:object-attrib (:word "find")) (:list (:variable "req") (:object-attrib (:word "request")) (:object-attrib (:word "get")) (:list (:string "address"))) (:terminator ";") (:comment) (:comment) (:variable "this") (:object-attrib (:word "em")) (:object-attrib (:word "remove")) (:incomplete-list (:variable "this") (:object-attrib (:word "em")) (:object-attrib nil))))))))) \ No newline at end of file diff --git a/test/fixtures/IndexClass1-indexed.eld b/test/fixtures/IndexClass1-indexed.eld index 53bcf3d..bdae921 100644 --- a/test/fixtures/IndexClass1-indexed.eld +++ b/test/fixtures/IndexClass1-indexed.eld @@ -1 +1 @@ -`(phpinspect--root-index (imports \, (list)) (classes ,(list (phpinspect--make-type :name "\\App\\Entity\\AuthToken" :collection nil :contains nil :fully-qualified t) `(phpinspect--indexed-class (class-name \, (phpinspect--make-type :name "\\App\\Entity\\AuthToken" :collection nil :contains nil :fully-qualified t)) (imports \, (list (cons (phpinspect-intern-name "ORM") (phpinspect--make-type :name "\\Doctrine\\ORM\\Mapping" :collection nil :contains nil :fully-qualified t)))) (methods \, (list (phpinspect--make-function :name "getCreationTime" :scope '(:public) :arguments (list) :return-type (phpinspect--make-type :name "\\DateTime" :collection nil :contains nil :fully-qualified t)) (phpinspect--make-function :name "isValid" :scope '(:public) :arguments (list) :return-type (phpinspect--make-type :name "\\bool" :collection nil :contains nil :fully-qualified t)) (phpinspect--make-function :name "hasStudentRole" :scope '(:public) :arguments (list) :return-type (phpinspect--make-type :name "\\bool" :collection nil :contains nil :fully-qualified t)) (phpinspect--make-function :name "getUser" :scope '(:public) :arguments (list) :return-type (phpinspect--make-type :name "\\App\\Entity\\User" :collection nil :contains nil :fully-qualified t)) (phpinspect--make-function :name "getToken" :scope '(:public) :arguments (list) :return-type (phpinspect--make-type :name "\\string" :collection nil :contains nil :fully-qualified t)) (phpinspect--make-function :name "__construct" :scope '(:public) :arguments (list (list "token" (phpinspect--make-type :name "\\string" :collection nil :contains nil :fully-qualified t)) (list "user" (phpinspect--make-type :name "\\App\\Entity\\User" :collection nil :contains nil :fully-qualified t)) (list "valid" (phpinspect--make-type :name "\\bool" :collection nil :contains nil :fully-qualified t)) (list "creation_time" (phpinspect--make-type :name "\\DateTime" :collection nil :contains nil :fully-qualified t))) :return-type (phpinspect--make-type :name "\\null" :collection nil :contains nil :fully-qualified t)))) (static-methods \, (list)) (static-variables \, (list)) (variables \, (list (phpinspect--make-variable :name "creation_time" :type (phpinspect--make-type :name "\\DateTime" :collection nil :contains nil :fully-qualified t) :scope '(:private)) (phpinspect--make-variable :name "valid" :type (phpinspect--make-type :name "\\bool" :collection nil :contains nil :fully-qualified t) :scope '(:private)) (phpinspect--make-variable :name "user" :type (phpinspect--make-type :name "\\App\\Entity\\App\\\\Entity\\\\User" :collection nil :contains nil :fully-qualified t) :scope '(:private)) (phpinspect--make-variable :name "token" :type (phpinspect--make-type :name "\\string" :collection nil :contains nil :fully-qualified t) :scope '(:private)))) (constants \, (list)) (extends \, (list)) (implements \, (list))))) (functions \, (list))) \ No newline at end of file +`(phpinspect--root-index (imports \, (list)) (classes ,(list (phpinspect--make-type :name "\\App\\Entity\\AuthToken" :collection nil :contains nil :fully-qualified t) `(phpinspect--indexed-class (class-name \, (phpinspect--make-type :name "\\App\\Entity\\AuthToken" :collection nil :contains nil :fully-qualified t)) (imports \, (list (cons (phpinspect-intern-name "ORM") (phpinspect--make-type :name "\\Doctrine\\ORM\\Mapping" :collection nil :contains nil :fully-qualified t)))) (methods \, (list (phpinspect--make-function :name "arrayReturn" :scope '(:public) :arguments (list) :return-type (phpinspect--make-type :name "\\array" :collection t :contains (phpinspect--make-type :name "\\App\\Entity\\DateTime" :collection nil :contains nil :fully-qualified t) :fully-qualified t)) (phpinspect--make-function :name "getCreationTime" :scope '(:public) :arguments (list) :return-type (phpinspect--make-type :name "\\DateTime" :collection nil :contains nil :fully-qualified t)) (phpinspect--make-function :name "isValid" :scope '(:public) :arguments (list) :return-type (phpinspect--make-type :name "\\bool" :collection nil :contains nil :fully-qualified t)) (phpinspect--make-function :name "hasStudentRole" :scope '(:public) :arguments (list) :return-type (phpinspect--make-type :name "\\bool" :collection nil :contains nil :fully-qualified t)) (phpinspect--make-function :name "getUser" :scope '(:public) :arguments (list) :return-type (phpinspect--make-type :name "\\App\\Entity\\User" :collection nil :contains nil :fully-qualified t)) (phpinspect--make-function :name "getToken" :scope '(:public) :arguments (list) :return-type (phpinspect--make-type :name "\\string" :collection nil :contains nil :fully-qualified t)) (phpinspect--make-function :name "__construct" :scope '(:public) :arguments (list (list "token" (phpinspect--make-type :name "\\string" :collection nil :contains nil :fully-qualified t)) (list "user" (phpinspect--make-type :name "\\App\\Entity\\User" :collection nil :contains nil :fully-qualified t)) (list "valid" (phpinspect--make-type :name "\\bool" :collection nil :contains nil :fully-qualified t)) (list "creation_time" (phpinspect--make-type :name "\\DateTime" :collection nil :contains nil :fully-qualified t))) :return-type (phpinspect--make-type :name "\\null" :collection nil :contains nil :fully-qualified t)))) (static-methods \, (list)) (static-variables \, (list)) (variables \, (list (phpinspect--make-variable :name "creation_time" :type (phpinspect--make-type :name "\\DateTime" :collection nil :contains nil :fully-qualified t) :scope '(:private)) (phpinspect--make-variable :name "valid" :type (phpinspect--make-type :name "\\bool" :collection nil :contains nil :fully-qualified t) :scope '(:private)) (phpinspect--make-variable :name "user" :type (phpinspect--make-type :name "\\App\\Entity\\App\\\\Entity\\\\User" :collection nil :contains nil :fully-qualified t) :scope '(:private)) (phpinspect--make-variable :name "token" :type (phpinspect--make-type :name "\\string" :collection nil :contains nil :fully-qualified t) :scope '(:private)))) (constants \, (list)) (extends \, (list)) (implements \, (list))))) (functions \, (list))) \ No newline at end of file diff --git a/test/fixtures/IndexClass1.eld b/test/fixtures/IndexClass1.eld index 2c815c4..bbecd1c 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 (:variable "token") (:terminator ";")) (:doc-block (:var-annotation (:word "App\\\\Entity\\\\User"))) (:private (:variable "user") (:terminator ";")) (:doc-block (:var-annotation (:word "bool"))) (:private (:variable "valid") (:terminator ";")) (:doc-block (:var-annotation (:word "\\DateTime"))) (:private (:variable "creation_time") (:terminator ";")) (:public (:function (:declaration (:word "function") (:word "__construct") (:list (:word "string") (:variable "token") (:comma ",") (:word "User") (:variable "user") (:comma ",") (:word "bool") (:variable "valid") (:assignment "=") (:word "false") (:comma ",") (:word "\\DateTime") (:variable "creation_time") (:assignment "=") (:word "null"))) (:block (:variable "this") (:object-attrib (:word "token")) (:assignment "=") (:variable "token") (:terminator ";") (:variable "this") (:object-attrib (:word "user")) (:assignment "=") (:variable "user") (:terminator ";") (:variable "this") (:object-attrib (:word "valid")) (:assignment "=") (:variable "valid") (:terminator ";") (:variable "this") (:object-attrib (:word "creation_time")) (:assignment "=") (:variable "creation_time") (:word "new") (:word "\\DateTime") (:list) (:terminator ";")))) (:public (:function (:declaration (:word "function") (:word "getToken") (:list) (:word "string")) (:block (:word "return") (:variable "this") (:object-attrib (:word "token")) (:terminator ";")))) (:public (:function (:declaration (:word "function") (:word "getUser") (:list) (:word "User")) (:block (:word "return") (:variable "this") (:object-attrib (:word "user")) (:terminator ";")))) (:public (:function (:declaration (:word "function") (:word "hasStudentRole") (:list) (:word "bool")) (:block (:word "return") (:variable "this") (:object-attrib (:word "role_student")) (:terminator ";")))) (:public (:function (:declaration (:word "function") (:word "isValid") (:list) (:word "bool")) (:block (:word "return") (:variable "this") (:object-attrib (:word "valid")) (:terminator ";")))) (:public (:function (:declaration (:word "function") (:word "getCreationTime") (:list) (:word "\\DateTime")) (:block (:word "return") (:variable "this") (:object-attrib (:word "creation_time")) (:terminator ";")))))))) \ No newline at end of file +(: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 (:variable "token") (:terminator ";")) (:doc-block (:var-annotation (:word "App\\\\Entity\\\\User"))) (:private (:variable "user") (:terminator ";")) (:doc-block (:var-annotation (:word "bool"))) (:private (:variable "valid") (:terminator ";")) (:doc-block (:var-annotation (:word "\\DateTime"))) (:private (:variable "creation_time") (:terminator ";")) (:public (:function (:declaration (:word "function") (:word "__construct") (:list (:word "string") (:variable "token") (:comma ",") (:word "User") (:variable "user") (:comma ",") (:word "bool") (:variable "valid") (:assignment "=") (:word "false") (:comma ",") (:word "\\DateTime") (:variable "creation_time") (:assignment "=") (:word "null"))) (:block (:variable "this") (:object-attrib (:word "token")) (:assignment "=") (:variable "token") (:terminator ";") (:variable "this") (:object-attrib (:word "user")) (:assignment "=") (:variable "user") (:terminator ";") (:variable "this") (:object-attrib (:word "valid")) (:assignment "=") (:variable "valid") (:terminator ";") (:variable "this") (:object-attrib (:word "creation_time")) (:assignment "=") (:variable "creation_time") (:word "new") (:word "\\DateTime") (:list) (:terminator ";")))) (:public (:function (:declaration (:word "function") (:word "getToken") (:list) (:word "string")) (:block (:word "return") (:variable "this") (:object-attrib (:word "token")) (:terminator ";")))) (:public (:function (:declaration (:word "function") (:word "getUser") (:list) (:word "User")) (:block (:word "return") (:variable "this") (:object-attrib (:word "user")) (:terminator ";")))) (:public (:function (:declaration (:word "function") (:word "hasStudentRole") (:list) (:word "bool")) (:block (:word "return") (:variable "this") (:object-attrib (:word "role_student")) (:terminator ";")))) (:public (:function (:declaration (:word "function") (:word "isValid") (:list) (:word "bool")) (:block (:word "return") (:variable "this") (:object-attrib (:word "valid")) (:terminator ";")))) (:public (:function (:declaration (:word "function") (:word "getCreationTime") (:list) (:word "\\DateTime")) (:block (:word "return") (:variable "this") (:object-attrib (:word "creation_time")) (:terminator ";")))) (:doc-block (:return-annotation (:word "DateTime[]"))) (:public (:function (:declaration (:word "function") (:word "arrayReturn") (:list) (:word "array")) (:block (:word "return") (:array (:word "new") (:word "\\DateTime") (:list)) (:terminator ";")))))))) \ No newline at end of file diff --git a/test/fixtures/IndexClass1.php b/test/fixtures/IndexClass1.php index 0bd6a43..2ab08ec 100644 --- a/test/fixtures/IndexClass1.php +++ b/test/fixtures/IndexClass1.php @@ -63,4 +63,12 @@ class AuthToken { return $this->creation_time; } + + /** + * @return DateTime[] + */ + public function arrayReturn(): array + { + return [ new \DateTime() ]; + } } diff --git a/test/phpinspect-test.el b/test/phpinspect-test.el index 684b667..478ecc1 100644 --- a/test/phpinspect-test.el +++ b/test/phpinspect-test.el @@ -30,6 +30,7 @@ ;; data types that are used in tests so that we don't depend on some global ;; worker object for tests. (phpinspect-ensure-worker) +(phpinspect-purge-cache) (defvar phpinspect-test-directory (file-name-directory @@ -60,6 +61,104 @@ (phpinspect-parse-file (concat phpinspect-test-php-file-directory "/" name ".php"))) +(ert-deftest phpinspect-get-variable-type-in-block () + (let* ((tokens (phpinspect-parse-string "class Foo { function a(\\Thing $baz) { $foo = new \\DateTime(); $bar = $foo;")) + (context (phpinspect--get-resolvecontext tokens)) + (project-root "could never be a real project root") + (phpinspect-project-root-function + (lambda (&rest _ignored) project-root)) + (project (phpinspect--make-project + :fs (phpinspect-make-virtual-fs) + :root project-root + :worker (phpinspect-make-worker)))) + + (puthash project-root project (phpinspect--cache-projects phpinspect-cache)) + + (let ((result (phpinspect-get-variable-type-in-block + context "foo" + (phpinspect-function-block + (car (phpinspect--resolvecontext-enclosing-tokens context))) + (phpinspect--make-type-resolver-for-resolvecontext context)))) + (should (phpinspect--type= (phpinspect--make-type :name "\\DateTime") + result))))) + +(ert-deftest phpinspect-get-variable-type-in-block-array-access () + (let* ((tokens (phpinspect-parse-string "class Foo { function a(\\Thing $baz) { $foo = []; $foo[] = $baz; $bar = $foo[0];")) + (context (phpinspect--get-resolvecontext tokens)) + (project-root "could never be a real project root") + (phpinspect-project-root-function + (lambda (&rest _ignored) project-root)) + (project (phpinspect--make-project + :fs (phpinspect-make-virtual-fs) + :root project-root + :worker (phpinspect-make-worker)))) + + (puthash project-root project (phpinspect--cache-projects phpinspect-cache)) + + (let* ((function-token (car (phpinspect--resolvecontext-enclosing-tokens context))) + (result (phpinspect-get-variable-type-in-block + context "bar" + (phpinspect-function-block function-token) + (phpinspect--make-type-resolver-for-resolvecontext context) + (phpinspect-function-argument-list function-token)))) + (should (phpinspect--type= (phpinspect--make-type :name "\\Thing") + result))))) + +(ert-deftest phpinspect-get-variable-type-in-block-array-foreach () + (let* ((tokens (phpinspect-parse-string "class Foo { function a(\\Thing $baz) { $foo = []; $foo[] = $baz; foreach ($foo as $bar) {$bar->")) + (context (phpinspect--get-resolvecontext tokens)) + (project-root "could never be a real project root") + (phpinspect-project-root-function + (lambda (&rest _ignored) project-root)) + (project (phpinspect--make-project + :fs (phpinspect-make-virtual-fs) + :root project-root + :worker (phpinspect-make-worker)))) + + (puthash project-root project (phpinspect--cache-projects phpinspect-cache)) + + (let* ((function-token (seq-find #'phpinspect-function-p + (phpinspect--resolvecontext-enclosing-tokens context))) + (result (phpinspect-get-variable-type-in-block + context "bar" + (phpinspect-function-block function-token) + (phpinspect--make-type-resolver-for-resolvecontext context) + (phpinspect-function-argument-list function-token)))) + + (should (phpinspect--type= (phpinspect--make-type :name "\\Thing") + result))))) + +(ert-deftest phpinspect--find-assignments-in-token () + (let* ((tokens (cadr + (phpinspect-parse-string "{ $foo = ['nr 1']; $bar = $nr2; if (true === ($foo = $nr3)) { $foo = $nr4; $notfoo = $nr5; if ([] === ($foo = [ $nr6 ])){ $foo = [ $nr7 ];}}}"))) + (expected '(((:variable "foo") + (:assignment "=") + (:array + (:variable "nr7"))) + ((:variable "foo") + (:assignment "=") + (:array + (:variable "nr6"))) + ((:variable "notfoo") + (:assignment "=") + (:variable "nr5")) + ((:variable "foo") + (:assignment "=") + (:variable "nr4")) + ((:variable "foo") + (:assignment "=") + (:variable "nr3")) + ((:variable "bar") + (:assignment "=") + (:variable "nr2")) + ((:variable "foo") + (:assignment "=") + (:array + (:string "nr 1"))))) + (assignments (phpinspect--find-assignments-in-token tokens))) + + (should (equal expected assignments)))) + (ert-deftest phpinspect-parse-namespaced-class () "Test phpinspect-parse on a namespaced class" (should @@ -456,6 +555,29 @@ class Thing (phpinspect--get-last-statement-in-token (phpinspect-parse-string php-code-bare)))))) +(ert-deftest phpinspect--find-assignments-by-predicate () + (let* ((token '(:block + (:variable "bam") (:object-attrib "boom") (:assignment "=") + (:variable "beng") (:terminator) + (:variable "notbam") (:word "nonsense") (:assignment "=") (:string) (:terminator) + (:variable "bam") (:comment) (:object-attrib "boom") (:assignment "=") + (:variable "wat") (:object-attrib "call") (:terminator))) + (result (phpinspect--find-assignments-by-predicate + token + (phpinspect--match-sequence-lambda :m `(:variable "bam") :m `(:object-attrib "boom"))))) + + (should (= 2 (length result))) + (dolist (assignment result) + (should (equal '((:variable "bam") (:object-attrib "boom")) + (phpinspect--assignment-to assignment)))) + + (should (equal '((:variable "beng")) + (phpinspect--assignment-from (cadr result)))) + + (should (equal '((:variable "wat") (:object-attrib "call")) + (phpinspect--assignment-from (car result)))))) + + (load-file (concat phpinspect-test-directory "/test-worker.el")) (load-file (concat phpinspect-test-directory "/test-autoload.el")) (load-file (concat phpinspect-test-directory "/test-fs.el")) @@ -464,7 +586,7 @@ class Thing (load-file (concat phpinspect-test-directory "/test-index.el")) (load-file (concat phpinspect-test-directory "/test-class.el")) (load-file (concat phpinspect-test-directory "/test-type.el")) - +(load-file (concat phpinspect-test-directory "/test-util.el")) (provide 'phpinspect-test) ;;; phpinspect-test.el ends here diff --git a/test/test-index.el b/test/test-index.el index 12d5af7..16d221f 100644 --- a/test/test-index.el +++ b/test/test-index.el @@ -55,6 +55,7 @@ :scope '(:public) :arguments `(("untyped" nil) ("things" ,(phpinspect--make-type :name "\\array" + :collection t :fully-qualified t))) :return-type phpinspect--null-type))) (static-variables) diff --git a/test/test-util.el b/test/test-util.el new file mode 100644 index 0000000..8a61fa1 --- /dev/null +++ b/test/test-util.el @@ -0,0 +1,47 @@ +;; test-util.el --- Unit tests for phpinspect.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2021 Free Software Foundation, Inc. + +;; Author: Hugo Thunnissen + +;; 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 . + +;;; Commentary: + +;; + +;;; Code: + +(ert-deftest phpinspect--pattern () + (let* ((a "a") + (pattern1 (phpinspect--make-pattern :m `(,a) :m '* :m "b")) + (pattern2 (phpinspect--make-pattern :f #'listp :m '* :m "b"))) + + (should (equal '(:m ("a") :m * :m "b") (phpinspect--pattern-code pattern1))) + (should (equal '(:f listp :m * :m "b") (phpinspect--pattern-code pattern2))) + + (dolist (pattern `(,pattern1 ,pattern2)) + (should (phpinspect--pattern-match pattern '(("a") "c" "b"))) + (should (phpinspect--pattern-match pattern '(("a") nil "b"))) + (should-not (phpinspect--pattern-match pattern '(1 nil "b"))) + (should-not (phpinspect--pattern-match pattern '(("a") nil "b" "c")))))) + +(ert-deftest phpinspect--pattern-concat () + (let* ((pattern1 (phpinspect--make-pattern :m "a" :m '* :m "b")) + (pattern2 (phpinspect--make-pattern :f #'stringp :m '* :m "D")) + (result (phpinspect--pattern-concat pattern1 pattern2))) + + (should (equal '(:m "a" :m * :m "b" :f stringp :m * :m "D") (phpinspect--pattern-code result))) + + (should (phpinspect--pattern-match result '("a" "anything" "b" "astring" nil "D")))))