branch: externals/phpinspect
commit 224bbd791602f724db8b81bb860b8444142b11e3
Author: Hugo Thunnissen <de...@hugot.nl>
Commit: Hugo Thunnissen <de...@hugot.nl>

    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 +++++++++++++++------
 test/fixtures/IncompleteClassBlockedNamespace.eld  |   2 +-
 .../fixtures/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(-)

diff --git a/phpinspect-cache.el b/phpinspect-cache.el
index a401b50d41..b6f25e5c0e 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 31e802f529..ba23303d0e 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 1265a7691f..514721ca72 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 550d22d50f..dce58057a2 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 56920f506b..458f95f3b7 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 fadc7a0766..2b02c83542 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 2bf9eaa99a..bb7e112c81 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") ( [...]
\ 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") ( [...]
\ No newline at end of file
diff --git a/test/fixtures/IncompleteClassMultipleNamespaces.eld 
b/test/fixtures/IncompleteClassMultipleNamespaces.eld
index f978752445..7d333155b3 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  [...]
\ 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  [...]
\ No newline at end of file
diff --git a/test/fixtures/IndexClass1-indexed.eld 
b/test/fixtures/IndexClass1-indexed.eld
index 53bcf3dac1..bdae92196b 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 \, [...]
\ 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 \, [...]
\ No newline at end of file
diff --git a/test/fixtures/IndexClass1.eld b/test/fixtures/IndexClass1.eld
index 2c815c4960..bbecd1cc3a 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 " [...]
\ 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 " [...]
\ No newline at end of file
diff --git a/test/fixtures/IndexClass1.php b/test/fixtures/IndexClass1.php
index 0bd6a43c3b..2ab08ecb27 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 684b667d89..478ecc13fa 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 12d5af73d4..16d221fba2 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 0000000000..8a61fa1b6f
--- /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 <de...@hugot.nl>
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; 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")))))

Reply via email to