From 681f13d4deaaec44cabac3de0f885c75b19697cf Mon Sep 17 00:00:00 2001 From: Hugo Thunnissen Date: Sat, 4 Sep 2021 20:18:25 +0200 Subject: [PATCH] Initial commit: Move out of personal dotfile repository. --- README.md | 26 + phpinspect-index.bash | 726 ++++++++++++++ phpinspect.el | 2204 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 2956 insertions(+) create mode 100644 README.md create mode 100755 phpinspect-index.bash create mode 100644 phpinspect.el diff --git a/README.md b/README.md new file mode 100644 index 0000000..afc6d2a --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# phpinspect.el + +WIP. Documentation is in the making. + +Example config: + +```elisp +;;;###autoload +(defun my-php-personal-hook () + (set (make-local-variable 'company-minimum-prefix-length) 0) + (set (make-local-variable 'company-tooltip-align-annotations) t) + (set (make-local-variable 'company-idle-delay) 0.1) + (set (make-local-variable '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) + + ;; Shortcuts to quickly search/open files of PHP classes. + (global-set-key (kbd "C-c a") 'phpinspect-find-class-file) + (global-set-key (kbd "C-c c") 'phpinspect-find-own-class-file) + + (phpinspect-mode)) + + (add-hook 'php-mode-hook 'my-php-personal-hook) + +``` diff --git a/phpinspect-index.bash b/phpinspect-index.bash new file mode 100755 index 0000000..ba4d2d7 --- /dev/null +++ b/phpinspect-index.bash @@ -0,0 +1,726 @@ +#!/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/phpns +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 + ;; + 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 phpns command.\n' "$command" >&2 + exit 1 + ;; + esac +} + +# shellcheck disable=SC2034 +fixMissingUseStatements() { + declare check_uses='false' check_needs='false' file="$1" + 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() { + if [[ ${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 $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_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 +} + +execute "$@" diff --git a/phpinspect.el b/phpinspect.el new file mode 100644 index 0000000..1ce308f --- /dev/null +++ b/phpinspect.el @@ -0,0 +1,2204 @@ +;; -*- lexical-binding: t; -*- + +(require 'php-project) +(require 'cl-lib) +(require 'json) + +(cl-defstruct (phpinspect--function (:constructor phpinspect--make-function)) + "A PHP function." + (name nil + :type string + :documentation + "A string containing the name of the function") + (scope nil + :type phpinspect-scope + :documentation + "When the function is a method, this should contain the +scope of the function as returned by `phpinspect-parse-scope`.") + (arguments nil + :type list + :documentation + "A simple list with function arguments and their +types in tuples. Each list should have the name of the variable +as first element and the type as second element.") + (return-type nil + :type string + :documentation + "A string containing the FQN of the return value +of the function.")) + +(cl-defstruct (phpinspect--variable (:constructor phpinspect--make-variable)) + "A PHP Variable." + (name nil + :type string + :documentation + "A string containing the name of the variable.") + (scope nil + :type phpinspect-scope + :documentation + "When the variable is an object attribute, this should +contain the scope of the variable as returned by +`phpinspect-parse-scope`") + (type nil + :type string + :documentation + "A string containing the FQN of the variable's type")) + +(cl-defstruct (phpinspect--completion + (:constructor phpinspect--construct-completion)) + "Contains a possible completion value with all it's attributes." + (value nil :type string) + (meta nil :type string) + (annotation nil :type string) + (kind nil :type symbol)) + +(defun phpinspect--format-type-name (name) + (string-remove-prefix "\\" name)) + +(cl-defgeneric phpinspect--make-completion (completion-candidate) + "Creates a `phpinspect--completion` for a possible completion +candidate. Candidates can be indexed functions and variables.") + +(cl-defmethod phpinspect--make-completion + ((completion-candidate phpinspect--function)) + "Create a `phpinspect--completion` for COMPLETION-CANDIDATE." + (phpinspect--construct-completion + :value (phpinspect--function-name completion-candidate) + :meta (concat "(" (mapconcat (lambda (arg) + (concat (phpinspect--format-type-name (cadr arg)) " " + "$" (if (> (length (car arg)) 8) + (truncate-string-to-width (car arg) 8 nil) + (car arg)))) + (phpinspect--function-arguments completion-candidate) + ", ") + ") " + (phpinspect--format-type-name (phpinspect--function-return-type completion-candidate))) + :annotation (concat " " + (phpinspect--get-bare-class-name-from-fqn + (or (phpinspect--function-return-type completion-candidate) + ""))) + :kind 'function)) + +(defvar phpinspect--debug nil + "Enable debug logs for phpinspect by setting this variable to true") + +(defun phpinspect-toggle-logging () + (interactive) + (if (setq phpinspect--debug (not phpinspect--debug)) + (message "Enabled phpinspect logging.") + (message "Disabled phpinspect logging."))) + +(defconst phpinspect-native-types + '("int" "string" "bool" "boolean" "iterator" "array" "float" "void")) + +(eval-when-compile + (defun phpinspect--word-end-regex () + "[[:blank:]]") + + (defsubst phpinspect--strip-last-char (string) + (substring string 0 (- (length string) 1))) + + (defmacro phpinspect--handler (regex function) + (list 'cons regex (list 'quote function)))) + +(defmacro phpinspect-defhandler (name regex docstring function) + ;; Lets make sure that defuns and substs are only referenced by + ;; their name and not their entire definitions. + (let ((function-name (cond ((and (listp function) + (or (eq (car function) 'defun) + (eq (car function) 'defsubst) + (and (eq (car function) 'quote) + (symbolp (car (last function)))))) + (eval function)) + (t (error (concat "`phpinspect-defhandler`: function must " + "be a quoted function name, a `defun` " + "or a `defsubst` %S provided") + function))))) + `(progn + ;; If function is a defun, we'll need to have evaluated it. + ,function + (defmacro ,name () + ,(concat "This is a generated macro, see `phpinspect-defhandler`\n\n" + "ATTRIBUTES:\n" + "Token regex: " (eval regex) "\n\n" + "Parser function:\n" (with-output-to-string (pp (list 'quote function-name))) + "\n\n" + "DESCRIPTION\n" + docstring) + (list 'phpinspect--handler ,regex (quote ,function-name)))))) + +(defmacro phpinspect-munch-token-without-attribs (text-object token-keyword) + "Return a token by name of `token-keyword` with the contents of +the passed text object as value. The text object will be +stripped of all text attributes" + `(let ((text ,text-object) (length (length ,text-object))) + (forward-char length) + (set-text-properties 0 length nil text) + (list ,token-keyword text))) + +(defmacro phpinspect-parse + (buffer tree-type handler-list max-point &optional continue-condition delimiter-predicate) + "Parse the current buffer using the handler macros provided in +`handler-list`, unrolling them in a `cond` statement which checks +their token regexes one by one and runs their parser functions +when one of them matches." + (unless continue-condition (setq continue-condition t)) + `(with-current-buffer ,buffer + (let ((tokens (list))) + (while ,(append `(and (< (point) ,max-point)) + (list continue-condition) + `((not ,(if (functionp (eval delimiter-predicate)) + (list (eval delimiter-predicate) + '(car (last tokens))) + nil)))) + ,(append `(cond) + (mapcar + (lambda (handler) + `((looking-at ,(car (eval handler))) + (let ((token (,(cdr (eval handler)) + (match-string 0) + ,max-point))) + (unless (null token) + (if (null tokens) + (setq tokens (list token)) + (nconc tokens (list token))))))) + handler-list) + '((t (forward-char))))) + (push ,tree-type tokens)))) + +(eval-and-compile + ;; Because some of the handler macros are mutually dependent on each + ;; other, we need to wrap their definition in an eval-and-compile + ;; body. + + (phpinspect-defhandler + phpinspect--comma-handler "," + "Handler for comma tokens" + (defun phpinspect--munch-comma (comma &rest ignored) + (phpinspect-munch-token-without-attribs comma :comma))) + + (phpinspect-defhandler + phpinspect--word-handler "[A-Za-z_\\][\\A-Za-z_0-9]*" + "Handler for bareword tokens" + (defun phpinspect--munch-word (word &rest ignored) + (let ((length (length word))) + (forward-char length) + (set-text-properties 0 length nil word) + (list :word word)))) + + + (defun phpinspect--parse-annotation-parameters (parameter-amount) + (let (words) + (while (not (or (looking-at "\\*/") (= (length words) parameter-amount))) + (forward-char) + (cond ((looking-at (car (phpinspect--word-handler))) + (push (phpinspect--munch-word (match-string 0)) words)) + ((looking-at (car (phpinspect--variable-handler))) + (push (phpinspect--parse-variable (match-string 0)) words)))) + (nreverse words))) + + (phpinspect-defhandler + phpinspect--annotation-handler "@" + "Handler for in-comment @annotations" + (defun phpinspect--parse-var-annotation (start-token max-point) + (forward-char (length start-token)) + (if (looking-at (car (phpinspect--word-handler))) + (let ((annotation-name (match-string 0))) + (forward-char (length annotation-name)) + (cond ((string= annotation-name "var") + ;; The @var annotation accepts 2 parameters: + ;; the type and the $variable name + (append (list :var-annotation) + (phpinspect--parse-annotation-parameters 2))) + ((string= annotation-name "return") + ;; The @return annotation only accepts 1 word as parameter: + ;; The return type + (append (list :return-annotation) + (phpinspect--parse-annotation-parameters 1))) + ((string= annotation-name "param") + (let ((word-count 0)) + ;; The @param annotation accepts 2 parameters: + ;; The type of the param, and the param's $name + (append (list :param-annotation) + (phpinspect--parse-annotation-parameters 2)))) + (t + (list :annotation annotation-name)))) + (list :annotation nil)))) + + (phpinspect-defhandler + phpinspect--tag-handler "\\?>" + "Handler that discards any inline HTML it encounters" + (defun phpinspect--discard-html (start-token max-point) + (forward-char (length start-token)) + (or (re-search-forward "<\\?php\\|<\\?" nil t) + (goto-char max-point)) + (list :html))) + + (phpinspect-defhandler + phpinspect--comment-handler "#\\|//\\|/\\*" + "Handler for comments and doc blocks" + (defun phpinspect--parse-comment (start-token max-point) + (forward-char (length start-token)) + (cond ((string-match "/\\*" start-token) + (let ((doc-block (phpinspect-parse + (current-buffer) + :doc-block + ((phpinspect--annotation-handler) + (phpinspect--whitespace-handler)) + max-point + (not (looking-at "\\*/"))))) + (forward-char 2) + doc-block)) + (t + (let ((end-position (line-end-position))) + (phpinspect-parse + (current-buffer) + :comment + ((phpinspect--tag-handler)) + end-position + t + 'phpinspect-html-p)))))) + + (phpinspect-defhandler + phpinspect--variable-handler "\\$" + "Handler for tokens indicating reference to a variable" + (defun phpinspect--parse-variable (start-token &rest ignored) + (forward-char (length start-token)) + (if (looking-at (car (phpinspect--word-handler))) + (phpinspect-munch-token-without-attribs (match-string 0) :variable) + (list :variable nil)))) + + + (phpinspect-defhandler + phpinspect--whitespace-handler "[[:blank:]]+" + "Handler that discards whitespace" + (defun phpinspect--discard-whitespace (whitespace &rest ignored) + (forward-char (length whitespace)))) + + (phpinspect-defhandler + phpinspect--equals-handler "===?" + "Handler for strict and unstrict equality comparison tokens" + (defun phpinspect--munch-equals (equals &rest ignored) + (phpinspect-munch-token-without-attribs equals :equals))) + + (phpinspect-defhandler + phpinspect--assignment-operator-handler "[+-]?=" + "Handler for tokens indicating that an assignment is taking place" + (defun phpinspect--munch-assignment-operator (operator &rest ignored) + (phpinspect-munch-token-without-attribs operator :assignment))) + + (phpinspect-defhandler + phpinspect--statement-terminator-handler ";" + "Handler for statement terminators" + (defun phpinspect--munch-statement-terminator (terminator &rest ignored) + (phpinspect-munch-token-without-attribs terminator :terminator))) + + (phpinspect-defhandler + phpinspect--use-keyword-handler (concat "use" (phpinspect--word-end-regex)) + "Handler for the use keyword and tokens that might follow to give it meaning" + (defun phpinspect--parse-use (start-token max-point) + (setq start-token (phpinspect--strip-last-char start-token)) + (forward-char (length start-token)) + (phpinspect-parse + (current-buffer) + :use + ((phpinspect--word-handler) + (phpinspect--tag-handler) + (phpinspect--block-without-classes-handler) + (phpinspect--statement-terminator-handler)) + max-point + t + 'phpinspect-end-of-use-p))) + + (phpinspect-defhandler + phpinspect--attribute-reference-handler "->\\|::" + "Handler for references to object attributes, or static class attributes" + (defun phpinspect--parse-attribute-reference (start-token &rest ignored) + (forward-char (length start-token)) + (looking-at (car (phpinspect--word-handler))) + (let ((name (if (looking-at (car (phpinspect--word-handler))) + (phpinspect--munch-word (match-string 0)) + nil))) + (cond + ((string= start-token "::") + (list :static-attrib name)) + ((string= start-token "->") + (list :object-attrib name)))))) + + (phpinspect-defhandler + phpinspect--namespace-keyword-handler (concat "namespace" (phpinspect--word-end-regex)) + "Handler for the namespace keyword. This is a special one + because it is not always delimited by a block like classes or + functions. This handler parses the namespace declaration, and + then continues to parse subsequent tokens, only stopping when + either a block has been parsed or another namespace keyword has + been encountered." + (defun phpinspect--parse-namespace (start-token max-point) + "Nest all statements after a 'namespace' keyword in its own token" + (setq start-token (phpinspect--strip-last-char start-token)) + (forward-char (length start-token)) + (phpinspect--parse-with-handler-alist + (current-buffer) + :namespace + max-point + (not (looking-at (car (phpinspect--namespace-keyword-handler)))) + 'phpinspect-block-p))) + + (phpinspect-defhandler + phpinspect--const-keyword-handler (concat "const" (phpinspect--word-end-regex)) + "Handler for the const keyword" + (defun phpinspect--parse-const (start-token max-point) + (setq start-token (phpinspect--strip-last-char start-token)) + (forward-char (length start-token)) + (let ((token (phpinspect-parse + (current-buffer) + :const + ((phpinspect--word-handler) + (phpinspect--comment-handler) + (phpinspect--assignment-operator-handler) + (phpinspect--string-handler) + (phpinspect--array-handler) + (phpinspect--statement-terminator-handler)) + max-point + t + 'phpinspect-end-of-statement-p))) + + (when (phpinspect-incomplete-token-p (car (last token))) + (setcar token :incomplete-const)) + token))) + + (phpinspect-defhandler + phpinspect--string-handler "\"\\|'" + "Handler for strings" + (defun phpinspect--parse-string (start-token &rest ignored) + (list :string (phpinspect--munch-string start-token)))) + + (phpinspect-defhandler + phpinspect--block-without-classes-handler "{" + "Handler for code blocks that cannot contain classes" + (defun phpinspect--parse-block-without-classes (start-token max-point) + (forward-char (length start-token)) + (let* ((complete-block nil) + (parsed (phpinspect-parse + (current-buffer) + :block + ((phpinspect--array-handler) + (phpinspect--tag-handler) + (phpinspect--equals-handler) + (phpinspect--list-handler) + (phpinspect--comma-handler) + (phpinspect--attribute-reference-handler) + (phpinspect--variable-handler) + (phpinspect--assignment-operator-handler) + (phpinspect--whitespace-handler) + (phpinspect--scope-keyword-handler) + (phpinspect--static-keyword-handler) + (phpinspect--const-keyword-handler) + (phpinspect--use-keyword-handler) + (phpinspect--function-keyword-handler) + (phpinspect--word-handler) + (phpinspect--statement-terminator-handler) + (phpinspect--here-doc-handler) + (phpinspect--string-handler) + (phpinspect--comment-handler) + (phpinspect--block-handler)) + max-point + (not (and (char-equal (char-after) ?}) (setq complete-block t)))))) + (if complete-block + (forward-char) + (setcar parsed :incomplete-block)) + parsed))) + + + (phpinspect-defhandler + phpinspect--block-handler "{" + "Handler for code blocks" + (defun phpinspect--parse-block (start-token max-point) + (forward-char (length start-token)) + (let* ((complete-block nil) + (parsed (phpinspect--parse-with-handler-alist + (current-buffer) + :block + max-point + ;; When we encounter a closing brace for this + ;; block, we can mark the block as complete. + (not (and (char-equal (char-after) ?}) (setq complete-block t)))))) + + (if complete-block + ;; After meeting the char-after requirement above, we need to move + ;; one char forward to prevent parent-blocks from exiting because + ;; of the same char. + (forward-char) + (setcar parsed :incomplete-block)) + parsed))) + + (phpinspect-defhandler + phpinspect--here-doc-handler "<<<" + "Handler for heredocs" + (defun phpinspect--discard-heredoc (start-token point-max) + (forward-char (length start-token)) + (if (looking-at "[A-Za-z0-9'\"\\_]+") + (re-search-forward (concat "^" (regexp-quote (match-string 0))) nil t)) + (list :here-doc))) + + + (defun phpinspect--munch-string (start-token) + (forward-char (length start-token)) + (let ((start-point (point))) + (cond ((looking-at start-token) + (forward-char) + "") + ((looking-at (concat "\\([\\][\\]\\)+" (regexp-quote start-token))) + (let ((match (match-string 0))) + (forward-char (length match)) + (buffer-substring-no-properties start-point + (+ start-point (- (length match) + (length start-token)))))) + (t + (re-search-forward (format "\\([^\\]\\([\\][\\]\\)+\\|[^\\]\\)%s" + (regexp-quote start-token)) + nil t) + (buffer-substring-no-properties start-point (- (point) 1)))))) + + (phpinspect-defhandler + phpinspect--list-handler "(" + "Handler for php syntactic lists (Note: this does not include + datatypes like arrays, merely lists that are of a syntactic + nature like argument lists" + (defun phpinspect--parse-list (start-token max-point) + (forward-char (length start-token)) + (let* ((complete-list nil) + (php-list (phpinspect--parse-with-handler-alist + (current-buffer) + :list + max-point + (not (and (char-equal (char-after) ?\)) (setq complete-list t)))))) + + (if complete-list + ;; Prevent parent-lists (if any) from exiting by skipping over the + ;; ")" character + (forward-char) + (setcar php-list :incomplete-list)) + php-list))) + + (phpinspect-defhandler + phpinspect--function-keyword-handler (concat "function" (phpinspect--word-end-regex)) + "Handler for the function keyword and tokens that follow to give it meaning" + (defun phpinspect--parse-function (start-token max-point) + (setq start-token (phpinspect--strip-last-char start-token)) + (let ((declaration (phpinspect-parse + (current-buffer) + :declaration + ((phpinspect--comment-handler) + (phpinspect--word-handler) + (phpinspect--list-handler) + (phpinspect--statement-terminator-handler) + (phpinspect--tag-handler)) + max-point + (not (char-equal (char-after) ?{)) + 'phpinspect-end-of-statement-p))) + (if (phpinspect-end-of-statement-p (car (last declaration))) + (list :function declaration) + (list :function + declaration + (phpinspect--parse-block (char-to-string (char-after)) max-point)))))) + + (phpinspect-defhandler + phpinspect--scope-keyword-handler (mapconcat (lambda (word) + (concat word (phpinspect--word-end-regex))) + (list "public" "private" "protected") + "\\|") + "Handler for scope keywords" + (defun phpinspect--parse-scope (start-token max-point) + (setq start-token (phpinspect--strip-last-char start-token)) + (forward-char (length start-token)) + (phpinspect-parse + (current-buffer) + (cond ((string= start-token "public") :public) + ((string= start-token "private") :private) + ((string= start-token "protected") :protected)) + ((phpinspect--function-keyword-handler) + (phpinspect--static-keyword-handler) + (phpinspect--const-keyword-handler) + (phpinspect--variable-handler) + (phpinspect--here-doc-handler) + (phpinspect--string-handler) + (phpinspect--statement-terminator-handler) + (phpinspect--tag-handler) + (phpinspect--comment-handler)) + max-point + nil + 'phpinspect--scope-terminator-p))) + + (phpinspect-defhandler + phpinspect--static-keyword-handler (concat "static" (phpinspect--word-end-regex)) + "Handler for the static keyword" + (defun phpinspect--parse-static (start-token max-point) + (setq start-token (phpinspect--strip-last-char start-token)) + (forward-char (length start-token)) + (phpinspect-parse + (current-buffer) + :static + ((phpinspect--comment-handler) + (phpinspect--function-keyword-handler) + (phpinspect--variable-handler) + (phpinspect--array-handler) + (phpinspect--word-handler) + (phpinspect--statement-terminator-handler) + (phpinspect--tag-handler)) + max-point + t + 'phpinspect--static-terminator-p))) + + (phpinspect-defhandler + phpinspect--fat-arrow-handler "=>" + "Handler for the \"fat arrow\" in arrays and foreach expressions" + (defun phpinspect--munch-fat-arrow (arrow &rest ignored) + (phpinspect-munch-token-without-attribs arrow :fat-arrow))) + + (phpinspect-defhandler + phpinspect--array-handler "\\[\\|array(" + "Handler for arrays, in the bracketet as well as the list notation" + (defun phpinspect--parse-array (start-token max-point) + (forward-char (length start-token)) + (let* ((end-char (cond ((string= start-token "[") ?\]) + ((string= start-token "array(") ?\)))) + (end-char-reached nil) + (token (phpinspect-parse + (current-buffer) + :array + ((phpinspect--comment-handler) + (phpinspect--comma-handler) + (phpinspect--list-handler) + (phpinspect--here-doc-handler) + (phpinspect--string-handler) + (phpinspect--array-handler) + (phpinspect--variable-handler) + (phpinspect--attribute-reference-handler) + (phpinspect--word-handler) + (phpinspect--fat-arrow-handler)) + max-point + (not (and (char-equal (char-after) end-char) (setq end-char-reached t)))))) + + ;; Skip over the end char to prevent enclosing arrays or lists + ;; from terminating. + (if end-char-reached + (forward-char) + ;; Signal incompleteness when terminated because of max-point + (setcar token :incomplete-array)) + token))) + + (phpinspect-defhandler + phpinspect--class-keyword-handler (concat "\\(abstract\\|final\\|class\\|interface\\|trait\\)" + (phpinspect--word-end-regex)) + "Handler for the class keyword, and tokens that follow to define +the properties of the class" + (defun phpinspect--parse-class (start-token max-point) + (setq start-token (phpinspect--strip-last-char start-token)) + (list :class (phpinspect-parse + (current-buffer) + :declaration + ((phpinspect--comment-handler) + (phpinspect--word-handler) + (phpinspect--tag-handler)) + max-point + (not (char-equal (char-after) ?{))) + (phpinspect--parse-block-without-classes (char-to-string (char-after)) max-point)))) + + (defmacro phpinspect--parse-with-handler-alist + (buffer tree-type max-point &optional continue-condition delimiter-predicate) + (list 'phpinspect-parse + buffer + tree-type + '((phpinspect--array-handler) + (phpinspect--tag-handler) + (phpinspect--equals-handler) + (phpinspect--list-handler) + (phpinspect--comma-handler) + (phpinspect--attribute-reference-handler) + (phpinspect--variable-handler) + (phpinspect--assignment-operator-handler) + (phpinspect--whitespace-handler) + (phpinspect--scope-keyword-handler) + (phpinspect--static-keyword-handler) + (phpinspect--const-keyword-handler) + (phpinspect--use-keyword-handler) + (phpinspect--class-keyword-handler) + (phpinspect--function-keyword-handler) + (phpinspect--word-handler) + (phpinspect--statement-terminator-handler) + (phpinspect--here-doc-handler) + (phpinspect--string-handler) + (phpinspect--comment-handler) + (phpinspect--block-handler)) + max-point + continue-condition + delimiter-predicate)) + + + (defun phpinspect-parse-buffer-until-point (buffer point) + (with-current-buffer buffer + (save-excursion + (goto-char (point-min)) + (re-search-forward "<\\?php\\|<\\?" nil t) + (phpinspect-parse + (current-buffer) + :root + ((phpinspect--namespace-keyword-handler) + (phpinspect--array-handler) + (phpinspect--equals-handler) + (phpinspect--list-handler) + (phpinspect--comma-handler) + (phpinspect--attribute-reference-handler) + (phpinspect--variable-handler) + (phpinspect--assignment-operator-handler) + (phpinspect--whitespace-handler) + (phpinspect--scope-keyword-handler) + (phpinspect--static-keyword-handler) + (phpinspect--const-keyword-handler) + (phpinspect--use-keyword-handler) + (phpinspect--class-keyword-handler) + (phpinspect--function-keyword-handler) + (phpinspect--word-handler) + (phpinspect--statement-terminator-handler) + (phpinspect--here-doc-handler) + (phpinspect--string-handler) + (phpinspect--comment-handler) + (phpinspect--tag-handler) + (phpinspect--block-handler)) + point)))) + + ;; End of eval-and-compile body + ) + +(defsubst phpinspect--log (&rest args) + (when phpinspect--debug + (with-current-buffer (get-buffer-create "**phpinspect-logs**") + (goto-char (buffer-end 1)) + (insert (concat (apply 'format args) "\n"))))) + +(defun phpinspect-parse-current-buffer () + (phpinspect-parse-buffer-until-point + (current-buffer) + (point-max))) + + +(defsubst phpinspect-type-p (object type) + "Returns t if OBJECT is a token of type TYPE. +Type can be any of the token types returned by +`phpinspect-parse-buffer-until-point`" + (and (listp object) (eq (car object) type))) + +(defun phpinspect-html-p (token) + (phpinspect-type-p token :html)) + +(defun phpinspect-comma-p (token) + (phpinspect-type-p token :comma)) + +(defun phpinspect-end-of-statement-p (token) + (or (phpinspect-terminator-p token) + (phpinspect-comma-p token) + (phpinspect-html-p token))) + +(defun phpinspect-end-of-use-p (token) + (or (phpinspect-block-p token) + (phpinspect-end-of-statement-p token))) + + +(defun phpinspect-static-p (token) + (phpinspect-type-p token :static)) + +(defun phpinspect-const-p (token) + (or (phpinspect-type-p token :const) + (phpinspect-incomplete-const-p token))) + +(defun phpinspect-scope-p (token) + (or (phpinspect-type-p token :public) + (phpinspect-type-p token :private) + (phpinspect-type-p token :protected))) + +(defun phpinspect-block-p (token) + (or (phpinspect-type-p token :block) (phpinspect-incomplete-block-p token))) + +(defun phpinspect-incomplete-block-p (token) + (phpinspect-type-p token :incomplete-block)) + +(defun phpinspect-incomplete-class-p (token) + (and (phpinspect-class-p token) + (phpinspect-incomplete-block-p (car (last token))))) + +(defun phpinspect-incomplete-namespace-p (token) + (and (phpinspect-namespace-p token) + (or (phpinspect-incomplete-block-p (car (last token))) + (phpinspect-incomplete-class-p (car (last token)))))) + + +(defun phpinspect-function-p (token) + (phpinspect-type-p token :function)) + +(defun phpinspect-class-p (token) + (phpinspect-type-p token :class)) + +(defun phpinspect-incomplete-method-p (token) + (or (phpinspect-incomplete-function-p token) + (and (phpinspect-scope-p token) + (phpinspect-incomplete-function-p (car (last token)))) + (and (phpinspect-scope-p token) + (phpinspect-static-p (car (last token))) + (phpinspect-incomplete-function-p (car (last (car (last token)))))) + (and (phpinspect-scope-p token) + (phpinspect-function-p (car (last token)))))) + +(defun phpinspect-incomplete-function-p (token) + (and (phpinspect-function-p token) + (phpinspect-incomplete-block-p (car (last token))))) + +(defun phpinspect-list-p (token) + (or (phpinspect-type-p token :list) + (phpinspect-incomplete-list-p token))) + +(defun phpinspect-declaration-p (token) + (phpinspect-type-p token :declaration)) + +(defsubst phpinspect-assignment-p (token) + (phpinspect-type-p token :assignment)) + +(defun phpinspect-function-argument-list (php-func) + "Get the argument list of a function" + (seq-find 'phpinspect-list-p (seq-find 'phpinspect-declaration-p php-func nil) nil)) + +(defun phpinspect-variable-p (token) + (phpinspect-type-p token :variable)) + +(defun phpinspect-word-p (token) + (phpinspect-type-p token :word)) + +(defsubst phpinspect-incomplete-list-p (token) + (phpinspect-type-p token :incomplete-list)) + +(defsubst phpinspect-array-p (token) + (or (phpinspect-type-p token :array) + (phpinspect-incomplete-array-p token))) + +(defsubst phpinspect-incomplete-array-p (token) + (phpinspect-type-p token :incomplete-array)) + +(defsubst phpinspect-incomplete-const-p (token) + (phpinspect-type-p token :incomplete-const)) + +(defun phpinspect-incomplete-token-p (token) + (or (phpinspect-incomplete-class-p token) + (phpinspect-incomplete-block-p token) + (phpinspect-incomplete-list-p token) + (phpinspect-incomplete-array-p token) + (phpinspect-incomplete-const-p token) + (phpinspect-incomplete-function-p token) + (phpinspect-incomplete-method-p token) + (phpinspect-incomplete-namespace-p token))) + +(defun phpinspect-get-variable-type-in-function-arg-list (variable-name arg-list) + "Infer VARIABLE-NAME's type from typehints in +ARG-LIST. ARG-LIST should be a list token as returned by +`phpinspect--list-handler` (see also `phpinspect-list-p`)" + (let ((arg-no (seq-position arg-list + variable-name + (lambda (token variable-name) + (and (phpinspect-variable-p token) + (string= (car (last token)) variable-name)))))) + (if (and arg-no + (> arg-no 0)) + (let ((arg (elt arg-list (- arg-no 1)))) + (if (phpinspect-word-p arg) + (car (last arg)) + nil))))) + +(defun phpinspect--static-terminator-p (token) + (or (phpinspect-function-p token) + (phpinspect-end-of-statement-p token))) + +(defun phpinspect--scope-terminator-p (token) + (or (phpinspect-function-p token) + (phpinspect-end-of-statement-p token) + (phpinspect-const-p token) + (phpinspect-static-p token))) + +(defun phpinspect-namespace-keyword-p (token) + (and (phpinspect-word-p token) (string= (car (last token)) "namespace"))) + +(defun phpinspect-terminator-p (token) + (phpinspect-type-p token :terminator)) + +(defun phpinspect-use-keyword-p (token) + (and (phpinspect-word-p token) (string= (car (last token)) "use"))) + +(defun phpinspect-namespace-p (object) + (phpinspect-type-p object :namespace)) + +(defun phpinspect-use-p (object) + (phpinspect-type-p object :use)) + +(defun phpinspect-comment-p (token) + (phpinspect-type-p token :comment)) + +(defun phpinspect--split-list (predicate list) + (seq-reduce (let ((current-sublist)) + (lambda (result elt) + (if (funcall predicate elt) + (progn + (push elt current-sublist) + (push (nreverse current-sublist) result) + (setq current-sublist nil)) + (push elt current-sublist)) + result)) + list + nil)) + +(defun phpinspect-eldoc-function () + "An `eldoc-documentation-function` implementation for PHP files. + +Ignores `eldoc-argument-case` and `eldoc-echo-area-use-multiline-p`. + +TODO: + - Respect `eldoc-echo-area-use-multiline-p` + - This function is too big and has repetitive code. Split up and simplify. +" + (phpinspect--log "Starting eldoc function execution") + (let* ((token-tree (phpinspect-parse-buffer-until-point (current-buffer) (point))) + (namespace (phpinspect--find-innermost-incomplete-namespace token-tree)) + (incomplete-token (phpinspect--find-innermost-incomplete-token token-tree)) + (enclosing-token (phpinspect--find-token-enclosing-innermost-incomplete-token token-tree)) + (type-resolver) + (static)) + (when (and (phpinspect-incomplete-list-p incomplete-token) + enclosing-token + (or (phpinspect-object-attrib-p (car (last enclosing-token 2))) + (setq static (phpinspect-static-attrib-p (car (last enclosing-token 2)))))) + (if namespace + (setq type-resolver (phpinspect--make-type-resolver-for-namespace + namespace + token-tree)) + ;; else + (setq type-resolver (phpinspect--make-type-resolver + (phpinspect--uses-to-types + (seq-filter 'phpinspect-use-p token-tree))))) + (let* ((previous-statement (phpinspect--get-last-statement-in-token (butlast enclosing-token 2))) + (type-of-previous-statement + (phpinspect-get-type-of-derived-statement-in-token + previous-statement + (or namespace token-tree) + type-resolver)) + (method-name (cadr (cadar (last enclosing-token 2)))) + (class-index (and type-of-previous-statement + (phpinspect--get-or-create-index-for-class-file + type-of-previous-statement))) + (method (and class-index + (seq-find + (lambda (func) + (when func + (string= method-name + (phpinspect--function-name func)))) + (alist-get (if static 'static-methods 'methods) + class-index))))) + (phpinspect--log "Eldoc method name: %s" method-name) + (phpinspect--log "Eldoc method: %s" method) + (when method + (let ((arg-count -1) + (comma-count + (length (seq-filter 'phpinspect-comma-p incomplete-token)))) + (concat (truncate-string-to-width + (phpinspect--function-name method) 14) ": (" + (mapconcat + (lambda (arg) + (setq arg-count (+ arg-count 1)) + (if (= arg-count comma-count) + (propertize (concat + "$" + (truncate-string-to-width (car arg) 8) + " " + (phpinspect--format-type-name (or (cadr arg) ""))) + 'face 'eldoc-highlight-function-argument) + (concat "$" + (truncate-string-to-width (car arg) 8) + " " + (phpinspect--format-type-name (or (cadr arg) ""))))) + (phpinspect--function-arguments method) + ", ") + "): " + (phpinspect--format-type-name + (phpinspect--function-return-type method))))))))) + + +(defun phpinspect--find-assignments-in-token (token) + "Find any assignments that are in TOKEN, at top level or nested in blocks" + (let ((assignments) + (code-block) + (statements (phpinspect--split-list + (lambda (elt) + (or (phpinspect-terminator-p elt) + (phpinspect-block-p elt))) + token))) + (dolist (statement statements) + (cond ((seq-find 'phpinspect-assignment-p statement) + (phpinspect--log "Found assignment statement") + (push statement assignments)) + ((setq code-block (seq-find 'phpinspect-block-p statement)) + (setq assignments + (append + (phpinspect--find-assignments-in-token code-block) + assignments))))) + ;; return + (phpinspect--log "assignments: %s" assignments) + assignments)) + +(defun phpinspect-not-assignment-p (token) + "Inverse of applying `phpinspect-assignment-p to TOKEN." + (not (phpinspect-assignment-p token))) + +(defun phpinspect--find-assignments-of-variable-in-token (variable-name token) + "Find all assignments of variable VARIABLE-NAME in TOKEN." + (let ((variable-assignments) + (all-assignments (phpinspect--find-assignments-in-token token))) + (dolist (assignment all-assignments) + (if (or (member `(:variable ,variable-name) + (seq-take-while 'phpinspect-not-assignment-p + assignment)) + (and (phpinspect-list-p (car assignment)) + ((member `(:variable ,variable-name) (car assignment))))) + (push assignment variable-assignments))) + (nreverse variable-assignments))) + + + +(defun phpinspect-get-derived-statement-type-in-block + (statement php-block type-resolver &optional function-arg-list) + "Get type of derived STATEMENT in PHP-BLOCK using +TYPE-RESOLVER and FUNCTION-ARG-LIST. + +An example of a derived statement would be the following php code: +$variable->attribute->method(); +$variable->attribute; +$variable->method(); +self::method(); +ClassName::method(); +$variable = ClassName::method(); +$variable = $variable->method();" + ;; A derived statement can be an assignment itself. + (when (seq-find 'phpinspect-assignment-p statement) + (phpinspect--log "Derived statement is an assignment: %s" statement) + (setq statement (cdr (seq-drop-while 'phpinspect-not-assignment-p statement)))) + (phpinspect--log "Get derived statement type in block: %s" statement) + (let* ((first-token (pop statement)) + (current-token) + (previous-attribute-type)) + ;; No first token means we were passed an empty list. + (when (and first-token + (setq previous-attribute-type + ;; Statements that are only bare words can be something preceding + ;; a static attribute that is not passed to this function. For + ;; example "return self" could have prefixed another attribute + ;; that the caller is trying to derive. Therefore we just try to + ;; resolve the type of the last bare word in the statement. + (or (when (and (phpinspect-word-p first-token) + (seq-every-p 'phpinspect-word-p statement)) + (setq statement (last statement)) + (funcall type-resolver (cadr (pop statement)))) + + ;; Statements starting with a bare word can indicate a static + ;; method call. These could be statements with "return" or + ;; another bare-word at the start though, so we dop tokens + ;; from the statement until it starts with a static attribute + ;; refererence (::something in PHP code). + (when (phpinspect-word-p first-token) + (while (and first-token + (not (phpinspect-static-attrib-p + (car statement)))) + (setq first-token (pop statement))) + (funcall type-resolver (cadr first-token))) + + ;; No bare word, assume we're dealing with a variable. + (phpinspect-get-variable-type-in-block + (cadr first-token) + php-block + type-resolver + function-arg-list)))) + + (phpinspect--log "Statement: %s" statement) + (phpinspect--log "Starting attribute type: %s" previous-attribute-type) + (while (setq current-token (pop statement)) + (phpinspect--log "Current derived statement token: %s" current-token) + (cond ((phpinspect-object-attrib-p current-token) + (let ((attribute-word (cadr current-token))) + (when (phpinspect-word-p attribute-word) + (if (phpinspect-list-p (car statement)) + (progn + (pop statement) + (setq previous-attribute-type + (or + (phpinspect-get-cached-project-class-method-type + (phpinspect--get-project-root) + (funcall type-resolver previous-attribute-type) + (cadr attribute-word)) + previous-attribute-type))) + (setq previous-attribute-type + (or + (phpinspect-get-cached-project-class-variable-type + (phpinspect--get-project-root) + (funcall type-resolver previous-attribute-type) + (cadr attribute-word)) + previous-attribute-type)))))) + ((phpinspect-static-attrib-p current-token) + (let ((attribute-word (cadr current-token))) + (phpinspect--log "Found attribute word: %s" attribute-word) + (phpinspect--log "checking if next token is a list. Token: %s" + (car statement)) + (when (phpinspect-word-p attribute-word) + (if (phpinspect-list-p (car statement)) + (progn + (pop statement) + (setq previous-attribute-type + (or + (phpinspect-get-cached-project-class-static-method-type + (phpinspect--get-project-root) + (funcall type-resolver previous-attribute-type) + (cadr attribute-word)) + previous-attribute-type))))))))) + (phpinspect--log "Found derived type: %s" previous-attribute-type) + ;; Make sure to always return a FQN + (funcall type-resolver previous-attribute-type)))) + +;;;; +;; TODO: since we're passing type-resolver to all of the get-variable-type functions now, +;; we may as well always return FQNs in stead of relative type names. +;;;; +(defun phpinspect-get-variable-type-in-block + (variable-name php-block type-resolver &optional function-arg-list) + "Find the type of VARIABLE-NAME in PHP-BLOCK using TYPE-RESOLVER. + +Returns either a FQN or a relative type name, depending on +whether or not the root variable of the assignment value (right +side of assignment) can be found in FUNCTION-ARG-LIST. + +When PHP-BLOCK belongs to a function, supply FUNCTION-ARG-LIST to +resolve types of function argument variables." + (phpinspect--log "Looking for assignments of variable %s in php block" variable-name) + (if (string= variable-name "this") + (funcall type-resolver "self") + ;; else + (let* ((assignments + (phpinspect--find-assignments-of-variable-in-token variable-name php-block)) + (last-assignment (when assignments (car (last assignments)))) + (right-of-assignment (when assignments (cdr (seq-drop-while 'phpinspect-not-assignment-p + last-assignment))))) + (phpinspect--log "Assignments: %s" assignments) + (phpinspect--log "Last assignment: %s" right-of-assignment) + ;; When the right of an assignment is more than $variable; or "string";(so + ;; (:variable "variable") (:terminator ";") or (:string "string") (:terminator ";") + ;; in tokens), we're likely working with a derived assignment like $object->method() + ;; or $object->attribute + (cond ((and (phpinspect-word-p (car right-of-assignment)) + (string= (cadar right-of-assignment) "new")) + (funcall type-resolver (cadadr right-of-assignment))) + ((and (> (length right-of-assignment) 2) + (seq-find 'phpinspect-attrib-p right-of-assignment)) + (phpinspect--log "Variable was assigned with a derived statement") + (phpinspect-get-derived-statement-type-in-block right-of-assignment + php-block + type-resolver + function-arg-list)) + ;; If the right of an assignment is just $variable;, we can check if it is a + ;; function argument and otherwise recurse to find the type of that variable. + ((phpinspect-variable-p (car right-of-assignment)) + (phpinspect--log "Variable was assigned with the value of another variable") + (or (when function-arg-list + (phpinspect-get-variable-type-in-function-arg-list (cadar right-of-assignment) + function-arg-list)) + (phpinspect-get-variable-type-in-block (cadar right-of-assignment) + php-block + type-resolver + function-arg-list))) + ((not assignments) + (phpinspect--log "No assignments found for variable %s, checking function arguments" variable-name) + (phpinspect-get-variable-type-in-function-arg-list variable-name function-arg-list)))))) + + +(defun phpinspect-get-derived-statement-type-in-function (statement php-func type-resolver) + "Attempt to find the type of the php statement in php function +token `php-func`. If no type can be found, this function +evaluates to nil." + (let* ((arg-list (phpinspect-function-argument-list php-func))) + (phpinspect-get-derived-statement-type-in-block statement + (car (last php-func)) + type-resolver + arg-list))) + +(defun phpinspect-get-derived-statement-type-in-method (statement php-method type-resolver) + "Find the type of a statement in a php method." + (cond ((phpinspect-function-p php-method) + (phpinspect-get-derived-statement-type-in-function statement + php-method + type-resolver)) + ((phpinspect-scope-p php-method) + (phpinspect-get-derived-statement-type-in-method statement + (car (last php-method)) + type-resolver)) + ((phpinspect-static-p php-method) + (phpinspect-get-derived-statement-type-in-method statement + (car (last php-method)) + type-resolver)) + (t (error (concat "phpinspect-get-derived-statement-type-in-method: " + "php-method must be either a function or a scoped function"))))) + +(defun phpinspect-get-derived-statement-type-in-class (statement php-class type-resolver) + (phpinspect--log "recursing into class") + (let ((last-token-in-class-block (car (last (car (last php-class)))))) + (phpinspect--log "last token in class block: %s" last-token-in-class-block) + (cond ((phpinspect-incomplete-method-p last-token-in-class-block) + (phpinspect-get-derived-statement-type-in-method statement + (car (last (car (last php-class)))) + type-resolver)) + ;; We're dealing with a const statement, so not a block, but that doesn't matter + ;; much for the outcome. We're not trying to check syntax after all, just trying + ;; to guess the type of the statement as well as we can. + ((phpinspect-incomplete-const-p last-token-in-class-block) + (phpinspect--log "Found incomplete constant") + (phpinspect-get-derived-statement-type-in-block + statement + last-token-in-class-block + type-resolver))))) + +(defun phpinspect-get-type-of-derived-statement-in-token (statement token type-resolver) + (phpinspect--log "Looking for type of statement: %s in token: %s" statement token) + (cond ((phpinspect-namespace-p token) + (if (phpinspect-incomplete-block-p (car (last token))) + (phpinspect-get-type-of-derived-statement-in-token statement + (car (last (car (last token)))) + type-resolver) + (phpinspect-get-type-of-derived-statement-in-token statement + (car (last token)) + type-resolver))) + ((phpinspect-incomplete-block-p token) + (phpinspect-get-derived-statement-type-in-block statement token type-resolver)) + ((phpinspect-class-p token) + (phpinspect-get-derived-statement-type-in-class statement token type-resolver)) + ((phpinspect-incomplete-function-p token) + (phpinspect-get-derived-statement-type-in-function statement token type-resolver)))) + +(defun phpinspect--function-from-scope (scope) + (cond ((and (phpinspect-static-p (cadr scope)) + (phpinspect-function-p (caddr scope))) + (caddr scope)) + ((phpinspect-function-p (cadr scope)) + (cadr scope)) + (t nil))) + +(defun phpinspect--make-type-resolver (types &optional token-tree namespace) + "Little wrapper closure to pass around and resolve types with." + (unless namespace (setq namespace "")) + (let* ((inside-class + (if token-tree (or (phpinspect--find-innermost-incomplete-class token-tree) + (phpinspect--find-class-token token-tree)))) + (inside-class-name (if inside-class (phpinspect--get-class-name-from-token + inside-class)))) + (lambda (type) + (phpinspect--real-type + types + namespace + (if (or (string= type "self") (string= type "static")) + (progn + (phpinspect--log "Returning inside class name for %s : %s" + type inside-class-name) + + inside-class-name) + ;; else + type))))) + +(defun phpinspect--real-type (types namespace type) + "Get the FQN for `type`, using `types` as an alist to retrieve +said FQN's by class name" + (phpinspect--log "Resolving %s from namespace %s" type namespace) + ;; Absolute FQN + (cond ((string-match "^\\\\" type) + type) + + ;; Native type + ((member type phpinspect-native-types) + (concat "\\" type)) + + ;; Relative FQN + ((string-match "\\\\" type) + (concat "\\" namespace "\\" type)) + + ;; Clas|interface|trait name + (t (concat "\\" (or (assoc-default type types 'string=) (concat namespace "\\" type)))))) + +(defun phpinspect-var-annotation-p (token) + (phpinspect-type-p token :var-annotation)) + +(defun phpinspect-return-annotation-p (token) + (phpinspect-type-p token :return-annotation)) + +(defun phpinspect--index-function-arg-list (type-resolver arg-list) + (let ((arg-index) + (current-token) + (arg-list (copy-list arg-list))) + (while (setq current-token (pop arg-list)) + (cond ((and (phpinspect-word-p current-token) + (phpinspect-variable-p (car arg-list))) + (push `(,(cadr (pop arg-list)) + ,(funcall type-resolver (cadr current-token))) + arg-index)) + ((phpinspect-variable-p (car arg-list)) + (push `(,(cadr (pop arg-list)) + nil) + arg-index)))) + (nreverse arg-index))) + +(defun phpinspect--index-function-from-scope (type-resolver scope comment-before) + (let* ((php-func (cadr scope)) + (declaration (cadr php-func)) + (type (if (phpinspect-word-p (car (last declaration))) + (cadar (last declaration)) + ;; @return annotation + (cadadr + (seq-find 'phpinspect-return-annotation-p + comment-before))))) + (phpinspect--make-function + :scope `(,(car scope)) + :name (cadadr (cdr declaration)) + :return-type (when type (funcall type-resolver type)) + :arguments (phpinspect--index-function-arg-list + type-resolver + (phpinspect-function-argument-list php-func))))) + +(defun phpinspect--index-const-from-scope (scope) + (phpinspect--make-variable + :scope `(,(car scope)) + :name (cadr (cadr (cadr scope))))) + +(defun phpinspect--var-annotations-from-token (token) + (seq-filter 'phpinspect-var-annotation-p token)) + +(defun phpinspect--index-variable-from-scope (type-resolver scope comment-before) + "Index the variable inside `scope`." + (let* ((var-annotations (phpinspect--var-annotations-from-token comment-before)) + (variable-name (cadr (cadr scope))) + (type (if var-annotations + ;; Find the right annotation by variable name + (or (cadr (cadr (seq-find (lambda (annotation) + (string= (cadr (caddr annotation)) variable-name)) + var-annotations))) + ;; Give up and just use the last one encountered + (cadr (cadr (car (last var-annotations)))))))) + (phpinspect--log "calling resolver from index-variable-from-scope") + (phpinspect--make-variable + :name variable-name + :scope `(,(car scope)) + :type (if type (funcall type-resolver type))))) + +(defun phpinspect-doc-block-p (token) + (phpinspect-type-p token :doc-block)) + +(defun phpinspect--get-class-name-from-token (class-token) + (let ((subtoken (seq-find (lambda (word) + (and (phpinspect-word-p word) + (not (string-match + (concat "^" (car (phpinspect--class-keyword-handler))) + (concat (cadr word) " "))))) + (cadr class-token)))) + (cadr subtoken))) + +(defun phpinspect--index-class (type-resolver class) + "Create an alist with relevant attributes of a parsed class." + (phpinspect--log "INDEXING CLASS") + (let ((methods) + (static-methods) + (static-variables) + (variables) + (constants) + (extends) + (implements) + (class-name (phpinspect--get-class-name-from-token class)) + ;; Keep track of encountered comments to be able to use type + ;; annotations. + (comment-before)) + + ;; Find out what the class extends or implements + (let ((enc-extends nil) + (enc-implements nil)) + (dolist (word (cadr class)) + (if (phpinspect-word-p word) + (cond ((string= (cadr word) "extends") + (phpinspect--log "Extends was true") + (setq enc-extends t)) + ((string= (cadr word) "implements") + (setq enc-extends nil) + (phpinspect--log "Implements was true") + (setq enc-implements t)) + (t + (phpinspect--log "Calling Resolver from index-class on %s" (cadr word)) + (cond (enc-extends (push (funcall type-resolver (cadr word)) extends)) + (enc-implements (push (funcall type-resolver (cadr word)) implements)))))))) + + (dolist (token (caddr class)) + (cond ((phpinspect-scope-p token) + (cond ((phpinspect-const-p (cadr token)) + (push (phpinspect--index-const-from-scope token) constants)) + + ((phpinspect-variable-p (cadr token)) + (push (phpinspect--index-variable-from-scope type-resolver + token + comment-before) + variables)) + + ((phpinspect-static-p (cadr token)) + (cond ((phpinspect-function-p (cadadr token)) + (push (phpinspect--index-function-from-scope type-resolver + (list (car token) + (cadadr token)) + comment-before) + static-methods)) + + ((phpinspect-variable-p (cadadr token)) + (push (phpinspect--index-variable-from-scope type-resolver + (list (car token) + (cadadr token)) + comment-before) + static-variables)))) + (t + (push (phpinspect--index-function-from-scope type-resolver + token + comment-before) + methods)))) + + ((phpinspect-const-p token) + ;; Bare constants are always public + (push (phpinspect--index-const-from-scope (list :public token)) + constants)) + ((phpinspect-function-p token) + ;; Bare functions are always public + (push (phpinspect--index-function-from-scope type-resolver (list :public token) comment-before) + methods)) + ((phpinspect-doc-block-p token) + (setq comment-before token)) + + ;; Prevent comments from sticking around too long + (t (setq comment-before nil)))) + + ;; Dirty hack that assumes the constructor argument names to be the same as the object + ;; attributes' names. + ;;; + ;; TODO: actually check the types of the variables assigned to object attributes + (let ((constructor (seq-find (lambda (method) + (string= (phpinspect--function-name method) + "__construct")) + methods))) + (when constructor + (phpinspect--log "Constructor was found") + (dolist (variable variables) + (when (not (phpinspect--variable-type variable)) + (phpinspect--log "Looking for variable type") + (let ((constructor-parameter-type + (car (alist-get (phpinspect--variable-name variable) + (phpinspect--function-arguments constructor) + nil nil 'string=)))) + (if constructor-parameter-type + (setf (phpinspect--variable-type variable) + (funcall type-resolver constructor-parameter-type)))))))) + + (let ((class-name (funcall type-resolver class-name))) + `(,class-name . + (phpinspect--class + (methods . ,methods) + (class-name . ,class-name) + (static-methods . ,static-methods) + (static-variables . ,static-variables) + (variables . ,variables) + (constants . ,constants) + (extends . ,extends) + (implements . ,implements)))))) + + +(defun phpinspect--index-classes (types classes &optional namespace indexed) + "Index the class tokens in `classes`, using the types in `types` +as Fully Qualified names. `namespace` will be assumed the root +namespace if not provided" + (if classes + (let ((class (pop classes))) + (push (phpinspect--index-class + (phpinspect--make-type-resolver types class namespace) + class) + indexed) + (phpinspect--index-classes types classes namespace indexed)) + (nreverse indexed))) + +(defun phpinspect--use-to-type (use) + (let* ((fqn (cadr (cadr use))) + (type-name (if (and (phpinspect-word-p (caddr use)) + (string= "as" (cadr (caddr use)))) + (cadr (cadddr use)) + (progn (string-match "[^\\]+$" fqn) + (match-string 0 fqn))))) + (cons type-name fqn))) + +(defun phpinspect--uses-to-types (uses) + (mapcar 'phpinspect--use-to-type uses)) + +(defun phpinspect--index-namespace (namespace) + (phpinspect--index-classes + (phpinspect--uses-to-types (seq-filter 'phpinspect-use-p namespace)) + (seq-filter 'phpinspect-class-p namespace) + (cadadr namespace))) + +(defun phpinspect--index-namespaces (namespaces &optional indexed) + (if namespaces + (progn + (push (phpinspect--index-namespace (pop namespaces)) indexed) + (phpinspect--index-namespaces namespaces 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) + "Index TOKENS as returned by `phpinspect--parse-current-buffer`." + `(phpinspect--root-index + ,(append + (append '(classes) + (phpinspect--index-namespaces (seq-filter 'phpinspect-namespace-p tokens)) + (phpinspect--index-classes + (phpinspect--uses-to-types (seq-filter 'phpinspect-use-p tokens)) + (seq-filter 'phpinspect-class-p tokens)))) + (functions)) + ;; TODO: Implement function indexation + ) + +(defun phpinspect--get-or-create-index-for-class-file (class-fqn) + (phpinspect--log "Getting or creating") + (phpinspect-get-or-create-cached-project-class + (phpinspect--get-project-root) + class-fqn)) + +(defun phpinspect-get-or-create-cached-project-class (project-root class-fqn) + (let ((existing-index (phpinspect-get-cached-project-class + project-root + class-fqn))) + (or + existing-index + (progn + (let* ((class-file (phpinspect-get-class-filepath class-fqn)) + (visited-buffer (when class-file (find-buffer-visiting class-file))) + (new-index)) + + (phpinspect--log "FQN: %s" class-fqn) + (phpinspect--log "filepath: %s" class-file) + (when class-file + (if visited-buffer + (setq new-index (with-current-buffer visited-buffer + (phpinspect--index-current-buffer))) + (setq new-index (with-temp-buffer + (insert-file-contents-literally class-file) + (phpinspect--index-current-buffer)))) + (phpinspect--log "New index: %s" new-index) + (dolist (class (alist-get 'classes new-index)) + (when class + (phpinspect-cache-project-class + (phpinspect--get-project-root) + (cdr class)))) + (alist-get class-fqn (alist-get 'classes new-index) + nil + nil + 'string=))))))) + + +(defun phpinspect--index-current-buffer () + (phpinspect--index-tokens (phpinspect-parse-current-buffer))) + +(defun phpinspect-index-current-buffer () + "Index a PHP file for classes and the methods they have" + (phpinspect--index-tokens (phpinspect-parse-current-buffer))) + +(defun phpinspect--get-variables-for-class (buffer-classes class &optional static) + (let ((class-index (or (assoc-default class buffer-classes 'string=) + (phpinspect--get-or-create-index-for-class-file class)))) + (when class-index + (if static + (append (alist-get 'static-variables class-index) + (alist-get 'constants class-index)) + (alist-get 'variables class-index))))) + + +(defun phpinspect--get-methods-for-class (buffer-classes class &optional static) + "Extract all possible methods for a class from `buffer-classes` and the class index. +`buffer-classes` will be preferred because their data should be +more recent" + (let ((class-index (or (alist-get class buffer-classes nil nil 'string=) + (phpinspect--get-or-create-index-for-class-file class)))) + (phpinspect--log "Getting methods for class (%s)" class) + (phpinspect--log "index: %s" class-index) + (if class-index + ;; Use nreverse to give precedence to interfaces and abstract class method + ;; typehints and doc blocks. + ;; TODO: Merge this somehow with phpinspect-get-cached-project-class-methods + (nreverse + (append (alist-get (if static 'static-methods 'methods) class-index) + (apply 'append + (mapcar (lambda (inherit-class) + (phpinspect--log "Inherit class: %s" inherit-class) + (phpinspect--get-methods-for-class + buffer-classes + inherit-class + static)) + (append (alist-get 'extends class-index) + (alist-get 'implements class-index)))))) + ;; else + (phpinspect--log "Unable to complete for %s :(" class) nil))) + +(defun phpinspect--init-mode () + "Initialize the phpinspect minor mode for the current buffer." + + (make-variable-buffer-local 'company-backends) + (add-to-list 'company-backends 'phpinspect-company-backend) + + (make-variable-buffer-local 'eldoc-documentation-function) + (setq eldoc-documentation-function 'phpinspect-eldoc-function) + + (make-variable-buffer-local 'eldoc-message-commands) + (eldoc-add-command 'c-electric-paren) + (eldoc-add-command 'c-electric-backspace) + + (phpinspect--after-save-action) + (add-hook 'after-save-hook 'phpinspect--after-save-action nil 'local)) + +(defun phpinspect--after-save-action () + "Hook that should be run after saving a buffer that has +phpinspect-mode enabled. Indexes the entire buffer and updates +`phpinspect--buffer-index`. Merges the buffer index into the +project-wide index afterwards." + (when (and (boundp phpinspect-mode) phpinspect-mode) + (setq phpinspect--buffer-index (phpinspect--index-current-buffer)) + (dolist (class (alist-get 'classes phpinspect--buffer-index)) + (when class + (phpinspect-cache-project-class (phpinspect--get-project-root) + (cdr class)))))) + +(defun phpinspect--disable-mode () + "Clean up the buffer environment for the mode to be disabled." + (kill-local-variable 'phpinspect--buffer-index) + (kill-local-variable 'company-backends) + (kill-local-variable 'eldoc-documentation-function) + (kill-local-variable 'eldoc-message-commands)) + +(defun phpinspect--mode-function () + (if (and (boundp phpinspect-mode) phpinspect-mode) + (phpinspect--init-mode) + (phpinspect--disable-mode))) + +(define-minor-mode phpinspect-mode "Activate phpinspect-mode" + :after-hook (phpinspect--mode-function)) + +(defun phpinspect--find-innermost-incomplete-namespace (token) + (let ((last-token (car (last token)))) + (cond ((phpinspect-incomplete-namespace-p token) token) + ((phpinspect-incomplete-token-p last-token) + (phpinspect--find-innermost-incomplete-namespace last-token))))) + +(defun phpinspect--find-innermost-incomplete-block (token &optional last-block) + (when (phpinspect-incomplete-block-p token) + (setq last-block token)) + + (let ((last-token (car (last token)))) + (if (phpinspect-incomplete-token-p last-token) + (phpinspect--find-innermost-incomplete-block last-token last-block) + last-block))) + + +(defun phpinspect--find-class-token (token) + "Recurse into token tree until a class is found." + (let ((last-token (car (last token)))) + (cond ((phpinspect-class-p token) token) + (last-token + (phpinspect--find-class-token last-token))))) + +(defun phpinspect--find-token-enclosing-innermost-incomplete-token (token &optional enclosing-token) + "Like `phpinspect--find-innermost-incomplete-token` but returns + the enclosing incomplete token if there is one" + (let ((last-token (car (last token)))) + (if (phpinspect-incomplete-token-p last-token) + (phpinspect--find-token-enclosing-innermost-incomplete-token last-token token) + enclosing-token))) + + +(defun phpinspect--find-innermost-incomplete-class (token) + (let ((last-token (car (last token)))) + (cond ((phpinspect-incomplete-class-p token) token) + ((phpinspect-incomplete-token-p last-token) + (phpinspect--find-innermost-incomplete-class last-token))))) + +(defun phpinspect--find-innermost-incomplete-function (token) + (let ((last-token (car (last token)))) + (cond ((phpinspect-incomplete-function-p token) token) + ((phpinspect-incomplete-token-p last-token) + (phpinspect--find-innermost-incomplete-function last-token))))) + +(defun phpinspect--find-innermost-incomplete-token (token) + (phpinspect--log "Checking token %s" token) + (let ((last-token (car (last token)))) + (if (phpinspect-incomplete-token-p last-token) + (phpinspect--find-innermost-incomplete-token last-token) + token))) + +(defvar-local phpinspect--buffer-index nil + "The result of the last successfull parse + index action + executed by phpinspect for the current buffer") + +(defvar phpinspect-projects '() + "Currently active phpinspect projects and their buffers") + +(defun phpinspect--find-last-variable-position-in-token (token) + "Find the last variable that can be encountered in the top +level of a token. Nested variables are ignored." + (let ((i (length token))) + (while (and (not (= 0 i)) + (not (phpinspect-variable-p + (car (last token i))))) + (setq i (- i 1))) + + (if (not (= i 0))(- (length token) i)))) + +(defun phpinspect--make-method-lister (buffer-classes &optional static) + (lambda (fqn) + (phpinspect--get-methods-for-class buffer-classes fqn static))) + +(defun phpinspect--make-method-resolver (buffer-classes) + (lambda (class method-name) + (seq-find (lambda (method) + (string= (cadr method) method-name)) + (phpinspect--get-methods-for-class buffer-classes)))) + +(defsubst phpinspect-object-attrib-p (token) + (phpinspect-type-p token :object-attrib)) + +(defsubst phpinspect-static-attrib-p (token) + (phpinspect-type-p token :static-attrib)) + +(defsubst phpinspect-attrib-p (token) + (or (phpinspect-object-attrib-p token) + (phpinspect-static-attrib-p token))) + +(defun phpinspect--buffer-index (buffer) + (with-current-buffer buffer phpinspect--buffer-index)) + +(cl-defgeneric phpinspect--merge-indexes (index1 index2) + "Merge two phpinspect index types into one and return it") + +(cl-defmethod phpinspect--merge-indexes + ((class1 (head phpinspect--class)) + (class2 (head phpinspect--class))) + "Merge two indexed classes." + (let* ((class1-methods (alist-get 'methods (cdr class1))) + (class1-variables (alist-get 'variables (cdr class1)))) + (dolist (method (alist-get 'methods (cdr class2))) + (add-to-list 'class1-methods method)) + (setf (alist-get 'methods (cdr class1)) class1-methods) + + (dolist (variable (alist-get 'variables (cdr class2))) + (add-to-list 'class1-variables variable)) + (setf (alist-get 'variables (cdr class1)) class1-variables)) + class1) + +(cl-defmethod phpinspect--merge-indexes + ((index1 (head phpinspect--root-index)) + (index2 (head phpinspect--root-index))) + (let ((index1-classes (alist-get 'classes (cdr index1))) + (index2-classes (alist-get 'classes (cdr index2)))) + (dolist (class index2-classes) + (when class + (let* ((class-name (alist-get 'class-name (cdr class))) + (existing-class (alist-get class-name index1-classes nil nil 'string=))) + (if existing-class + (progn + (phpinspect--log "Found existing class in root index: %s" class-name) + (setcdr (assoc class-name index1-classes) + (phpinspect--merge-indexes existing-class (cdr class)))) + ;; else + (phpinspect--log "Didn't find existing class in root index: %s" class-name) + (push class index1-classes))))) + (setf (alist-get 'classes index1) index1-classes) + index1)) + +(defsubst phpinspect-not-variable-p (token) + (not (phpinspect-variable-p token))) + +(defun phpinspect--get-bare-class-name-from-fqn (fqn) + (car (last (split-string fqn "\\\\")))) + +(cl-defmethod phpinspect--make-completion + ((completion-candidate phpinspect--variable)) + (phpinspect--construct-completion + :value (phpinspect--variable-name completion-candidate) + :meta (phpinspect--variable-type completion-candidate) + :annotation (concat " " + (phpinspect--get-bare-class-name-from-fqn + (or (phpinspect--variable-type completion-candidate) + ""))) + :kind 'variable)) + +(cl-defstruct (phpinspect--completion-list + (:constructor phpinspect--make-completion-list)) + "Contains all data for a completion at point" + (completions nil + :type list + :documentation + "A list of completion strings") + (metadata (make-hash-table :size 20 :test 'equal) + :type hash-table + :documentation + "A hash-table with `phpinspect--completion` structures.")) + +(cl-defgeneric phpinspect--completion-list-add + (comp-list completion) + "Add a completion to a completion-list.") + +(cl-defmethod phpinspect--completion-list-add + ((comp-list phpinspect--completion-list) (completion phpinspect--completion)) + (when (not (gethash (phpinspect--completion-value completion) + (phpinspect--completion-list-metadata comp-list))) + (push (phpinspect--completion-value completion) + (phpinspect--completion-list-completions comp-list)) + (puthash (phpinspect--completion-value completion) + completion + (phpinspect--completion-list-metadata comp-list)))) + + +(defun phpinspect--suggest-attributes-at-point (token-tree incomplete-token &optional static) + (let* ((buffer-classes (phpinspect--merge-indexes + phpinspect--buffer-index + (phpinspect--index-tokens token-tree))) + (namespace (phpinspect--find-innermost-incomplete-namespace + token-tree)) + (type-resolver (phpinspect--make-type-resolver-for-namespace namespace token-tree)) + (method-lister (phpinspect--make-method-lister buffer-classes static))) + (let ((statement-type (phpinspect-get-type-of-derived-statement-in-token + (phpinspect--get-last-statement-in-token incomplete-token) + namespace + type-resolver))) + (when statement-type + (let ((completion-list (phpinspect--make-completion-list)) + (type (funcall type-resolver statement-type))) + (append (phpinspect--get-variables-for-class + buffer-classes + type + static) + (funcall method-lister type))))))) + +(defun phpinspect--make-type-resolver-for-namespace (namespace-token &optional token-tree) + (phpinspect--make-type-resolver + (phpinspect--uses-to-types + (seq-filter 'phpinspect-use-p namespace-token)) + token-tree + (cadadr namespace-token))) + +(defun phpinspect--get-last-statement-in-token (token) + (nreverse + (seq-take-while + (let ((keep-taking t) (last-test nil)) + (lambda (elt) + (when last-test + (setq keep-taking nil)) + (setq last-test (phpinspect-variable-p elt)) + (and keep-taking + (not (phpinspect-terminator-p elt)) + (listp elt)))) + (reverse token)))) + +(defun phpinspect--suggest-variables-at-point (token-tree token) + (let ((assignments (phpinspect--find-assignments-in-token + (if (phpinspect-incomplete-list-p token) + (phpinspect--find-innermost-incomplete-block token-tree) + token))) + (variables) + (func (phpinspect--find-innermost-incomplete-function token-tree))) + (dolist (assignment assignments) + (dolist (token assignment) + (when (phpinspect-variable-p token) + (push (phpinspect--make-variable + :name (cadr token) + :type "") + variables)))) + + (when func + (dolist (token (phpinspect-function-argument-list func)) + (when (phpinspect-variable-p token) + (push (phpinspect--make-variable + :name (cadr token) + :type "") + variables)))) + variables)) + +(defun phpinspect--suggest-at-point () + (let* ((token-tree (phpinspect-parse-buffer-until-point (current-buffer) (point))) + (incomplete-token (phpinspect--find-innermost-incomplete-token token-tree)) + (last-tokens (last incomplete-token 2))) + (cond ((and (phpinspect-object-attrib-p (car last-tokens)) + (phpinspect-word-p (cadr last-tokens))) + (phpinspect--log "word-attributes") + (phpinspect--suggest-attributes-at-point token-tree + incomplete-token)) + ((phpinspect-object-attrib-p (cadr last-tokens)) + (phpinspect--log "object-attributes") + (phpinspect--suggest-attributes-at-point token-tree incomplete-token)) + ((phpinspect-static-attrib-p (cadr last-tokens)) + (phpinspect--log "static-attributes") + (phpinspect--suggest-attributes-at-point token-tree incomplete-token t)) + ((phpinspect-variable-p (cadr last-tokens)) + (phpinspect--suggest-variables-at-point token-tree incomplete-token))))) + +(defvar phpinspect--last-completion-list nil + "Used internally to save metadata about completion options + between company backend calls") + +(defun phpinspect-company-backend (command &optional arg &rest ignored) + (interactive (list 'interactive)) + (cond + ((eq command 'interactive) + (company-begin-backend 'company-phpinspect-backend)) + ((eq command 'prefix) + (cond ((looking-back "->[A-Za-z_0-9-]*") + (let ((match (match-string 0))) + (substring match 2 (length match)))) + ((looking-back "::[A-Za-z_0-9-]*") + (let ((match (match-string 0))) + (substring match 2 (length match)))) + ((looking-back "\$[A-Za-z_0-9-]*") + (let ((match (match-string 0))) + (substring match 1 (length match)))))) + ((eq command 'post-completion) + (when (eq 'function (phpinspect--completion-kind + (gethash arg (phpinspect--completion-list-metadata + phpinspect--last-completion-list)))) + (insert "("))) + ((eq command 'candidates) + (let ((completion-list (phpinspect--make-completion-list)) + (candidates)) + (dolist (completion (phpinspect--suggest-at-point)) + (phpinspect--completion-list-add + completion-list + (phpinspect--make-completion completion))) + + (setq candidates + (seq-filter (lambda (completion) + (when completion + (string-match (concat "^" (regexp-quote arg)) + completion))) + (seq-uniq (phpinspect--completion-list-completions + completion-list) + 'string=))) + (setq phpinspect--last-completion-list completion-list) + candidates)) + ((eq command 'annotation) + (concat " " (phpinspect--completion-annotation + (gethash arg + (phpinspect--completion-list-metadata + phpinspect--last-completion-list))))) + ((eq command 'kind) + (phpinspect--completion-kind + (gethash arg (phpinspect--completion-list-metadata + phpinspect--last-completion-list)))) + ((eq command 'meta) + (phpinspect--completion-meta + (gethash arg + (phpinspect--completion-list-metadata phpinspect--last-completion-list)))))) + +(defvar phpinspect-cache () + "In-memory nested key-value store used for caching by +phpinspect") + +(cl-defstruct (phpinspect--cache (:constructor phpinspect--make-cache)) + (active-projects nil + :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 + "A `hash-table` with the root directories of projects +as keys and project caches as values.")) + +(cl-defstruct (phpinspect--project (:constructor phpinspect--make-project-cache)) + (class-index (make-hash-table :test 'equal :size 100 :rehash-size 40) + :type hash-table + :documentation + "A `hash-table` that contains all of the currently +indexed classes in the project")) + +(cl-defgeneric phpinspect--cache-getproject + ((cache phpinspect--cache) (project-name string)) + "Get project that is located in `project-root`.") + +(cl-defmethod phpinspect--cache-getproject + ((cache phpinspect--cache) (project-root string)) + (gethash project-root (phpinspect--cache-projects cache))) + +(cl-defgeneric phpinspect--cache-get-project-create + ((cache phpinspect--cache) (project-root string)) + "Get a project that is located in `project-root` from the cache. If no such project exists in the cache yet, it is created and 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) + (phpinspect--cache-projects cache)))) + +(cl-defgeneric phpinspect--project-add-class + ((project phpinspect--project) (class (head phpinspect--class))) + "Add an indexed class to a `phpinspect--project`") + +(cl-defmethod phpinspect--project-add-class + ((project phpinspect--project) (class (head phpinspect--class))) + (let* ((class-name (alist-get 'class-name (cdr class))) + (existing-class (gethash class-name + (phpinspect--project-class-index project)))) + (puthash class-name + (if existing-class + (phpinspect--merge-indexes existing-class class) + class) + (phpinspect--project-class-index project)))) + +(cl-defgeneric phpinspect--project-get-class + ((project phpinspect--project) (class-fqn string)) + "Get indexed class by name of CLASS-FQN stored in PROJECT") + +(cl-defmethod phpinspect--project-get-class + ((project phpinspect--project) (class-fqn string)) + (gethash class-fqn + (phpinspect--project-class-index project))) + +(defun phpinspect--get-or-create-global-cache () + (or phpinspect-cache + (setq phpinspect-cache (phpinspect--make-cache)))) + +(defsubst phpinspect-cache-project-class (project-root indexed-class) + (phpinspect--project-add-class + (phpinspect--cache-get-project-create (phpinspect--get-or-create-global-cache) + project-root) + indexed-class)) + +(defsubst phpinspect-get-cached-project-class (project-root class-fqn) + (phpinspect--project-get-class + (phpinspect--cache-get-project-create (phpinspect--get-or-create-global-cache) + project-root) + class-fqn)) + +(defsubst phpinspect-get-cached-project-class-method-type + (project-root class-fqn method-name) + (phpinspect--log "Getting cached project class method type for %s (%s::%s)" + project-root class-fqn method-name) + (let ((found-method + (seq-find (lambda (method) + (and (string= (phpinspect--function-name method) method-name) + (phpinspect--function-return-type method))) + (phpinspect-get-cached-project-class-methods + project-root + class-fqn)))) + (when found-method + (phpinspect--log "Found method: %s" found-method) + (phpinspect--function-return-type found-method)))) + +(defsubst phpinspect-get-cached-project-class-variable-type + (project-root class-fqn variable-name) + (let ((found-variable + (seq-find (lambda (variable) + (string= (phpinspect--variable-name variable) variable-name)) + (alist-get 'variables + (phpinspect-get-or-create-cached-project-class + project-root + class-fqn))))) + (when found-variable + (phpinspect--variable-type found-variable)))) + +(defun phpinspect-get-cached-project-class-methods + (project-root class-fqn &optional static) + (phpinspect--log "Getting cached project class methods for %s (%s)" + project-root class-fqn) + (let ((index (phpinspect-get-or-create-cached-project-class + project-root + class-fqn))) + (when index + (phpinspect--log "Retrieved class index, starting method collection %s (%s)" + project-root class-fqn) + ;; Use nreverse to give precedence to interface and abstract class return + ;; types. Those are usually more well documented. + (nreverse + (append (alist-get (if static 'static-methods 'methods) + index) + (apply 'append + (mapcar (lambda (class-fqn) + (phpinspect-get-cached-project-class-methods + project-root class-fqn static)) + (append + (alist-get 'extends index) + (alist-get 'implements index))))))))) + +(defsubst phpinspect-get-cached-project-class-static-method-type + (project-root class-fqn method-name) + (let* ((found-method + (seq-find (lambda (method) + (and (string= (phpinspect--function-name method) method-name) + (phpinspect--function-return-type method))) + (phpinspect-get-cached-project-class-methods + project-root + class-fqn + 'static)))) + (when found-method + (phpinspect--function-return-type found-method)))) + +(defun phpinspect-purge-cache () + (interactive) + (setq phpinspect-cache (phpinspect--make-cache))) + + +(defvar phpinspect-index-executable + (concat (file-name-directory + (or load-file-name + buffer-file-name)) + "/phpinspect-index.bash") + "The path to the exexutable file that indexes class file names + for phpinspect. Should normally be set to + \"phpinspect-index.bash\" in the source file directory.") + +(defun phpinspect--get-project-root () + (let ((project-root-slugs (split-string (php-project-get-root-dir) "/"))) + (expand-file-name (string-join + (if (member "vendor" project-root-slugs) + (seq-take-while (lambda (elt) (not (string= elt "vendor"))) + project-root-slugs) + project-root-slugs) + "/")))) + +;; Use statements +;;;###autoload +(defun phpinspect-fix-uses-interactive () "Add missing use statements to a php file" + (interactive) + (save-buffer) + (let* ((project-root (phpinspect--get-project-root)) + (phpinspect-json (shell-command-to-string + (format "cd %s && %s fxu --json %s" + (shell-quote-argument (phpinspect--get-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)))))) + +(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." + (if (string-match "^\\( ?\\*\\|/\\)" (thing-at-point 'line t)) + ((lambda () + (forward-line -1) + (phpinspect-goto-first-line-no-comment-up))))) + + + +(defun phpinspect-get-all-fqns (&optional fqn-file) + (unless fqn-file + (setq fqn-file "uses")) + (with-temp-buffer + (insert-file-contents-literally + (concat (phpinspect--get-project-root) "/.cache/phpinspect/" fqn-file)) + (split-string (buffer-string) (char-to-string ?\n)))) + +;;;###autoload +(defun phpinspect-find-class-file (class) + (interactive (list (completing-read "Class: " (phpinspect-get-all-fqns)))) + (find-file (phpinspect-get-class-filepath class))) + +(defun phpinspect-find-own-class-file (class) + (interactive (list (completing-read "Class: " (phpinspect-get-all-fqns "uses_own")))) + (find-file (phpinspect-get-class-filepath class))) + + +(defun phpinspect-get-class-filepath (class &optional index-new) + (phpinspect--log "%s" (phpinspect--get-project-root)) + (when (eq index-new 'index-new) + (with-temp-buffer + (call-process phpinspect-index-executable nil (current-buffer) nil "index" "--new"))) + (let* ((default-directory (phpinspect--get-project-root)) + (result (with-temp-buffer + (phpinspect--log "dir: %s" default-directory) + (phpinspect--log "class: %s" (string-remove-prefix "\\" class)) + (list (call-process phpinspect-index-executable + nil + (current-buffer) + nil + "fp" (string-remove-prefix "\\" class)) + (buffer-string))))) + (if (not (= (car result) 0)) + ;; 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))))))) + +(defun phpinspect-unique-strings (strings) + (seq-filter + (let ((last-line nil)) + (lambda (line) + (let ((return-line (unless (and last-line (string= last-line line)) + line))) + (setq last-line line) + return-line))) + strings)) + +(defun phpinspect-index-current-project () + (interactive) + (let* ((default-directory (phpinspect--get-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))))) + +(defun phpinspect-unique-lines () + (let ((unique-lines (phpinspect-unique-strings (split-string (buffer-string) "\n" nil nil)))) + (erase-buffer) + (insert (string-join unique-lines "\n")))) + +(provide 'phpinspect)