Move more functionalities from main file to separate modules

WIP-cache
Hugo Thunnissen 10 months ago
parent 1f145665ef
commit d1d34a4249

@ -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))

@ -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))

@ -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 <devel@hugot.nl>
;; 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 <https://www.gnu.org/licenses/>.
;;; 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)

@ -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

@ -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)

@ -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.")

@ -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 <devel@hugot.nl>
;; 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 <https://www.gnu.org/licenses/>.
;;; 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)

@ -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)

@ -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 <devel@hugot.nl>
;; 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 <https://www.gnu.org/licenses/>.
;;; 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)

@ -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

@ -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

@ -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

Loading…
Cancel
Save