#!/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"

#### 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-plugin/libtooling/build/FacebookClangPlugin.dylib"
PLUGIN_PATH="${SCRIPT_DIR}/../../../../${CLANG_PLUGIN_REL_PATH}"
# name of the plugin to use
PLUGIN_NAME="YojsonASTExporter"
# 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
        ATTACH_PLUGIN="1"
        IFS=$'\n'
        EXTRA_ARGS=("-Xclang" "-load"
            "-Xclang" "${PLUGIN_PATH}"
            "-Xclang" "$ADD_PLUGIN_FLAG"
            "-Xclang" "${PLUGIN_NAME}")
        if [ -n "$SYNTAX_ONLY" ]; then
            EXTRA_ARGS+=("-fsyntax-only")
        fi
        unset IFS

        # 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" ]; then
    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

    [[ "$SOURCE_FILE" = /* ]] || { SOURCE_FILE="${CWD}/$SOURCE_FILE"; }
    INFERCLANG_CMD=(
        "${BIN_DIR}/InferClang"
        "-c" "$SOURCE_FILE"
        "-results_dir" "$RESULTS_DIR"
        "${INFER_FRONTEND_ARGS[@]}")

    INFERCLANG_LOG_FILE="/dev/null"

    if [ -n "$DEBUG_MODE" ]; then
        # Emit the clang command with the extra args
        echo "${CLANG_CMD[@]}" > "${OBJECT_FILENAME}${CMD_FILE_EXT}"
        # Emit the InferClang cmd used to run the frontend
        INFERCLANG_LOG_FILE="${OBJECT_FILENAME}${INFERCLANG_LOG_FILE_EXT}"
        echo "${INFERCLANG_CMD[@]}" > "$INFERCLANG_LOG_FILE"
    fi

    export CLANG_FRONTEND_PLUGIN__PREPEND_CURRENT_DIR="1"
    # run clang and pipe its output to InferClang, or flush it in case the latter crashes
    "${CLANG_CMD[@]}" | ("${INFERCLANG_CMD[@]}" || { EC=$?; cat > /dev/null; exit $EC; }) >> "$INFERCLANG_LOG_FILE" 2>&1
    STATUSES=("${PIPESTATUS[@]}")
    STATUS="${STATUSES[0]}"
    INFERCLANG_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="$INFERCLANG_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
