From 4430aaccb9060ebe7be5da9fcd0954bb8a0dbbe4 Mon Sep 17 00:00:00 2001 From: Hugo Thunnissen Date: Sun, 12 Sep 2021 18:42:50 +0200 Subject: [PATCH] Rework parser code The parser is now JIT compiled. Parser functions are generated and byte-compiled on the fly as needed and stored in an obarray. Handler functions are stored in an obarray as well and are compiled at load time. See docstrings and code for more information. Macro use has been brought back to a minimum and macros have been reworked to make the code easier to reason about. To be able to redifine parser functions during runtime, "M-x phpinspect-purge-parser-cache" has been added. To redifine a handler function, one can use `phpinspect-defhandler` after loading phpinspect.el and purge the parser cache to make sure that the new function definition is used. --- phpinspect.el | 1422 ++++++++++++++++++++++++------------------------- 1 file changed, 708 insertions(+), 714 deletions(-) diff --git a/phpinspect.el b/phpinspect.el index 1df1270..039a004 100644 --- a/phpinspect.el +++ b/phpinspect.el @@ -26,696 +26,60 @@ (require 'cl-lib) (require 'json) +(require 'obarray) -(cl-defstruct (phpinspect--function (:constructor phpinspect--make-function)) - "A PHP function." - (name nil - :type string - :documentation - "A string containing the name of the function") - (scope nil - :type phpinspect-scope - :documentation - "When the function is a method, this should contain the -scope of the function as returned by `phpinspect-parse-scope`.") - (arguments nil - :type list - :documentation - "A simple list with function arguments and their -types in tuples. Each list should have the name of the variable -as first element and the type as second element.") - (return-type nil - :type string - :documentation - "A string containing the FQN of the return value -of the function.")) +(defvar phpinspect-parser-obarray (obarray-make) + "An obarray containing symbols for all phpinspect (sub)parsers.") -(cl-defstruct (phpinspect--variable (:constructor phpinspect--make-variable)) - "A PHP Variable." - (name nil - :type string - :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`") - (type nil - :type string - :documentation - "A string containing the FQN of the variable's type")) +(defvar phpinspect-handler-obarray (obarray-make) + "An obarray containing symbols for all phpinspect parser handlers.") -(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)) - -(defun phpinspect--format-type-name (name) - (string-remove-prefix "\\" name)) - -(cl-defgeneric phpinspect--make-completion (completion-candidate) - "Creates a `phpinspect--completion` for a possible completion -candidate. Candidates can be indexed functions and variables.") +(defvar-local phpinspect--buffer-index nil + "The result of the last successfull parse + index action + executed by phpinspect for the current buffer") -(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--get-bare-class-name-from-fqn - (or (phpinspect--function-return-type completion-candidate) - ""))) - :kind 'function)) +(defvar phpinspect-projects '() + "Currently active phpinspect projects and their buffers") (defvar phpinspect--debug nil "Enable debug logs for phpinspect by setting this variable to true") +(defvar phpinspect-cache () + "In-memory nested key-value store used for caching by +phpinspect") + (defvar phpinspect-project-root-file-list '("composer.json" "composer.lock" ".git" ".svn" ".hg") "List of files that could indicate a project root directory.") -(defun phpinspect-toggle-logging () - (interactive) - (if (setq phpinspect--debug (not phpinspect--debug)) - (message "Enabled phpinspect logging.") - (message "Disabled phpinspect logging."))) +(defvar phpinspect--last-completion-list nil + "Used internally to save metadata about completion options + between company backend calls") -(defconst phpinspect-native-types - ;; self, parent and resource are not valid type name. - ;; see https://www.php.net/manual/ja/language.types.declarations.php - '("array" "bool" "callable" "float" "int" "iterable" "mixed" "object" "string" "void")) +(defvar phpinspect-index-executable + (concat (file-name-directory + (or load-file-name + buffer-file-name)) + "/phpinspect-index.bash") + "The path to the exexutable file that indexes class file names. +Should normally be set to \"phpinspect-index.bash\" in the source + file directory.") (eval-when-compile - (defun phpinspect--word-end-regex () - "[[:blank:]]") - - (defsubst phpinspect--strip-last-char (string) - (substring string 0 (- (length string) 1))) - - (defmacro phpinspect--handler (regex function) - (list 'cons regex (list 'quote function)))) - -(defmacro phpinspect-defhandler (name regex docstring function) - ;; Lets make sure that defuns and substs are only referenced by - ;; their name and not their entire definitions. - (let ((function-name (cond ((and (listp function) - (or (eq (car function) 'defun) - (eq (car function) 'defsubst) - (and (eq (car function) 'quote) - (symbolp (car (last function)))))) - (eval function)) - (t (error (concat "`phpinspect-defhandler`: function must " - "be a quoted function name, a `defun` " - "or a `defsubst` %S provided") - function))))) - `(progn - ;; If function is a defun, we'll need to have evaluated it. - ,function - (defmacro ,name () - ,(concat "This is a generated macro, see `phpinspect-defhandler`\n\n" - "ATTRIBUTES:\n" - "Token regex: " (eval regex) "\n\n" - "Parser function:\n" (with-output-to-string (pp (list 'quote function-name))) - "\n\n" - "DESCRIPTION\n" - docstring) - (list 'phpinspect--handler ,regex (quote ,function-name)))))) - -(defmacro phpinspect-munch-token-without-attribs (text-object token-keyword) - "Return a token by name of `token-keyword` with the contents of -the passed text object as value. The text object will be -stripped of all text attributes" - `(let ((text ,text-object) (length (length ,text-object))) - (forward-char length) - (set-text-properties 0 length nil text) - (list ,token-keyword text))) - -(defmacro phpinspect-parse - (buffer tree-type handler-list max-point &optional continue-condition delimiter-predicate) - "Parse the current buffer using the handler macros provided in -`handler-list`, unrolling them in a `cond` statement which checks -their token regexes one by one and runs their parser functions -when one of them matches." - (unless continue-condition (setq continue-condition t)) - `(with-current-buffer ,buffer - (let ((tokens (list))) - (while ,(append `(and (< (point) ,max-point)) - (list continue-condition) - `((not ,(if (functionp (eval delimiter-predicate)) - (list (eval delimiter-predicate) - '(car (last tokens))) - nil)))) - ,(append `(cond) - (mapcar - (lambda (handler) - `((looking-at ,(car (eval handler))) - (let ((token (,(cdr (eval handler)) - (match-string 0) - ,max-point))) - (unless (null token) - (if (null tokens) - (setq tokens (list token)) - (nconc tokens (list token))))))) - handler-list) - '((t (forward-char))))) - (push ,tree-type tokens)))) - -(eval-and-compile - ;; Because some of the handler macros are mutually dependent on each - ;; other, we need to wrap their definition in an eval-and-compile - ;; body. - - (phpinspect-defhandler - phpinspect--comma-handler "," - "Handler for comma tokens" - (defun phpinspect--munch-comma (comma &rest ignored) - (phpinspect-munch-token-without-attribs comma :comma))) - - (phpinspect-defhandler - phpinspect--word-handler "[A-Za-z_\\][\\A-Za-z_0-9]*" - "Handler for bareword tokens" - (defun phpinspect--munch-word (word &rest ignored) - (let ((length (length word))) - (forward-char length) - (set-text-properties 0 length nil word) - (list :word word)))) - - - (defun phpinspect--parse-annotation-parameters (parameter-amount) - (let (words) - (while (not (or (looking-at "\\*/") (= (length words) parameter-amount))) - (forward-char) - (cond ((looking-at (car (phpinspect--word-handler))) - (push (phpinspect--munch-word (match-string 0)) words)) - ((looking-at (car (phpinspect--variable-handler))) - (push (phpinspect--parse-variable (match-string 0)) words)))) - (nreverse words))) - - (phpinspect-defhandler - phpinspect--annotation-handler "@" - "Handler for in-comment @annotations" - (defun phpinspect--parse-var-annotation (start-token max-point) - (forward-char (length start-token)) - (if (looking-at (car (phpinspect--word-handler))) - (let ((annotation-name (match-string 0))) - (forward-char (length annotation-name)) - (cond ((string= annotation-name "var") - ;; The @var annotation accepts 2 parameters: - ;; the type and the $variable name - (append (list :var-annotation) - (phpinspect--parse-annotation-parameters 2))) - ((string= annotation-name "return") - ;; The @return annotation only accepts 1 word as parameter: - ;; The return type - (append (list :return-annotation) - (phpinspect--parse-annotation-parameters 1))) - ((string= annotation-name "param") - (let ((word-count 0)) - ;; The @param annotation accepts 2 parameters: - ;; The type of the param, and the param's $name - (append (list :param-annotation) - (phpinspect--parse-annotation-parameters 2)))) - (t - (list :annotation annotation-name)))) - (list :annotation nil)))) - - (phpinspect-defhandler - phpinspect--tag-handler "\\?>" - "Handler that discards any inline HTML it encounters" - (defun phpinspect--discard-html (start-token max-point) - (forward-char (length start-token)) - (or (re-search-forward "<\\?php\\|<\\?" nil t) - (goto-char max-point)) - (list :html))) - - (phpinspect-defhandler - phpinspect--comment-handler "#\\|//\\|/\\*" - "Handler for comments and doc blocks" - (defun phpinspect--parse-comment (start-token max-point) - (forward-char (length start-token)) - (cond ((string-match "/\\*" start-token) - (let ((doc-block (phpinspect-parse - (current-buffer) - :doc-block - ((phpinspect--annotation-handler) - (phpinspect--whitespace-handler)) - max-point - (not (looking-at "\\*/"))))) - (forward-char 2) - doc-block)) - (t - (let ((end-position (line-end-position))) - (phpinspect-parse - (current-buffer) - :comment - ((phpinspect--tag-handler)) - end-position - t - 'phpinspect-html-p)))))) - - (phpinspect-defhandler - phpinspect--variable-handler "\\$" - "Handler for tokens indicating reference to a variable" - (defun phpinspect--parse-variable (start-token &rest ignored) - (forward-char (length start-token)) - (if (looking-at (car (phpinspect--word-handler))) - (phpinspect-munch-token-without-attribs (match-string 0) :variable) - (list :variable nil)))) - - - (phpinspect-defhandler - phpinspect--whitespace-handler "[[:blank:]]+" - "Handler that discards whitespace" - (defun phpinspect--discard-whitespace (whitespace &rest ignored) - (forward-char (length whitespace)))) - - (phpinspect-defhandler - phpinspect--equals-handler "===?" - "Handler for strict and unstrict equality comparison tokens" - (defun phpinspect--munch-equals (equals &rest ignored) - (phpinspect-munch-token-without-attribs equals :equals))) - - (phpinspect-defhandler - phpinspect--assignment-operator-handler "[+-]?=" - "Handler for tokens indicating that an assignment is taking place" - (defun phpinspect--munch-assignment-operator (operator &rest ignored) - (phpinspect-munch-token-without-attribs operator :assignment))) - - (phpinspect-defhandler - phpinspect--statement-terminator-handler ";" - "Handler for statement terminators" - (defun phpinspect--munch-statement-terminator (terminator &rest ignored) - (phpinspect-munch-token-without-attribs terminator :terminator))) - - (phpinspect-defhandler - phpinspect--use-keyword-handler (concat "use" (phpinspect--word-end-regex)) - "Handler for the use keyword and tokens that might follow to give it meaning" - (defun phpinspect--parse-use (start-token max-point) - (setq start-token (phpinspect--strip-last-char start-token)) - (forward-char (length start-token)) - (phpinspect-parse - (current-buffer) - :use - ((phpinspect--word-handler) - (phpinspect--tag-handler) - (phpinspect--block-without-classes-handler) - (phpinspect--statement-terminator-handler)) - max-point - t - 'phpinspect-end-of-use-p))) - - (phpinspect-defhandler - phpinspect--attribute-reference-handler "->\\|::" - "Handler for references to object attributes, or static class attributes" - (defun phpinspect--parse-attribute-reference (start-token &rest ignored) - (forward-char (length start-token)) - (looking-at (car (phpinspect--word-handler))) - (let ((name (if (looking-at (car (phpinspect--word-handler))) - (phpinspect--munch-word (match-string 0)) - nil))) - (cond - ((string= start-token "::") - (list :static-attrib name)) - ((string= start-token "->") - (list :object-attrib name)))))) - - (phpinspect-defhandler - phpinspect--namespace-keyword-handler (concat "namespace" (phpinspect--word-end-regex)) - "Handler for the namespace keyword. This is a special one - because it is not always delimited by a block like classes or - functions. This handler parses the namespace declaration, and - then continues to parse subsequent tokens, only stopping when - either a block has been parsed or another namespace keyword has - been encountered." - (defun phpinspect--parse-namespace (start-token max-point) - "Nest all statements after a 'namespace' keyword in its own token" - (setq start-token (phpinspect--strip-last-char start-token)) - (forward-char (length start-token)) - (phpinspect--parse-with-handler-alist - (current-buffer) - :namespace - max-point - (not (looking-at (car (phpinspect--namespace-keyword-handler)))) - 'phpinspect-block-p))) - - (phpinspect-defhandler - phpinspect--const-keyword-handler (concat "const" (phpinspect--word-end-regex)) - "Handler for the const keyword" - (defun phpinspect--parse-const (start-token max-point) - (setq start-token (phpinspect--strip-last-char start-token)) - (forward-char (length start-token)) - (let ((token (phpinspect-parse - (current-buffer) - :const - ((phpinspect--word-handler) - (phpinspect--comment-handler) - (phpinspect--assignment-operator-handler) - (phpinspect--string-handler) - (phpinspect--array-handler) - (phpinspect--statement-terminator-handler)) - max-point - t - 'phpinspect-end-of-statement-p))) - - (when (phpinspect-incomplete-token-p (car (last token))) - (setcar token :incomplete-const)) - token))) - - (phpinspect-defhandler - phpinspect--string-handler "\"\\|'" - "Handler for strings" - (defun phpinspect--parse-string (start-token &rest ignored) - (list :string (phpinspect--munch-string start-token)))) - - (phpinspect-defhandler - phpinspect--block-without-classes-handler "{" - "Handler for code blocks that cannot contain classes" - (defun phpinspect--parse-block-without-classes (start-token max-point) - (forward-char (length start-token)) - (let* ((complete-block nil) - (parsed (phpinspect-parse - (current-buffer) - :block - ((phpinspect--array-handler) - (phpinspect--tag-handler) - (phpinspect--equals-handler) - (phpinspect--list-handler) - (phpinspect--comma-handler) - (phpinspect--attribute-reference-handler) - (phpinspect--variable-handler) - (phpinspect--assignment-operator-handler) - (phpinspect--whitespace-handler) - (phpinspect--scope-keyword-handler) - (phpinspect--static-keyword-handler) - (phpinspect--const-keyword-handler) - (phpinspect--use-keyword-handler) - (phpinspect--function-keyword-handler) - (phpinspect--word-handler) - (phpinspect--statement-terminator-handler) - (phpinspect--here-doc-handler) - (phpinspect--string-handler) - (phpinspect--comment-handler) - (phpinspect--block-handler)) - max-point - (not (and (char-equal (char-after) ?}) (setq complete-block t)))))) - (if complete-block - (forward-char) - (setcar parsed :incomplete-block)) - parsed))) - - - (phpinspect-defhandler - phpinspect--block-handler "{" - "Handler for code blocks" - (defun phpinspect--parse-block (start-token max-point) - (forward-char (length start-token)) - (let* ((complete-block nil) - (parsed (phpinspect--parse-with-handler-alist - (current-buffer) - :block - max-point - ;; When we encounter a closing brace for this - ;; block, we can mark the block as complete. - (not (and (char-equal (char-after) ?}) (setq complete-block t)))))) - - (if complete-block - ;; After meeting the char-after requirement above, we need to move - ;; one char forward to prevent parent-blocks from exiting because - ;; of the same char. - (forward-char) - (setcar parsed :incomplete-block)) - parsed))) - - (phpinspect-defhandler - phpinspect--here-doc-handler "<<<" - "Handler for heredocs" - (defun phpinspect--discard-heredoc (start-token point-max) - (forward-char (length start-token)) - (if (looking-at "[A-Za-z0-9'\"\\_]+") - (re-search-forward (concat "^" (regexp-quote (match-string 0))) nil t)) - (list :here-doc))) - - - (defun phpinspect--munch-string (start-token) - (forward-char (length start-token)) - (let ((start-point (point))) - (cond ((looking-at start-token) - (forward-char) - "") - ((looking-at (concat "\\([\\][\\]\\)+" (regexp-quote start-token))) - (let ((match (match-string 0))) - (forward-char (length match)) - (buffer-substring-no-properties start-point - (+ start-point (- (length match) - (length start-token)))))) - (t - (re-search-forward (format "\\([^\\]\\([\\][\\]\\)+\\|[^\\]\\)%s" - (regexp-quote start-token)) - nil t) - (buffer-substring-no-properties start-point (- (point) 1)))))) - - (phpinspect-defhandler - phpinspect--list-handler "(" - "Handler for php syntactic lists (Note: this does not include - datatypes like arrays, merely lists that are of a syntactic - nature like argument lists" - (defun phpinspect--parse-list (start-token max-point) - (forward-char (length start-token)) - (let* ((complete-list nil) - (php-list (phpinspect--parse-with-handler-alist - (current-buffer) - :list - max-point - (not (and (char-equal (char-after) ?\)) (setq complete-list t)))))) - - (if complete-list - ;; Prevent parent-lists (if any) from exiting by skipping over the - ;; ")" character - (forward-char) - (setcar php-list :incomplete-list)) - php-list))) - - (phpinspect-defhandler - phpinspect--function-keyword-handler (concat "function" (phpinspect--word-end-regex)) - "Handler for the function keyword and tokens that follow to give it meaning" - (defun phpinspect--parse-function (start-token max-point) - (setq start-token (phpinspect--strip-last-char start-token)) - (let ((declaration (phpinspect-parse - (current-buffer) - :declaration - ((phpinspect--comment-handler) - (phpinspect--word-handler) - (phpinspect--list-handler) - (phpinspect--statement-terminator-handler) - (phpinspect--tag-handler)) - max-point - (not (char-equal (char-after) ?{)) - 'phpinspect-end-of-statement-p))) - (if (phpinspect-end-of-statement-p (car (last declaration))) - (list :function declaration) - (list :function - declaration - (phpinspect--parse-block (char-to-string (char-after)) max-point)))))) - - (phpinspect-defhandler - phpinspect--scope-keyword-handler (mapconcat (lambda (word) - (concat word (phpinspect--word-end-regex))) - (list "public" "private" "protected") - "\\|") - "Handler for scope keywords" - (defun phpinspect--parse-scope (start-token max-point) - (setq start-token (phpinspect--strip-last-char start-token)) - (forward-char (length start-token)) - (phpinspect-parse - (current-buffer) - (cond ((string= start-token "public") :public) - ((string= start-token "private") :private) - ((string= start-token "protected") :protected)) - ((phpinspect--function-keyword-handler) - (phpinspect--static-keyword-handler) - (phpinspect--const-keyword-handler) - (phpinspect--variable-handler) - (phpinspect--here-doc-handler) - (phpinspect--string-handler) - (phpinspect--statement-terminator-handler) - (phpinspect--tag-handler) - (phpinspect--comment-handler)) - max-point - nil - 'phpinspect--scope-terminator-p))) - - (phpinspect-defhandler - phpinspect--static-keyword-handler (concat "static" (phpinspect--word-end-regex)) - "Handler for the static keyword" - (defun phpinspect--parse-static (start-token max-point) - (setq start-token (phpinspect--strip-last-char start-token)) - (forward-char (length start-token)) - (phpinspect-parse - (current-buffer) - :static - ((phpinspect--comment-handler) - (phpinspect--function-keyword-handler) - (phpinspect--variable-handler) - (phpinspect--array-handler) - (phpinspect--word-handler) - (phpinspect--statement-terminator-handler) - (phpinspect--tag-handler)) - max-point - t - 'phpinspect--static-terminator-p))) - - (phpinspect-defhandler - phpinspect--fat-arrow-handler "=>" - "Handler for the \"fat arrow\" in arrays and foreach expressions" - (defun phpinspect--munch-fat-arrow (arrow &rest ignored) - (phpinspect-munch-token-without-attribs arrow :fat-arrow))) - - (phpinspect-defhandler - phpinspect--array-handler "\\[\\|array(" - "Handler for arrays, in the bracketet as well as the list notation" - (defun phpinspect--parse-array (start-token max-point) - (forward-char (length start-token)) - (let* ((end-char (cond ((string= start-token "[") ?\]) - ((string= start-token "array(") ?\)))) - (end-char-reached nil) - (token (phpinspect-parse - (current-buffer) - :array - ((phpinspect--comment-handler) - (phpinspect--comma-handler) - (phpinspect--list-handler) - (phpinspect--here-doc-handler) - (phpinspect--string-handler) - (phpinspect--array-handler) - (phpinspect--variable-handler) - (phpinspect--attribute-reference-handler) - (phpinspect--word-handler) - (phpinspect--fat-arrow-handler)) - max-point - (not (and (char-equal (char-after) end-char) (setq end-char-reached t)))))) - - ;; Skip over the end char to prevent enclosing arrays or lists - ;; from terminating. - (if end-char-reached - (forward-char) - ;; Signal incompleteness when terminated because of max-point - (setcar token :incomplete-array)) - token))) - - (phpinspect-defhandler - phpinspect--class-keyword-handler (concat "\\(abstract\\|final\\|class\\|interface\\|trait\\)" - (phpinspect--word-end-regex)) - "Handler for the class keyword, and tokens that follow to define -the properties of the class" - (defun phpinspect--parse-class (start-token max-point) - (setq start-token (phpinspect--strip-last-char start-token)) - (list :class (phpinspect-parse - (current-buffer) - :declaration - ((phpinspect--comment-handler) - (phpinspect--word-handler) - (phpinspect--tag-handler)) - max-point - (not (char-equal (char-after) ?{))) - (phpinspect--parse-block-without-classes (char-to-string (char-after)) max-point)))) - - (defmacro phpinspect--parse-with-handler-alist - (buffer tree-type max-point &optional continue-condition delimiter-predicate) - (list 'phpinspect-parse - buffer - tree-type - '((phpinspect--array-handler) - (phpinspect--tag-handler) - (phpinspect--equals-handler) - (phpinspect--list-handler) - (phpinspect--comma-handler) - (phpinspect--attribute-reference-handler) - (phpinspect--variable-handler) - (phpinspect--assignment-operator-handler) - (phpinspect--whitespace-handler) - (phpinspect--scope-keyword-handler) - (phpinspect--static-keyword-handler) - (phpinspect--const-keyword-handler) - (phpinspect--use-keyword-handler) - (phpinspect--class-keyword-handler) - (phpinspect--function-keyword-handler) - (phpinspect--word-handler) - (phpinspect--statement-terminator-handler) - (phpinspect--here-doc-handler) - (phpinspect--string-handler) - (phpinspect--comment-handler) - (phpinspect--block-handler)) - max-point - continue-condition - delimiter-predicate)) - - - (defun phpinspect-parse-buffer-until-point (buffer point) - (with-current-buffer buffer - (save-excursion - (goto-char (point-min)) - (re-search-forward "<\\?php\\|<\\?" nil t) - (phpinspect-parse - (current-buffer) - :root - ((phpinspect--namespace-keyword-handler) - (phpinspect--array-handler) - (phpinspect--equals-handler) - (phpinspect--list-handler) - (phpinspect--comma-handler) - (phpinspect--attribute-reference-handler) - (phpinspect--variable-handler) - (phpinspect--assignment-operator-handler) - (phpinspect--whitespace-handler) - (phpinspect--scope-keyword-handler) - (phpinspect--static-keyword-handler) - (phpinspect--const-keyword-handler) - (phpinspect--use-keyword-handler) - (phpinspect--class-keyword-handler) - (phpinspect--function-keyword-handler) - (phpinspect--word-handler) - (phpinspect--statement-terminator-handler) - (phpinspect--here-doc-handler) - (phpinspect--string-handler) - (phpinspect--comment-handler) - (phpinspect--tag-handler) - (phpinspect--block-handler)) - point)))) - - ;; End of eval-and-compile body - ) - -(defsubst phpinspect--log (&rest args) - (when phpinspect--debug - (with-current-buffer (get-buffer-create "**phpinspect-logs**") - (goto-char (buffer-end 1)) - (insert (concat (apply 'format args) "\n"))))) - -(defun phpinspect-parse-file (file) - (with-temp-buffer - (insert-file-contents-literally file) - (phpinspect-parse-current-buffer))) + (define-inline phpinspect--word-end-regex () + (inline-quote "[[:blank:]]"))) +(defsubst phpinspect--strip-last-char (string) + (substring string 0 (- (length string) 1))) -(defun phpinspect-parse-current-buffer () - (phpinspect-parse-buffer-until-point - (current-buffer) - (point-max))) +(defsubst phpinspect-munch-token-without-attribs (string token-keyword) + "Return a token of type TOKEN-KEYWORD with STRING as value. +If STRING has text properties, they are stripped." + (let ((value (copy-sequence string)) + (length (length string))) + (forward-char length) + (set-text-properties 0 length nil value) + (list token-keyword value))) (defsubst phpinspect-type-p (object type) @@ -831,22 +195,6 @@ Type can be any of the token types returned by (phpinspect-incomplete-method-p token) (phpinspect-incomplete-namespace-p token))) -(defun phpinspect-get-variable-type-in-function-arg-list (variable-name 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) - (car (last arg)) - nil))))) - (defun phpinspect--static-terminator-p (token) (or (phpinspect-function-p token) (phpinspect-end-of-statement-p token))) @@ -875,6 +223,655 @@ ARG-LIST. ARG-LIST should be a list token as returned by (defun phpinspect-comment-p (token) (phpinspect-type-p token :comment)) +(defmacro phpinspect-defhandler (name arguments docstring attribute-plist &rest body) + "Define a parser handler that becomes available for use with phpinspect-parse. + +A parser handler is a function that is able to identify and parse +tokens from PHP code at `point` in the current buffer. It's +return value must be the resulting token. Aside from parsing it +has to manage the state of `point` in a way that it skips over +the tokens it has parsed. That way the next handler can +correctly pick up from where it has left off. + +Parser handlers are unrolled in a `cond` statement by +`phpinspect-make-parser`. The resulting code is something akin +to the following: + +(while ... + (cond (((looking-at \"{\") + (funcall block-handler (match-string 0) max-point) + ((looking-at \"\\$\") + (funcall variable-handler ... +etc. + +NAME must be a symbol. It does not need to be prefixed with a +\"namespace\" because parser handlers are stored in their own +obarray (`phpinspect-handler-obarray`). + +ARGUMENTS must an argument list as accepted by `lambda`. A +handler must be able to accept 2 arguments: START-TOKEN and +MAX-POINT. START-TOKEN is the match string that resulted from +the comparison of the handlers' `regexp` attribute with the text +at `point`. MAX-POINT is the point in the current buffer up +until which the parser is supposed to parse. For some tokens you +may not want/need to respect MAX-POINT, in which case you can +ignore it. + +DOCSTRING is mandatory. It should contain a description of the +tokens the handler is able to process and (if present) any +particularities of the handler. + +ATTRIBUTE-PLIST is a plist that must contain at least a `regexp` key. + Possible keys: + - regexp: The regular expression that marks the start of the token. + +BODY is a function body as accepted by `lambda` that parses the +text at point and returns the resulting token." + (declare (indent defun)) + (when (not (symbolp name)) + (error "In definition of phpinspect handler %s: NAME bust be a symbol" name)) + + (when (not (plist-member attribute-plist 'regexp)) + (error "In definition of phpinspect handler %s ATTRIBUTE-PLIST must contain key `regexp`" + name)) + + ;; Eval regexp. It might be a `concat` statement and we don't want to be executing that + ;; every time the parser advances one character and has to check for the regexp + ;; occurence. + (setq attribute-plist (plist-put attribute-plist 'regexp + (eval (plist-get attribute-plist 'regexp)))) + (let ((name (symbol-name name))) + `(progn + (set (intern ,name phpinspect-handler-obarray) (quote ,attribute-plist)) + (defalias (intern ,name phpinspect-handler-obarray) + #'(lambda (,@arguments) + ,docstring + ,@body)) + (byte-compile (intern ,name phpinspect-handler-obarray))))) + +(defun phpinspect-get-parser-create (tree-type &rest parser-parameters) + "Retrieve a parser for TREE-TYPE from `phpinspect-parser-obarray`. + +TREE-TYPE must be a symbol or keyword representing the type of +the token the parser is able to parse. + +If a parser by TREE-TYPE doesn't exist, it is created by callng +`phpinspect-make-parser` with TREE-TYPE as first argument and +PARSER-PARAMETERS as the rest of the arguments. The resulting +parser function is then returned in byte-compiled form." + (let ((parser-name (symbol-name tree-type))) + (or (intern-soft parser-name phpinspect-parser-obarray) + (defalias (intern parser-name phpinspect-parser-obarray) + (byte-compile (apply 'phpinspect-make-parser `(,tree-type ,@parser-parameters))))))) + +(defun phpinspect-purge-parser-cache () + "Empty `phpinspect-parser-obarray`. + +This is useful when you need to change parser handlers or parsers +during runtime. Parsers are implemented with macros, so changing +handler functions without calling this function will often not +have any effect." + (interactive) + (setq phpinspect-parser-obarray (obarray-make))) + +(defun phpinspect-make-parser (tree-type handler-list &optional delimiter-predicate) + "Create a parser function using the handlers by names defined in HANDLER-LIST. + +See also `phpinspect-defhandler`. + +TREE-TYPE must be a symbol or a keyword representing the token +type. + +HANDLER-LIST must be a list of either symbol or string +representation of handler symbols which can be found in +`phpinspect-handler-obarray`. + +DELIMITER-PREDICATE must be a function. It is passed the last +parsed token after every handler iteration. If it evaluates to +something other than nil, parsing is deemed completed and the +loop exits. An example use case of this is to determine the end +of a statement. You can use `phpinspect-terminator-p` as +delimiter predicate and have parsing stop when the last parsed +token is \";\", which marks the end of a statement in PHP." + (let ((handlers (mapcar + (lambda (handler-name) + (let* ((handler-name (symbol-name handler-name)) + (handler (intern-soft handler-name phpinspect-handler-obarray))) + (if handler + handler + (error "No handler found by name \"%s\"" handler-name)))) + handler-list)) + (delimiter-predicate (if (symbolp delimiter-predicate) + `(quote ,delimiter-predicate) + delimiter-predicate))) + `(lambda (buffer max-point &optional continue-condition) + (with-current-buffer buffer + (let ((tokens) + (delimiter-predicate (when (functionp ,delimiter-predicate) ,delimiter-predicate))) + (while (and (< (point) max-point) + (if continue-condition (funcall continue-condition) t) + (not (if delimiter-predicate + (funcall delimiter-predicate (car (last tokens))) + nil))) + (cond ,@(mapcar + (lambda (handler) + `((looking-at ,(plist-get (symbol-value handler) 'regexp)) + (let ((token (funcall ,(symbol-function handler) + (match-string 0) + max-point))) + (when token + (if (null tokens) + (setq tokens (list token)) + (nconc tokens (list token))))))) + handlers) + (t (forward-char)))) + (push ,tree-type tokens)))))) + +(phpinspect-defhandler comma (comma &rest ignored) + "Handler for comma tokens" + (regexp ",") + (phpinspect-munch-token-without-attribs comma :comma)) + +(phpinspect-defhandler word (word &rest ignored) + "Handler for bareword tokens" + (regexp "[A-Za-z_\\][\\A-Za-z_0-9]*") + (let ((length (length word))) + (forward-char length) + (set-text-properties 0 length nil word) + (list :word word))) + +(defsubst phpinspect-handler (handler-name) + (intern-soft (symbol-name handler-name) phpinspect-handler-obarray)) + +(defsubst phpinspect-handler-regexp (handler-name) + (plist-get (symbol-value (phpinspect-handler handler-name)) 'regexp)) + +(defsubst phpinspect--parse-annotation-parameters (parameter-amount) + (let* ((words) + (word-handler (phpinspect-handler 'word)) + (word-regexp (phpinspect-handler-regexp 'word)) + (variable-handler (phpinspect-handler 'variable)) + (variable-regexp (phpinspect-handler-regexp 'variable))) + (while (not (or (looking-at "\\*/") (= (length words) parameter-amount))) + (forward-char) + (cond ((looking-at word-regexp) + (push (funcall word-handler (match-string 0)) words)) + ((looking-at variable-regexp) + (push (funcall variable-handler (match-string 0)) words)))) + (nreverse words))) + +(phpinspect-defhandler annotation (start-token &rest ignored) + "Handler for in-comment @annotations" + (regexp "@") + (forward-char (length start-token)) + (if (looking-at (phpinspect-handler-regexp 'word)) + (let ((annotation-name (match-string 0))) + (forward-char (length annotation-name)) + (cond ((string= annotation-name "var") + ;; The @var annotation accepts 2 parameters: + ;; the type and the $variable name + (append (list :var-annotation) + (phpinspect--parse-annotation-parameters 2))) + ((string= annotation-name "return") + ;; The @return annotation only accepts 1 word as parameter: + ;; The return type + (append (list :return-annotation) + (phpinspect--parse-annotation-parameters 1))) + ((string= annotation-name "param") + ;; The @param annotation accepts 2 parameters: + ;; The type of the param, and the param's $name + (append (list :param-annotation) + (phpinspect--parse-annotation-parameters 2))) + (t + (list :annotation annotation-name)))) + (list :annotation nil))) + +(phpinspect-defhandler tag (start-token max-point) + "Handler that discards any inline HTML it encounters" + (regexp "\\?>") + (forward-char (length start-token)) + (or (re-search-forward "<\\?php\\|<\\?" nil t) + (goto-char max-point)) + (list :html)) + +(phpinspect-defhandler comment (start-token max-point) + "Handler for comments and doc blocks" + (regexp "#\\|//\\|/\\*") + (forward-char (length start-token)) + + (cond ((string-match "/\\*" start-token) + (let* ((continue-condition (lambda () (not (looking-at "\\*/")))) + (parser (phpinspect-get-parser-create + :doc-block + '(annotation whitespace))) + (doc-block (funcall parser (current-buffer) max-point continue-condition))) + (forward-char 2) + doc-block)) + (t + (let ((parser (phpinspect-get-parser-create :comment '(tag) 'phpinspect-html-p)) + (end-position (line-end-position))) + (funcall parser (current-buffer) end-position))))) + +(phpinspect-defhandler variable (start-token &rest ignored) + "Handler for tokens indicating reference to a variable" + (regexp "\\$") + (forward-char (length start-token)) + (if (looking-at (phpinspect-handler-regexp 'word)) + (phpinspect-munch-token-without-attribs (match-string 0) :variable) + (list :variable nil))) + +(phpinspect-defhandler whitespace (whitespace &rest ignored) + "Handler that discards whitespace" + (regexp "[[:blank:]]+") + (forward-char (length whitespace))) + +(phpinspect-defhandler equals (equals &rest ignored) + "Handler for strict and unstrict equality comparison tokens." + (regexp "===?") + (phpinspect-munch-token-without-attribs equals :equals)) + +(phpinspect-defhandler assignment-operator (operator &rest ignored) + "Handler for tokens indicating that an assignment is taking place" + (regexp "[+-]?=") + (phpinspect-munch-token-without-attribs operator :assignment)) + +(phpinspect-defhandler terminator (terminator &rest ignored) + "Handler for statement terminators." + (regexp ";") + (phpinspect-munch-token-without-attribs terminator :terminator)) + +(phpinspect-defhandler use-keyword (start-token max-point) + "Handler for the use keyword and tokens that might follow to give it meaning" + (regexp (concat "use" (phpinspect--word-end-regex))) + (setq start-token (phpinspect--strip-last-char start-token)) + (forward-char (length start-token)) + + (let ((parser (phpinspect-get-parser-create + :use + '(word tag block-without-classes terminator) + 'phpinspect-end-of-use-p))) + (funcall parser (current-buffer) max-point))) + +(phpinspect-defhandler attribute-reference (start-token &rest ignored) + "Handler for references to object attributes, or static class attributes." + (regexp "->\\|::") + (forward-char (length start-token)) + (looking-at (phpinspect-handler-regexp 'word)) + (let ((name (if (looking-at (phpinspect-handler-regexp 'word)) + (funcall (phpinspect-handler 'word) (match-string 0)) + nil))) + (cond + ((string= start-token "::") + (list :static-attrib name)) + ((string= start-token "->") + (list :object-attrib name))))) + +(phpinspect-defhandler namespace (start-token max-point) + "Handler for the namespace keyword. This is a special one + because it is not always delimited by a block like classes or + functions. This handler parses the namespace declaration, and + then continues to parse subsequent tokens, only stopping when + either a block has been parsed or another namespace keyword has + been encountered." + (regexp (concat "namespace" (phpinspect--word-end-regex))) + (setq start-token (phpinspect--strip-last-char start-token)) + (forward-char (length start-token)) + (phpinspect-parse-with-handler-list + (current-buffer) + :namespace + max-point + (lambda () (not (looking-at (phpinspect-handler-regexp 'namespace)))) + 'phpinspect-block-p)) + +(phpinspect-defhandler const-keyword (start-token max-point) + "Handler for the const keyword." + (regexp (concat "const" (phpinspect--word-end-regex))) + (setq start-token (phpinspect--strip-last-char start-token)) + (forward-char (length start-token)) + (let* ((parser (phpinspect-get-parser-create + :const + '(word comment assignment-operator string array + terminator) + 'phpinspect-end-of-statement-p)) + (token (funcall parser (current-buffer) max-point))) + (when (phpinspect-incomplete-token-p (car (last token))) + (setcar token :incomplete-const)) + token)) + +(phpinspect-defhandler string (start-token &rest ignored) + "Handler for strings" + (regexp "\"\\|'") + (list :string (phpinspect--munch-string start-token))) + +(phpinspect-defhandler block-without-classes (start-token max-point) + "Handler for code blocks that cannot contain classes" + (regexp "{") + (forward-char (length start-token)) + (let* ((complete-block nil) + (parser (phpinspect-get-parser-create + :block + '(array tag equals list comma + attribute-reference variable + assignment-operator whitespace scope-keyword + static-keyword const-keyword use-keyword + function-keyword word terminator here-doc + string comment block))) + (continue-condition (lambda () + (not (and (char-equal (char-after) ?}) + (setq complete-block t))))) + (parsed (funcall parser (current-buffer) max-point continue-condition))) + (if complete-block + (forward-char) + (setcar parsed :incomplete-block)) + parsed)) + +(phpinspect-defhandler block (start-token max-point) + "Handler for code blocks" + (regexp "{") + (forward-char (length start-token)) + (let* ((complete-block nil) + (continue-condition (lambda () + ;; When we encounter a closing brace for this + ;; block, we can mark the block as complete. + (not (and (char-equal (char-after) ?}) + (setq complete-block t))))) + (parsed (phpinspect-parse-with-handler-list + (current-buffer) :block max-point continue-condition))) + (if complete-block + ;; After meeting the char-after requirement above, we need to move + ;; one char forward to prevent parent-blocks from exiting because + ;; of the same char. + (forward-char) + (setcar parsed :incomplete-block)) + parsed)) + +(phpinspect-defhandler here-doc (start-token &rest ignored) + "Handler for heredocs. Discards their contents." + (regexp "<<<") + (forward-char (length start-token)) + (if (looking-at "[A-Za-z0-9'\"\\_]+") + (re-search-forward (concat "^" (regexp-quote (match-string 0))) nil t)) + (list :here-doc)) + + +(defun phpinspect--munch-string (start-token) + "Consume text at point until a non-escaped `START-TOKEN` is found. + +Returns the consumed text string without face properties." + (forward-char (length start-token)) + (let ((start-point (point))) + (cond ((looking-at start-token) + (forward-char) + "") + ((looking-at (concat "\\([\\][\\]\\)+" (regexp-quote start-token))) + (let ((match (match-string 0))) + (forward-char (length match)) + (buffer-substring-no-properties start-point + (+ start-point (- (length match) + (length start-token)))))) + (t + (re-search-forward (format "\\([^\\]\\([\\][\\]\\)+\\|[^\\]\\)%s" + (regexp-quote start-token)) + nil t) + (buffer-substring-no-properties start-point (- (point) 1)))))) + +(phpinspect-defhandler list (start-token max-point) + "Handler for php syntactic lists (Note: this does not include +datatypes like arrays, merely lists that are of a syntactic +nature like argument lists" + (regexp "(") + (forward-char (length start-token)) + (let* ((complete-list nil) + (php-list (phpinspect-parse-with-handler-list + (current-buffer) + :list + max-point + (lambda () (not (and (char-equal (char-after) ?\)) (setq complete-list t))))))) + + (if complete-list + ;; Prevent parent-lists (if any) from exiting by skipping over the + ;; ")" character + (forward-char) + (setcar php-list :incomplete-list)) + php-list)) + +;; TODO: Look into using different names for function and class :declaration tokens. They +;; don't necessarily require the same handlers to parse. +(defsubst phpinspect-get-or-create-declaration-parser () + (phpinspect-get-parser-create :declaration + '(comment word list terminator tag) + 'phpinspect-end-of-statement-p)) + + +(phpinspect-defhandler function-keyword (start-token max-point) + "Handler for the function keyword and tokens that follow to give it meaning" + (regexp (concat "function" (phpinspect--word-end-regex))) + (setq start-token (phpinspect--strip-last-char start-token)) + (let* ((parser (phpinspect-get-or-create-declaration-parser)) + (continue-condition (lambda () (not (char-equal (char-after) ?{)))) + (declaration (funcall parser (current-buffer) max-point continue-condition))) + (if (phpinspect-end-of-statement-p (car (last declaration))) + (list :function declaration) + (list :function + declaration + (funcall (phpinspect-handler 'block) + (char-to-string (char-after)) max-point))))) + +(phpinspect-defhandler scope-keyword (start-token max-point) + "Handler for scope keywords" + (regexp (mapconcat (lambda (word) + (concat word (phpinspect--word-end-regex))) + (list "public" "private" "protected") + "\\|")) + (setq start-token (phpinspect--strip-last-char start-token)) + (forward-char (length start-token)) + (funcall (phpinspect-get-parser-create + (cond ((string= start-token "public") :public) + ((string= start-token "private") :private) + ((string= start-token "protected") :protected)) + '(function-keyword static-keyword const-keyword + variable here-doc string terminator tag comment) + 'phpinspect--scope-terminator-p) + (current-buffer) + max-point)) + +(phpinspect-defhandler static-keyword (start-token max-point) + "Handler for the static keyword" + (regexp (concat "static" (phpinspect--word-end-regex))) + (setq start-token (phpinspect--strip-last-char start-token)) + (forward-char (length start-token)) + (funcall (phpinspect-get-parser-create + :static + '(comment function-keyword variable array word + terminator tag) + 'phpinspect--static-terminator-p) + (current-buffer) + max-point)) + +(phpinspect-defhandler fat-arrow (arrow &rest ignored) + "Handler for the \"fat arrow\" in arrays and foreach expressions" + (regexp "=>") + (phpinspect-munch-token-without-attribs arrow :fat-arrow)) + +(phpinspect-defhandler array (start-token max-point) + "Handler for arrays, in the bracketet as well as the list notation" + (regexp "\\[\\|array(") + (forward-char (length start-token)) + (let* ((end-char (cond ((string= start-token "[") ?\]) + ((string= start-token "array(") ?\)))) + (end-char-reached nil) + (token (funcall (phpinspect-get-parser-create + :array + '(comment comma list here-doc string + array variable attribute-reference + word fat-arrow)) + (current-buffer) + max-point + (lambda () (not (and (char-equal (char-after) end-char) + (setq end-char-reached t))))))) + + ;; Skip over the end char to prevent enclosing arrays or lists + ;; from terminating. + (if end-char-reached + (forward-char) + ;; Signal incompleteness when terminated because of max-point + (setcar token :incomplete-array)) + token)) + +(phpinspect-defhandler class-keyword (start-token max-point) + "Handler for the class keyword, and tokens that follow to define +the properties of the class" + (regexp (concat "\\(abstract\\|final\\|class\\|interface\\|trait\\)" + (phpinspect--word-end-regex))) + (setq start-token (phpinspect--strip-last-char start-token)) + (list :class (funcall (phpinspect-get-or-create-declaration-parser) + (current-buffer) + max-point + (lambda () (not (char-equal (char-after) ?{)))) + (funcall (phpinspect-handler 'block-without-classes) + (char-to-string (char-after)) max-point))) + +(defun phpinspect-parse-with-handler-list + (buffer tree-type max-point &optional continue-condition delimiter-predicate) + "Parse BUFFER for TREE-TYPE tokens until MAX-POINT. + +Stop at CONTINUE-CONDITION or DELIMITER-PREDICATE. + +This just calls `phpinspect-get-parser-create` to make a parser +that contains all handlers necessary to parse code." + (let ((parser (phpinspect-get-parser-create + tree-type + '(array tag equals list comma + attribute-reference variable + assignment-operator whitespace scope-keyword + static-keyword const-keyword use-keyword + class-keyword function-keyword word terminator + here-doc string comment block) + delimiter-predicate))) + (funcall parser buffer max-point continue-condition))) + + +(defun phpinspect-parse-buffer-until-point (buffer point) + (with-current-buffer buffer + (save-excursion + (goto-char (point-min)) + (re-search-forward "<\\?php\\|<\\?" nil t) + (funcall (phpinspect-get-parser-create + :root + '(namespace array equals list comma + attribute-reference variable assignment-operator + whitespace scope-keyword static-keyword + const-keyword use-keyword class-keyword + function-keyword word terminator here-doc string + comment tag block)) + (current-buffer) + point)))) + +(cl-defstruct (phpinspect--function (:constructor phpinspect--make-function)) + "A PHP function." + (name nil + :type string + :documentation + "A string containing the name of the function") + (scope nil + :type phpinspect-scope + :documentation + "When the function is a method, this should contain the +scope of the function as returned by `phpinspect-parse-scope`.") + (arguments nil + :type list + :documentation + "A simple list with function arguments and their +types in tuples. Each list should have the name of the variable +as first element and the type as second element.") + (return-type nil + :type string + :documentation + "A string containing the FQN of the return value +of the function.")) + +(cl-defstruct (phpinspect--variable (:constructor phpinspect--make-variable)) + "A PHP Variable." + (name nil + :type string + :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`") + (type nil + :type string + :documentation + "A string containing the FQN of the variable's type")) + +(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)) + +(defun phpinspect--format-type-name (name) + (string-remove-prefix "\\" name)) + +(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--get-bare-class-name-from-fqn + (or (phpinspect--function-return-type completion-candidate) + ""))) + :kind 'function)) + + +(defun phpinspect-toggle-logging () + (interactive) + (if (setq phpinspect--debug (not phpinspect--debug)) + (message "Enabled phpinspect logging.") + (message "Disabled phpinspect logging."))) + +(defconst phpinspect-native-types + ;; self, parent and resource are not valid type name. + ;; see https://www.php.net/manual/ja/language.types.declarations.php + '("array" "bool" "callable" "float" "int" "iterable" "mixed" "object" "string" "void")) + +(defsubst phpinspect--log (&rest args) + (when phpinspect--debug + (with-current-buffer (get-buffer-create "**phpinspect-logs**") + (goto-char (buffer-end 1)) + (insert (concat (apply 'format args) "\n"))))) + +(defun phpinspect-parse-file (file) + (with-temp-buffer + (insert-file-contents-literally file) + (phpinspect-parse-current-buffer))) + + +(defun phpinspect-parse-current-buffer () + (phpinspect-parse-buffer-until-point + (current-buffer) + (point-max))) + + (defun phpinspect--split-list (predicate list) (seq-reduce (let ((current-sublist)) (lambda (result elt) @@ -888,6 +885,23 @@ ARG-LIST. ARG-LIST should be a list token as returned by list nil)) + +(defun phpinspect-get-variable-type-in-function-arg-list (variable-name 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) + (car (last arg)) + nil))))) + (defun phpinspect-eldoc-function () "An `eldoc-documentation-function` implementation for PHP files. @@ -998,7 +1012,7 @@ TODO: (seq-take-while 'phpinspect-not-assignment-p assignment)) (and (phpinspect-list-p (car assignment)) - ((member `(:variable ,variable-name) (car assignment))))) + (member `(:variable ,variable-name) (car assignment)))) (push assignment variable-assignments))) (nreverse variable-assignments))) @@ -1337,7 +1351,7 @@ said FQN's by class name" (let ((subtoken (seq-find (lambda (word) (and (phpinspect-word-p word) (not (string-match - (concat "^" (car (phpinspect--class-keyword-handler))) + (concat "^" (phpinspect-handler-regexp 'class-keyword)) (concat (cadr word) " "))))) (cadr class-token)))) (cadr subtoken))) @@ -1592,13 +1606,13 @@ more recent" (defun phpinspect--init-mode () "Initialize the phpinspect minor mode for the current buffer." - (make-variable-buffer-local 'company-backends) + (make-local-variable 'company-backends company-backends) (add-to-list 'company-backends 'phpinspect-company-backend) - (make-variable-buffer-local 'eldoc-documentation-function) + (make-local-variable 'eldoc-documentation-function) (setq eldoc-documentation-function 'phpinspect-eldoc-function) - (make-variable-buffer-local 'eldoc-message-commands) + (make-local-variable 'eldoc-message-commands) (eldoc-add-command 'c-electric-paren) (eldoc-add-command 'c-electric-backspace) @@ -1683,12 +1697,6 @@ project-wide index afterwards." (phpinspect--find-innermost-incomplete-token last-token) token))) -(defvar-local phpinspect--buffer-index nil - "The result of the last successfull parse + index action - executed by phpinspect for the current buffer") - -(defvar phpinspect-projects '() - "Currently active phpinspect projects and their buffers") (defun phpinspect--find-last-variable-position-in-token (token) "Find the last variable that can be encountered in the top @@ -1889,9 +1897,6 @@ level of a token. Nested variables are ignored." ((phpinspect-variable-p (cadr last-tokens)) (phpinspect--suggest-variables-at-point token-tree incomplete-token))))) -(defvar phpinspect--last-completion-list nil - "Used internally to save metadata about completion options - between company backend calls") (defun phpinspect-company-backend (command &optional arg &rest ignored) (interactive (list 'interactive)) @@ -1945,9 +1950,6 @@ level of a token. Nested variables are ignored." (gethash arg (phpinspect--completion-list-metadata phpinspect--last-completion-list)))))) -(defvar phpinspect-cache () - "In-memory nested key-value store used for caching by -phpinspect") (cl-defstruct (phpinspect--cache (:constructor phpinspect--make-cache)) (active-projects nil @@ -2096,14 +2098,6 @@ indexed classes in the project")) (setq phpinspect-cache (phpinspect--make-cache))) -(defvar phpinspect-index-executable - (concat (file-name-directory - (or load-file-name - buffer-file-name)) - "/phpinspect-index.bash") - "The path to the exexutable file that indexes class file names. -Should normally be set to \"phpinspect-index.bash\" in the source - file directory.") (defun phpinspect--locate-dominating-project-file (start-file) "Locate the first dominating file in `phpinspect-project-root-file-list`.