From dbf0ec03903e4ec795659c982e772d147ea1144c Mon Sep 17 00:00:00 2001 From: Hugo Thunnissen Date: Tue, 1 Nov 2022 22:13:41 +0100 Subject: [PATCH] Transition from index script to autoloader --- README.md | 2 +- phpinspect-autoload.el | 56 ++- phpinspect-buffer.el | 81 ++++ phpinspect-cache.el | 25 +- phpinspect-class.el | 7 +- phpinspect-imports.el | 155 ++++++++ phpinspect-index.bash | 853 ---------------------------------------- phpinspect-index.el | 131 ++++-- phpinspect-parser.el | 17 +- phpinspect-project.el | 58 ++- phpinspect-type.el | 2 +- phpinspect-worker.el | 73 ++-- phpinspect.el | 172 +++----- test/phpinspect-test.el | 6 +- test/test-buffer.el | 68 ++++ test/test-index.el | 103 +++++ test/test-project.el | 46 +++ 17 files changed, 782 insertions(+), 1073 deletions(-) create mode 100644 phpinspect-buffer.el create mode 100644 phpinspect-imports.el delete mode 100755 phpinspect-index.bash create mode 100644 test/test-buffer.el create mode 100644 test/test-index.el create mode 100644 test/test-project.el diff --git a/README.md b/README.md index cb50b05..b5013d5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ WIP. More documentation is in the making. (setq-local company-backends '(phpinspect-company-backend)) ;; Shortcut to add use statements for classes you use. - (define-key php-mode-map (kbd "C-c u") 'phpinspect-fix-uses-interactive) + (define-key php-mode-map (kbd "C-c u") 'phpinspect-fix-imports) ;; Shortcuts to quickly search/open files of PHP classes. (global-set-key (kbd "C-c a") 'phpinspect-find-class-file) diff --git a/phpinspect-autoload.el b/phpinspect-autoload.el index 51efa36..fb921f3 100644 --- a/phpinspect-autoload.el +++ b/phpinspect-autoload.el @@ -66,7 +66,13 @@ (types (make-hash-table :test 'eq :size 10000 :rehash-size 10000) :type hash-table :documentation - "The external types that can be autoloaded through this autoloader.")) + "The external types that can be autoloaded through this autoloader.") + (type-name-fqn-bags (make-hash-table :test 'eq :size 3000 :rehash-size 3000) + :type hash-table + :documentation + "Hash table that contains lists of fully +qualified names congruent with a bareword type name. Keyed by +bareword typenames.")) (defun phpinspect-make-autoload-definition-closure (project-root fs typehash) "Create a closure that can be used to `maphash' the autoload section of a composer-json." @@ -108,13 +114,22 @@ (let* ((project-root (phpinspect--project-root (phpinspect-autoloader-project autoloader))) (fs (phpinspect--project-fs (phpinspect-autoloader-project autoloader))) (vendor-dir (concat project-root "/vendor")) - (composer-json (phpinspect--read-json-file - fs - (concat project-root "/composer.json"))) - (project-autoload (gethash "autoload" composer-json)) + (composer-json-path (concat project-root "/composer.json")) + (composer-json) + (project-autoload ) + (type-name-fqn-bags (make-hash-table :test 'eq :size 3000 :rehash-size 3000)) (own-types (make-hash-table :test 'eq :size 10000 :rehash-size 10000)) (types (make-hash-table :test 'eq :size 10000 :rehash-size 10000))) + (when (phpinspect-fs-file-exists-p fs composer-json-path) + (setq composer-json (phpinspect--read-json-file fs composer-json-path)) + + (if (hash-table-p composer-json) + (setq project-autoload (gethash "autoload" composer-json)) + (phpinspect--log "Error: Parsing %s did not return a hashmap." + composer-json-path))) + + (when project-autoload (maphash (phpinspect-make-autoload-definition-closure project-root fs own-types) project-autoload) @@ -137,8 +152,18 @@ dependency-dir fs types) dependency-autoload)))))))) - (setf (phpinspect-autoloader-own-types autoloader) own-types) - (setf (phpinspect-autoloader-types autoloader) types))) + (maphash (lambda (type-fqn _) + (let* ((type-name (phpinspect-intern-name + (car (last (split-string (symbol-name type-fqn) "\\\\"))))) + (bag (gethash type-name type-name-fqn-bags))) + (push type-fqn bag) + (puthash type-name bag type-name-fqn-bags))) + types) + + (setf (phpinspect-autoloader-own-types autoloader) own-types) + (setf (phpinspect-autoloader-types autoloader) types) + (setf (phpinspect-autoloader-type-name-fqn-bags autoloader) + type-name-fqn-bags))) (cl-defmethod phpinspect-autoloader-resolve ((autoloader phpinspect-autoloader) typename-symbol) @@ -152,13 +177,16 @@ (defsubst phpinspect-filename-to-typename (dir filename &optional prefix) (phpinspect-intern-name - (concat "\\" - (or prefix "") - (replace-regexp-in-string - "/" "\\\\" - (string-remove-suffix - ".php" - (string-remove-prefix dir filename)))))) + (replace-regexp-in-string + "[\\\\]+" + "\\\\" + (concat "\\" + (or prefix "") + (replace-regexp-in-string + "/" "\\\\" + (string-remove-suffix + ".php" + (string-remove-prefix dir filename))))))) (cl-defmethod phpinspect-al-strategy-fill-typehash ((strategy phpinspect-psr0) fs diff --git a/phpinspect-buffer.el b/phpinspect-buffer.el new file mode 100644 index 0000000..21e9d4e --- /dev/null +++ b/phpinspect-buffer.el @@ -0,0 +1,81 @@ +;;; phpinspect-buffer.el --- PHP parsing and completion package -*- lexical-binding: t; -*- + +;; Copyright (C) 2021 Free Software Foundation, Inc + +;; Author: Hugo Thunnissen +;; Keywords: php, languages, tools, convenience +;; Version: 0 + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;;; Code: + +(defvar-local phpinspect-current-buffer nil + "An instance of `phpinspect-buffer' local to the active +buffer. This variable is only set for buffers where +`phpinspect-mode' is active. Also see `phpinspect-buffer'.") + +(defsubst phpinspect-make-region (start end) + (list start end)) + +(defalias 'phpinspect-region-start #'car) +(defalias 'phpinspect-region-end #'cadr) + +(cl-defstruct (phpinspect-buffer (:constructor phpinspect-make-buffer)) + "An object containing phpinspect related metadata linked to an +emacs buffer." + (buffer nil + :type buffer + :documentation "The underlying emacs buffer") + (location-map (make-hash-table :test 'eq :size 400 :rehash-size 400) + :type hash-table + :documentation + "A map that lets us look up the character +positions of a token within this buffer.") + (tree nil + :type list + :documentation + "An instance of a token tree as returned by +`phpinspect--index-tokens'. Meant to be eventually consistent +with the contents of the buffer.")) + +(cl-defmethod phpinspect-buffer-parse ((buffer phpinspect-buffer)) + "Parse the PHP code in the the emacs buffer that this object is +linked with." + (with-current-buffer (phpinspect-buffer-buffer buffer) + (setf (phpinspect-buffer-location-map buffer) + (make-hash-table :test 'eq + :size 400 + :rehash-size 400)) + + (let ((tree (phpinspect-parse-current-buffer))) + (setf (phpinspect-buffer-tree buffer) tree) + tree))) + +(cl-defmethod phpinspect-buffer-token-location ((buffer phpinspect-buffer) token) + (gethash token (phpinspect-buffer-location-map buffer))) + +(cl-defmethod phpinspect-buffer-tokens-enclosing-point ((buffer phpinspect-buffer) point) + (let ((tokens)) + (maphash + (lambda (token region) + (when (and (<= (phpinspect-region-start region) point) + (>= (phpinspect-region-end region) point)) + (push token tokens))) + (phpinspect-buffer-location-map buffer)) + tokens)) + +(provide 'phpinspect-buffer) diff --git a/phpinspect-cache.el b/phpinspect-cache.el index c235358..a401b50 100644 --- a/phpinspect-cache.el +++ b/phpinspect-cache.el @@ -24,14 +24,9 @@ ;;; Code: (require 'phpinspect-project) +(require 'phpinspect-autoload) (cl-defstruct (phpinspect--cache (:constructor phpinspect--make-cache)) - (active-projects nil - :type alist - :documentation - "An `alist` that contains the root directory - paths of all currently active phpinspect - projects") (projects (make-hash-table :test 'equal :size 10) :type hash-table :documentation @@ -54,12 +49,18 @@ then returned.") (cl-defmethod phpinspect--cache-get-project-create ((cache phpinspect--cache) (project-root string)) - (or (phpinspect--cache-getproject cache project-root) - (puthash project-root - (phpinspect--make-project-cache - :root project-root - :worker (phpinspect-make-dynamic-worker)) - (phpinspect--cache-projects cache)))) + (let ((project (phpinspect--cache-getproject cache project-root))) + (unless project + (setq project (puthash project-root + (phpinspect--make-project + :fs (phpinspect-make-fs) + :root project-root + :worker (phpinspect-make-dynamic-worker)) + (phpinspect--cache-projects cache))) + (let ((autoload (phpinspect-make-autoloader :project project))) + (setf (phpinspect--project-autoload project) autoload) + (phpinspect-autoloader-refresh autoload))) + project)) (provide 'phpinspect-cache) ;;; phpinspect.el ends here diff --git a/phpinspect-class.el b/phpinspect-class.el index c524ad1..199b1fb 100644 --- a/phpinspect-class.el +++ b/phpinspect-class.el @@ -61,12 +61,7 @@ :type bool :documentation "A boolean indicating whether or not this class - has been indexed yet.") - (index-queued nil - :type bool - :documentation - "A boolean indicating whether the class type has - been queued for indexation")) + has been indexed yet.")) (cl-defmethod phpinspect--class-trigger-update ((class phpinspect--class)) (dolist (sub (phpinspect--class-subscriptions class)) diff --git a/phpinspect-imports.el b/phpinspect-imports.el new file mode 100644 index 0000000..c6c7d09 --- /dev/null +++ b/phpinspect-imports.el @@ -0,0 +1,155 @@ +; phpinspect-imports.el --- PHP parsing and completion package -*- lexical-binding: t; -*- + +;; Copyright (C) 2021 Free Software Foundation, Inc + +;; Author: Hugo Thunnissen +;; Keywords: php, languages, tools, convenience +;; Version: 0 + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; See docstrings for documentation, starting with `phpinspect-mode'. + +;;; Code: + +(require 'phpinspect-parser) +(require 'phpinspect-index) +(require 'phpinspect-autoload) +(require 'phpinspect-buffer) + +(defun phpinspect-insert-at-point (point data) + (save-excursion + (goto-char point) + (insert data))) + +(defun phpinspect-add-use (fqn buffer &optional namespace-token) + "Add use statement for FQN to BUFFER. + +If NAMESPACE-TOKEN is non-nil, it is assumed to be a token that +was parsed from BUFFER and its location will be used to find a +buffer position to insert the use statement at." + (when (string-match "^\\\\" fqn) + (setq fqn (string-trim-left fqn "\\\\"))) + + (if namespace-token + (let* ((region (gethash + namespace-token (phpinspect-buffer-location-map buffer))) + (existing-use (seq-find #'phpinspect-use-p + (phpinspect-namespace-body namespace-token))) + (namespace-block (phpinspect-namespace-block namespace-token))) + (if existing-use + (phpinspect-insert-at-point + (phpinspect-region-start + (phpinspect-buffer-token-location buffer existing-use)) + (format "use %s;%c" fqn ?\n)) + (if namespace-block + (phpinspect-insert-at-point + (+ 1 (phpinspect-region-start + (phpinspect-buffer-token-location buffer namespace-block))) + (format "%c%cuse %s;%c" ?\n ?\n fqn ?\n)) + (phpinspect-insert-at-point + (phpinspect-region-end + (phpinspect-buffer-token-location + buffer (seq-find #'phpinspect-terminator-p namespace-token))) + (format "%c%cuse %s;%c" ?\n ?\n fqn ?\n))))) + ;; else + (let ((existing-use (seq-find #'phpinspect-use-p + (phpinspect-buffer-tree buffer)))) + (if existing-use + (phpinspect-insert-at-point + (phpinspect-region-start + (phpinspect-buffer-token-location buffer existing-use)) + (format "use %s;%c" fqn ?\n)) + (let ((first-token (cadr (phpinspect-buffer-tree buffer)))) + (if (and (phpinspect-word-p first-token) + (string= "declare" (cadr first-token))) + (phpinspect-insert-at-point + (phpinspect-region-end + (phpinspect-buffer-token-location + buffer (seq-find #'phpinspect-terminator-p (phpinspect-buffer-tree buffer)))) + (format "%c%cuse %s;%c" ?\n ?\n fqn ?\n)) + (phpinspect-insert-at-point + (phpinspect-region-start + (phpinspect-buffer-token-location buffer first-token)) + (format "%c%cuse %s;%c%c" ?\n ?\n fqn ?\n ?\n)))))))) + +(defun phpinspect-add-use-interactive (typename buffer project &optional namespace-token) + (let* ((autoloader (phpinspect--project-autoload project)) + (fqn-bags (phpinspect-autoloader-type-name-fqn-bags autoloader))) + + (let ((fqns (gethash typename fqn-bags))) + (cond ((= 1 (length fqns)) + (phpinspect-add-use (symbol-name (car fqns)) buffer namespace-token)) + ((> (length fqns) 1) + (phpinspect-add-use (symbol-name (completing-read "Class: " fqns)) + buffer namespace-token)) + (t (message "No import found for type %s" typename)))))) + +(defun phpinspect-namespace-part-of-typename (typename) + (string-trim-right typename "\\\\?[^\\\\]+")) + +(defalias 'phpinspect-fix-uses-interactive #'phpinspect-fix-imports + "Alias for backwards compatibility") + +(defun phpinspect-fix-imports () + "Find types that are used in the current buffer and make sure +that there are import (\"use\") statements for them." + (interactive) + (if phpinspect-current-buffer + (let* ((tree (phpinspect-buffer-parse phpinspect-current-buffer)) + (location-map (phpinspect-buffer-location-map phpinspect-current-buffer)) + (index (phpinspect--index-tokens + tree nil (lambda (token) (gethash token location-map)))) + (classes (alist-get 'classes index)) + (imports (alist-get 'imports index)) + (project (phpinspect--cache-get-project-create + (phpinspect--get-or-create-global-cache) + (phpinspect-project-root)))) + (dolist (class classes) + (let* ((class-imports (alist-get 'imports class)) + (used-types (alist-get 'used-types class)) + (region (alist-get 'location class))) + (dolist (type used-types) + (let ((namespace + (seq-find #'phpinspect-namespace-p + (phpinspect-buffer-tokens-enclosing-point + phpinspect-current-buffer (phpinspect-region-start region))))) + ;; Add use statements for types that aren't imported. + + (unless (or (or (alist-get type class-imports) + (alist-get type imports)) + (gethash (phpinspect-intern-name + (concat (phpinspect-namespace-part-of-typename + (phpinspect--type-name (alist-get 'class-name class))) + "\\" + (symbol-name type))) + (phpinspect-autoloader-types + (phpinspect--project-autoload project)))) + (phpinspect-add-use-interactive + type phpinspect-current-buffer project namespace) + ;; Buffer has been modified by adding type, update tree + + ;; location map. This is not optimal but will have to do until + ;; partial parsing is implemented. + ;; + ;; Note: this basically implements a bug where the locations + ;; of classes are no longer congruent with their location in + ;; the buffer's code. In files that contain multiple namespace + ;; blocks this could cause problems as a namespace may grow by + ;; added import statements and start envelopping the classes + ;; below it. + (phpinspect-buffer-parse phpinspect-current-buffer))))))))) + +(provide 'phpinspect-imports) diff --git a/phpinspect-index.bash b/phpinspect-index.bash deleted file mode 100755 index 357444f..0000000 --- a/phpinspect-index.bash +++ /dev/null @@ -1,853 +0,0 @@ -#!/bin/bash -## -# phpinspect-index.bash - Resolve namespaces and fix missing use statements in your PHP -# scripts. -### -# This script is derived from phpns, an earlier project that had a much wider scope than -# just index files for phpinspect.el. Much of the code and command line argument options -# can be removed. -# TODO: remove whatever functionality is not required for phpinspect.el - -# shellcheck disable=SC2155 -declare CACHE_DIR=./.cache/phpinspect -declare INFO=1 - -# Cache locations -declare CLASSES="$CACHE_DIR/classes" -declare NAMESPACES="$CACHE_DIR/namespaces" -declare USES="$CACHE_DIR/uses" -declare USES_OWN="$CACHE_DIR/uses_own" -declare USES_LOOKUP="$CACHE_DIR/uses_lookup" -declare USES_LOOKUP_OWN="$CACHE_DIR/uses_lookup_own" -declare FILE_PATHS="$CACHE_DIR/file_paths" -declare NAMESPACE_FILE_PATHS="$CACHE_DIR/namespace_file_paths" -declare INDEXED="$CACHE_DIR/indexed" - -[[ $DEBUG -eq 2 ]] && set -x -shopt -s extglob -shopt -so pipefail - -read -rd '' USAGE <<'EOF' - phpns - Resolve namespaces and fix missing use statements in your PHP scripts. - - USAGE: - phpns COMMAND [ ARGUMENTS ] [ OPTIONS ] - - COMMANDS: - i, index Index the PHP project in the current directory - fu, find-use CLASS_NAME Echo the FQN of a class - fxu, fix-uses FILE Add needed use statements to FILE - cns, classes-in-namespace NAMESPACE Echo the classes that reside in NAMESPACE - fp, filepath FQN Echo the filepath of the class by the name of FQN. - - TO BE IMPLEMENTED: - rmuu, remove-unneeded-uses FILE: Remove all use statements for classes that are not being used. - - OPTIONS FOR ALL COMMANDS: - -s --silent Don't print info. - - UNIQUE OPTIONS PER COMMAND: - index: - -d, --diff Show differences between the files in the index and the files in the project directory. - -N, --new Only index new files - find-use: - -j, --json Provide possible use FQN's as a json array. - -p, --prefer-own If there are matches inside the "src" dir, only use those. - -a, --auto-pick Use first encountered match, don't provide a choice. - -b. --bare Print FQN's without any additives. - fix-uses: - -j, --json Provide possible use FQN's per class as a json object with the class names as keys. - -p, --prefer-own If there are matches inside the "src" dir, only use those. - -a, --auto-pick Use first encountered match, for every class, don't provide a choice. - -o, --stdout Print to stdout in stead of printing to the selected file. - filepath: - - -EOF - -execute() { - declare command="$1" INFO="$INFO" - declare -a CONFIG=() - shift - - if [[ $command == @(-h|--help|help) ]]; then - echo "$USAGE" >&2 - exit 0 - fi - - if ! [[ -f ./composer.json ]] && ! [[ -d ./.git ]]; then - echo "No composer.json or .git file found, not in root of poject, exiting." >&2 - exit 1 - fi - - case "$command" in - i | index) - handleArguments index "$@" || return $? - - # The arguments to grep need to be dynamic here, because the diff option - # requires different arguments to be passed to grep. - declare -a grep_args=( - -H - '^\(class\|abstract[[:blank:]]\+class\|\(final[[:blank:]]\+\|/\*[[:blank:]]*final[[:blank:]]*\*/[[:blank:]]*\)class\|namespace\|interface\|trait\)[[:blank:]]\+[A-Za-z]\+' - --exclude-dir={.cache,var,bin} - --binary-files=without-match - ) - - # Only index new files - if [[ ${CONFIG[$INDEX_NEW]} == '--new' ]]; then - declare -a new_files=() deleted_files=() - - # Extract new files from diff. - while IFS=':' read -ra diff_file; do - if [[ ${diff_file[0]} == '-' ]]; then - deleted_files=("${diff_file[1]}" "${deleted_files[@]}") - elif [[ ${diff_file[0]} == '+' ]]; then - new_files=("${diff_file[1]}" "${new_files[@]}") - fi - done < <(diffIndex) - - # Inform the user if non-existent files were found. Right now the only - # way to fix this is to reindex entirely. - if [[ ${#deleted_files[@]} -gt 0 ]]; then - info "There are ${#deleted_files[@]} non-existent files in your index. Consider reindexing to prevent incorrect results." - info 'Some of these none existent files are:' - for i in {0..19}; do - [[ $i -ge ${#deleted_files[@]} ]] && break - infof ' - "%s"\n' "${deleted_files[$i]}" - done - fi - - if [[ ${#new_files[@]} -eq 0 ]]; then - info 'No new files were found.' - return 0 - else - info "${#new_files[@]} new files found to index." - fi - - # To exclusively index new files, add the filenames to the arguments array - grep_args=("${grep_args[@]}" "${new_files[@]}") - elif [[ ${CONFIG[$INDEX_DIFF]} == '--diff' ]]; then - diffIndex - return $? - else - grep_args=("${grep_args[@]}" '-r' '--include=*.php') - fi - - # Index matching files - grep -m 2 "${grep_args[@]}" | grep -v '^vendor/bin' | fillIndex - - # Add non-matching files to the file with indexed files. - # This is necessary to be able to diff the index. - grep -L "${grep_args[@]}" | grep -v '^vendor/bin' >> "$INDEXED" - ;; - fu | find-use) - checkCache - handleArguments find-use "$@" || return $? - declare use_path='' class_name="${CONFIG[$CLASS_NAME]}" - if [[ "$class_name" == @(array|string|float|int|void|mixed) ]]; then - infof 'Type "%s" is not a class, but a primitive type.\n' "$class_name" - return 1 - fi - - findUsePathForClass "$class_name" - ;; - fxu | fix-uses) - checkCache - handleArguments fix-uses "$@" || return $? - - declare file="${CONFIG[$FILE]}" - - if ! [[ -f $file ]]; then - infof 'File "%s" does not exist or is not a regular file.\n' "$file" - - return 1 - elif [[ ${CONFIG[$STDOUT]} == '--stdout' ]]; then - fixMissingUseStatements "$file" - else - # shellcheck disable=SC2005 - echo "$(fixMissingUseStatements "$file")" > "$file" - fi - ;; - ns | namespace) - checkCache - declare file="$1" - - # Try the index, if that doesn't work, attempt to extract the namespace from the file itself. - if ! grep "(?<=$file:).*" "$NAMESPACE_FILE_PATHS"; then - grep -Po '(?<=^namespace[[:blank:]])[A-Za-z_\\]+' "$file" - fi - ;; - cns | classes-in-namespace) - handleArguments classes-in-namespace "$@" || return $? - checkCache - - declare namespace="${CONFIG[$NAMESPACE]}\\" - debug "Checking for namespace $namespace" - - awk -F ':' "/:${namespace//\\/\\\\}"'[^\\]+$/{ print $1; }' "$USES_LOOKUP" - ;; - fp | filepath) - handleArguments filepath "$@" || return $? - checkCache - - grep -Po "^.*(?=:${CONFIG[$CLASS_PATH]//\\/\\\\}$)" "$FILE_PATHS" - ;; - *) - printf 'Command "%s" is not a valid subcommand.\n' "$command" >&2 - exit 1 - ;; - esac -} - -# shellcheck disable=SC2034 -fixMissingUseStatements() { - declare check_uses='false' check_needs='false' file="$1" namespace="$2" - declare -A uses=() needs=() namespace=() - declare -a classes=() - - classes=($(execute cns "$(execute ns "$file")")) - for class in "${classes[@]}"; do - namespace["$class"]='in_namespace' - done - - findUsesAndNeeds < "$file" - addUseStatements "${!needs[@]}" < "$file" -} - -findUsePathForClass() { - declare class="$1" - if [[ ${CONFIG[$PREFER_OWN]} == '--prefer-own' ]]; then - declare -a possibilities=($(grep -Po "(?<=^${CONFIG[$CLASS_NAME]}:).*" "$USES_LOOKUP_OWN")) - else - declare -a possibilities=($(grep -Po "(?<=^${CONFIG[$CLASS_NAME]}:).*" "$USES_LOOKUP")) - fi - - if [[ ${#possibilities[@]} -eq 1 ]]; then - use_path="${possibilities[0]}" - debugf 'Single use path "%s" found' "${possibilities[0]}" - - # Provide an escaped string for json output if requested. - [[ ${CONFIG[$JSON]} == '--json' ]] && printf -v use_path '"%s"' "${use_path//\\/\\\\}" - elif [[ ${#possibilities[@]} -eq 0 ]]; then - _handle_no_use - return $? - else - _handle_multiple_uses - fi - - infof 'Found use statement for "%s"\n' "$use_path" >&2 - if [[ ${CONFIG[$JSON]} == '--json' ]]; then - echo '[' - echo "$use_path" - printf ']' - elif [[ ${CONFIG[$BARE]} ]]; then - echo "$use_path" - else - echo "use $use_path;" - fi -} - -_handle_no_use() { - declare tried_index_new="$1" - - if [[ $tried_index_new != true ]]; then - execute index --silent --new - execute fu "${CONFIG[@]}" - return $? - elif [[ ${CONFIG[$PREFER_OWN]} == '--prefer-own' ]]; then - CONFIG[$PREFER_OWN]= - execute fu "${CONFIG[@]}" - return $? - else - infof 'No match found for class "%s"\n' "$class_name" >&2 - [[ ${CONFIG[$JSON]} == '--json' ]] && printf '[]' - fi - return 1 -} - -_handle_multiple_uses() { - if [[ ${CONFIG[$AUTO_PICK]} == '--auto-pick' ]]; then - use_path="${possibilities[0]}" - - return 0 - elif [[ ${CONFIG[$BARE]} == '--bare' ]]; then - use_path="$(printf '%s\n' "${possibilities[@]}")" - - return 0 - elif [[ ${CONFIG[$JSON]} == '--json' ]]; then - use_path="$( - for i in "${!possibilities[@]}"; do - printf '"%s"' "${possibilities[$i]//\\/\\\\}" - [[ $i -lt $((${#possibilities[@]}-1)) ]] && printf ',' - echo - done - )" - - return 0 - fi - - infof 'Multiple matches for class "%s", please pick one.\n' "$class_name" >&2 - select match in "${possibilities[@]}"; do - use_path="$match" - break - done < /dev/tty -} - -addUseStatements() { - declare -a needs=("$@") - declare use_statements='' - if [[ ${CONFIG[$JSON]} == '--json' ]]; then - declare -i length="$((${#needs[@]}-1))" current=0 - echo '{' - for needed in "${needs[@]}"; do - printf '"%s": ' "$needed" - execute fu --json "$needed" "${CONFIG[$PREFER_OWN]}" "${CONFIG[$AUTO_PICK]}" - [[ $((current++)) -lt $length ]] && printf ',' - echo - done - echo '}' - - return 0 - fi - - while IFS='' read -r line; do - echo "$line" - - if [[ $line == namespace* ]]; then - IFS='' read -r line && echo "$line" - - use_statements="$( - for needed in "${needs[@]}"; do - execute fu "$needed" "${CONFIG[$PREFER_OWN]}" "${CONFIG[$AUTO_PICK]}" - done | sort - )" - - [[ -n $use_statements ]] && echo "$use_statements" - fi - done - - declare -i added_uses=0 - added_uses="$(echo -n "$use_statements" | wc -l)" - [[ -n $use_statements ]] && ((added_uses++)) - info "$added_uses use statements added out of ${#needs[@]} needed types. Types that were needed:" >&2 - infof ' - "%s"\n' "${needs[@]}" >&2 -} - -debug() { - if [[ $DEBUG -ge 1 ]]; then - echo "[DEBUG] => $1" >&2 - fi -} - -# shellcheck disable=SC2059 -debugf() { - if [[ $DEBUG -ge 1 ]]; then - declare format_string="$1" - shift - printf "[DEBUG] => $format_string" "$@" >&2 - fi -} - -info() { - if [[ $INFO -eq 1 ]]; then - echo "[INFO] => $1" >&2 - fi -} - -# shellcheck disable=SC2059 -infof() { - if [[ $INFO -eq 1 ]]; then - declare format_string="$1" - shift - printf "[INFO] => $format_string" "$@" >&2 - fi -} - -## -# Functions for parameter parsing - -# Enum for config -declare -gri CLASS_NAME=0 -declare -gri PREFER_OWN=1 -declare -gri AUTO_PICK=2 -declare -gri STDOUT=3 -declare -gri JSON=4 -declare -gri BARE=5 -declare -gri WORD=6 -declare -gri EXPAND_CLASSES=7 -declare -gri NO_CLASSES=8 -declare -gri NAMESPACE=9 -declare -gri CLASS_PATH=10 -declare -gri INDEX_DIFF=11 -declare -gri NO_VENDOR=12 # Keep this around as it might be used later on -declare -gri INDEX_NEW=13 -declare -gri FILE=14 - -handleArguments() { - declare -p CONFIG &>>/dev/null || return 1 - declare command="$1" - shift - case "$command" in - find-use) - _handle_find_use_arguments "$@" || return $? - ;; - fix-uses) - _handle_fix_uses_arguments "$@" || return $? - ;; - index) - _handle_index_arguments "$@" || return $? - ;; - classes-in-namespace) - _handle_classes_in_namespace_arguments "$@" || return $? - ;; - filepath) - _handle_filepath_arguments "$@" || return $? - ;; - *) - printf 'handleArguments (line %s): Unknown command "%s" passed.\n' "$(caller)" "$command">&2 - return 1 - ;; - esac -} - -_handle_filepath_arguments() { - declare arg="$1" - while shift; do - case "$arg" in - -s | --silent) - INFO=0 - ;; - --*) - printf 'Unknown option: "%s"\n' "${arg}" >&2 - return 1 - ;; - -*) - if [[ ${#arg} -gt 2 ]]; then - - declare -i i=1 - while [[ $i -lt ${#arg} ]]; do - _handle_filepath_arguments "-${arg:$i:1}" - ((i++)) - done - else - printf 'Unknown option: "%s"\n' "${arg}" >&2 - return 1 - fi - ;; - '') - : - ;; - *) - if [[ -n ${CONFIG[$CLASS_PATH]} ]]; then - printf 'Unexpected argument: "%s"\n' "$arg" >&2 - return 1 - fi - CONFIG[$CLASS_PATH]="$arg" - esac - arg="$1" - done -} - -_handle_classes_in_namespace_arguments() { - declare arg="$1" - while shift; do - case "$arg" in - -s | --silent) - INFO=0 - ;; - --*) - printf 'Unknown option: "%s"\n' "${arg}" >&2 - return 1 - ;; - -*) - if [[ ${#arg} -gt 2 ]]; then - declare -i i=1 - while [[ $i -lt ${#arg} ]]; do - _handle_classes_in_namespace_arguments "-${arg:$i:1}" - ((i++)) - done - else - printf 'Unknown option: "%s"\n' "${arg}" >&2 - return 1 - fi - ;; - '') - : - ;; - *) - if [[ -n ${CONFIG[$NAMESPACE]} ]]; then - printf 'Unexpected argument: "%s"\n' "$arg" >&2 - return 1 - fi - CONFIG[$NAMESPACE]="$arg" - esac - arg="$1" - done -} - -_handle_index_arguments() { - declare arg="$1" - while shift; do - case "$arg" in - -s | --silent) - INFO=0 - ;; - -d | --diff) - CONFIG[$INDEX_DIFF]='--diff' - ;; - -N | --new) - CONFIG[$INDEX_NEW]='--new' - ;; - --*) - printf 'Unknown option: "%s"\n' "${arg}" >&2 - return 1 - ;; - -*) - if [[ ${#arg} -gt 2 ]]; then - declare -i i=1 - while [[ $i -lt ${#arg} ]]; do - _handle_index_arguments "-${arg:$i:1}" - ((i++)) - done - else - printf 'Unknown option: "%s"\n' "${arg}" >&2 - return 1 - fi - ;; - *) - printf 'Unexpected argument: "%s"\n' "$arg" >&2 - return 1 - ;; - esac - arg="$1" - done -} - -_handle_fix_uses_arguments() { - declare arg="$1" - while shift; do - case "$arg" in - -s | --silent) - INFO=0 - ;; - -p | --prefer-own) - CONFIG[$PREFER_OWN]='--prefer-own' - ;; - -a | --auto-pick) - CONFIG[$AUTO_PICK]='--auto-pick' - ;; - -o | --stdout) - CONFIG[$STDOUT]='--stdout' - INFO=0 - ;; - -j | --json) - CONFIG[$STDOUT]='--stdout' - CONFIG[$JSON]='--json' - ;; - --*) - printf 'Unknown option: "%s"\n' "${arg}" >&2 - return 1 - ;; - -*) - if [[ ${#arg} -gt 2 ]]; then - declare -i i=1 - while [[ $i -lt ${#arg} ]]; do - _handle_fix_uses_arguments "-${arg:$i:1}" - ((i++)) - done - else - printf 'Unknown option: "%s"\n' "${arg}" >&2 - return 1 - fi - ;; - '') - : - ;; - *) - if [[ -n ${CONFIG[$FILE]} ]]; then - printf 'Unexpected argument: "%s"\n' "$arg" >&2 - return 1 - fi - CONFIG[$FILE]="$arg" - esac - arg="$1" - done -} - -# shellcheck disable=SC2034 -_handle_find_use_arguments() { - declare arg="$1" - while shift; do - case "$arg" in - -s | --silent) - INFO=0 - ;; - -b | --bare) - CONFIG[$BARE]='--bare' - ;; - -p | --prefer-own) - CONFIG[$PREFER_OWN]='--prefer-own' - ;; - -a | --auto-pick) - CONFIG[$AUTO_PICK]='--auto-pick' - ;; - -j | --json) CONFIG[$STDOUT]='--stdout' - CONFIG[$JSON]='--json' - INFO=0 - ;; - --*) - printf 'Unknown option: "%s"\n' "${arg}" >&2 - return 1 - ;; - -*) - if [[ ${#arg} -gt 2 ]]; then - declare -i i=1 - while [[ $i -lt ${#arg} ]]; do - _handle_find_use_arguments "-${arg:$i:1}" - ((i++)) - done - else - printf 'Unknown option: "%s"\n' "${arg}" >&2 - return 1 - fi - ;; - '') - : - ;; - *) - if [[ -n ${CONFIG[$CLASS_NAME]} ]]; then - printf 'Unexpected argument: "%s"\n' "$arg" >&2 - return 1 - fi - CONFIG[$CLASS_NAME]="$arg" - esac - arg="$1" - done -} - -## -# This function outputs the difference between the files that are present in the -# index and the files that are present in the project directory. The output format is: -# +:NEW_FILE (**Not in index but exists on disk**) -# -:DELETED_FILE (**In index but does not exist on disk**) -## -diffIndex() { - diff --unchanged-line-format='' --new-line-format='+:%L' --old-line-format='-:%L' \ - <(sort -u < "$INDEXED" | sed '/^[[:blank:]]*$/d') \ - <(find ./ -name '*.php' -type f | sed 's!^\./\|^./\(var\|.cache\|vendor/bin\)/.\+$!!g; /^[[:blank:]]*$/d' | sort) -} - -## -# This function reads the output of a grep command with the option -H or -# --with-filename enabled. The lines containing class and namespace declarations -# will be parsed and added to the index. -# -# shellcheck disable=SC2153 -## -fillIndex() { - [[ -n $CACHE_DIR ]] || return 1 - [[ -n $CLASSES ]] || return 1 - [[ -n $NAMESPACES ]] || return 1 - [[ -n $USES ]] || return 1 - [[ -n $USES_LOOKUP ]] || return 1 - [[ -n $USES_LOOKUP_OWN ]] || return 1 - [[ -n $FILE_PATHS ]] || return 1 - [[ -n $NAMESPACE_FILE_PATHS ]] || return 1 - [[ -n $INDEXED ]] || return 1 - - [[ -d $CACHE_DIR ]] || mkdir -p "$CACHE_DIR" - - # Clean up index files if not diffing. - if [[ ${CONFIG[$INDEX_NEW]} != '--new' ]]; then - echo > "$NAMESPACES" - echo > "$CLASSES" - echo > "$USES" - echo > "$USES_LOOKUP" - echo > "$FILE_PATHS" - echo > "$USES_OWN" - echo > "$USES_LOOKUP_OWN" - echo > "$NAMESPACE_FILE_PATHS" - echo > "$INDEXED" - fi - - declare -A namespaces=() classes=() - while IFS=':' read -ra line; do - declare file="${line[0]}" - - # Save the namespace or class to add to the FQN cache later on. - if [[ "${line[1]}" =~ (class|trait|interface)[[:blank:]]+([A-Za-z_]+) ]]; then - classes[$file]="${BASH_REMATCH[2]}" - elif [[ "${line[1]}" =~ namespace[[:blank:]]+([A-Za-z_\\]+) ]]; then - namespaces[$file]="${BASH_REMATCH[1]}" - else - debugf 'No class or namespace found in line "%s"' "${line[0]}" - fi - - # Add filename to file with indexed filenames. This is required - # for diffing the index. - echo "$file" >> "$INDEXED" - - if [[ $((++lines%500)) -eq 0 ]]; then - info "indexed $lines lines." - fi - done - - # Fill up the index - declare -i uses=0 - for file in "${!classes[@]}"; do - declare namespace="${namespaces[$file]}" - declare class="${classes[$file]}" - - if [[ -z $class ]]; then - debugf 'Class is missing for file "%s"\n' "$file" - debugf 'Namespace: "%s"\n' "$namespace" - continue - fi - - ((uses++)) - [[ $((uses%500)) -eq 0 ]] && info "Found FQN's for $uses classes." - - echo "$namespace" >> "$NAMESPACES" - echo "$class" >> "$CLASSES" - echo "$namespace\\$class" >> "$USES" - echo "$class:$namespace\\$class" >> "$USES_LOOKUP" - echo "$file:$namespace\\$class" >> "$FILE_PATHS" - echo "$file:$namespace" >> "$NAMESPACE_FILE_PATHS" - - if [[ $file != 'vendor/'* ]]; then - echo "$namespace\\$class" >> "$USES_OWN" - echo "$class:$namespace\\$class" >> "$USES_LOOKUP_OWN" - fi - - done - - # This keeps the index of class names unique, so that completing class names takes as little - # time as possible. - # Use echo and a subshell here to prevent changing the file before the command is done. - # shellcheck disable=SC2005 - echo "$(sort -u < "$CLASSES")" > "$CLASSES" - - # Ditto for the namespaces index - # shellcheck disable=SC2005 - echo "$(sort -u < "$NAMESPACES")" > "$NAMESPACES" - - info "Finished indexing. Indexed ${lines} lines and found FQN's for $uses classes." >&2 -} - -checkCache() { - if ! [[ -d "$CACHE_DIR" ]]; then - info "No cache dir found, indexing." >&2 - execute index - fi -} - -## -# Find use statements and needed classes in a file. - -findUsesAndNeeds() { - declare -p needs &>>/dev/null || return 1 - declare -p uses &>>/dev/null || return 1 - # shellcheck disable=SC2154 - declare -p namespace &>>/dev/null || return 1 - - while read -r line; do - [[ $line == namespace* ]] && check_uses='true' - if [[ $line == ?(@(abstract|final) )@(class|interface|trait)* ]]; then - check_uses='false' - check_needs='true' - - read -ra line_array <<<"$line" - set -- "${line_array[@]}" - while shift && [[ "$1" != @(extends|implements) ]]; do :; done; - while shift && [[ -n $1 ]]; do - [[ $1 == 'implements' ]] && shift - [[ $1 == \\* ]] || _set_needed_if_not_used "$1" - done - fi - - if $check_uses; then - if [[ $line == use* ]]; then - declare class_name="${line##*\\}" - [[ $class_name == *as* ]] && class_name="${class_name##*as }" - debug "Class name: $class_name" - class_name="${class_name%%[^a-zA-Z]*}" - uses["$class_name"]='used' - fi - fi - - if $check_needs; then - if [[ $line == *function*([[:space:]])*([[:alnum:]_])\(* ]]; then - _check_function_needs "$line" - continue - fi - _check_needs "$line" - fi - done -} - -_check_function_needs() { - # Strip everything up until function name and argument declaration. - declare line="${1#*function}" function_declaration="${1#*function}" - - # Collect the entire argument declaration - while [[ $line != *'{'* ]] && read -r line; do - function_declaration="$function_declaration $line" - done - - declare -a words=() - read -ra words <<<"$function_declaration" - for i in "${!words[@]}"; do - if [[ "${words[$i]}" =~ ^'$'[a-zA-Z_]+ ]]; then - declare prev_word="${words[$((i-1))]}" - if [[ $prev_word =~ ^([^\(]*\()?([A-Za-z]+)$ ]]; then - declare class_name="${BASH_REMATCH[2]}" - debugf 'Found parameter type "%s" for function "%s"\n' "$class_name" "$function_declaration" - _set_needed_if_not_used "$class_name" - fi - fi - done - if [[ "$function_declaration" =~ \):[[:space:]]+([a-zA-Z]+) ]]; then - declare class_name="${BASH_REMATCH[1]}" - debugf 'Found return type "%s" for function "%s"\n' "$class_name" "$function_declaration" - _set_needed_if_not_used "$class_name" - fi -} - -_check_needs() { - declare line="$1" match='' - if _line_matches "$line"; then - declare class_name="${match//[^a-zA-Z]/}" - - debugf 'Extracted type "%s" from line "%s". Entire match: "%s"\n' "$class_name" "$line" "${BASH_REMATCH[0]}" - _set_needed_if_not_used "$class_name" - - line="${line/"${BASH_REMATCH[0]/}"}" - _check_needs "$line" - fi -} - -# shellcheck disable=SC2049 -_line_matches() { - if [[ $line =~ 'new'[[:space:]]+([^\\][A-Za-z]+)\( ]] \ - || [[ $line =~ 'instanceof'[[:space:]]+([A-Za-z]+) ]] \ - || [[ $line =~ catch[[:space:]]*\(([A-Za-z]+) ]] \ - || [[ $line =~ \*[[:blank:]]*@([A-Z][a-zA-Z]*) ]]; then - match="${BASH_REMATCH[1]}" - return $? - elif [[ $line =~ @(var|param|return|throws)[[:space:]]+([A-Za-z]+) ]] \ - || [[ $line =~ (^|[\(\[\{[:blank:]])([A-Za-z]+)'::' ]]; then - match="${BASH_REMATCH[2]}" - return $? - fi - return 1 -} - -_set_needed_if_not_used() { - declare class_name="$1" - if [[ -z ${uses["$class_name"]} ]] \ - && [[ -z ${namespace["$class_name"]} ]] \ - && [[ "$class_name" != @(static|self|string|int|float|array|object|bool|mixed|parent|void) ]]; then - needs["$class_name"]='needed' - fi -} - -execute "$@" diff --git a/phpinspect-index.el b/phpinspect-index.el index dd164dc..c952c4e 100644 --- a/phpinspect-index.el +++ b/phpinspect-index.el @@ -42,7 +42,7 @@ (defun phpinspect-return-annotation-p (token) (phpinspect-token-type-p token :return-annotation)) -(defun phpinspect--index-function-arg-list (type-resolver arg-list) +(defun phpinspect--index-function-arg-list (type-resolver arg-list &optional add-used-types) (let ((arg-index) (current-token) (arg-list (cl-copy-list arg-list))) @@ -51,7 +51,8 @@ (phpinspect-variable-p (car arg-list))) (push `(,(cadr (pop arg-list)) ,(funcall type-resolver (phpinspect--make-type :name (cadr current-token)))) - arg-index)) + arg-index) + (when add-used-types (funcall add-used-types (list (cadr current-token))))) ((phpinspect-variable-p (car arg-list)) (push `(,(cadr (pop arg-list)) nil) @@ -63,7 +64,12 @@ (or (not type) (phpinspect--type= type phpinspect--object-type))) -(defun phpinspect--index-function-from-scope (type-resolver scope comment-before) +(defun phpinspect--index-function-from-scope (type-resolver scope comment-before &optional add-used-types) + "Index a function inside SCOPE token using phpdoc metadata in COMMENT-BEFORE. + +If ADD-USED-TYPES is set, it must be a function and will be +called with a list of the types that are used within the +function (think \"new\" statements, return types etc.)." (let* ((php-func (cadr scope)) (declaration (cadr php-func)) (type (if (phpinspect-word-p (car (last declaration))) @@ -95,6 +101,12 @@ (phpinspect--make-type :name return-annotation-type))) (setf (phpinspect--type-collection type) t))))) + (when add-used-types + (let ((used-types (phpinspect--find-used-types-in-tokens + `(,(seq-find #'phpinspect-block-p php-func))))) + (when type (push (phpinspect--type-bare-name type) used-types)) + (funcall add-used-types used-types))) + (phpinspect--make-function :scope `(,(car scope)) :name (cadadr (cdr declaration)) @@ -102,7 +114,8 @@ phpinspect--null-type) :arguments (phpinspect--index-function-arg-list type-resolver - (phpinspect-function-argument-list php-func))))) + (phpinspect-function-argument-list php-func) + add-used-types)))) (defun phpinspect--index-const-from-scope (scope) (phpinspect--make-variable @@ -141,7 +154,7 @@ (cadr class-token)))) (cadr subtoken))) -(defun phpinspect--index-class (imports type-resolver class) +(defun phpinspect--index-class (imports type-resolver location-resolver class) "Create an alist with relevant attributes of a parsed class." (phpinspect--log "INDEXING CLASS") (let ((methods) @@ -154,7 +167,15 @@ (class-name (phpinspect--get-class-name-from-token class)) ;; Keep track of encountered comments to be able to use type ;; annotations. - (comment-before)) + (comment-before) + ;; The types that are used within the code of this class' methods. + (used-types) + (add-used-types)) + (setq add-used-types + (lambda (additional-used-types) + (if used-types + (nconc used-types additional-used-types) + (setq used-types additional-used-types)))) ;; Find out what the class extends or implements (let ((enc-extends nil) @@ -195,7 +216,8 @@ (push (phpinspect--index-function-from-scope type-resolver (list (car token) (cadadr token)) - comment-before) + comment-before + add-used-types) static-methods)) ((phpinspect-variable-p (cadadr token)) @@ -208,14 +230,16 @@ (phpinspect--log "comment-before is: %s" comment-before) (push (phpinspect--index-function-from-scope type-resolver token - comment-before) + comment-before + add-used-types) methods)))) ((phpinspect-static-p token) (cond ((phpinspect-function-p (cadr token)) (push (phpinspect--index-function-from-scope type-resolver `(:public ,(cadr token)) - comment-before) + comment-before + add-used-types) static-methods)) ((phpinspect-variable-p (cadr token)) @@ -232,7 +256,8 @@ ;; Bare functions are always public (push (phpinspect--index-function-from-scope type-resolver (list :public token) - comment-before) + comment-before + add-used-types) methods)) ((phpinspect-doc-block-p token) (phpinspect--log "setting comment-before %s" token) @@ -270,6 +295,7 @@ `(,class-name . (phpinspect--indexed-class (class-name . ,class-name) + (location . ,(funcall location-resolver class)) (imports . ,imports) (methods . ,methods) (static-methods . ,static-methods) @@ -277,7 +303,9 @@ (variables . ,variables) (constants . ,constants) (extends . ,extends) - (implements . ,implements)))))) + (implements . ,implements) + (used-types . ,(mapcar #'phpinspect-intern-name + (seq-uniq used-types #'string=)))))))) (defsubst phpinspect-namespace-body (namespace) "Return the nested tokens in NAMESPACE tokens' body. @@ -286,18 +314,19 @@ Accounts for namespaces that are defined with '{}' blocks." (cdaddr namespace) (cdr namespace))) -(defun phpinspect--index-classes (imports classes &optional namespace indexed) +(defun phpinspect--index-classes + (imports classes type-resolver-factory location-resolver &optional namespace indexed) "Index the class tokens in `classes`, using the imports in `imports` as Fully Qualified names. `namespace` will be assumed the root namespace if not provided" (if classes (let ((class (pop classes))) (push (phpinspect--index-class - imports - (phpinspect--make-type-resolver imports class namespace) - class) + imports (funcall type-resolver-factory imports class namespace) + location-resolver class) indexed) - (phpinspect--index-classes imports classes namespace indexed)) + (phpinspect--index-classes imports classes type-resolver-factory + location-resolver namespace indexed)) (nreverse indexed))) (defun phpinspect--use-to-type (use) @@ -316,33 +345,82 @@ namespace if not provided" (defun phpinspect--uses-to-types (uses) (mapcar #'phpinspect--use-to-type uses)) -(defun phpinspect--index-namespace (namespace) +(defun phpinspect--index-namespace (namespace type-resolver-factory location-resolver) (phpinspect--index-classes (phpinspect--uses-to-types (seq-filter #'phpinspect-use-p namespace)) (seq-filter #'phpinspect-class-p namespace) - (cadadr namespace))) + type-resolver-factory location-resolver (cadadr namespace) nil)) -(defun phpinspect--index-namespaces (namespaces &optional indexed) +(defun phpinspect--index-namespaces + (namespaces type-resolver-factory location-resolver &optional indexed) (if namespaces (progn - (push (phpinspect--index-namespace (pop namespaces)) indexed) - (phpinspect--index-namespaces namespaces indexed)) + (push (phpinspect--index-namespace (pop namespaces) + type-resolver-factory + location-resolver) + indexed) + (phpinspect--index-namespaces namespaces type-resolver-factory + location-resolver indexed)) (apply #'append (nreverse indexed)))) (defun phpinspect--index-functions (&rest _args) "TODO: implement function indexation. This is a stub function.") -(defun phpinspect--index-tokens (tokens) +(defun phpinspect--find-used-types-in-tokens (tokens) + "Find usage of the \"new\" keyword in TOKENS. + +Return value is a list of the types that are \"newed\"." + (let ((previous-tokens) + (used-types)) + (while tokens + (let ((token (pop tokens)) + (previous-token (car previous-tokens))) + (cond ((and (phpinspect-word-p previous-token) + (string= "new" (cadr previous-token)) + (phpinspect-word-p token)) + (let ((type (cadr token))) + (when (not (string-match-p "\\\\" type)) + (push type used-types)))) + ((and (phpinspect-static-attrib-p token) + (phpinspect-word-p previous-token)) + (let ((type (cadr previous-token))) + (when (not (string-match-p "\\\\" type)) + (push type used-types)))) + ((phpinspect-object-attrib-p token) + (let ((lists (seq-filter #'phpinspect-list-p token))) + (dolist (list lists) + (setq used-types (append (phpinspect--find-used-types-in-tokens (cdr list)) + used-types))))) + ((or (phpinspect-list-p token) (phpinspect-block-p token)) + (setq used-types (append (phpinspect--find-used-types-in-tokens (cdr token)) + used-types)))) + + (push token previous-tokens))) + used-types)) + +(defun phpinspect--index-tokens (tokens &optional type-resolver-factory location-resolver) "Index TOKENS as returned by `phpinspect--parse-current-buffer`." + (unless type-resolver-factory + (setq type-resolver-factory #'phpinspect--make-type-resolver)) + + (unless location-resolver + (setq location-resolver (lambda (_) (list 0 0)))) + (let ((imports (phpinspect--uses-to-types (seq-filter #'phpinspect-use-p tokens)))) `(phpinspect--root-index (imports . ,imports) ,(append (append '(classes) - (phpinspect--index-namespaces (seq-filter #'phpinspect-namespace-p tokens)) + (phpinspect--index-namespaces (seq-filter #'phpinspect-namespace-p tokens) + type-resolver-factory + location-resolver) (phpinspect--index-classes imports - (seq-filter #'phpinspect-class-p tokens)))) + (seq-filter #'phpinspect-class-p tokens) + type-resolver-factory + location-resolver))) + ,(append '(used-types) + (phpinspect--find-used-types-in-tokens tokens)) (functions)) ;; TODO: Implement function indexation )) @@ -381,10 +459,5 @@ namespace if not provided" (phpinspect--log "Failed to find file for type %s: %s" type error) nil))) -(defsubst phpinspect--index-thread-enqueue (task) - (phpinspect--queue-enqueue-noduplicate phpinspect--index-queue - task - #'phpinspect--index-task=)) - (provide 'phpinspect-index) ;;; phpinspect-index.el ends here diff --git a/phpinspect-parser.el b/phpinspect-parser.el index 9d2609b..8471a90 100644 --- a/phpinspect-parser.el +++ b/phpinspect-parser.el @@ -384,13 +384,26 @@ token is \";\", which marks the end of a statement in PHP." (cond ,@(mapcar (lambda (handler) `((looking-at ,(plist-get (symbol-value handler) 'regexp)) - (let ((token (funcall ,(symbol-function handler) + (let ((start-position (point)) + (token (funcall ,(symbol-function handler) (match-string 0) max-point))) (when token (if (null tokens) (setq tokens (list token)) - (nconc tokens (list token))))))) + (progn + (nconc tokens (list token)))) + + ;; When parsing within a buffer that has + ;; `phpinspect-current-buffer` set, update the + ;; token location map. Usually, this variable + ;; is set when `phpinspect-mode` is active. + (when phpinspect-current-buffer + (puthash token + (phpinspect-make-region start-position + (point)) + (phpinspect-buffer-location-map + phpinspect-current-buffer))))))) handlers) (t (forward-char)))) (push ,tree-type tokens)))))) diff --git a/phpinspect-project.el b/phpinspect-project.el index 26e12ca..0f73122 100644 --- a/phpinspect-project.el +++ b/phpinspect-project.el @@ -25,13 +25,25 @@ (require 'phpinspect-class) (require 'phpinspect-type) +(require 'phpinspect-fs) +(require 'filenotify) -(cl-defstruct (phpinspect--project (:constructor phpinspect--make-project-cache)) +(cl-defstruct (phpinspect--project (:constructor phpinspect--make-project)) (class-index (make-hash-table :test 'eq :size 100 :rehash-size 40) :type hash-table :documentation "A `hash-table` that contains all of the currently indexed classes in the project") + (fs nil + :type phpinspect-fs + :documentation + "The filesystem object through which this project's files +can be accessed.") + (autoload nil + :type phpinspect-autoload + :documentation + "The autoload object through which this project's type +definitions can be retrieved") (worker nil :type phpinspect-worker :documentation @@ -39,12 +51,35 @@ indexed classes in the project") (root nil :type string :documentation - "The root directory of this project")) + "The root directory of this project") + (purged nil + :type boolean + :documentation "Whether or not the project has been purged or not. +Projects get purged when they are removed from the global cache.") + (file-watchers (make-hash-table :test #'equal :size 10000 :rehash-size 10000) + :type hash-table + :documentation "All active file watchers in this project, +indexed by the absolute paths of the files they're watching.")) (cl-defgeneric phpinspect--project-add-class ((project phpinspect--project) (class (head phpinspect--indexed-class))) "Add an indexed CLASS to PROJECT.") +(cl-defmethod phpinspect--project-purge ((project phpinspect--project)) + "Disable all background processes for project and put it in a `purged` state." + (maphash (lambda (_ watcher) (file-notify-rm-watch watcher)) + (phpinspect--project-file-watchers project)) + + (setf (phpinspect--project-file-watchers project) + (make-hash-table :test #'equal :size 10000 :rehash-size 10000)) + (setf (phpinspect--project-purged project) t)) + +(cl-defmethod phpinspect--project-watch-file ((project phpinspect--project) + filepath + callback) + (let ((watcher (file-notify-add-watch filepath '(change) callback))) + (puthash filepath watcher (phpinspect--project-file-watchers project)))) + (cl-defmethod phpinspect--project-add-return-types-to-index-queueue ((project phpinspect--project) methods) (dolist (method methods) @@ -61,16 +96,15 @@ indexed classes in the project") (cl-defmethod phpinspect--project-enqueue-if-not-present ((project phpinspect--project) (type phpinspect--type)) - (let ((class (phpinspect--project-get-class project type))) - (when (or (not class) - (not (or (phpinspect--class-initial-index class) - (phpinspect--class-index-queued class)))) - (when (not class) - (setq class (phpinspect--project-create-class project type))) - (phpinspect--log "Adding unpresent class %s to index queue" type) - (setf (phpinspect--class-index-queued class) t) - (phpinspect-worker-enqueue (phpinspect--project-worker project) - (phpinspect-make-index-task project type))))) + (unless (phpinspect--type-is-native type) + (let ((class (phpinspect--project-get-class project type))) + (when (or (not class) + (not (or (phpinspect--class-initial-index class)))) + (when (not class) + (setq class (phpinspect--project-create-class project type))) + (phpinspect--log "Adding unpresent class %s to index queue" type) + (phpinspect-worker-enqueue (phpinspect--project-worker project) + (phpinspect-make-index-task project type)))))) (cl-defmethod phpinspect--project-add-class-attribute-types-to-index-queue ((project phpinspect--project) (class phpinspect--class)) diff --git a/phpinspect-type.el b/phpinspect-type.el index c197cea..550d22d 100644 --- a/phpinspect-type.el +++ b/phpinspect-type.el @@ -104,6 +104,7 @@ See https://wiki.php.net/rfc/static_return_type ." (car (last (split-string fqn "\\\\")))) (cl-defmethod phpinspect--type-bare-name ((type phpinspect--type)) + "Return just the name, without namespace part, of TYPE." (phpinspect--get-bare-class-name-from-fqn (phpinspect--type-name type))) (cl-defmethod phpinspect--type= ((type1 phpinspect--type) (type2 phpinspect--type)) @@ -145,7 +146,6 @@ NAMESPACE may be nil, or a string with a namespace FQN." (defun phpinspect--make-type-resolver (types &optional token-tree namespace) "Little wrapper closure to pass around and resolve types with." - (let* ((inside-class (if token-tree (or (phpinspect--find-innermost-incomplete-class token-tree) (phpinspect--find-class-token token-tree)))) diff --git a/phpinspect-worker.el b/phpinspect-worker.el index 5945245..f63d97b 100644 --- a/phpinspect-worker.el +++ b/phpinspect-worker.el @@ -31,6 +31,18 @@ (defvar phpinspect-worker nil "Contains the phpinspect worker that is used by all projects.") +(cl-defstruct (phpinspect-index-task + (:constructor phpinspect-make-index-task-generated)) + "Represents an index task that can be executed by a `phpinspect-worker`." + (project nil + :type phpinspect--project + :documentation + "The project that the task should be executed for.") + (type nil + :type phpinspect--type + :documentation + "The type whose file should be indexed.")) + (cl-defstruct (phpinspect-queue-item (:constructor phpinspect-make-queue-item)) (next nil @@ -210,6 +222,18 @@ on the worker independent of dynamic variables during testing.") (cl-defmethod phpinspect-worker-enqueue ((worker phpinspect-worker) task) (phpinspect-queue-enqueue (phpinspect-worker-queue worker) task)) +(cl-defmethod phpinspect-worker-enqueue ((worker phpinspect-worker) + (task phpinspect-index-task)) + "Specialized enqueuement method for index tasks. Prevents +indexation tasks from being added when there are identical tasks +already present in the queue." + (phpinspect-queue-enqueue-noduplicate (phpinspect-worker-queue worker) task #'phpinspect-index-task=)) + +(cl-defmethod phpinspect-index-task= ((task1 phpinspect-index-task) (task2 phpinspect-index-task)) + (and (eq (phpinspect-index-task-project task1) + (phpinspect-index-task-project task2)) + (phpinspect--type= (phpinspect-index-task-type task1) (phpinspect-index-task-type task2)))) + (cl-defmethod phpinspect-worker-enqueue ((worker phpinspect-dynamic-worker) task) (phpinspect-worker-enqueue (phpinspect-resolve-dynamic-worker worker) task)) @@ -241,7 +265,10 @@ CONTINUE must be a condition-variable" (mx (make-mutex)) (continue (make-condition-variable mx))) (if task - (phpinspect-task-execute task worker) + ;; Execute task if it belongs to a project that has not been + ;; purged (meaning that it is still actively used). + (unless (phpinspect--project-purged (phpinspect-task-project task)) + (phpinspect-task-execute task worker)) ;; else: join with the main thread until wakeup is signaled (thread-join main-thread)) @@ -292,24 +319,18 @@ CONTINUE must be a condition-variable" (interactive) (phpinspect-worker-stop phpinspect-worker)) -(cl-defstruct (phpinspect-index-task - (:constructor phpinspect-make-index-task-generated)) - "Represents an index task that can be executed by a `phpinspect-worker`." - (project nil - :type phpinspect--project - :documentation - "The project that the task should be executed for.") - (type nil - :type phpinspect--type - :documentation - "The type whose file should be indexed.")) - (cl-defgeneric phpinspect-make-index-task ((project phpinspect--project) (type phpinspect--type)) (phpinspect-make-index-task-generated :project project :type type)) +(cl-defgeneric phpinspect-task-project (task) + "The project that this task belongs to.") + +(cl-defmethod phpinspect-task-project ((task phpinspect-index-task)) + (phpinspect-index-task-project task)) + (cl-defgeneric phpinspect-task-execute (task worker) "Execute TASK for WORKER.") @@ -323,19 +344,19 @@ CONTINUE must be a condition-variable" (phpinspect-index-task-type task) (phpinspect--project-root project)) - (if is-native-type - (progn - (phpinspect--log "Skipping indexation of native type %s" - (phpinspect-index-task-type task)) - - ;; We can skip pausing when a native type is encountered - ;; and skipped, as we haven't done any intensive work that - ;; may cause hangups. - (setf (phpinspect-worker-skip-next-pause worker) t)) - (let* ((type (phpinspect-index-task-type task)) - (root-index (phpinspect--index-type-file project type))) - (when root-index - (phpinspect--project-add-index project root-index)))))) + (cond (is-native-type + (phpinspect--log "Skipping indexation of native type %s" + (phpinspect-index-task-type task)) + + ;; We can skip pausing when a native type is encountered + ;; and skipped, as we haven't done any intensive work that + ;; may cause hangups. + (setf (phpinspect-worker-skip-next-pause worker) t)) + (t + (let* ((type (phpinspect-index-task-type task)) + (root-index (phpinspect--index-type-file project type))) + (when root-index + (phpinspect--project-add-index project root-index))))))) (provide 'phpinspect-worker) diff --git a/phpinspect.el b/phpinspect.el index 0e39cb0..fb1b590 100644 --- a/phpinspect.el +++ b/phpinspect.el @@ -1,4 +1,4 @@ -;; phpinspect.el --- PHP parsing and completion package -*- lexical-binding: t; -*- +; phpinspect.el --- PHP parsing and completion package -*- lexical-binding: t; -*- ;; Copyright (C) 2021 Free Software Foundation, Inc @@ -38,6 +38,9 @@ (require 'phpinspect-index) (require 'phpinspect-class) (require 'phpinspect-worker) +(require 'phpinspect-autoload) +(require 'phpinspect-imports) +(require 'phpinspect-buffer) (defvar-local phpinspect--buffer-index nil "The result of the last successfull parse + index action @@ -67,16 +70,6 @@ phpinspect") (defvar phpinspect-eldoc-word-width 14 "The maximum width of words in eldoc strings.") -(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.") - - (cl-defstruct (phpinspect--completion (:constructor phpinspect--construct-completion)) "Contains a possible completion value with all it's attributes." @@ -708,7 +701,7 @@ more recent" (defun phpinspect--init-mode () "Initialize the phpinspect minor mode for the current buffer." - + (setq phpinspect-current-buffer (phpinspect-make-buffer :buffer (current-buffer))) (make-local-variable 'company-backends) (add-to-list 'company-backends #'phpinspect-company-backend) @@ -755,6 +748,7 @@ users will have to use \\[phpinspect-purge-cache]." (defun phpinspect--disable-mode () "Clean up the buffer environment for the mode to be disabled." + (setq phpinspect-current-buffer nil) (kill-local-variable 'phpinspect--buffer-project) (kill-local-variable 'phpinspect--buffer-index) (kill-local-variable 'company-backends) @@ -784,8 +778,8 @@ For finding/opening class files see `phpinspect-find-class-file' (bound to \\[phpinspect-find-class-file]). To automatically add missing use statements for used classes to a -visited file, use `phpinspect-fix-uses-interactive' -(bound to \\[phpinspect-fix-uses-interactive]].) +visited file, use `phpinspect-fix-imports' +(bound to \\[phpinspect-fix-imports]].) Example configuration: @@ -806,7 +800,7 @@ Example configuration: (setq-local company-backends '(phpinspect-company-backend)) ;; Shortcut to add use statements for classes you use. - (define-key php-mode-map (kbd \"C-c u\") 'phpinspect-fix-uses-interactive) + (define-key php-mode-map (kbd \"C-c u\") 'phpinspect-fix-imports) ;; Shortcuts to quickly search/open files of PHP classes. ;; You can make these local to php-mode, but making them global @@ -1094,6 +1088,13 @@ If its value is nil, it is created and then returned." This effectively purges any cached code information from all currently opened projects." (interactive) + (when phpinspect-cache + ;; Allow currently known cached projects to cleanup after themselves + (maphash (lambda (_ project) + (phpinspect--project-purge project)) + (phpinspect--cache-projects phpinspect-cache))) + + ;; Assign a fresh cache object (setq phpinspect-cache (phpinspect--make-cache))) (defun phpinspect--locate-dominating-project-file (start-file) @@ -1146,69 +1147,26 @@ level of START-FILE in stead of `default-directory`." (json-key-type 'string)) ,@body)) -;; Use statements -;;;###autoload -(defun phpinspect-fix-uses-interactive () - "Add missing use statements to the currently visited PHP file." - (interactive) - (let ((project-root (phpinspect-project-root))) - (when project-root - (save-buffer) - (let* ((phpinspect-json (shell-command-to-string - (format "cd %s && %s fxu --json %s" - (shell-quote-argument project-root) - (shell-quote-argument phpinspect-index-executable) - (shell-quote-argument buffer-file-name))))) - (let* ((json-object-type 'hash-table) - (json-array-type 'list) - (json-key-type 'string) - (phpinspect-json-data (json-read-from-string phpinspect-json))) - (maphash #'phpinspect-handle-phpinspect-json phpinspect-json-data)))))) - -(defun phpinspect-handle-phpinspect-json (class-name candidates) - "Handle key value pair of classname and FQN's" - (let ((ncandidates (length candidates))) - (cond ((= 1 ncandidates) - (phpinspect-add-use (pop candidates))) - ((= 0 ncandidates) - (message "No use statement found for class \"%s\"" class-name)) - (t - (phpinspect-add-use (completing-read "Class: " candidates)))))) - -;; TODO: Implement this using the parser in stead of regexes. -(defun phpinspect-add-use (fqn) "Add use statement to a php file" - (save-excursion - (let ((current-char (point))) - (goto-char (point-min)) - (cond - ((re-search-forward "^use" nil t) (forward-line 1)) - ((re-search-forward "^namespace" nil t) (forward-line 2)) - ((re-search-forward - "^\\(abstract \\|/\\* final \\*/ ?\\|final \\|\\)\\(class\\|trait\\|interface\\)" - nil ) - (forward-line -1) - (phpinspect-goto-first-line-no-comment-up))) - - (insert (format "use %s;%c" fqn ?\n)) - (goto-char current-char)))) - -(defun phpinspect-goto-first-line-no-comment-up () - "Go up until a line is encountered that does not start with a comment." - (when (string-match "^\\( ?\\*\\|/\\)" (thing-at-point 'line t)) - (forward-line -1) - (phpinspect-goto-first-line-no-comment-up))) - (defsubst phpinspect-insert-file-contents (&rest args) "Call `phpinspect-insert-file-contents-function' with ARGS as arguments." (apply phpinspect-insert-file-contents-function args)) -(defun phpinspect-get-all-fqns (&optional fqn-file) - (unless fqn-file - (setq fqn-file "uses")) - (with-temp-buffer - (phpinspect-insert-file-contents - (concat (phpinspect-project-root) "/.cache/phpinspect/" fqn-file)) - (split-string (buffer-string) (char-to-string ?\n)))) +(defun phpinspect-get-all-fqns (&optional filter) + "Return a list of all FQNS congruent with FILTER in the currently active project. + +FILTER must be nil or the symbol 'own' if FILTER is 'own', only +fully qualified names from the project's source, and not its +dependencies, are returned." + (let* ((project (phpinspect--cache-get-project-create + (phpinspect--get-or-create-global-cache) + (phpinspect-project-root))) + (autoloader (phpinspect--project-autoload project))) + (let ((fqns)) + (maphash (lambda (type _) (push (symbol-name type) fqns)) + (if (eq 'own filter) + (phpinspect-autoloader-own-types autoloader) + (phpinspect-autoloader-types autoloader))) + fqns))) ;;;###autoload (defun phpinspect-find-class-file (fqn) @@ -1230,7 +1188,7 @@ available FQNs for classes in the current project, which aren't located in \"vendor\" folder." (interactive (list (phpinspect--make-type :name - (completing-read "Class: " (phpinspect-get-all-fqns "uses_own"))))) + (completing-read "Class: " (phpinspect-get-all-fqns 'own))))) (find-file (phpinspect-type-filepath fqn))) (defsubst phpinspect-type-filepath (fqn) @@ -1242,35 +1200,22 @@ located in \"vendor\" folder." when INDEX-NEW is non-nil, new files are added to the index before the search is executed." - (when (eq index-new 'index-new) - (with-temp-buffer - (call-process phpinspect-index-executable nil (current-buffer) nil "index" "--new"))) - (let* ((default-directory (phpinspect-project-root)) - (result (with-temp-buffer - (phpinspect--log "dir: %s" default-directory) - (phpinspect--log "class: %s" (string-remove-prefix - "\\" - (phpinspect--type-name class))) - (list (call-process phpinspect-index-executable - nil - (current-buffer) - nil - "fp" (string-remove-prefix - "\\" - (phpinspect--type-name class))) - (buffer-string))))) - (if (not (= (car result) 0)) - (progn - (phpinspect--log "Got non-zero return value %d Retrying with reindex. output: \"%s\"" - (car result) - (cadr result)) + (let* ((project (phpinspect--cache-get-project-create + (phpinspect--get-or-create-global-cache) + (phpinspect-project-root))) + (autoloader (phpinspect--project-autoload project))) + (when (eq index-new 'index-new) + (phpinspect-autoloader-refresh autoloader)) + (let* ((result (phpinspect-autoloader-resolve autoloader (phpinspect--type-name-symbol class)))) + (if (not result) ;; Index new files and try again if not done already. (if (eq index-new 'index-new) nil - (phpinspect-get-class-filepath class 'index-new))) - (concat (string-remove-suffix "/" default-directory) - "/" - (string-remove-prefix "/" (string-trim (cadr result))))))) + (progn + (phpinspect--log "Failed finding filepath for type %s. Retrying with reindex." + (phpinspect--type-name class)) + (phpinspect-get-class-filepath class 'index-new))) + result)))) (defun phpinspect-unique-strings (strings) (seq-filter @@ -1283,22 +1228,17 @@ before the search is executed." strings)) (defun phpinspect-index-current-project () - "Index all available FQNs in the current project. - -Index is stored in files in the .cache directory of -the project root." + "Index all available FQNs in the current project." (interactive) - (let* ((default-directory (phpinspect-project-root))) - (with-current-buffer (get-buffer-create "**phpinspect-index**") - (goto-char (point-max)) - (make-process - :command `(,phpinspect-index-executable "index") - :name "phpinspect-index-current-project" - :buffer (current-buffer)) - - (display-buffer (current-buffer) `(display-buffer-at-bottom (window-height . 10))) - (set-window-point (get-buffer-window (current-buffer) nil) - (point-max))))) + (let* ((project (phpinspect--cache-get-project-create + (phpinspect--get-or-create-global-cache) + (phpinspect-project-root))) + (autoloader (phpinspect--project-autoload project))) + (phpinspect-autoloader-refresh autoloader) + (message (concat "Refreshed project autoloader. Found %d types within project," + " %d types total.") + (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)))) diff --git a/test/phpinspect-test.el b/test/phpinspect-test.el index 4c4ba1c..0c48f2c 100644 --- a/test/phpinspect-test.el +++ b/test/phpinspect-test.el @@ -1,4 +1,4 @@ -;;; phpinspect-test.el --- Unit tests for phpinslect.el -*- lexical-binding: t; -*- +;;; phpinspect-test.el --- Unit tests for phpinspect.el -*- lexical-binding: t; -*- ;; Copyright (C) 2021 Free Software Foundation, Inc. @@ -500,6 +500,10 @@ class Thing (load-file (concat phpinspect-test-directory "/test-worker.el")) (load-file (concat phpinspect-test-directory "/test-autoload.el")) (load-file (concat phpinspect-test-directory "/test-fs.el")) +(load-file (concat phpinspect-test-directory "/test-project.el")) +(load-file (concat phpinspect-test-directory "/test-buffer.el")) +(load-file (concat phpinspect-test-directory "/test-index.el")) + (provide 'phpinspect-test) ;;; phpinspect-test.el ends here diff --git a/test/test-buffer.el b/test/test-buffer.el new file mode 100644 index 0000000..ed36ae5 --- /dev/null +++ b/test/test-buffer.el @@ -0,0 +1,68 @@ +;;; test-buffer.el --- Unit tests for phpinspect.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2021 Free Software Foundation, Inc. + +;; Author: Hugo Thunnissen + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(require 'ert) +(require 'phpinspect-parser) +(require 'phpinspect-buffer) + +(ert-deftest phpinspect-parse-buffer-location-map () + "Confirm that the location map of `phpinspect-current-buffer' is +populated when the variable is set and the data in it is accurate." + (let* ((location-map) + (parsed) + (class)) + (with-temp-buffer + (insert-file-contents (concat phpinspect-test-php-file-directory "/NamespacedClass.php")) + (setq phpinspect-current-buffer + (phpinspect-make-buffer :buffer (current-buffer))) + (setq parsed (phpinspect-buffer-parse phpinspect-current-buffer)) + (setq location-map + (phpinspect-buffer-location-map phpinspect-current-buffer))) + + (let* ((class (seq-find #'phpinspect-class-p + (seq-find #'phpinspect-namespace-p parsed))) + (class-region (gethash class location-map)) + (classname-region (gethash (car (cddadr class)) location-map))) + (should class) + (should class-region) + (should classname-region) + ;; Character position of the start of the class token. + (should (= 417 (phpinspect-region-start class-region))) + (should (= 2173 (phpinspect-region-end class-region))) + + (should (= 423 (phpinspect-region-start classname-region))) + (should (= 440 (phpinspect-region-end classname-region)))))) + +(ert-deftest phpinspect-parse-buffer-no-current () + "Confirm that the parser is still functional with +`phpinspect-current-buffer' unset." + (let*((buffer) + (parsed)) + (with-temp-buffer + (should-not phpinspect-current-buffer) + (insert-file-contents (concat phpinspect-test-php-file-directory "/NamespacedClass.php")) + (setq parsed (phpinspect-parse-current-buffer))) + + (should (cdr parsed)))) diff --git a/test/test-index.el b/test/test-index.el new file mode 100644 index 0000000..e254ac4 --- /dev/null +++ b/test/test-index.el @@ -0,0 +1,103 @@ +;;; test-index.el --- Unit tests for phpinspect.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2021 Free Software Foundation, Inc. + +;; Author: Hugo Thunnissen + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: +(require 'ert) +(require 'phpinspect-index) + +(ert-deftest phpinspect-index-static-methods () + (let* ((class-tokens + `(:root + (:class + (:declaration (:word "class") (:word "Potato")) + (:block + (:static + (:function (:declaration (:word "function") + (:word "staticMethod") + (:list (:variable "untyped") + (:comma) + (:word "array") + (:variable "things"))) + (:block))))))) + (index (phpinspect--index-tokens class-tokens)) + (expected-index + `(phpinspect--root-index + (imports) + (classes + (,(phpinspect--make-type :name"\\Potato" :fully-qualified t) + phpinspect--indexed-class + (class-name . ,(phpinspect--make-type :name "\\Potato" :fully-qualified t)) + (location . (0 0)) + (imports) + (methods) + (static-methods . (,(phpinspect--make-function + :name "staticMethod" + :scope '(:public) + :arguments `(("untyped" nil) + ("things" ,(phpinspect--make-type :name "\\array" + :fully-qualified t))) + :return-type phpinspect--null-type))) + (static-variables) + (variables) + (constants) + (extends) + (implements) + (used-types))) + (used-types) + (functions)))) + (should (equal expected-index index)))) + +(ert-deftest phpinspect-index-used-types-in-class () + (let* ((result (phpinspect--index-tokens + (phpinspect-parse-string + "tree() === true) { + return new ExtendedThing(); +} +return StaticThing::create(new ThingFactory())->makeThing((((new Potato())->antiPotato(new OtherThing())))); +}"))) + (used-types (alist-get 'used-types (car (alist-get 'classes result))))) + (should (equal + (mapcar #'phpinspect-intern-name + (sort + '("Monkey" "ExtendedThing" "StaticThing" "Thing" "ThingFactory" "Potato" "OtherThing") + #'string<)) + (sort used-types (lambda (s1 s2) (string< (symbol-name s1) (symbol-name s2)))))))) + +(ert-deftest phpinspect--find-used-types-in-tokens () + (let ((blocks `( + ((:block (:word "return") + (:word "new") + (:word "Response") + (:list)) + ("Response")) + ((:block (:list (:word "new") (:word "Response")) + (:object-attrib (:word "someMethod") + (:list (:word "new") + (:word "Request")))) + ("Request" "Response"))))) + (dolist (set blocks) + (let ((result (phpinspect--find-used-types-in-tokens (car set)))) + (should (equal (cadr set) result)))))) diff --git a/test/test-project.el b/test/test-project.el new file mode 100644 index 0000000..fa0ee27 --- /dev/null +++ b/test/test-project.el @@ -0,0 +1,46 @@ +;; test-project.el --- Unit tests for phpinspect.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2021 Free Software Foundation, Inc. + +;; Author: Hugo Thunnissen + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(require 'ert) +(require 'phpinspect-project) + +(ert-deftest phpinspect-project-purge () + (let ((project (phpinspect--make-project))) + (phpinspect--project-purge project) + + (should (eq t (phpinspect--project-purged project))))) + +(ert-deftest phpinspect-project-watch-file-and-purge () + (let* ((root (make-temp-file "phpinspect-test" 'dir)) + (fs (phpinspect-make-fs)) + (worker (phpinspect-make-worker)) + (watch-file (concat root "/watch1")) + (project (phpinspect--make-project :fs fs :root root))) + (phpinspect--project-watch-file project watch-file + (lambda (&rest ignored))) + + (phpinspect--project-purge project) + + (should (= 0 (length (hash-table-values (phpinspect--project-file-watchers project)))))))