#!/bin/bash

# This script runs the Checker Framework's whole-program inference
# iteratively on a program, adding type annotations to the program, until the
# .jaif files from one iteration are the same as the .jaif files from the
# previous iteration (which means there is nothing new to infer anymore).

# To use this script, the $CHECKERFRAMEWORK variable must be set to the
# Checker Framework's directory. Also, the AFU's insert-annotations-to-source
# program must be on the search path (that is, in the PATH environment variable).

# This script receives as arguments:
# 0. Any number of cmd-line arguments to insert-annotations-to-source (optional).
# 1. Processor's name (in any form recognized by CF's javac -processor argument).
# 2. Classpath (target project's classpath).
# 3. Any number of extra processor arguments to be passed to the checker.
# 4. Any number of paths to .jaif files -- used as input (optional).
# 5. Any number of paths to .java files in a program.

# Example of usage:
# ./infer-and-annotate.sh "LockChecker,NullnessChecker" \
#     plume-util/build/libs/plume-util-all.jar \
#     $(find plume-util/src/main/java/ -name "*.java")

# In case of using this script for Android projects, the classpath must include
# paths to: android.jar, gen folder, all libs used, source code folder.
# The Android project must be built with "ant debug" before running this script.

# TODO: This script deletes all .unannotated files, including ones that could
# have been generated previously by another means other than using this script.
# We must decide if we want to make a backup of previously generated
# .unannotated files, or if we want to keep the first set of generated
# .unannotated files.

# Halts the script when a nonzero value is returned from a command.
set -e

# Path to directory that will contain .jaif files after running the CF
# with -Ainfer
WHOLE_PROGRAM_INFERENCE_DIR=build/whole-program-inference

# Path that will contain .class files after running CF's javac. This dir will
# be deleted after executing this script.
TEMP_DIR=build/temp-whole-program-inference-output

# Path to directory that will contain .jaif files from the previous iteration.
PREV_ITERATION_DIR=build/prev-whole-program-inference

debug=
interactive=
# For debugging.
# debug=1
# Require user confirmation before running each command.
# interactive=1

CHECKERBIN=$(dirname "$0")

insert_to_source_args=()
extra_args=()
java_files=()
jaif_files=()

# This function separates extra arguments passed to the checker from Java files
# received as arguments.
# TODO: Handle the following limitation: This function makes the assumption
# that every argument starts with a hyphen. It means one cannot pass arguments
# such as -processorpath and -source, which are followed by a value.
read_input() {

  # Collect command-line arguments that come before the preprocessor.
  # Assumes that every command line argument starts with a hyphen.
  for i in "$@"; do
    case "$1" in
      -*)
        insert_to_source_args+=("$1")
        shift
        ;;
      *)
        break
        ;;
    esac
  done

  # First two arguments are processor and classpath.
  processor=$1
  classpath=$2
  shift
  shift

  # shellcheck disable=SC2034
  for i in "$@"; do
    # This function makes the assumption that every extra argument
    # starts with a hyphen. The rest are .java/.jaif files.
    case "$1" in
      -*)
        extra_args+=("$1")
        ;;
      *.jaif)
        jaif_files+=("$1")
        ;;
      *.java)
        java_files+=("$1")
        ;;
    esac
    shift
  done
}

# Iteratively runs the Checker
infer_and_annotate() {
  mkdir -p $TEMP_DIR
  DIFF_JAIF=firstdiff
  # Create/clean whole-program-inference directory.
  rm -rf $WHOLE_PROGRAM_INFERENCE_DIR
  mkdir -p $WHOLE_PROGRAM_INFERENCE_DIR
  # If there are .jaif files as input, copy them.
  for file in "${jaif_files[@]}"; do
    cp "$file" "$WHOLE_PROGRAM_INFERENCE_DIR/"
  done

  # Perform inference and add annotations from .jaif to .java files until
  # $PREV_ITERATION_DIR has the same contents as $WHOLE_PROGRAM_INFERENCE_DIR.
  while [ "$DIFF_JAIF" != "" ]; do
    # Updates $PREV_ITERATION_DIR folder
    rm -rf $PREV_ITERATION_DIR
    mv $WHOLE_PROGRAM_INFERENCE_DIR $PREV_ITERATION_DIR
    mkdir -p $WHOLE_PROGRAM_INFERENCE_DIR

    # Runs CF's javac
    command="$CHECKERBIN/javac -d $TEMP_DIR/ -cp $classpath -processor $processor -Ainfer=jaifs -Awarns -Xmaxwarns 10000 ${extra_args[*]} ${java_files[*]}"
    echo "About to run: ${command}"
    if [ "$interactive" ]; then
      echo "Press any key to run command... "
      IFS="" read -r _
    fi
    "$CHECKERBIN"/javac -d "$TEMP_DIR/" -cp "$classpath" -processor "$processor" -Ainfer=jaifs -Awarns -Xmaxwarns 10000 "${extra_args[@]}" "${java_files[@]}" || true
    # Deletes .unannotated backup files. This is necessary otherwise the
    # insert-annotations-to-source tool will use this file instead of the
    # updated .java one.
    # See TODO about .unannotated file at the top of this file.
    for file in "${java_files[@]}"; do
      rm -f "${file}.unannotated"
    done
    if [ "$(find $WHOLE_PROGRAM_INFERENCE_DIR -prune -empty)" ]; then
      echo "No .jaif files found."
    else
      # There is at least one .jaif file, so insert annotations.
      new_jaif_files="$(find $WHOLE_PROGRAM_INFERENCE_DIR -name "*.jaif")"
      command="insert-annotations-to-source -cp $classpath ${insert_to_source_args[*]} -i $new_jaif_files ${java_files[*]}"
      echo "About to run: ${command}"
      # shellcheck disable=SC2086
      insert-annotations-to-source -cp $classpath "${insert_to_source_args[@]}" -i $new_jaif_files "${java_files[@]}"
    fi
    # Updates DIFF_JAIF variable.
    # diff returns exit-value 1 when there are differences between files.
    # When this happens, this script halts due to the "set -e"
    # in its header. To avoid this problem, we add the "|| true" below.
    DIFF_JAIF="$(diff -qr $PREV_ITERATION_DIR $WHOLE_PROGRAM_INFERENCE_DIR || true)"
  done
  if [ ! "$debug" ]; then
    clean
  fi
}

clean() {
  # It might be good to keep the final .jaif files.
  # rm -rf $WHOLE_PROGRAM_INFERENCE_DIR
  rm -rf $PREV_ITERATION_DIR
  rm -rf $TEMP_DIR
  # See TODO about .unannotated file at the top of this file.
  for file in "${java_files[@]}"; do
    rm -f "${file}.unannotated"
  done
}

# Main
if [ "$#" -lt 3 ]; then
  echo "Aborting infer-and-annotate.sh: Expected at least 3 arguments, received $#."
  echo "Received the following arguments: $*"
  exit 1
fi

read_input "$@"
infer_and_annotate
