#!/bin/bash
# This scipt lints files for common errors.
#
# For go files, it runs gofmt and go vet, and optionally golint and
# gocyclo, if they are installed.
#
# For shell files, it runs shfmt. If you don't have that installed, you can get
# it with:
#   curl -fsSLo shfmt https://github.com/mvdan/sh/releases/download/v1.3.0/shfmt_v1.3.0_linux_amd64
#   chmod +x shfmt
#
# With no arguments, it lints the current files staged
# for git commit.  Or you can pass it explicit filenames
# (or directories) and it will lint them.
#
# To use this script automatically, run:
#   ln -s ../../bin/lint .git/hooks/pre-commit

set -e

LINT_IGNORE_FILE=${LINT_IGNORE_FILE:-".lintignore"}

IGNORE_LINT_COMMENT=
IGNORE_SPELLINGS=
while true; do
    case "$1" in
        -nocomment)
            IGNORE_LINT_COMMENT=1
            shift 1
            ;;
        -notestpackage)
            # NOOP, still accepted for backwards compatibility
            shift 1
            ;;
        -ignorespelling)
            IGNORE_SPELLINGS="$2,$IGNORE_SPELLINGS"
            shift 2
            ;;
        *)
            break
            ;;
    esac
done

spell_check() {
    local filename="$1"
    local lint_result=0

    # misspell is completely optional.  If you don't like it
    # don't have it installed.
    if ! type misspell >/dev/null 2>&1; then
        return $lint_result
    fi

    if ! misspell -error -i "$IGNORE_SPELLINGS" "${filename}"; then
        lint_result=1
    fi

    return $lint_result
}

lint_go() {
    # This function is called on a whole directory containing Go files
    local filename="$1"
    local lint_result=0

    if [ -n "$(gofmt -s -l "${filename}")" ]; then
        lint_result=1
        echo "${filename}: run gofmt -s -w ${filename}"
    fi

    go vet "${filename}" || lint_result=$?

    # golint is completely optional.  If you don't like it
    # don't have it installed.
    if type golint >/dev/null 2>&1; then
        # golint doesn't set an exit code it seems
        if [ -z "$IGNORE_LINT_COMMENT" ]; then
            lintoutput=$(golint "${filename}")
        else
            lintoutput=$(golint "${filename}" | grep -vE 'comment|dot imports|ALL_CAPS')
        fi
        if [ -n "$lintoutput" ]; then
            lint_result=1
            echo "$lintoutput"
        fi
    fi

    # gocyclo is completely optional.  If you don't like it
    # don't have it installed.  Also never blocks a commit,
    # it just warns.
    if type gocyclo >/dev/null 2>&1; then
        gocyclo -over 25 "${filename}" | while read -r line; do
            echo "${filename}": higher than 25 cyclomatic complexity - "${line}"
        done
    fi

    return $lint_result
}

lint_sh() {
    local filename="$1"
    local lint_result=0

    # Skip shfmt validation, if not installed
    if type shfmt >/dev/null 2>&1; then
        if ! diff -u "${filename}" <(shfmt -i 4 "${filename}"); then
            lint_result=1
            echo "${filename}: run shfmt -i 4 -w ${filename}"
        fi
    fi

    # the shellcheck is completely optional.  If you don't like it
    # don't have it installed.
    if type shellcheck >/dev/null 2>&1; then
        shellcheck "${filename}" || lint_result=1
    fi

    return $lint_result
}

lint_tf() {
    local filename="$1"
    local lint_result=0

    if ! diff -u <(hclfmt "${filename}") "${filename}"; then
        lint_result=1
        echo "${filename}: run hclfmt -w ${filename}"
    fi

    return $lint_result
}

lint_md() {
    local filename="$1"
    local lint_result=0

    for i in '=======' '>>>>>>>'; do
        if grep -q "${i}" "${filename}"; then
            lint_result=1
            echo "${filename}: bad merge/rebase!"
        fi
    done

    return $lint_result
}

lint_py() {
    local filename="$1"
    local lint_result=0

    if yapf --diff "${filename}" | grep -qE '^[+-]'; then
        lint_result=1
        echo "${filename} needs reformatting. Run: yapf --in-place ${filename}"
    else
        # Only run flake8 if yapf passes, since they pick up a lot of similar issues
        flake8 "${filename}" || lint_result=1
    fi

    return $lint_result
}

lint() {
    filename="$1"
    ext="${filename##*\.}"
    local lint_result=0

    # Don't lint deleted files
    if [ ! -f "$filename" ]; then
        return
    fi

    # Don't lint specific files
    case "$(basename "${filename}")" in
        static.go) return ;;
        coverage.html) return ;;
        *.pb.go) return ;;
    esac

    mimetype=$(file --mime-type "${filename}" | awk '{print $2}')

    case "$mimetype.$ext" in
        text/x-shellscript.*) lint_sh "${filename}" || lint_result=1 ;;
        *.go) ;; # done at directory level
        *.tf) lint_tf "${filename}" || lint_result=1 ;;
        *.md) lint_md "${filename}" || lint_result=1 ;;
        *.py) lint_py "${filename}" || lint_result=1 ;;
    esac

    # we don't want to spell check tar balls, binaries, Makefile and json files
    case "$mimetype.$ext" in
        *.tar | *.gz | *.json) ;;
        *.req | *.key | *.pem | *.crt) ;;
        application/x-executable.*) ;;
        text/x-makefile.*) ;;
        *) spell_check "${filename}" || lint_result=1 ;;
    esac

    return $lint_result
}

lint_files() {
    local lint_result=0
    while read -r filename; do
        lint "${filename}" || lint_result=1
    done
    return $lint_result
}

matches_any() {
    local filename="$1"
    local patterns="$2"
    while read -r pattern; do
        # shellcheck disable=SC2053
        # Use the [[ operator without quotes on $pattern
        # in order to "glob" the provided filename:
        [[ "$filename" == $pattern ]] && return 0
    done <<<"$patterns"
    return 1
}

read_patterns() {
    local patterns_file="$1"
    if [ -n "$patterns_file" ] && [ -r "$patterns_file" ]; then
        sed '/^#.*$/d ; /^\s*$/d' "$patterns_file" # Remove blank lines and comments.
    fi
}

filter_out() {
    local patterns="$1"
    if [ -n "$patterns" ]; then
        local filtered_out=()
        while read -r filename; do
            matches_any "$filename" "$patterns" && filtered_out+=("$filename") || echo "$filename"
        done
        [ -n "$DEBUG" ] && echo >&2 "> Files filtered out (i.e. NOT linted):" && printf >&2 '%s\n' "${filtered_out[@]}"
    else
        cat # No patterns provided: simply propagate stdin to stdout.
    fi
}

lint_directory() {
    local dirname="$1"
    local lint_result=0
    matches_any "$dirname" "$PATTERNS" && return 0
    [ -n "$DEBUG" ] && echo >&2 "> Linting directory: $dirname"
    # This test is just checking if there are any Go files in the directory
    if compgen -G "$dirname/*.go" >/dev/null; then
        lint_go "${dirname}" || lint_result=1
    fi
    find "$dirname" -maxdepth 1 | filter_out "$PATTERNS" | lint_files || lint_result=1
    return $lint_result
}

lint_directories() {
    local lint_result=0
    while read -r dirname; do
        lint_directory "${dirname}" || lint_result=1
    done
    exit $lint_result
}

list_directories() {
    find "$@" \( -name vendor -o -name .git -o -name .cache -o -name .pkg \) -prune -o -type d
}

PATTERNS=$(read_patterns "$LINT_IGNORE_FILE")
[ -n "$DEBUG" ] && echo >&2 "> Filters:" && echo >&2 "$PATTERNS"

if [ $# = 1 ] && [ -f "$1" ]; then
    lint "$1"
else
    list_directories "$@" | lint_directories
fi
