diff --git a/phpinspect-autoload.el b/phpinspect-autoload.el index d7b1fed..e145755 100644 --- a/phpinspect-autoload.el +++ b/phpinspect-autoload.el @@ -26,6 +26,7 @@ (require 'cl-lib) (require 'phpinspect-project) (require 'phpinspect-fs) +(require 'phpinspect-util) (cl-defstruct (phpinspect-psr0 (:constructor phpinspect-make-psr0-generated)) diff --git a/phpinspect-bmap.el b/phpinspect-bmap.el index a9907f2..cc8fe8c 100644 --- a/phpinspect-bmap.el +++ b/phpinspect-bmap.el @@ -325,15 +325,24 @@ LIMIT is the maximum number of positions to check backward before giving up. If not provided, this is 100." (unless limit (setq limit 100)) - (let* ((ends (phpinspect-bmap-ends bmap)) - (ending) + (let* ((ending) (point-limit (- point limit))) - (unless (hash-table-empty-p ends) + (unless (hash-table-empty-p (phpinspect-bmap-ends bmap)) (while (not (or (<= point 0) (<= point point-limit) (setq ending (phpinspect-bmap-tokens-ending-at bmap point)))) (setq point (- point 1))) (car (last ending))))) +(cl-defmethod phpinspect-bmap-last-token-starting-before-point ((bmap phpinspect-bmap) point &optional limit) + (unless limit (setq limit 100)) + (let* ((starting) + (point-limit (- point limit))) + (unless (hash-table-empty-p (phpinspect-bmap-starts bmap)) + (while (not (or (<= point 0) (<= point point-limit) + (setq starting (phpinspect-bmap-token-starting-at bmap point)))) + (setq point (- point 1))) + starting))) + (defsubst phpinspect-bmap-overlay (bmap bmap-overlay token-meta pos-delta &optional whitespace-before) (let* ((overlays (phpinspect-bmap-overlays bmap)) (start (+ (phpinspect-meta-start token-meta) pos-delta)) diff --git a/phpinspect-completion.el b/phpinspect-completion.el new file mode 100644 index 0000000..c41894c --- /dev/null +++ b/phpinspect-completion.el @@ -0,0 +1,187 @@ +;;; phpinspect-type.el --- PHP parsing and completion package -*- lexical-binding: t; -*- + +;; Copyright (C) 2021 Free Software Foundation, Inc + +;; Author: Hugo Thunnissen +;; Keywords: php, languages, tools, convenience +;; Version: 0 + +;; 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-bmap) +(require 'phpinspect-buffer) +(require 'phpinspect-resolvecontext) +(require 'phpinspect-suggest) + +(defvar phpinspect--last-completion-list nil + "Used internally to save metadata about completion options + between company backend calls") + +(cl-defstruct (phpinspect--completion + (:constructor phpinspect--construct-completion)) + "Contains a possible completion value with all it's attributes." + (value nil :type string) + (meta nil :type string) + (annotation nil :type string) + (kind nil :type symbol)) + +(cl-defgeneric phpinspect--make-completion (completion-candidate) + "Creates a `phpinspect--completion` for a possible completion +candidate. Candidates can be indexed functions and variables.") + +(cl-defstruct (phpinspect--completion-list + (:constructor phpinspect--make-completion-list)) + "Contains all data for a completion at point" + (completions (obarray-make) + :type obarray + :documentation + "A list of completion strings")) + +(cl-defgeneric phpinspect--completion-list-add + (comp-list completion) + "Add a completion to a completion-list.") + +(cl-defmethod phpinspect--completion-list-add + ((comp-list phpinspect--completion-list) (completion phpinspect--completion)) + (unless (intern-soft (phpinspect--completion-value completion) + (phpinspect--completion-list-completions comp-list)) + (set (intern (phpinspect--completion-value completion) + (phpinspect--completion-list-completions comp-list)) + completion))) + +(cl-defmethod phpinspect--completion-list-get-metadata + ((comp-list phpinspect--completion-list) (completion-name string)) + (let ((comp-sym (intern-soft completion-name + (phpinspect--completion-list-completions comp-list)))) + (when comp-sym + (symbol-value comp-sym)))) + + +(cl-defmethod phpinspect--completion-list-strings + ((comp-list phpinspect--completion-list)) + (let ((strings)) + (obarray-map (lambda (sym) (push (symbol-name sym) strings)) + (phpinspect--completion-list-completions comp-list)) + strings)) + +(cl-defstruct (phpinspect-completion-query (:constructor phpinspect-make-completion-query)) + (completion-point 0 + :type integer + :documentation + "Position in the buffer from where the resolvecontext is determined.") + (point 0 + :type integer + :documentation "Position in buffer for which to provide completions") + (buffer nil + :type phpinspect-buffer)) + +(cl-defmethod phpinspect-completion-query-execute ((query phpinspect-completion-query)) + "Execute QUERY. + +Returns list of `phpinspect--completion'." + (let* ((buffer (phpinspect-completion-query-buffer query)) + (point (phpinspect-completion-query-point query)) + (buffer-map (phpinspect-buffer-parse-map buffer)) + (rctx (phpinspect-get-resolvecontext buffer-map point)) + (candidates)) + (dolist (strategy phpinspect-completion-strategies) + (when (phpinspect-comp-strategy-supports strategy query rctx) + (phpinspect--log "Found matching completion strategy. Executing...") + (nconc candidates (phpinspect-comp-strategy-execute strategy query rctx)))) + + (mapcar #'phpinspect--make-completion candidates))) + +(cl-defgeneric phpinspect-comp-strategy-supports (strategy (query phpinspect-completion-query) (context phpinspect--resolvecontext)) + "Should return non-nil if STRATEGY should be deployed for QUERY +and CONTEXT. All strategies must implement this method.") + +(cl-defgeneric phpinspect-comp-strategy-execute (strategy (query phpinspect-completion-query) (context phpinspect--resolvecontext)) + "Should return a list of objects for which `phpinspect--make-completion' is implemented.") + +(cl-defstruct (phpinspect-comp-sigil (:constructor phpinspect-make-comp-sigil)) + "Completion strategy for the sigil ($) character.") + +(cl-defmethod phpinspect-comp-strategy-supports + ((strat phpinspect-comp-sigil) (q phpinspect-completion-query) + (rctx phpinspect--resolvecontext)) + (and (= (phpinspect-completion-query-completion-point q) + (phpinspect-completion-query-point q)) + (phpinspect-variable-p + (phpinspect-meta-token + (phpinspect-bmap-last-token-starting-before-point + (phpinspect-buffer-parse-map (phpinspect-completion-query-buffer q)) + (phpinspect-completion-query-point q)))))) + +(cl-defmethod phpinspect-comp-strategy-execute + ((strat phpinspect-comp-sigil) (q phpinspect-completion-query) + (rctx phpinspect--resolvecontext)) + (phpinspect-suggest-variables-at-point rctx)) + +(cl-defstruct (phpinspect-comp-attribute (:constructor phpinspect-make-comp-attribute)) + "Completion strategy for object attributes") + +(cl-defmethod phpinspect-comp-strategy-supports + ((strat phpinspect-comp-attribute) (q phpinspect-completion-query) + (context phpinspect--resolvecontext)) + (phpinspect-object-attrib-p (car (last (phpinspect--resolvecontext-subject rctx))))) + +(cl-defmethod phpinspect-comp-strategy-execute + ((strat phpinspect-comp-sigil) (q phpinspect-completion-query) + (rctx phpinspect--resolvecontext)) + (phpinspect-suggest-variables-at-point rctx)) + +(cl-defstruct (phpinspect-comp-static-attribute (:constructor phpinspect-make-comp-static-attribute)) + "Completion strategy for static attributes") + +(cl-defstruct (phpinspect-comp-bareword (:constructor phpinspect-make-comp-bareword)) + "Completion strategy for bare words") + + +(cl-defmethod phpinspect--make-completion + ((completion-candidate phpinspect--function)) + "Create a `phpinspect--completion` for COMPLETION-CANDIDATE." + (phpinspect--construct-completion + :value (phpinspect--function-name completion-candidate) + :meta (concat "(" (mapconcat (lambda (arg) + (concat (phpinspect--format-type-name (cadr arg)) " " + "$" (if (> (length (car arg)) 8) + (truncate-string-to-width (car arg) 8 nil) + (car arg)))) + (phpinspect--function-arguments completion-candidate) + ", ") + ") " + (phpinspect--format-type-name (phpinspect--function-return-type completion-candidate))) + :annotation (concat " " + (phpinspect--type-bare-name + (phpinspect--function-return-type completion-candidate))) + :kind 'function)) + +(cl-defmethod phpinspect--make-completion + ((completion-candidate phpinspect--variable)) + (phpinspect--construct-completion + :value (phpinspect--variable-name completion-candidate) + :meta (phpinspect--format-type-name + (or (phpinspect--variable-type completion-candidate) + phpinspect--null-type)) + :annotation (concat " " + (phpinspect--type-bare-name + (or (phpinspect--variable-type completion-candidate) + phpinspect--null-type))) + :kind 'variable)) + +(provide 'phpinspect-completion) diff --git a/phpinspect-eldoc.el b/phpinspect-eldoc.el index bc65c35..26d82b6 100644 --- a/phpinspect-eldoc.el +++ b/phpinspect-eldoc.el @@ -23,7 +23,8 @@ ;;; Code: - +(defvar phpinspect-eldoc-word-width 14 + "The maximum width of words in eldoc strings.") (cl-defstruct (phpinspect-eldoc-query (:constructor phpinspect-make-eldoc-query)) (point 0 diff --git a/phpinspect-index.el b/phpinspect-index.el index 0e2e509..b0a6b25 100644 --- a/phpinspect-index.el +++ b/phpinspect-index.el @@ -113,12 +113,13 @@ function (think \"new\" statements, return types etc.)." (defun phpinspect--index-const-from-scope (scope) (phpinspect--make-variable :scope `(,(car scope)) + :mutability `(,(caadr scope)) :name (cadr (cadr (cadr scope))))) (defun phpinspect--var-annotations-from-token (token) (seq-filter #'phpinspect-var-annotation-p token)) -(defun phpinspect--index-variable-from-scope (type-resolver scope comment-before) +(defun phpinspect--index-variable-from-scope (type-resolver scope comment-before &optional static) "Index the variable inside `scope`." (let* ((var-annotations (phpinspect--var-annotations-from-token comment-before)) (variable-name (cadr (cadr scope))) @@ -133,6 +134,7 @@ function (think \"new\" statements, return types etc.)." (phpinspect--make-variable :name variable-name :scope `(,(car scope)) + :lifetime (when static '(:static)) :type (if type (funcall type-resolver (phpinspect--make-type :name type)))))) (defun phpinspect-doc-block-p (token) @@ -248,7 +250,8 @@ function (think \"new\" statements, return types etc.)." (push (phpinspect--index-variable-from-scope type-resolver (list (car token) (cadadr token)) - comment-before) + comment-before + 'static) static-variables)))) (t (phpinspect--log "comment-before is: %s" comment-before) diff --git a/phpinspect-project.el b/phpinspect-project.el index a90ac7d..723a218 100644 --- a/phpinspect-project.el +++ b/phpinspect-project.el @@ -28,6 +28,11 @@ (require 'phpinspect-fs) (require 'filenotify) +(defvar phpinspect-auto-reindex nil + "Whether or not phpinspect should automatically search for new +files. The current implementation is clumsy and can result in +serious performance hits. Enable at your own risk (:") + (defvar phpinspect-project-root-function #'phpinspect--find-project-root "Function that phpinspect uses to find the root directory of a project.") diff --git a/phpinspect-resolve.el b/phpinspect-resolve.el new file mode 100644 index 0000000..b3efb24 --- /dev/null +++ b/phpinspect-resolve.el @@ -0,0 +1,508 @@ +;;; phpinspect-resolve.el --- PHP parsing and completion package -*- lexical-binding: t; -*- + +;; Copyright (C) 2021 Free Software Foundation, Inc + +;; Author: Hugo Thunnissen +;; Keywords: php, languages, tools, convenience +;; Version: 0 + +;; 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-type) +(require 'phpinspect-parser) + +(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))) + +(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))) + +(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) + (blocks-or-lists) + (statements (phpinspect--split-statements token))) + (dolist (statement statements) + (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)) + +(defsubst phpinspect-not-assignment-p (token) + "Inverse of applying `phpinspect-assignment-p to TOKEN." + (not (phpinspect-maybe-assignment-p token))) + +(defsubst phpinspect-not-comment-p (token) + (not (phpinspect-comment-p token))) + +(defun phpinspect--find-assignments-by-predicate (token predicate) + (let ((variable-assignments) + (all-assignments (phpinspect--find-assignments-in-token token))) + (dolist (assignment all-assignments) + (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)))) + (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))) + +(defsubst phpinspect-drop-preceding-barewords (statement) + (while (and statement (phpinspect-word-p (cadr statement))) + (pop statement)) + statement) + +;; TODO: the use of this function and similar ones should be replaced with code +;; that uses locally injected project objects in stead of retrieving the project +;; object through global variables. +(defsubst phpinspect-get-cached-project-class (project-root class-fqn) + (when project-root + (phpinspect-project-get-class + (phpinspect--cache-get-project-create (phpinspect--get-or-create-global-cache) + project-root) + class-fqn))) + +(defun phpinspect-get-cached-project-class-methods (project-root class-fqn &optional static) + (phpinspect--log "Getting cached project class methods for %s (%s)" + project-root class-fqn) + (when project-root + (let ((class (phpinspect-get-or-create-cached-project-class + project-root + class-fqn))) + (when class + (phpinspect--log "Retrieved class index, starting method collection %s (%s)" + project-root class-fqn) + (if static + (phpinspect--class-get-static-method-list class) + (phpinspect--class-get-method-list class)))))) + +(defmacro phpinspect-find-function-in-list (method-name list) + (let ((break-sym (gensym)) + (method-name-sym (gensym))) + `(let ((,method-name-sym (phpinspect-intern-name ,method-name))) + (catch (quote ,break-sym) + (dolist (func ,list) + (when (eq (phpinspect--function-name-symbol func) + ,method-name-sym) + (throw (quote ,break-sym) func))))))) + +(defsubst phpinspect-get-cached-project-class-method-type + (project-root class-fqn method-name) + (when project-root + (let* ((class (phpinspect-get-or-create-cached-project-class project-root 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 + (project-root class-fqn variable-name) + (phpinspect--log "Getting cached project class variable type for %s (%s::%s)" + project-root class-fqn variable-name) + (when project-root + (let ((found-variable + (phpinspect--class-get-variable + (phpinspect-get-or-create-cached-project-class project-root class-fqn) + variable-name))) + (when found-variable + (phpinspect--variable-type found-variable))))) + +(defsubst phpinspect-get-cached-project-class-static-method-type + (project-root class-fqn method-name) + (when project-root + (let* ((class (phpinspect-get-or-create-cached-project-class project-root 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) + "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();" + ;; 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: %s" 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)))) + + ;; No bare word, assume we're dealing with a variable. + (phpinspect-get-variable-type-in-block + resolvecontext + (cadr first-token) + php-block + 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 + (phpinspect--resolvecontext-project-root + 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 + (phpinspect--resolvecontext-project-root + 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 + (phpinspect--resolvecontext-project-root + 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)))) + +;;;; +;; TODO: since we're passing type-resolver to all of the get-variable-type functions now, +;; we may as well always return FQNs in stead of relative type names. +;;;; +(defun phpinspect-get-variable-type-in-block + (resolvecontext variable-name php-block 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 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." + (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)))) + + ; 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-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))) + (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) + "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 (lambda (part) (or (phpinspect-attrib-p part) + (phpinspect-array-p part))) + 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 &optional type-resolver) + (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)) + +(provide 'phpinspect-resolve) diff --git a/phpinspect-resolvecontext.el b/phpinspect-resolvecontext.el index 526ad40..16dca76 100644 --- a/phpinspect-resolvecontext.el +++ b/phpinspect-resolvecontext.el @@ -76,6 +76,24 @@ (push (phpinspect-meta-token child) previous-siblings))))) previous-siblings)))) +(defun phpinspect--get-last-statement-in-token (token) + (setq token (cond ((phpinspect-function-p token) + (phpinspect-function-block token)) + ((phpinspect-namespace-p token) + (phpinspect-namespace-block token)) + (t token))) + (nreverse + (seq-take-while + (let ((keep-taking t) (last-test nil)) + (lambda (elt) + (when last-test + (setq keep-taking nil)) + (setq last-test (phpinspect-variable-p elt)) + (and keep-taking + (not (phpinspect-end-of-statement-p elt)) + (listp elt)))) + (reverse token)))) + (cl-defmethod phpinspect-get-resolvecontext ((bmap phpinspect-bmap) (point integer)) (let* ((enclosing-tokens) diff --git a/phpinspect-suggest.el b/phpinspect-suggest.el new file mode 100644 index 0000000..7953142 --- /dev/null +++ b/phpinspect-suggest.el @@ -0,0 +1,139 @@ +;;; phpinspect-suggest.el --- PHP parsing and completion package -*- lexical-binding: t; -*- + +;; Copyright (C) 2021 Free Software Foundation, Inc + +;; Author: Hugo Thunnissen +;; Keywords: php, languages, tools, convenience +;; Version: 0 + +;; 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-resolve) +(require 'phpinspect-parser) +(require 'phpinspect-type) +(require 'phpinspect-project) +(require 'phpinspect-class) + +(defun phpinspect-suggest-variables-at-point (resolvecontext) + (phpinspect--log "Suggesting variables at point") + (let ((variables)) + (dolist (token (phpinspect--resolvecontext-enclosing-tokens resolvecontext)) + (when (phpinspect-not-class-p token) + (let ((token-list token) + (potential-variable)) + (while token-list + (setq potential-variable (pop token-list)) + (cond ((phpinspect-variable-p potential-variable) + (phpinspect--log "Pushing variable %s" potential-variable) + (push (phpinspect--make-variable + :name (cadr potential-variable) + :type phpinspect--null-type) + variables)) + ((phpinspect-function-p potential-variable) + (push (phpinspect-function-block potential-variable) token-list) + (dolist (argument (phpinspect-function-argument-list potential-variable)) + (when (phpinspect-variable-p argument) + (push (phpinspect--make-variable + :name (cadr argument) + :type phpinspect--null-type) + variables)))) + ((phpinspect-block-p potential-variable) + (dolist (nested-token (cdr potential-variable)) + (push nested-token token-list)))))))) + + ;; Only return variables that have a name. Unnamed variables are just dollar + ;; signs (: + (seq-filter #'phpinspect--variable-name variables))) + +(defun phpinspect-get-cached-project-class-methods (project-root class-fqn &optional static) + (phpinspect--log "Getting cached project class methods for %s (%s)" + project-root class-fqn) + (when project-root + (let ((class (phpinspect-get-or-create-cached-project-class + project-root + class-fqn))) + (when class + (phpinspect--log "Retrieved class index, starting method collection %s (%s)" + project-root class-fqn) + (if static + (phpinspect--class-get-static-method-list class) + (phpinspect--class-get-method-list class)))))) + +(defun phpinspect--get-methods-for-class + (resolvecontext buffer-classes class &optional static) + "Extract all possible methods for a class from `buffer-classes` and the class index. +`buffer-classes` will be preferred because their data should be +more recent" + (let ((methods (phpinspect-get-cached-project-class-methods + (phpinspect--resolvecontext-project-root + resolvecontext) + class + static)) + (buffer-index (alist-get class buffer-classes nil nil #'phpinspect--type=))) + (phpinspect--log "Getting methods for class (%s)" class) + (when buffer-index + (dolist (method (alist-get (if static 'static-methods 'methods) + buffer-index)) + (push method methods))) + (unless methods + (phpinspect--log "Failed to find methods for class %s :(" class)) + methods)) + +(defun phpinspect--get-variables-for-class (buffer-classes class-name &optional static) + (let ((class (phpinspect-get-or-create-cached-project-class + (phpinspect-current-project-root) + class-name))) + ;; TODO return static variables/constants when static is set + (when class + (phpinspect--class-variables class)))) + +(defun phpinspect--make-method-lister (resolvecontext buffer-classes &optional static) + (lambda (fqn) + (phpinspect--get-methods-for-class resolvecontext buffer-classes fqn static))) + +(defun phpinspect-suggest-attributes-at-point + (resolvecontext &optional static) + "Suggest object or class attributes at point. + +RESOLVECONTEXT must be a structure of the type +`phpinspect--resolvecontext'. The PHP type of its subject is +resolved to provide completion candidates. + +If STATIC is non-nil, candidates are provided for constants, +static variables and static methods." + (let* ((buffer-index phpinspect--buffer-index) + (buffer-classes (alist-get 'classes (cdr buffer-index))) + (type-resolver (phpinspect--make-type-resolver-for-resolvecontext + resolvecontext)) + (method-lister (phpinspect--make-method-lister + resolvecontext + buffer-classes + static))) + (let ((statement-type (phpinspect-resolve-type-from-context + resolvecontext + type-resolver))) + (when statement-type + (let ((type (funcall type-resolver statement-type))) + (append (phpinspect--get-variables-for-class + buffer-classes + type + static) + (funcall method-lister type))))))) + +(provide 'phpinspect-suggest) diff --git a/phpinspect-type.el b/phpinspect-type.el index dce5805..37ee587 100644 --- a/phpinspect-type.el +++ b/phpinspect-type.el @@ -154,6 +154,20 @@ NAMESPACE may be nil, or a string with a namespace FQN." (setf (phpinspect--type-collection type) t)) type) +(defun phpinspect--find-innermost-incomplete-class (token) + (let ((last-token (car (last token)))) + (cond ((phpinspect-incomplete-class-p token) token) + ((phpinspect-incomplete-token-p last-token) + (phpinspect--find-innermost-incomplete-class last-token))))) + +(defun phpinspect--find-class-token (token) + "Recurse into token tree until a class is found." + (when (and (listp token) (> (length token) 1)) + (let ((last-token (car (last token)))) + (cond ((phpinspect-class-p token) token) + (last-token + (phpinspect--find-class-token last-token)))))) + (defun phpinspect--make-type-resolver (types &optional token-tree namespace) "Little wrapper closure to pass around and resolve types with." (let* ((inside-class @@ -215,7 +229,6 @@ return type of the function.")) (cl-defmethod phpinspect--function-name ((func phpinspect--function)) (symbol-name (phpinspect--function-name-symbol func))) - (cl-defstruct (phpinspect--variable (:constructor phpinspect--make-variable)) "A PHP Variable." (name nil @@ -223,16 +236,29 @@ return type of the function.")) :documentation "A string containing the name of the variable.") (scope nil - :type phpinspect-scope :documentation "When the variable is an object attribute, this should contain the scope of the variable as returned by -`phpinspect-parse-scope`") +`phpinspect-parse-scope'") + (lifetime nil + :documentation + "The lifetime of the variable (e.g. whether it is static or not). Will +contain the parsed keyword token indicating the lifetime of the variable") + (mutability nil + :documentation + "The mutability of the variable (e.g. whether it is constant or +not). Will contain the parsed keyword token indicating the +mutability of the variable") (type nil :type string :documentation "A string containing the FQN of the variable's type")) +(defun phpinspect--variable-static-p (variable) + (phpinspect-static-p (phpinspect--variable-lifetime variable))) + +(defun phpinspect--variable-const-p (variable) + (phpinspect-const-p (phpinspect--variable-mutability variable))) (provide 'phpinspect-type) ;;; phpinspect-type.el ends here diff --git a/phpinspect-util.el b/phpinspect-util.el index 7edab54..d4ca48f 100644 --- a/phpinspect-util.el +++ b/phpinspect-util.el @@ -172,5 +172,20 @@ hierarchy as long as no matching files are found. See also phpinspect-project-root-file-list) dominating-file)) +(defun phpinspect--determine-completion-point () + "Find first point backwards that could contain any kind of +context for completion." + (save-excursion + (re-search-backward "[^[:blank:]\n]") + (forward-char) + (point))) + +(defmacro phpinspect-json-preset (&rest body) + "Default options to wrap around `json-read' and similar BODY." + `(let ((json-object-type 'hash-table) + (json-array-type 'list) + (json-key-type 'string)) + ,@body)) + (provide 'phpinspect-util) ;;; phpinspect-util.el ends here diff --git a/phpinspect.el b/phpinspect.el index e9e367c..33cc004 100644 --- a/phpinspect.el +++ b/phpinspect.el @@ -43,11 +43,8 @@ (require 'phpinspect-buffer) (require 'phpinspect-resolvecontext) (require 'phpinspect-eldoc) - -(defvar phpinspect-auto-reindex nil - "Whether or not phpinspect should automatically search for new -files. The current implementation is clumsy and can result in -serious performance hits. Enable at your own risk (:") +(require 'phpinspect-suggest) +(require 'phpinspect-completion) (defvar-local phpinspect--buffer-index nil "The result of the last successfull parse + index action @@ -67,46 +64,6 @@ phpinspect") '("composer.json" "composer.lock" ".git" ".svn" ".hg") "List of files that could indicate a project root directory.") -(defvar phpinspect--last-completion-list nil - "Used internally to save metadata about completion options - between company backend calls") - -(defvar phpinspect-eldoc-word-width 14 - "The maximum width of words in eldoc strings.") - -(cl-defstruct (phpinspect--completion - (:constructor phpinspect--construct-completion)) - "Contains a possible completion value with all it's attributes." - (value nil :type string) - (meta nil :type string) - (annotation nil :type string) - (kind nil :type symbol)) - - -(cl-defgeneric phpinspect--make-completion (completion-candidate) - "Creates a `phpinspect--completion` for a possible completion -candidate. Candidates can be indexed functions and variables.") - -(cl-defmethod phpinspect--make-completion - ((completion-candidate phpinspect--function)) - "Create a `phpinspect--completion` for COMPLETION-CANDIDATE." - (phpinspect--construct-completion - :value (phpinspect--function-name completion-candidate) - :meta (concat "(" (mapconcat (lambda (arg) - (concat (phpinspect--format-type-name (cadr arg)) " " - "$" (if (> (length (car arg)) 8) - (truncate-string-to-width (car arg) 8 nil) - (car arg)))) - (phpinspect--function-arguments completion-candidate) - ", ") - ") " - (phpinspect--format-type-name (phpinspect--function-return-type completion-candidate))) - :annotation (concat " " - (phpinspect--type-bare-name - (phpinspect--function-return-type completion-candidate))) - :kind 'function)) - - (defsubst phpinspect-cache-project-class (project-root indexed-class) (when project-root (phpinspect-project-add-class @@ -114,101 +71,6 @@ candidate. Candidates can be indexed functions and variables.") project-root) indexed-class))) -(defsubst phpinspect-get-cached-project-class (project-root class-fqn) - (when project-root - (phpinspect-project-get-class - (phpinspect--cache-get-project-create (phpinspect--get-or-create-global-cache) - project-root) - class-fqn))) - -(defun phpinspect-get-project-class-inherit-classes (project-root class) - (let ((classnames `(,@(alist-get 'extends class) - ,@(alist-get 'implements class))) - (classes)) - - (phpinspect--log "Found inherit classes: %s" classnames) - (while classnames - (let ((inherit-class (phpinspect-get-or-create-cached-project-class - project-root - (pop classnames)))) - (push inherit-class classes) - (dolist (nested-class (phpinspect-get-project-class-inherit-classes - project-root - inherit-class)) - (push nested-class classes)))) - - (seq-uniq classes #'eq))) - -(defun phpinspect-get-cached-project-class-methods (project-root class-fqn &optional static) - (phpinspect--log "Getting cached project class methods for %s (%s)" - project-root class-fqn) - (when project-root - (let ((class (phpinspect-get-or-create-cached-project-class - project-root - class-fqn))) - (when class - (phpinspect--log "Retrieved class index, starting method collection %s (%s)" - project-root class-fqn) - (if static - (phpinspect--class-get-static-method-list class) - (phpinspect--class-get-method-list class)))))) - -(defmacro phpinspect-find-function-in-list (method-name list) - (let ((break-sym (gensym)) - (method-name-sym (gensym))) - `(let ((,method-name-sym (phpinspect-intern-name ,method-name))) - (catch (quote ,break-sym) - (dolist (func ,list) - (when (eq (phpinspect--function-name-symbol func) - ,method-name-sym) - (throw (quote ,break-sym) func))))))) - -(defsubst phpinspect-get-cached-project-class-method-type - (project-root class-fqn method-name) - (when project-root - (let* ((class (phpinspect-get-or-create-cached-project-class project-root 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 - (project-root class-fqn variable-name) - (phpinspect--log "Getting cached project class variable type for %s (%s::%s)" - project-root class-fqn variable-name) - (when project-root - (let ((found-variable - (phpinspect--class-get-variable - (phpinspect-get-or-create-cached-project-class project-root class-fqn) - variable-name))) - (when found-variable - (phpinspect--variable-type found-variable))))) - -;; (defsubst phpinspect-get-cached-project-class-static-method-type -;; (project-root class-fqn method-name) -;; (when project-root -;; (let* ((found-method -;; (phpinspect-find-function-in-list -;; method-name -;; (phpinspect-get-cached-project-class-methods project-root class-fqn 'static)))) -;; (when found-method -;; (phpinspect--function-return-type found-method))))) - -(defsubst phpinspect-get-cached-project-class-static-method-type - (project-root class-fqn method-name) - (when project-root - (let* ((class (phpinspect-get-or-create-cached-project-class project-root 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-parse-file (file) (with-temp-buffer (phpinspect-insert-file-contents file) @@ -234,452 +96,6 @@ candidate. Candidates can be indexed functions and variables.") (insert string) (phpinspect-parse-current-buffer))) -(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-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))) - (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--determine-completion-point () - "Find first point backwards that could contain any kind of -context for completion." - (save-excursion - (re-search-backward "[^[:blank:]\n]") - (forward-char) - (point))) - -(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))) - -(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))) - -(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) - (blocks-or-lists) - (statements (phpinspect--split-statements token))) - (dolist (statement statements) - (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)) - -(defsubst phpinspect-not-assignment-p (token) - "Inverse of applying `phpinspect-assignment-p to TOKEN." - (not (phpinspect-maybe-assignment-p token))) - -(defsubst phpinspect-not-comment-p (token) - (not (phpinspect-comment-p token))) - -(defun phpinspect--find-assignments-by-predicate (token predicate) - (let ((variable-assignments) - (all-assignments (phpinspect--find-assignments-in-token token))) - (dolist (assignment all-assignments) - (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)))) - (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))) - -(defsubst phpinspect-drop-preceding-barewords (statement) - (while (and statement (phpinspect-word-p (cadr statement))) - (pop statement)) - statement) - -(defun phpinspect-get-derived-statement-type-in-block - (resolvecontext statement php-block type-resolver &optional function-arg-list) - "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();" - ;; 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: %s" 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)))) - - ;; No bare word, assume we're dealing with a variable. - (phpinspect-get-variable-type-in-block - resolvecontext - (cadr first-token) - php-block - 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 - (phpinspect--resolvecontext-project-root - 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 - (phpinspect--resolvecontext-project-root - 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 - (phpinspect--resolvecontext-project-root - 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)))) - -;;;; -;; TODO: since we're passing type-resolver to all of the get-variable-type functions now, -;; we may as well always return FQNs in stead of relative type names. -;;;; -(defun phpinspect-get-variable-type-in-block - (resolvecontext variable-name php-block 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 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." - (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)))) - - ; return - 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 (lambda (part) (or (phpinspect-attrib-p part) - (phpinspect-array-p part))) - 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 &optional type-resolver) - (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--get-variables-for-class (buffer-classes class-name &optional static) - (let ((class (phpinspect-get-or-create-cached-project-class - (phpinspect-current-project-root) - class-name))) - ;; TODO return static variables/constants when static is set - (when class - (phpinspect--class-variables class)))) - -(defun phpinspect--get-methods-for-class - (resolvecontext buffer-classes class &optional static) - "Extract all possible methods for a class from `buffer-classes` and the class index. -`buffer-classes` will be preferred because their data should be -more recent" - (let ((methods (phpinspect-get-cached-project-class-methods - (phpinspect--resolvecontext-project-root - resolvecontext) - class - static)) - (buffer-index (alist-get class buffer-classes nil nil #'phpinspect--type=))) - (phpinspect--log "Getting methods for class (%s)" class) - (when buffer-index - (dolist (method (alist-get (if static 'static-methods 'methods) - buffer-index)) - (push method methods))) - (unless methods - (phpinspect--log "Failed to find methods for class %s :(" class)) - methods)) - (defun phpinspect-after-change-function (start end pre-change-length) (when phpinspect-current-buffer (phpinspect-buffer-register-edit phpinspect-current-buffer start end pre-change-length))) @@ -805,167 +221,9 @@ Example configuration: ;; End example configuration." :after-hook (phpinspect--mode-function)) -(defun phpinspect--find-class-token (token) - "Recurse into token tree until a class is found." - (when (and (listp token) (> (length token) 1)) - (let ((last-token (car (last token)))) - (cond ((phpinspect-class-p token) token) - (last-token - (phpinspect--find-class-token last-token)))))) - -(defun phpinspect--find-innermost-incomplete-class (token) - (let ((last-token (car (last token)))) - (cond ((phpinspect-incomplete-class-p token) token) - ((phpinspect-incomplete-token-p last-token) - (phpinspect--find-innermost-incomplete-class last-token))))) - -(defun phpinspect--find-last-variable-position-in-token (token) - "Find the last variable that can be encountered in the top -level of a token. Nested variables are ignored." - (let ((i (length token))) - (while (and (not (= 0 i)) - (not (phpinspect-variable-p - (car (last token i))))) - (setq i (- i 1))) - - (if (not (= i 0))(- (length token) i)))) - -(defun phpinspect--make-method-lister (resolvecontext buffer-classes &optional static) - (lambda (fqn) - (phpinspect--get-methods-for-class resolvecontext buffer-classes fqn static))) - (defun phpinspect--buffer-index (buffer) (with-current-buffer buffer phpinspect--buffer-index)) -(defsubst phpinspect-not-variable-p (token) - (not (phpinspect-variable-p token))) - -(cl-defmethod phpinspect--make-completion - ((completion-candidate phpinspect--variable)) - (phpinspect--construct-completion - :value (phpinspect--variable-name completion-candidate) - :meta (phpinspect--format-type-name - (or (phpinspect--variable-type completion-candidate) - phpinspect--null-type)) - :annotation (concat " " - (phpinspect--type-bare-name - (or (phpinspect--variable-type completion-candidate) - phpinspect--null-type))) - :kind 'variable)) - -(cl-defstruct (phpinspect--completion-list - (:constructor phpinspect--make-completion-list)) - "Contains all data for a completion at point" - (completions (obarray-make) - :type obarray - :documentation - "A list of completion strings")) - -(cl-defgeneric phpinspect--completion-list-add - (comp-list completion) - "Add a completion to a completion-list.") - -(cl-defmethod phpinspect--completion-list-add - ((comp-list phpinspect--completion-list) (completion phpinspect--completion)) - (unless (intern-soft (phpinspect--completion-value completion) - (phpinspect--completion-list-completions comp-list)) - (set (intern (phpinspect--completion-value completion) - (phpinspect--completion-list-completions comp-list)) - completion))) - -(cl-defmethod phpinspect--completion-list-get-metadata - ((comp-list phpinspect--completion-list) (completion-name string)) - (let ((comp-sym (intern-soft completion-name - (phpinspect--completion-list-completions comp-list)))) - (when comp-sym - (symbol-value comp-sym)))) - - -(cl-defmethod phpinspect--completion-list-strings - ((comp-list phpinspect--completion-list)) - (let ((strings)) - (obarray-map (lambda (sym) (push (symbol-name sym) strings)) - (phpinspect--completion-list-completions comp-list)) - strings)) - -(defun phpinspect--suggest-attributes-at-point - (resolvecontext &optional static) - "Suggest object or class attributes at point. - -RESOLVECONTEXT must be a structure of the type -`phpinspect--resolvecontext'. The PHP type of its subject is -resolved to provide completion candidates. - -If STATIC is non-nil, candidates are provided for constants, -static variables and static methods." - (let* ((buffer-index phpinspect--buffer-index) - (buffer-classes (alist-get 'classes (cdr buffer-index))) - (type-resolver (phpinspect--make-type-resolver-for-resolvecontext - resolvecontext)) - (method-lister (phpinspect--make-method-lister - resolvecontext - buffer-classes - static))) - (let ((statement-type (phpinspect-resolve-type-from-context - resolvecontext - type-resolver))) - (when statement-type - (let ((type (funcall type-resolver statement-type))) - (append (phpinspect--get-variables-for-class - buffer-classes - type - static) - (funcall method-lister type))))))) - -(defun phpinspect--get-last-statement-in-token (token) - (setq token (cond ((phpinspect-function-p token) - (phpinspect-function-block token)) - ((phpinspect-namespace-p token) - (phpinspect-namespace-block token)) - (t token))) - (nreverse - (seq-take-while - (let ((keep-taking t) (last-test nil)) - (lambda (elt) - (when last-test - (setq keep-taking nil)) - (setq last-test (phpinspect-variable-p elt)) - (and keep-taking - (not (phpinspect-end-of-statement-p elt)) - (listp elt)))) - (reverse token)))) - -(defun phpinspect--suggest-variables-at-point (resolvecontext) - (phpinspect--log "Suggesting variables at point") - (let ((variables)) - (dolist (token (phpinspect--resolvecontext-enclosing-tokens resolvecontext)) - (when (phpinspect-not-class-p token) - (let ((token-list token) - (potential-variable)) - (while token-list - (setq potential-variable (pop token-list)) - (cond ((phpinspect-variable-p potential-variable) - (phpinspect--log "Pushing variable %s" potential-variable) - (push (phpinspect--make-variable - :name (cadr potential-variable) - :type phpinspect--null-type) - variables)) - ((phpinspect-function-p potential-variable) - (push (phpinspect-function-block potential-variable) token-list) - (dolist (argument (phpinspect-function-argument-list potential-variable)) - (when (phpinspect-variable-p argument) - (push (phpinspect--make-variable - :name (cadr argument) - :type phpinspect--null-type) - variables)))) - ((phpinspect-block-p potential-variable) - (dolist (nested-token (cdr potential-variable)) - (push nested-token token-list)))))))) - - ;; Only return variables that have a name. Unnamed variables are just dollar - ;; signs (: - (seq-filter #'phpinspect--variable-name variables))) - (defun phpinspect--suggest-at-point () (phpinspect--log "Entering suggest at point. Point: %d" (point)) (let* ((bmap (phpinspect-buffer-parse-map phpinspect-current-buffer)) @@ -977,16 +235,16 @@ static variables and static methods." (cond ((and (phpinspect-object-attrib-p (car last-tokens)) (phpinspect-word-p (cadr last-tokens))) (phpinspect--log "word-attributes") - (phpinspect--suggest-attributes-at-point resolvecontext)) + (phpinspect-suggest-attributes-at-point resolvecontext)) ((phpinspect-object-attrib-p (cadr last-tokens)) (phpinspect--log "object-attributes") - (phpinspect--suggest-attributes-at-point resolvecontext)) + (phpinspect-suggest-attributes-at-point resolvecontext)) ((phpinspect-static-attrib-p (cadr last-tokens)) (phpinspect--log "static-attributes") - (phpinspect--suggest-attributes-at-point token-tree resolvecontext t)) + (phpinspect-suggest-attributes-at-point token-tree resolvecontext t)) ((phpinspect-variable-p (car(phpinspect--resolvecontext-subject resolvecontext))) - (phpinspect--suggest-variables-at-point resolvecontext))))) + (phpinspect-suggest-variables-at-point resolvecontext))))) (defun phpinspect-company-backend (command &optional arg &rest _ignored) @@ -1063,14 +321,6 @@ currently opened projects." ;; Assign a fresh cache object (setq phpinspect-cache (phpinspect--make-cache))) - -(defmacro phpinspect-json-preset (&rest body) - "Default options to wrap around `json-read' and similar BODY." - `(let ((json-object-type 'hash-table) - (json-array-type 'list) - (json-key-type 'string)) - ,@body)) - (defsubst phpinspect-insert-file-contents (&rest args) "Call `phpinspect-insert-file-contents-function' with ARGS as arguments." (apply phpinspect-insert-file-contents-function args)) @@ -1129,17 +379,6 @@ before the search is executed." (phpinspect-current-project-root)))) (phpinspect-project-get-type-filepath project class index-new))) - -(defun phpinspect-unique-strings (strings) - (seq-filter - (let ((last-line nil)) - (lambda (line) - (let ((return-line (unless (and last-line (string= last-line line)) - line))) - (setq last-line line) - return-line))) - strings)) - (defun phpinspect-index-current-project () "Index all available FQNs in the current project." (interactive) @@ -1153,10 +392,5 @@ before the search is executed." (hash-table-count (phpinspect-autoloader-own-types autoloader)) (hash-table-count (phpinspect-autoloader-types autoloader))))) -(defun phpinspect-unique-lines () - (let ((unique-lines (phpinspect-unique-strings (split-string (buffer-string) "\n" nil nil)))) - (erase-buffer) - (insert (string-join unique-lines "\n")))) - (provide 'phpinspect) ;;; phpinspect.el ends here