emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/phpinspect 681f13d4de 001/126: Initial commit: Move out


From: ELPA Syncer
Subject: [elpa] externals/phpinspect 681f13d4de 001/126: Initial commit: Move out of personal dotfile repository.
Date: Sat, 12 Aug 2023 00:58:35 -0400 (EDT)

branch: externals/phpinspect
commit 681f13d4deaaec44cabac3de0f885c75b19697cf
Author: Hugo Thunnissen <hugo@hugot.nl>
Commit: Hugo Thunnissen <hugo@hugot.nl>

    Initial commit: Move out of personal dotfile repository.
---
 README.md             |   26 +
 phpinspect-index.bash |  726 ++++++++++++++++
 phpinspect.el         | 2204 +++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 2956 insertions(+)

diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..afc6d2a9e6
--- /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 0000000000..ba4d2d77a2
--- /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 0000000000..1ce308f667
--- /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)



reply via email to

[Prev in Thread] Current Thread [Next in Thread]