You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
854 lines
27 KiB
Bash
854 lines
27 KiB
Bash
#!/bin/bash
|
|
##
|
|
# phpinspect-index.bash - Resolve namespaces and fix missing use statements in your PHP
|
|
# scripts.
|
|
###
|
|
# This script is derived from phpns, an earlier project that had a much wider scope than
|
|
# just index files for phpinspect.el. Much of the code and command line argument options
|
|
# can be removed.
|
|
# TODO: remove whatever functionality is not required for phpinspect.el
|
|
|
|
# shellcheck disable=SC2155
|
|
declare CACHE_DIR=./.cache/phpinspect
|
|
declare INFO=1
|
|
|
|
# Cache locations
|
|
declare CLASSES="$CACHE_DIR/classes"
|
|
declare NAMESPACES="$CACHE_DIR/namespaces"
|
|
declare USES="$CACHE_DIR/uses"
|
|
declare USES_OWN="$CACHE_DIR/uses_own"
|
|
declare USES_LOOKUP="$CACHE_DIR/uses_lookup"
|
|
declare USES_LOOKUP_OWN="$CACHE_DIR/uses_lookup_own"
|
|
declare FILE_PATHS="$CACHE_DIR/file_paths"
|
|
declare NAMESPACE_FILE_PATHS="$CACHE_DIR/namespace_file_paths"
|
|
declare INDEXED="$CACHE_DIR/indexed"
|
|
|
|
[[ $DEBUG -eq 2 ]] && set -x
|
|
shopt -s extglob
|
|
shopt -so pipefail
|
|
|
|
read -rd '' USAGE <<'EOF'
|
|
phpns - Resolve namespaces and fix missing use statements in your PHP scripts.
|
|
|
|
USAGE:
|
|
phpns COMMAND [ ARGUMENTS ] [ OPTIONS ]
|
|
|
|
COMMANDS:
|
|
i, index Index the PHP project in the current directory
|
|
fu, find-use CLASS_NAME Echo the FQN of a class
|
|
fxu, fix-uses FILE Add needed use statements to FILE
|
|
cns, classes-in-namespace NAMESPACE Echo the classes that reside in NAMESPACE
|
|
fp, filepath FQN Echo the filepath of the class by the name of FQN.
|
|
|
|
TO BE IMPLEMENTED:
|
|
rmuu, remove-unneeded-uses FILE: Remove all use statements for classes that are not being used.
|
|
|
|
OPTIONS FOR ALL COMMANDS:
|
|
-s --silent Don't print info.
|
|
|
|
UNIQUE OPTIONS PER COMMAND:
|
|
index:
|
|
-d, --diff Show differences between the files in the index and the files in the project directory.
|
|
-N, --new Only index new files
|
|
find-use:
|
|
-j, --json Provide possible use FQN's as a json array.
|
|
-p, --prefer-own If there are matches inside the "src" dir, only use those.
|
|
-a, --auto-pick Use first encountered match, don't provide a choice.
|
|
-b. --bare Print FQN's without any additives.
|
|
fix-uses:
|
|
-j, --json Provide possible use FQN's per class as a json object with the class names as keys.
|
|
-p, --prefer-own If there are matches inside the "src" dir, only use those.
|
|
-a, --auto-pick Use first encountered match, for every class, don't provide a choice.
|
|
-o, --stdout Print to stdout in stead of printing to the selected file.
|
|
filepath: -
|
|
|
|
EOF
|
|
|
|
execute() {
|
|
declare command="$1" INFO="$INFO"
|
|
declare -a CONFIG=()
|
|
shift
|
|
|
|
if [[ $command == @(-h|--help|help) ]]; then
|
|
echo "$USAGE" >&2
|
|
exit 0
|
|
fi
|
|
|
|
if ! [[ -f ./composer.json ]] && ! [[ -d ./.git ]]; then
|
|
echo "No composer.json or .git file found, not in root of poject, exiting." >&2
|
|
exit 1
|
|
fi
|
|
|
|
case "$command" in
|
|
i | index)
|
|
handleArguments index "$@" || return $?
|
|
|
|
# The arguments to grep need to be dynamic here, because the diff option
|
|
# requires different arguments to be passed to grep.
|
|
declare -a grep_args=(
|
|
-H
|
|
'^\(class\|abstract[[:blank:]]\+class\|\(final[[:blank:]]\+\|/\*[[:blank:]]*final[[:blank:]]*\*/[[:blank:]]*\)class\|namespace\|interface\|trait\)[[:blank:]]\+[A-Za-z]\+'
|
|
--exclude-dir={.cache,var,bin}
|
|
--binary-files=without-match
|
|
)
|
|
|
|
# Only index new files
|
|
if [[ ${CONFIG[$INDEX_NEW]} == '--new' ]]; then
|
|
declare -a new_files=() deleted_files=()
|
|
|
|
# Extract new files from diff.
|
|
while IFS=':' read -ra diff_file; do
|
|
if [[ ${diff_file[0]} == '-' ]]; then
|
|
deleted_files=("${diff_file[1]}" "${deleted_files[@]}")
|
|
elif [[ ${diff_file[0]} == '+' ]]; then
|
|
new_files=("${diff_file[1]}" "${new_files[@]}")
|
|
fi
|
|
done < <(diffIndex)
|
|
|
|
# Inform the user if non-existent files were found. Right now the only
|
|
# way to fix this is to reindex entirely.
|
|
if [[ ${#deleted_files[@]} -gt 0 ]]; then
|
|
info "There are ${#deleted_files[@]} non-existent files in your index. Consider reindexing to prevent incorrect results."
|
|
info 'Some of these none existent files are:'
|
|
for i in {0..19}; do
|
|
[[ $i -ge ${#deleted_files[@]} ]] && break
|
|
infof ' - "%s"\n' "${deleted_files[$i]}"
|
|
done
|
|
fi
|
|
|
|
if [[ ${#new_files[@]} -eq 0 ]]; then
|
|
info 'No new files were found.'
|
|
return 0
|
|
else
|
|
info "${#new_files[@]} new files found to index."
|
|
fi
|
|
|
|
# To exclusively index new files, add the filenames to the arguments array
|
|
grep_args=("${grep_args[@]}" "${new_files[@]}")
|
|
elif [[ ${CONFIG[$INDEX_DIFF]} == '--diff' ]]; then
|
|
diffIndex
|
|
return $?
|
|
else
|
|
grep_args=("${grep_args[@]}" '-r' '--include=*.php')
|
|
fi
|
|
|
|
# Index matching files
|
|
grep -m 2 "${grep_args[@]}" | grep -v '^vendor/bin' | fillIndex
|
|
|
|
# Add non-matching files to the file with indexed files.
|
|
# This is necessary to be able to diff the index.
|
|
grep -L "${grep_args[@]}" | grep -v '^vendor/bin' >> "$INDEXED"
|
|
;;
|
|
fu | find-use)
|
|
checkCache
|
|
handleArguments find-use "$@" || return $?
|
|
declare use_path='' class_name="${CONFIG[$CLASS_NAME]}"
|
|
if [[ "$class_name" == @(array|string|float|int|void|mixed) ]]; then
|
|
infof 'Type "%s" is not a class, but a primitive type.\n' "$class_name"
|
|
return 1
|
|
fi
|
|
|
|
findUsePathForClass "$class_name"
|
|
;;
|
|
fxu | fix-uses)
|
|
checkCache
|
|
handleArguments fix-uses "$@" || return $?
|
|
|
|
declare file="${CONFIG[$FILE]}"
|
|
|
|
if ! [[ -f $file ]]; then
|
|
infof 'File "%s" does not exist or is not a regular file.\n' "$file"
|
|
|
|
return 1
|
|
elif [[ ${CONFIG[$STDOUT]} == '--stdout' ]]; then
|
|
fixMissingUseStatements "$file"
|
|
else
|
|
# shellcheck disable=SC2005
|
|
echo "$(fixMissingUseStatements "$file")" > "$file"
|
|
fi
|
|
;;
|
|
ns | namespace)
|
|
checkCache
|
|
declare file="$1"
|
|
|
|
# Try the index, if that doesn't work, attempt to extract the namespace from the file itself.
|
|
if ! grep "(?<=$file:).*" "$NAMESPACE_FILE_PATHS"; then
|
|
grep -Po '(?<=^namespace[[:blank:]])[A-Za-z_\\]+' "$file"
|
|
fi
|
|
;;
|
|
cns | classes-in-namespace)
|
|
handleArguments classes-in-namespace "$@" || return $?
|
|
checkCache
|
|
|
|
declare namespace="${CONFIG[$NAMESPACE]}\\"
|
|
debug "Checking for namespace $namespace"
|
|
|
|
awk -F ':' "/:${namespace//\\/\\\\}"'[^\\]+$/{ print $1; }' "$USES_LOOKUP"
|
|
;;
|
|
fp | filepath)
|
|
handleArguments filepath "$@" || return $?
|
|
checkCache
|
|
|
|
grep -Po "^.*(?=:${CONFIG[$CLASS_PATH]//\\/\\\\}$)" "$FILE_PATHS"
|
|
;;
|
|
*)
|
|
printf 'Command "%s" is not a valid subcommand.\n' "$command" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# shellcheck disable=SC2034
|
|
fixMissingUseStatements() {
|
|
declare check_uses='false' check_needs='false' file="$1" namespace="$2"
|
|
declare -A uses=() needs=() namespace=()
|
|
declare -a classes=()
|
|
|
|
classes=($(execute cns "$(execute ns "$file")"))
|
|
for class in "${classes[@]}"; do
|
|
namespace["$class"]='in_namespace'
|
|
done
|
|
|
|
findUsesAndNeeds < "$file"
|
|
addUseStatements "${!needs[@]}" < "$file"
|
|
}
|
|
|
|
findUsePathForClass() {
|
|
declare class="$1"
|
|
if [[ ${CONFIG[$PREFER_OWN]} == '--prefer-own' ]]; then
|
|
declare -a possibilities=($(grep -Po "(?<=^${CONFIG[$CLASS_NAME]}:).*" "$USES_LOOKUP_OWN"))
|
|
else
|
|
declare -a possibilities=($(grep -Po "(?<=^${CONFIG[$CLASS_NAME]}:).*" "$USES_LOOKUP"))
|
|
fi
|
|
|
|
if [[ ${#possibilities[@]} -eq 1 ]]; then
|
|
use_path="${possibilities[0]}"
|
|
debugf 'Single use path "%s" found' "${possibilities[0]}"
|
|
|
|
# Provide an escaped string for json output if requested.
|
|
[[ ${CONFIG[$JSON]} == '--json' ]] && printf -v use_path '"%s"' "${use_path//\\/\\\\}"
|
|
elif [[ ${#possibilities[@]} -eq 0 ]]; then
|
|
_handle_no_use
|
|
return $?
|
|
else
|
|
_handle_multiple_uses
|
|
fi
|
|
|
|
infof 'Found use statement for "%s"\n' "$use_path" >&2
|
|
if [[ ${CONFIG[$JSON]} == '--json' ]]; then
|
|
echo '['
|
|
echo "$use_path"
|
|
printf ']'
|
|
elif [[ ${CONFIG[$BARE]} ]]; then
|
|
echo "$use_path"
|
|
else
|
|
echo "use $use_path;"
|
|
fi
|
|
}
|
|
|
|
_handle_no_use() {
|
|
declare tried_index_new="$1"
|
|
|
|
if [[ $tried_index_new != true ]]; then
|
|
execute index --silent --new
|
|
execute fu "${CONFIG[@]}"
|
|
return $?
|
|
elif [[ ${CONFIG[$PREFER_OWN]} == '--prefer-own' ]]; then
|
|
CONFIG[$PREFER_OWN]=
|
|
execute fu "${CONFIG[@]}"
|
|
return $?
|
|
else
|
|
infof 'No match found for class "%s"\n' "$class_name" >&2
|
|
[[ ${CONFIG[$JSON]} == '--json' ]] && printf '[]'
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
_handle_multiple_uses() {
|
|
if [[ ${CONFIG[$AUTO_PICK]} == '--auto-pick' ]]; then
|
|
use_path="${possibilities[0]}"
|
|
|
|
return 0
|
|
elif [[ ${CONFIG[$BARE]} == '--bare' ]]; then
|
|
use_path="$(printf '%s\n' "${possibilities[@]}")"
|
|
|
|
return 0
|
|
elif [[ ${CONFIG[$JSON]} == '--json' ]]; then
|
|
use_path="$(
|
|
for i in "${!possibilities[@]}"; do
|
|
printf '"%s"' "${possibilities[$i]//\\/\\\\}"
|
|
[[ $i -lt $((${#possibilities[@]}-1)) ]] && printf ','
|
|
echo
|
|
done
|
|
)"
|
|
|
|
return 0
|
|
fi
|
|
|
|
infof 'Multiple matches for class "%s", please pick one.\n' "$class_name" >&2
|
|
select match in "${possibilities[@]}"; do
|
|
use_path="$match"
|
|
break
|
|
done < /dev/tty
|
|
}
|
|
|
|
addUseStatements() {
|
|
declare -a needs=("$@")
|
|
declare use_statements=''
|
|
if [[ ${CONFIG[$JSON]} == '--json' ]]; then
|
|
declare -i length="$((${#needs[@]}-1))" current=0
|
|
echo '{'
|
|
for needed in "${needs[@]}"; do
|
|
printf '"%s": ' "$needed"
|
|
execute fu --json "$needed" "${CONFIG[$PREFER_OWN]}" "${CONFIG[$AUTO_PICK]}"
|
|
[[ $((current++)) -lt $length ]] && printf ','
|
|
echo
|
|
done
|
|
echo '}'
|
|
|
|
return 0
|
|
fi
|
|
|
|
while IFS='' read -r line; do
|
|
echo "$line"
|
|
|
|
if [[ $line == namespace* ]]; then
|
|
IFS='' read -r line && echo "$line"
|
|
|
|
use_statements="$(
|
|
for needed in "${needs[@]}"; do
|
|
execute fu "$needed" "${CONFIG[$PREFER_OWN]}" "${CONFIG[$AUTO_PICK]}"
|
|
done | sort
|
|
)"
|
|
|
|
[[ -n $use_statements ]] && echo "$use_statements"
|
|
fi
|
|
done
|
|
|
|
declare -i added_uses=0
|
|
added_uses="$(echo -n "$use_statements" | wc -l)"
|
|
[[ -n $use_statements ]] && ((added_uses++))
|
|
info "$added_uses use statements added out of ${#needs[@]} needed types. Types that were needed:" >&2
|
|
infof ' - "%s"\n' "${needs[@]}" >&2
|
|
}
|
|
|
|
debug() {
|
|
if [[ $DEBUG -ge 1 ]]; then
|
|
echo "[DEBUG] => $1" >&2
|
|
fi
|
|
}
|
|
|
|
# shellcheck disable=SC2059
|
|
debugf() {
|
|
if [[ $DEBUG -ge 1 ]]; then
|
|
declare format_string="$1"
|
|
shift
|
|
printf "[DEBUG] => $format_string" "$@" >&2
|
|
fi
|
|
}
|
|
|
|
info() {
|
|
if [[ $INFO -eq 1 ]]; then
|
|
echo "[INFO] => $1" >&2
|
|
fi
|
|
}
|
|
|
|
# shellcheck disable=SC2059
|
|
infof() {
|
|
if [[ $INFO -eq 1 ]]; then
|
|
declare format_string="$1"
|
|
shift
|
|
printf "[INFO] => $format_string" "$@" >&2
|
|
fi
|
|
}
|
|
|
|
##
|
|
# Functions for parameter parsing
|
|
|
|
# Enum for config
|
|
declare -gri CLASS_NAME=0
|
|
declare -gri PREFER_OWN=1
|
|
declare -gri AUTO_PICK=2
|
|
declare -gri STDOUT=3
|
|
declare -gri JSON=4
|
|
declare -gri BARE=5
|
|
declare -gri WORD=6
|
|
declare -gri EXPAND_CLASSES=7
|
|
declare -gri NO_CLASSES=8
|
|
declare -gri NAMESPACE=9
|
|
declare -gri CLASS_PATH=10
|
|
declare -gri INDEX_DIFF=11
|
|
declare -gri NO_VENDOR=12 # Keep this around as it might be used later on
|
|
declare -gri INDEX_NEW=13
|
|
declare -gri FILE=14
|
|
|
|
handleArguments() {
|
|
declare -p CONFIG &>>/dev/null || return 1
|
|
declare command="$1"
|
|
shift
|
|
case "$command" in
|
|
find-use)
|
|
_handle_find_use_arguments "$@" || return $?
|
|
;;
|
|
fix-uses)
|
|
_handle_fix_uses_arguments "$@" || return $?
|
|
;;
|
|
index)
|
|
_handle_index_arguments "$@" || return $?
|
|
;;
|
|
classes-in-namespace)
|
|
_handle_classes_in_namespace_arguments "$@" || return $?
|
|
;;
|
|
filepath)
|
|
_handle_filepath_arguments "$@" || return $?
|
|
;;
|
|
*)
|
|
printf 'handleArguments (line %s): Unknown command "%s" passed.\n' "$(caller)" "$command">&2
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
_handle_filepath_arguments() {
|
|
declare arg="$1"
|
|
while shift; do
|
|
case "$arg" in
|
|
-s | --silent)
|
|
INFO=0
|
|
;;
|
|
--*)
|
|
printf 'Unknown option: "%s"\n' "${arg}" >&2
|
|
return 1
|
|
;;
|
|
-*)
|
|
if [[ ${#arg} -gt 2 ]]; then
|
|
|
|
declare -i i=1
|
|
while [[ $i -lt ${#arg} ]]; do
|
|
_handle_filepath_arguments "-${arg:$i:1}"
|
|
((i++))
|
|
done
|
|
else
|
|
printf 'Unknown option: "%s"\n' "${arg}" >&2
|
|
return 1
|
|
fi
|
|
;;
|
|
'')
|
|
:
|
|
;;
|
|
*)
|
|
if [[ -n ${CONFIG[$CLASS_PATH]} ]]; then
|
|
printf 'Unexpected argument: "%s"\n' "$arg" >&2
|
|
return 1
|
|
fi
|
|
CONFIG[$CLASS_PATH]="$arg"
|
|
esac
|
|
arg="$1"
|
|
done
|
|
}
|
|
|
|
_handle_classes_in_namespace_arguments() {
|
|
declare arg="$1"
|
|
while shift; do
|
|
case "$arg" in
|
|
-s | --silent)
|
|
INFO=0
|
|
;;
|
|
--*)
|
|
printf 'Unknown option: "%s"\n' "${arg}" >&2
|
|
return 1
|
|
;;
|
|
-*)
|
|
if [[ ${#arg} -gt 2 ]]; then
|
|
declare -i i=1
|
|
while [[ $i -lt ${#arg} ]]; do
|
|
_handle_classes_in_namespace_arguments "-${arg:$i:1}"
|
|
((i++))
|
|
done
|
|
else
|
|
printf 'Unknown option: "%s"\n' "${arg}" >&2
|
|
return 1
|
|
fi
|
|
;;
|
|
'')
|
|
:
|
|
;;
|
|
*)
|
|
if [[ -n ${CONFIG[$NAMESPACE]} ]]; then
|
|
printf 'Unexpected argument: "%s"\n' "$arg" >&2
|
|
return 1
|
|
fi
|
|
CONFIG[$NAMESPACE]="$arg"
|
|
esac
|
|
arg="$1"
|
|
done
|
|
}
|
|
|
|
_handle_index_arguments() {
|
|
declare arg="$1"
|
|
while shift; do
|
|
case "$arg" in
|
|
-s | --silent)
|
|
INFO=0
|
|
;;
|
|
-d | --diff)
|
|
CONFIG[$INDEX_DIFF]='--diff'
|
|
;;
|
|
-N | --new)
|
|
CONFIG[$INDEX_NEW]='--new'
|
|
;;
|
|
--*)
|
|
printf 'Unknown option: "%s"\n' "${arg}" >&2
|
|
return 1
|
|
;;
|
|
-*)
|
|
if [[ ${#arg} -gt 2 ]]; then
|
|
declare -i i=1
|
|
while [[ $i -lt ${#arg} ]]; do
|
|
_handle_index_arguments "-${arg:$i:1}"
|
|
((i++))
|
|
done
|
|
else
|
|
printf 'Unknown option: "%s"\n' "${arg}" >&2
|
|
return 1
|
|
fi
|
|
;;
|
|
*)
|
|
printf 'Unexpected argument: "%s"\n' "$arg" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
arg="$1"
|
|
done
|
|
}
|
|
|
|
_handle_fix_uses_arguments() {
|
|
declare arg="$1"
|
|
while shift; do
|
|
case "$arg" in
|
|
-s | --silent)
|
|
INFO=0
|
|
;;
|
|
-p | --prefer-own)
|
|
CONFIG[$PREFER_OWN]='--prefer-own'
|
|
;;
|
|
-a | --auto-pick)
|
|
CONFIG[$AUTO_PICK]='--auto-pick'
|
|
;;
|
|
-o | --stdout)
|
|
CONFIG[$STDOUT]='--stdout'
|
|
INFO=0
|
|
;;
|
|
-j | --json)
|
|
CONFIG[$STDOUT]='--stdout'
|
|
CONFIG[$JSON]='--json'
|
|
;;
|
|
--*)
|
|
printf 'Unknown option: "%s"\n' "${arg}" >&2
|
|
return 1
|
|
;;
|
|
-*)
|
|
if [[ ${#arg} -gt 2 ]]; then
|
|
declare -i i=1
|
|
while [[ $i -lt ${#arg} ]]; do
|
|
_handle_fix_uses_arguments "-${arg:$i:1}"
|
|
((i++))
|
|
done
|
|
else
|
|
printf 'Unknown option: "%s"\n' "${arg}" >&2
|
|
return 1
|
|
fi
|
|
;;
|
|
'')
|
|
:
|
|
;;
|
|
*)
|
|
if [[ -n ${CONFIG[$FILE]} ]]; then
|
|
printf 'Unexpected argument: "%s"\n' "$arg" >&2
|
|
return 1
|
|
fi
|
|
CONFIG[$FILE]="$arg"
|
|
esac
|
|
arg="$1"
|
|
done
|
|
}
|
|
|
|
# shellcheck disable=SC2034
|
|
_handle_find_use_arguments() {
|
|
declare arg="$1"
|
|
while shift; do
|
|
case "$arg" in
|
|
-s | --silent)
|
|
INFO=0
|
|
;;
|
|
-b | --bare)
|
|
CONFIG[$BARE]='--bare'
|
|
;;
|
|
-p | --prefer-own)
|
|
CONFIG[$PREFER_OWN]='--prefer-own'
|
|
;;
|
|
-a | --auto-pick)
|
|
CONFIG[$AUTO_PICK]='--auto-pick'
|
|
;;
|
|
-j | --json) CONFIG[$STDOUT]='--stdout'
|
|
CONFIG[$JSON]='--json'
|
|
INFO=0
|
|
;;
|
|
--*)
|
|
printf 'Unknown option: "%s"\n' "${arg}" >&2
|
|
return 1
|
|
;;
|
|
-*)
|
|
if [[ ${#arg} -gt 2 ]]; then
|
|
declare -i i=1
|
|
while [[ $i -lt ${#arg} ]]; do
|
|
_handle_find_use_arguments "-${arg:$i:1}"
|
|
((i++))
|
|
done
|
|
else
|
|
printf 'Unknown option: "%s"\n' "${arg}" >&2
|
|
return 1
|
|
fi
|
|
;;
|
|
'')
|
|
:
|
|
;;
|
|
*)
|
|
if [[ -n ${CONFIG[$CLASS_NAME]} ]]; then
|
|
printf 'Unexpected argument: "%s"\n' "$arg" >&2
|
|
return 1
|
|
fi
|
|
CONFIG[$CLASS_NAME]="$arg"
|
|
esac
|
|
arg="$1"
|
|
done
|
|
}
|
|
|
|
##
|
|
# This function outputs the difference between the files that are present in the
|
|
# index and the files that are present in the project directory. The output format is:
|
|
# +:NEW_FILE (**Not in index but exists on disk**)
|
|
# -:DELETED_FILE (**In index but does not exist on disk**)
|
|
##
|
|
diffIndex() {
|
|
diff --unchanged-line-format='' --new-line-format='+:%L' --old-line-format='-:%L' \
|
|
<(sort -u < "$INDEXED" | sed '/^[[:blank:]]*$/d') \
|
|
<(find ./ -name '*.php' -type f | sed 's!^\./\|^./\(var\|.cache\|vendor/bin\)/.\+$!!g; /^[[:blank:]]*$/d' | sort)
|
|
}
|
|
|
|
##
|
|
# This function reads the output of a grep command with the option -H or
|
|
# --with-filename enabled. The lines containing class and namespace declarations
|
|
# will be parsed and added to the index.
|
|
#
|
|
# shellcheck disable=SC2153
|
|
##
|
|
fillIndex() {
|
|
[[ -n $CACHE_DIR ]] || return 1
|
|
[[ -n $CLASSES ]] || return 1
|
|
[[ -n $NAMESPACES ]] || return 1
|
|
[[ -n $USES ]] || return 1
|
|
[[ -n $USES_LOOKUP ]] || return 1
|
|
[[ -n $USES_LOOKUP_OWN ]] || return 1
|
|
[[ -n $FILE_PATHS ]] || return 1
|
|
[[ -n $NAMESPACE_FILE_PATHS ]] || return 1
|
|
[[ -n $INDEXED ]] || return 1
|
|
|
|
[[ -d $CACHE_DIR ]] || mkdir -p "$CACHE_DIR"
|
|
|
|
# Clean up index files if not diffing.
|
|
if [[ ${CONFIG[$INDEX_NEW]} != '--new' ]]; then
|
|
echo > "$NAMESPACES"
|
|
echo > "$CLASSES"
|
|
echo > "$USES"
|
|
echo > "$USES_LOOKUP"
|
|
echo > "$FILE_PATHS"
|
|
echo > "$USES_OWN"
|
|
echo > "$USES_LOOKUP_OWN"
|
|
echo > "$NAMESPACE_FILE_PATHS"
|
|
echo > "$INDEXED"
|
|
fi
|
|
|
|
declare -A namespaces=() classes=()
|
|
while IFS=':' read -ra line; do
|
|
declare file="${line[0]}"
|
|
|
|
# Save the namespace or class to add to the FQN cache later on.
|
|
if [[ "${line[1]}" =~ (class|trait|interface)[[:blank:]]+([A-Za-z_]+) ]]; then
|
|
classes[$file]="${BASH_REMATCH[2]}"
|
|
elif [[ "${line[1]}" =~ namespace[[:blank:]]+([A-Za-z_\\]+) ]]; then
|
|
namespaces[$file]="${BASH_REMATCH[1]}"
|
|
else
|
|
debugf 'No class or namespace found in line "%s"' "${line[0]}"
|
|
fi
|
|
|
|
# Add filename to file with indexed filenames. This is required
|
|
# for diffing the index.
|
|
echo "$file" >> "$INDEXED"
|
|
|
|
if [[ $((++lines%500)) -eq 0 ]]; then
|
|
info "indexed $lines lines."
|
|
fi
|
|
done
|
|
|
|
# Fill up the index
|
|
declare -i uses=0
|
|
for file in "${!classes[@]}"; do
|
|
declare namespace="${namespaces[$file]}"
|
|
declare class="${classes[$file]}"
|
|
|
|
if [[ -z $class ]]; then
|
|
debugf 'Class is missing for file "%s"\n' "$file"
|
|
debugf 'Namespace: "%s"\n' "$namespace"
|
|
continue
|
|
fi
|
|
|
|
((uses++))
|
|
[[ $((uses%500)) -eq 0 ]] && info "Found FQN's for $uses classes."
|
|
|
|
echo "$namespace" >> "$NAMESPACES"
|
|
echo "$class" >> "$CLASSES"
|
|
echo "$namespace\\$class" >> "$USES"
|
|
echo "$class:$namespace\\$class" >> "$USES_LOOKUP"
|
|
echo "$file:$namespace\\$class" >> "$FILE_PATHS"
|
|
echo "$file:$namespace" >> "$NAMESPACE_FILE_PATHS"
|
|
|
|
if [[ $file != 'vendor/'* ]]; then
|
|
echo "$namespace\\$class" >> "$USES_OWN"
|
|
echo "$class:$namespace\\$class" >> "$USES_LOOKUP_OWN"
|
|
fi
|
|
|
|
done
|
|
|
|
# This keeps the index of class names unique, so that completing class names takes as little
|
|
# time as possible.
|
|
# Use echo and a subshell here to prevent changing the file before the command is done.
|
|
# shellcheck disable=SC2005
|
|
echo "$(sort -u < "$CLASSES")" > "$CLASSES"
|
|
|
|
# Ditto for the namespaces index
|
|
# shellcheck disable=SC2005
|
|
echo "$(sort -u < "$NAMESPACES")" > "$NAMESPACES"
|
|
|
|
info "Finished indexing. Indexed ${lines} lines and found FQN's for $uses classes." >&2
|
|
}
|
|
|
|
checkCache() {
|
|
if ! [[ -d "$CACHE_DIR" ]]; then
|
|
info "No cache dir found, indexing." >&2
|
|
execute index
|
|
fi
|
|
}
|
|
|
|
##
|
|
# Find use statements and needed classes in a file.
|
|
|
|
findUsesAndNeeds() {
|
|
declare -p needs &>>/dev/null || return 1
|
|
declare -p uses &>>/dev/null || return 1
|
|
# shellcheck disable=SC2154
|
|
declare -p namespace &>>/dev/null || return 1
|
|
|
|
while read -r line; do
|
|
[[ $line == namespace* ]] && check_uses='true'
|
|
if [[ $line == ?(@(abstract|final) )@(class|interface|trait)* ]]; then
|
|
check_uses='false'
|
|
check_needs='true'
|
|
|
|
read -ra line_array <<<"$line"
|
|
set -- "${line_array[@]}"
|
|
while shift && [[ "$1" != @(extends|implements) ]]; do :; done;
|
|
while shift && [[ -n $1 ]]; do
|
|
[[ $1 == 'implements' ]] && shift
|
|
[[ $1 == \\* ]] || _set_needed_if_not_used "$1"
|
|
done
|
|
fi
|
|
|
|
if $check_uses; then
|
|
if [[ $line == use* ]]; then
|
|
declare class_name="${line##*\\}"
|
|
[[ $class_name == *as* ]] && class_name="${class_name##*as }"
|
|
debug "Class name: $class_name"
|
|
class_name="${class_name%%[^a-zA-Z]*}"
|
|
uses["$class_name"]='used'
|
|
fi
|
|
fi
|
|
|
|
if $check_needs; then
|
|
if [[ $line == *function*([[:space:]])*([[:alnum:]_])\(* ]]; then
|
|
_check_function_needs "$line"
|
|
continue
|
|
fi
|
|
_check_needs "$line"
|
|
fi
|
|
done
|
|
}
|
|
|
|
_check_function_needs() {
|
|
# Strip everything up until function name and argument declaration.
|
|
declare line="${1#*function}" function_declaration="${1#*function}"
|
|
|
|
# Collect the entire argument declaration
|
|
while [[ $line != *'{'* ]] && read -r line; do
|
|
function_declaration="$function_declaration $line"
|
|
done
|
|
|
|
declare -a words=()
|
|
read -ra words <<<"$function_declaration"
|
|
for i in "${!words[@]}"; do
|
|
if [[ "${words[$i]}" =~ ^'$'[a-zA-Z_]+ ]]; then
|
|
declare prev_word="${words[$((i-1))]}"
|
|
if [[ $prev_word =~ ^([^\(]*\()?([A-Za-z]+)$ ]]; then
|
|
declare class_name="${BASH_REMATCH[2]}"
|
|
debugf 'Found parameter type "%s" for function "%s"\n' "$class_name" "$function_declaration"
|
|
_set_needed_if_not_used "$class_name"
|
|
fi
|
|
fi
|
|
done
|
|
if [[ "$function_declaration" =~ \):[[:space:]]+([a-zA-Z]+) ]]; then
|
|
declare class_name="${BASH_REMATCH[1]}"
|
|
debugf 'Found return type "%s" for function "%s"\n' "$class_name" "$function_declaration"
|
|
_set_needed_if_not_used "$class_name"
|
|
fi
|
|
}
|
|
|
|
_check_needs() {
|
|
declare line="$1" match=''
|
|
if _line_matches "$line"; then
|
|
declare class_name="${match//[^a-zA-Z]/}"
|
|
|
|
debugf 'Extracted type "%s" from line "%s". Entire match: "%s"\n' "$class_name" "$line" "${BASH_REMATCH[0]}"
|
|
_set_needed_if_not_used "$class_name"
|
|
|
|
line="${line/"${BASH_REMATCH[0]/}"}"
|
|
_check_needs "$line"
|
|
fi
|
|
}
|
|
|
|
# shellcheck disable=SC2049
|
|
_line_matches() {
|
|
if [[ $line =~ 'new'[[:space:]]+([^\\][A-Za-z]+)\( ]] \
|
|
|| [[ $line =~ 'instanceof'[[:space:]]+([A-Za-z]+) ]] \
|
|
|| [[ $line =~ catch[[:space:]]*\(([A-Za-z]+) ]] \
|
|
|| [[ $line =~ \*[[:blank:]]*@([A-Z][a-zA-Z]*) ]]; then
|
|
match="${BASH_REMATCH[1]}"
|
|
return $?
|
|
elif [[ $line =~ @(var|param|return|throws)[[:space:]]+([A-Za-z]+) ]] \
|
|
|| [[ $line =~ (^|[\(\[\{[:blank:]])([A-Za-z]+)'::' ]]; then
|
|
match="${BASH_REMATCH[2]}"
|
|
return $?
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
_set_needed_if_not_used() {
|
|
declare class_name="$1"
|
|
if [[ -z ${uses["$class_name"]} ]] \
|
|
&& [[ -z ${namespace["$class_name"]} ]] \
|
|
&& [[ "$class_name" != @(static|self|string|int|float|array|object|bool|mixed|parent|void) ]]; then
|
|
needs["$class_name"]='needed'
|
|
fi
|
|
}
|
|
|
|
execute "$@"
|