#!/bin/bash
# Clang wrapper to inject the execution of a plugin and execute the infer frontend

# Initialization
PARENT=$(dirname "$0")
SCRIPT_DIR=$(cd "$PARENT" && pwd)
SCRIPT_DIR="${SCRIPT_DIR%/}"
BIN_DIR="${SCRIPT_DIR}/../../bin"
ETC_DIR="${SCRIPT_DIR}/../../etc"

#### Configuration ####
# path to the wrapped clang compiler to invoke
CLANG_COMPILER="${SCRIPT_DIR}/clang_wrapper"
# extension of the file containing the clang cmd intercepted
CMD_FILE_EXT=".sh"
# extenion of the file containing the output of the Infer Clang frontend
INFERCLANG_LOG_FILE_EXT=".astlog"
# path of the plugin to load in clang
CLANG_PLUGIN_REL_PATH="facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib"
PLUGIN_PATH="${SCRIPT_DIR}/../../../${CLANG_PLUGIN_REL_PATH}"
# name of the plugin to use
PLUGIN_NAME="BiniouASTExporter"
# output directory of the plugin
RESULTS_DIR="${FCP_RESULTS_DIR}"
# space-separated list of source file extensions to compile, where the plugin is needed
# e.g. EXTENSIONS="c cpp m mm" (*note* no dots needed)
EXTENSIONS="${FCP_EXTENSIONS}"
# this forces the wrapper to invoke get_standard_commandline_args to get
# a more precise clang command with all the arguments in the right place (slow)
USE_STD_CLANG_CMD="${FCP_USE_STD_CLANG_CMD}"
# this skips the creation of .o files
SYNTAX_ONLY="${FCP_RUN_SYNTAX_ONLY}"
# extra arguments to pass during the execution of the infer frontend
INFER_FRONTEND_ARGS=($FCP_INFER_FRONTEND_ARGS)
# this fails the execution of clang if the frontend fails
REPORT_FRONTEND_FAILURE="${FCP_REPORT_FRONTEND_FAILURE}"
# enable debug mode (to get more data saved to disk for future inspections)
DEBUG_MODE="${FCP_DEBUG_MODE}"
# specify where is located Apple's clang
APPLE_CLANG="${FCP_APPLE_CLANG}"

if [ -z "$RESULTS_DIR" ]; then
    echo '$FCP_RESULTS_DIR with the name of the output directory not provided.' > /dev/stderr
    exit 1
fi

if [ "${0%++}" != "$0" ]; then XX="++"; else XX=""; fi
CLANG_CMD=("${CLANG_COMPILER}${XX}" "$@")
CWD=$(pwd)
CWD="${CWD%/}"
[ ! -d "$RESULTS_DIR" ] && mkdir -p "$RESULTS_DIR"

# If no extensions provided, will use default ones (c, h, cc, cpp, m, mm)
if [ -z "$EXTENSIONS" ]; then
    EXTENSIONS="c h cc cpp m mm"
fi

# regular expression for grep to look for specific extensions
# (for example c,h,m files)
EXTENSIONS_REGEX="\.($(echo $EXTENSIONS | tr ' ' '|'))$"

# Functions
function get_option_argument {
    # retrieves the value passed to an argument of a clang command
    OPT="$1"
    shift
    while [ -n "$1" ] && [ "$1" != "$OPT" ]; do shift; done
    echo "$2"
}

function has_flag {
    # return if the given flag is part of the given command or not
    local FLAG="$1"
    shift
    while [ -n "$1" ] && [ "$1" != "$FLAG" ]; do shift; done
    [ -n "$1" ]; echo "$?"
}

# Main
INPUT_ARGUMENTS=("$@")
if [ -n "$USE_STD_CLANG_CMD" ]; then
    # this will run clang with the -### argument to get the command in a more
    # standard format.
    # Slow since it spawns clang as a separate process
    STD_CMD="$($CLANG_COMPILER$XX -### "$@" 2>&1 | grep '^[[:space:]]\"' -m 1)"
    # use sed to split all the arguments, and remove their surrounding double quotes
    SED_CMD=$(echo "$STD_CMD" | sed -e 's/" "/\'$'\n/g' -e 's/^[[:space:]]*"//' -e 's/"[[:space:]]*$//')
    IFS=$'\n'
    # create an array of arguments using newline as separator
    INPUT_ARGUMENTS=($SED_CMD)
    unset IFS
fi

OBJECT_FILENAME="$(get_option_argument "-o" "${INPUT_ARGUMENTS[@]}")"

if echo "$OBJECT_FILENAME" | grep -q "\.o$"
then
    # get the source file name
    if [ -n "$USE_STD_CLANG_CMD" ]; then
        # the source file is at the end of the command, match it with the wanted extensions
        SOURCE_FILE=$(echo ${INPUT_ARGUMENTS[${#INPUT_ARGUMENTS[@]} - 1]} \
            | grep -i -E "$EXTENSIONS_REGEX")
    else
        # in this case we search for the argument after -c, match it with the wanted extensions
        SOURCE_FILE=$(get_option_argument "-c" "${INPUT_ARGUMENTS[@]}" \
            | grep -i -E "$EXTENSIONS_REGEX")
    fi

    if [ -n "$SOURCE_FILE" ]
    then
        # (t7400979) this is a workaround to avoid that clang crashes when the -fmodules flag
        # and the YojsonASTExporter plugin are used. Since the -plugin argument disables
        # the generation of .o files, we invoke apple clang again to generate the expected
        # artifacts. This will keep xcodebuild plus all the sub-steps happy.
        if [ -n "$APPLE_CLANG" ]; then
            ADD_PLUGIN_FLAG="-plugin"
        else
            ADD_PLUGIN_FLAG="-add-plugin"
        fi
        if [ -z "$LLVM_MODE" ]; then
            ATTACH_PLUGIN="1"
        fi
        IFS=$'\n'
        if [ -n "$ATTACH_PLUGIN" ]; then
            EXTRA_ARGS=("-Xclang" "-load"
                "-Xclang" "${PLUGIN_PATH}"
                "-Xclang" "$ADD_PLUGIN_FLAG"
                "-Xclang" "${PLUGIN_NAME}"
                "-Xclang" "-plugin-arg-${PLUGIN_NAME}"
                "-Xclang" "-"
                "-Xclang" "-plugin-arg-${PLUGIN_NAME}"
                "-Xclang" "PREPEND_CURRENT_DIR=1")
        fi
        if [ -n "$SYNTAX_ONLY" ]; then
            EXTRA_ARGS+=("-fsyntax-only")
        fi
        unset IFS

        if [ -n "$LLVM_MODE" ]; then
            EXTRA_ARGS+=("-o" "-" "-g" "-S" "-emit-llvm")
        fi

        # using always the original clang command for several reasons:
        # - to avoid handling the presence/absence of -Xclang if the standard command is used
        # - to emit the same command that was captured by this wrapper
        # - to invoke the linker, whenever is needed
        CLANG_CMD+=("${EXTRA_ARGS[@]}")
    fi
fi

if [ -n "$ATTACH_PLUGIN" ] || [ -n "$LLVM_MODE" ]; then
    [[ "$SOURCE_FILE" = /* ]] || { SOURCE_FILE="${CWD}/$SOURCE_FILE"; }

    if [ -n "$LLVM_MODE" ]; then
        INFER_FRONTEND_CMD=(
            "${BIN_DIR}/InferLLVM"
            "-c" "$SOURCE_FILE"
            "-results_dir" "$RESULTS_DIR"
            "${INFER_FRONTEND_ARGS[@]}")
        INFER_FRONTEND_LOG_FILE="/dev/stdout"
    else
        FOBJC_ARC_FLAG=$(has_flag "-fobjc-arc" "${INPUT_ARGUMENTS[@]}")
        LANGUAGE=$(get_option_argument "-x" "${INPUT_ARGUMENTS[@]}")
        if [ -n "$LANGUAGE" ]; then INFER_FRONTEND_ARGS+=("-x" "$LANGUAGE"); fi
        if [ "$FOBJC_ARC_FLAG" == 0 ]; then INFER_FRONTEND_ARGS+=("-fobjc-arc"); fi

        INFER_FRONTEND_CMD=(
            "${BIN_DIR}/InferClang"
            "-c" "$SOURCE_FILE"
            "-results_dir" "$RESULTS_DIR"
            "${INFER_FRONTEND_ARGS[@]}")

        if [ -n "$DEBUG_MODE" ]; then
            # Emit the clang command with the extra args piped to InferClang
            echo "${CLANG_CMD[@]} " \
                 "| tee ${OBJECT_FILENAME}.biniou " \
                 "| ${INFER_FRONTEND_CMD[@]}" \
                 > "${OBJECT_FILENAME}${CMD_FILE_EXT}"
            echo "bdump -x -d ${ETC_DIR}/clang_ast.dict -w '!!DUMMY!!' ${OBJECT_FILENAME}.biniou " \
                 "> ${OBJECT_FILENAME}.bdump" \
                 >> "${OBJECT_FILENAME}${CMD_FILE_EXT}"
            # Emit the InferClang cmd used to run the frontend
            INFER_FRONTEND_LOG_FILE="${OBJECT_FILENAME}${INFERCLANG_LOG_FILE_EXT}"
            echo "${INFER_FRONTEND_CMD[@]}" > "$INFER_FRONTEND_LOG_FILE"
        else
            INFER_FRONTEND_LOG_FILE="/dev/null"
        fi
    fi

    # run clang and pipe its output to InferClang/InferLLVM, or flush it in case the latter crashes
    "${CLANG_CMD[@]}" | ("${INFER_FRONTEND_CMD[@]}" || { EC=$?; cat > /dev/null; exit $EC; }) >> "$INFER_FRONTEND_LOG_FILE" 2>&1
    STATUSES=("${PIPESTATUS[@]}")
    STATUS="${STATUSES[0]}"
    INFER_STATUS="${STATUSES[1]}"

    # if clang fails, then fail, otherwise, fail with the frontend's exitcode if required
    if [ "$STATUS" == 0 ] && [ -n "$REPORT_FRONTEND_FAILURE" ]; then
        STATUS="$INFER_STATUS"
    fi
else
    "${CLANG_CMD[@]}"
    STATUS=$?
fi

# run apple clang if required (and if any)
if [ -n "$APPLE_CLANG" ]; then
    "${APPLE_CLANG}$XX" "$@" || exit $?
fi

exit $STATUS