Transition from index script to autoloader
continuous-integration/drone/push Build is failing Details

WIP-incremental-parsing
Hugo Thunnissen 1 year ago
parent ca8d0972ff
commit dbf0ec0390

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

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

@ -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 <devel@hugot.nl>
;; Keywords: php, languages, tools, convenience
;; Version: 0
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;; Code:
(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)

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

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

@ -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 <devel@hugot.nl>
;; Keywords: php, languages, tools, convenience
;; Version: 0
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; 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)

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

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

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

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

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

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

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

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

@ -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 <devel@hugot.nl>
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
(require '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))))

@ -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 <devel@hugot.nl>
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
(require '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
"<?php namespace Field; class Potato {
public function makeThing(): Thing
{
if ((new Monkey())->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))))))

@ -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 <devel@hugot.nl>
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
(require '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)))))))
Loading…
Cancel
Save