;;; phpinspect-resolve.el --- PHP parsing and completion package -*- lexical-binding: t; -*- ;; Copyright (C) 2021-2023 Free Software Foundation, Inc ;; Author: Hugo Thunnissen ;; Keywords: php, languages, tools, convenience ;; Version: 1 ;; 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: (require 'phpinspect-resolvecontext) (require 'phpinspect-cache) (require 'phpinspect-class) (require 'phpinspect-type) (require 'phpinspect-token-predicates) (phpinspect--declare-log-group 'resolve) (cl-defstruct (phpinspect--assignment (:constructor phpinspect--make-assignment)) (ctx nil :type phpinspect--assignment-context :documentation "The context that the assignment was found in") (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-maybe-assignment-p (token) "Like `phpinspect-assignment-p', but includes \"as\" barewords as possible tokens." (or (phpinspect-assignment-p token) (equal '(:word "as") token))) (define-inline phpinspect-not-assignment-p (token) "Inverse of applying `phpinspect-assignment-p to TOKEN." (inline-quote (not (phpinspect-maybe-assignment-p ,token)))) (cl-defstruct (phpinspect--assignment-context (:constructor phpinspect--make-assignment-context) (:conc-name phpinspect--actx-)) (annotations nil :type list :documentation "List of var annotations available in context") (tokens nil :type list) (preceding-assignments nil :type list)) (defun phpinspect--find-assignment-ctxs-in-token (token &optional assignments-before var-annotations) (when (keywordp (car token)) (setq token (cdr token))) (setq var-annotations (or var-annotations (cons nil nil))) (let ((statements (phpinspect--split-statements token)) assignments blocks-or-lists) (dolist (statement statements) (phpinspect--log "Finding assignment in statement '%s'" statement) (when (seq-find #'phpinspect-maybe-assignment-p statement) (phpinspect--log "Found assignment statement") (push (phpinspect--make-assignment-context :annotations var-annotations :tokens statement :preceding-assignments assignments-before) assignments) (setq assignments-before assignments)) ;; Find all var annotations in statement. (when-let* ((comments (seq-filter #'phpinspect-comment-p statement))) (dolist (comment comments) (dolist (token comment) (when (phpinspect-var-annotation-p token) ;; Intentionally destructively modify annotation list so that all ;; assignments have the same annotations available to them. (push token (cdr var-annotations)))))) (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-assignment-ctxs-in-token block-or-list assignments-before var-annotations))) (dolist (local-assignment (nreverse local-assignments)) (push local-assignment assignments)) (setq assignments-before assignments))))) ;; return (phpinspect--log "Found assignments in token: %s" assignments) (phpinspect--log "Found statements in token: %s" statements) assignments)) (defun phpinspect--find-assignment-by-predicate (assignment-ctxs predicate) "Find first assignment in ASSIGNMENT-TOKENS matching PREDICATE. Destructively removes tokens from the end of ASSIGNMENT-TOKENS." (catch 'return (dolist (actx assignment-ctxs) (let ((assignment (phpinspect--actx-tokens actx))) (let* ((is-loop-assignment nil) (left-of-assignment (seq-filter #'phpinspect-not-comment-p (seq-take-while #'phpinspect-not-assignment-p assignment))) (right-of-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 (funcall predicate right-of-assignment) ;; Masquerade as an array access assignment (setq left-of-assignment (append left-of-assignment '((:array)))) (throw 'return (phpinspect--make-assignment :to right-of-assignment :from left-of-assignment :ctx actx))) (when (funcall predicate left-of-assignment) (throw 'return (phpinspect--make-assignment :from right-of-assignment :to left-of-assignment :ctx actx))))))) nil)) (defsubst phpinspect-drop-preceding-barewords (statement) (while (and statement (phpinspect-word-p (cadr statement))) (pop statement)) statement) (defsubst phpinspect-get-cached-project-class (rctx class-fqn) (phpinspect-project-get-class-or-extra (phpinspect--resolvecontext-project rctx) class-fqn)) (defun phpinspect-get-cached-project-class-methods (rctx class-fqn &optional static) (phpinspect--log "Getting cached project class methods for %s" class-fqn) (let ((class (phpinspect-rctx-get-or-create-cached-project-class rctx class-fqn))) (when class (phpinspect--log "Retrieved class index, starting method collection for %s" class-fqn) (if static (phpinspect--class-get-static-method-list class) (phpinspect--class-get-method-list class))))) (defsubst phpinspect-get-cached-project-class-method-type (rctx class-fqn method-name) (let* ((class (phpinspect-rctx-get-or-create-cached-project-class rctx class-fqn)) (method)) (when class (setq method (phpinspect--class-get-method class (phpinspect-intern-name method-name))) (when method (phpinspect--function-return-type method))))) (defsubst phpinspect-get-cached-project-class-variable-type (rctx class-fqn variable-name) (phpinspect--log "Getting cached project class variable type %s::%s" class-fqn variable-name) (let ((found-variable (phpinspect--class-get-variable (phpinspect-rctx-get-or-create-cached-project-class rctx class-fqn) variable-name))) (when found-variable (phpinspect--variable-type found-variable)))) (defsubst phpinspect-get-cached-project-class-static-method-type (rctx class-fqn method-name) (let* ((class (phpinspect-rctx-get-or-create-cached-project-class rctx class-fqn)) (method)) (when class (setq method (phpinspect--class-get-static-method class (phpinspect-intern-name method-name))) (when method (phpinspect--function-return-type method))))) (defun phpinspect-get-derived-statement-type-in-block (resolvecontext statement php-block type-resolver &optional function-arg-list assignments) "Get type of RESOLVECONTEXT subject in PHP-BLOCK. Use TYPE-RESOLVER and FUNCTION-ARG-LIST in the process. An example of a derived statement would be the following php code: $variable->attribute->method(); $variable->attribute; $variable->method(); self::method(); ClassName::method(); $variable = ClassName::method(); $variable = $variable->method();" (phpinspect--get-derived-statement-type-in-block resolvecontext statement php-block (or assignments (phpinspect--find-assignment-ctxs-in-token php-block)) type-resolver function-arg-list)) (defun phpinspect--get-derived-statement-type-in-block (resolvecontext statement php-block assignments type-resolver &optional function-arg-list) "Determine the type that STATEMENT evaluates to in RESOLVECONTEXT. PHP-BLOCK should be the block that STATEMENT was found in. ASSIGNMENTS should be a list of assignment-contexts. A statement is derived when it contains multiple components that derive off a base token. Object property access is an example of a derived statement. In the statement $foo->bar which is parsed into ((:variable foo) (:object-attrib (:word bar))), the value/type of ->bar must be derived from the type of $foo. So ->bar derives from the base token $foo." ;; A derived statement can be an assignment itself. (when (seq-find #'phpinspect-assignment-p statement) (phpinspect--log "Derived statement is an assignment: %s" statement) (setq statement (cdr (seq-drop-while #'phpinspect-not-assignment-p statement)))) (phpinspect--log "Get derived statement type in block: (truncated, real length: %d) %s" (length statement) (take 10 statement)) (let* ((first-token (pop statement)) (current-token) (previous-attribute-type)) ;; No first token means we were passed an empty list. (when (and first-token (setq previous-attribute-type (or ;; Statements starting with a bare word can indicate a static ;; method call. These could be statements with "return" or ;; another bare-word at the start though, so we drop preceding ;; barewords when they are present. (when (phpinspect-word-p first-token) (when (phpinspect-word-p (car statement)) (setq statement (phpinspect-drop-preceding-barewords statement)) (setq first-token (pop statement))) (funcall type-resolver (phpinspect--make-type :name (cadr first-token)))) ;; First token is a list, for example "(new DateTime())" (when (phpinspect-list-p first-token) (phpinspect--interpret-expression-type-in-context resolvecontext php-block type-resolver (cdr first-token) function-arg-list assignments)) ;; No bare word, assume we're dealing with a variable. (when (phpinspect-variable-p first-token) (phpinspect--get-variable-type-in-block resolvecontext (cadr first-token) php-block assignments type-resolver function-arg-list))))) (phpinspect--log "Statement: %s" statement) (phpinspect--log "Starting attribute type: %s" previous-attribute-type) (while (setq current-token (pop statement)) (phpinspect--log "Current derived statement token: %s" current-token) (cond ((phpinspect-object-attrib-p current-token) (let ((attribute-word (cadr current-token))) (when (phpinspect-word-p attribute-word) (if (phpinspect-list-p (car statement)) (progn (pop statement) (setq previous-attribute-type (or (phpinspect-get-cached-project-class-method-type resolvecontext (funcall type-resolver previous-attribute-type) (cadr attribute-word)) previous-attribute-type))) (setq previous-attribute-type (or (phpinspect-get-cached-project-class-variable-type resolvecontext (funcall type-resolver previous-attribute-type) (cadr attribute-word)) previous-attribute-type)))))) ((phpinspect-static-attrib-p current-token) (let ((attribute-word (cadr current-token))) (phpinspect--log "Found attribute word: %s" attribute-word) (phpinspect--log "checking if next token is a list. Token: %s" (car statement)) (when (phpinspect-word-p attribute-word) (if (phpinspect-list-p (car statement)) (progn (pop statement) (setq previous-attribute-type (or (phpinspect-get-cached-project-class-static-method-type resolvecontext (funcall type-resolver previous-attribute-type) (cadr attribute-word)) 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)))) (defun phpinspect-get-variable-type-in-block (resolvecontext variable-name php-block type-resolver &optional function-arg-list) (phpinspect--get-variable-type-in-block resolvecontext variable-name php-block (phpinspect--find-assignment-ctxs-in-token php-block) type-resolver function-arg-list)) (defun phpinspect--get-variable-type-in-block (resolvecontext variable-name php-block assignments type-resolver &optional function-arg-list) "Find the type of VARIABLE-NAME in PHP-BLOCK using TYPE-RESOLVER. 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) can be found in FUNCTION-ARG-LIST. When PHP-BLOCK belongs to a function, supply FUNCTION-ARG-LIST to 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 assignments type-resolver function-arg-list))) (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." (phpinspect--get-pattern-type-from-assignments resolvecontext pattern php-block (phpinspect--find-assignment-ctxs-in-token php-block) type-resolver function-arg-list)) (defun phpinspect--get-pattern-type-in-block (resolvecontext pattern php-block assignments type-resolver &optional function-arg-list) (phpinspect--get-pattern-type-from-assignments resolvecontext pattern php-block assignments type-resolver function-arg-list)) (defun phpinspect--get-pattern-type-from-assignments (resolvecontext pattern php-block assignments type-resolver &optional function-arg-list) (let* ((last-assignment (phpinspect--find-assignment-by-predicate assignments (phpinspect--pattern-matcher pattern))) (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 assignment list of length %d" pattern-code (length assignments)) (if (not last-assignment) (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--actx-preceding-assignments (phpinspect--assignment-ctx last-assignment))))) (phpinspect--log "Type interpreted from last assignment expression of pattern %s: %s" pattern-code result) ;; Unable to resolve a type from the code. When the pattern is for a ;; variable, attempt to find a @var annotation for the specified variable. (when (and (not result) last-assignment (and (= (phpinspect--pattern-length pattern) 1) (phpinspect-variable-p (cadr pattern-code)))) (when-let* ((annotation (phpinspect--find-var-annotation-for-variable (phpinspect--actx-annotations (phpinspect--assignment-ctx last-assignment)) (cadadr pattern-code))) (annotation-type (phpinspect-var-annotation-type annotation))) (setq result (funcall type-resolver (phpinspect--make-type :name annotation-type))))) (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-from-assignments resolvecontext concat-pattern php-block assignments type-resolver function-arg-list)))) ;; return result)) (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 tokens) (if (or (phpinspect-statement-introduction-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))) (when current-sublist (push (nreverse current-sublist) sublists)) (nreverse sublists))) (defun phpinspect-get-variable-type-in-function-arg-list (variable-name type-resolver arg-list) "Infer VARIABLE-NAME's type from typehints in ARG-LIST. ARG-LIST should be a list token as returned by `phpinspect--list-handler` (see also `phpinspect-list-p`)" (let ((arg-no (seq-position arg-list variable-name (lambda (token variable-name) (and (phpinspect-variable-p token) (string= (car (last token)) variable-name)))))) (if (and arg-no (> arg-no 0)) (let ((arg (elt arg-list (- arg-no 1)))) (if (phpinspect-word-p arg) (funcall type-resolver (phpinspect--make-type :name (car (last arg)))) nil))))) (defun phpinspect--interpret-expression-type-in-context (resolvecontext php-block type-resolver expression &optional function-arg-list assignments) "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. An expression can be any sequence of tokens that evaluates to a value/type." (phpinspect--log "Interpreting type of expression (truncated, full-length: %s) %s" (length expression) (take 10 expression)) (cond ((phpinspect-array-p (car expression)) (let ((collection-contains) (collection-items (phpinspect--split-statements (cdr (car expression)))) (count 0)) (phpinspect--log "Checking collection items in array token of length: %d" (length 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 assignments) 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 (phpinspect-list-p (car expression)) (= 1 (length (cdar expression))) (phpinspect-word-p (cadar expression))) ;; expression starts with "(word)", so it is a type cast. Return the ;; type of the cast. (funcall type-resolver (phpinspect--make-type :name (car (cdadar expression))))) ((and (> (length expression) 1) (seq-find (lambda (part) (or (phpinspect-attrib-p part) (phpinspect-array-p part))) expression)) (phpinspect--log "Expression is a derived statement") (phpinspect--get-derived-statement-type-in-block resolvecontext expression php-block assignments type-resolver function-arg-list)) ((phpinspect-list-p (car expression)) (phpinspect--interpret-expression-type-in-context resolvecontext php-block type-resolver (cdar expression) function-arg-list assignments)) ;; Expression is a (chain of) assignments. The right-most subexpression ;; is the type it evaluates to. ((seq-find #'phpinspect-assignment-p expression) (phpinspect--interpret-expression-type-in-context resolvecontext php-block type-resolver (car (last (phpinspect--split-statements expression #'phpinspect-maybe-assignment-p))) function-arg-list assignments)) ;; 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 assignments type-resolver function-arg-list))))) (defun phpinspect-resolve-type-from-context (resolvecontext &optional type-resolver) "Resolve the type that RESOLVECONTEXT's subject evaluates to." ;; Subject should be a statement, not a single token. (when (phpinspect-probably-token-p (phpinspect--resolvecontext-subject resolvecontext)) (setf (phpinspect--resolvecontext-subject resolvecontext) (list (phpinspect--resolvecontext-subject resolvecontext)))) (unless type-resolver (setq type-resolver (phpinspect--make-type-resolver-for-resolvecontext resolvecontext))) (phpinspect--log "Looking for type of statement: %s in nested token" (phpinspect--resolvecontext-subject resolvecontext)) ;; Find all enclosing tokens that aren't classes. Classes do not contain variable ;; assignments which have effect in the current scope, which is what we're trying ;; to find here to infer the statement type. (let ((enclosing-tokens (seq-filter #'phpinspect-not-class-p (phpinspect--resolvecontext-enclosing-tokens resolvecontext))) (enclosing-token) (type)) (while (and enclosing-tokens (not type)) ;;(phpinspect--log "Trying to find type in %s" enclosing-token) (setq enclosing-token (pop enclosing-tokens)) (setq type (cond ((phpinspect-namespace-p enclosing-token) (phpinspect-get-derived-statement-type-in-block resolvecontext (phpinspect--resolvecontext-subject resolvecontext) (or (phpinspect-namespace-block enclosing-token) enclosing-token) type-resolver)) ((or (phpinspect-block-p enclosing-token) (phpinspect-root-p enclosing-token)) (phpinspect-get-derived-statement-type-in-block resolvecontext (phpinspect--resolvecontext-subject resolvecontext) enclosing-token type-resolver)) ((phpinspect-function-p enclosing-token) (phpinspect-get-derived-statement-type-in-block resolvecontext (phpinspect--resolvecontext-subject resolvecontext) (phpinspect-function-block enclosing-token) type-resolver (phpinspect-function-argument-list enclosing-token)))))) type)) (defun phpinspect--function-get-pattern-type (fn rctx pattern type-resolver) (phpinspect-get-pattern-type-in-block rctx pattern (phpinspect-function-block (phpinspect--function-token fn)) type-resolver (phpinspect-function-argument-list (phpinspect--function-token fn)))) (cl-defmethod phpinspect--class-resolve-property-type ((class phpinspect--class) (project phpinspect-project) (property-name string) type-resolver class-token-meta) "Resolve type of POPERTY-NAME in the context of CLASS using CLASS-TOKEN-META as parse result." (let ((pattern (phpinspect--make-pattern :m `(:variable "this") :m `(:object-attrib (:word ,property-name)))) (rctx (phpinspect--make-resolvecontext :enclosing-tokens (list (phpinspect-meta-token class-token-meta)) :enclosing-metadata (list class-token-meta) :project project)) (constructor-name (phpinspect-intern-name "__construct"))) (or (when-let ((constructor (phpinspect--class-get-method class constructor-name))) (phpinspect--function-get-pattern-type constructor rctx pattern type-resolver)) (catch 'found (dolist (method (phpinspect--class-get-method-list class)) (unless (eq constructor-name (phpinspect--function-name-symbol method)) (when-let ((result (phpinspect--function-get-pattern-type method rctx pattern type-resolver))) (throw 'found result)))) nil)))) (cl-defmethod phpinspect--class-resolve-property-type ((_class phpinspect--class) (_project phpinspect-project) property-name &rest _ignored) ;; Catch-all for cases where one attempts to resolve a nil property ;; name. Saves an if-statement for the caller. ;; Can't resolve property type when property name is nil, so we do nothing. (cl-assert (not property-name)) nil) (provide 'phpinspect-resolve)