#!/usr/bin/env bash

# Copyright (c) 2017-2023 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0
#
# Description: Central script to run all static checks.
#   This script should be called by all other repositories to ensure
#   there is only a single source of all static checks.

set -e

[[ -n "${DEBUG}" ]] && set -x

cidir=$(realpath "$(dirname "$0")")
# shellcheck source=/dev/null
source "${cidir}/common.bash"

# By default in Golang >= 1.16 GO111MODULE is set to "on",
# some subprojects in this repo may not support "go modules",
# set GO111MODULE to "auto" to enable module-aware mode only when
# a go.mod file is present in the current directory.
export GO111MODULE="auto"

# List of files to delete on exit
files_to_remove=()

script_name=${0##*/}

# Static check functions must follow the following naming conventions:
#

# All static check function names must match this pattern.
typeset -r check_func_regex="^static_check_"

# All architecture-specific static check functions must match this pattern.
typeset -r arch_func_regex="_arch_specific$"

repo=""
repo_path=""
specific_branch="false"
force="false"
branch=${branch:-main}

# Which static check functions to consider.
handle_funcs="all"

single_func_only="false"
list_only="false"

# number of seconds to wait for curl to check a URL
typeset url_check_timeout_secs="${url_check_timeout_secs:-60}"

# number of attempts that will be made to check an individual URL.
typeset url_check_max_tries="${url_check_max_tries:-3}"

typeset -A long_options

# Generated code
ignore_clh_generated_code="virtcontainers/pkg/cloud-hypervisor/client"

paths_to_skip=(
	"${ignore_clh_generated_code}"
	"vendor"
)

# Skip paths that are not statically checked
# $1 : List of paths to check, space separated list
# If you have a list in a bash array call in this way:
# list=$(skip_paths "${list[@]}")
# If you still want to use it as an array do:
# list=(${list})
skip_paths(){
	local list_param="${1}"
	[[ -z "${list_param}" ]] && return
	local list
	read -r -a list <<< "${list_param}"

	for p in "${paths_to_skip[@]}"; do
		new_list=()
		for l in "${list[@]}"; do
			if echo "${l}" | grep -qv "${p}"; then
				new_list=("${new_list[@]}" "${l}")
			fi
		done
		list=("${new_list[@]}")
	done
	echo "${list[@]}"
}


long_options=(
	[all]="Force checking of all changes, including files in the base branch"
	[branch]="Specify upstream branch to compare against (default '${branch}')"
	[docs]="Check document files"
	[dockerfiles]="Check dockerfiles"
	[files]="Check files"
	[force]="Force a skipped test to run"
	[golang]="Check '.go' files"
	[help]="Display usage statement"
	[json]="Check JSON files"
	[labels]="Check labels databases"
	[licenses]="Check licenses"
	[list]="List tests that would run"
	[no-arch]="Run/list all tests except architecture-specific ones"
	[only-arch]="Only run/list architecture-specific tests"
	[repo:]="Specify GitHub URL of repo to use (github.com/user/repo)"
	[repo-path:]="Specify path to repository to check (default: \$GOPATH/src/\$repo)"
	[scripts]="Check script files"
	[versions]="Check versions files"
	[xml]="Check XML files"
)

yamllint_cmd="yamllint"
have_yamllint_cmd=$(command -v "${yamllint_cmd}" || true)

chronic=chronic

# Disable chronic on OSX to avoid having to update the Travis config files
# for additional packages on that platform.
[[ "$(uname -s)" == "Darwin" ]] && chronic=

usage()
{
	cat <<EOF

Usage: ${script_name} help
       ${script_name} [options] [repo-name] [true]

Options:

EOF

	local option
	local description

	local long_option_names="${!long_options[*]}"

	# Sort space-separated list by converting to newline separated list
	# and back again.
	long_option_names=$(echo "${long_option_names}"|tr ' ' '\n'|sort|tr '\n' ' ')

	# Display long options
	for option in ${long_option_names}
	do
		description=${long_options[${option}]}

		# Remove any trailing colon which is for getopt(1) alone.
		option="${option%%:}"

		printf "    --%-10.10s # %s\n" "${option}" "${description}"
	done

	cat <<EOF

Parameters:

  help      : Show usage.
  repo-name : GitHub URL of repo to check in form "github.com/user/repo"
              (equivalent to "--repo \$URL").
  true      : Specify as "true" if testing a specific branch, else assume a
              PR branch (equivalent to "--all").

Notes:

- If no options are specified, all non-skipped tests will be run.
- Some required tools may be installed in \$GOPATH/bin, so you should ensure
  that it is in your \$PATH.

Examples:

- Run all tests on a specific branch (stable or main) of kata-containers repo:

  $ ${script_name} github.com/kata-containers/kata-containers true

- Auto-detect repository and run golang tests for current repository:

  $ ${script_name} --golang

- Run all tests on the kata-containers repository, forcing the tests to
  consider all files, not just those changed by a PR branch:

  $ ${script_name} github.com/kata-containers/kata-containers --all


EOF
}

# Calls die() if the specified function is not valid.
func_is_valid() {
	local name="$1"

	type -t "${name}" &>/dev/null || die "function '${name}' does not exist"
}

# Calls die() if the specified function is not valid or not a check function.
ensure_func_is_check_func() {
	local name="$1"

	func_is_valid "${name}"

	local ret
	{ echo "${name}" | grep -q "${check_func_regex}"; ret=$?; }

	[[ "${ret}" = 0 ]] || die "function '${name}' is not a check function"
}

# Returns "yes" if the specified function needs to run on all architectures,
# else "no".
func_is_arch_specific() {
	local name="$1"

	ensure_func_is_check_func "${name}"

	local ret
	{ echo "${name}" | grep -q "${arch_func_regex}"; ret=$?; }

	if [[ "${ret}" = 0 ]]; then
		echo "yes"
	else
		echo "no"
	fi
}

function remove_tmp_files() {
	rm -rf "${files_to_remove[@]}"
}

# Convert a golang package to a full path
pkg_to_path()
{
	local pkg="$1"

	go list -f '{{.Dir}}' "${pkg}"
}

# Check that chronic is installed, otherwise die.
need_chronic() {
	[[ -z "${chronic}" ]] && return
	# shellcheck disable=SC2034
	local first_word="${chronic%% *}"
	command -v chronic &>/dev/null || \
		die "chronic command not found. You must have it installed to run this check." \
		"Usually it is distributed with the 'moreutils' package of your Linux distribution."
}


static_check_go_arch_specific()
{
	local go_packages
	local submodule_packages
	local all_packages

	pushd "${repo_path}"

	# List of all golang packages found in all submodules
	#
	# These will be ignored: since they are references to other
	# repositories, we assume they are tested independently in their
	# repository so do not need to be re-tested here.
	submodule_packages=$(mktemp)
	git submodule -q foreach "go list ./..." | sort > "${submodule_packages}" || true

	# all packages
	all_packages=$(mktemp)
	go list ./... | sort > "${all_packages}" || true

	files_to_remove+=("${submodule_packages}" "${all_packages}")

	# List of packages to consider which is defined as:
	#
	#   "all packages" - "submodule packages"
	#
	# Note: the vendor filtering is required for versions of go older than 1.9
	go_packages=$(comm -3 "${all_packages}" "${submodule_packages}" || true)
	# shellcheck disable=SC2128
	go_packages=$(skip_paths "${go_packages[@]}")

	# No packages to test
	[[ -z "${go_packages}" ]] && popd && return

	local linter="golangci-lint"

	# Run golang checks
	if [[ ! "$(command -v "${linter}")" ]]
	then
		info "Installing ${linter}"

		local linter_url
		linter_url=$(get_test_version "languages.golangci-lint.url")
		local linter_version
		linter_version=$(get_test_version "languages.golangci-lint.version")

		info "Forcing ${linter} version ${linter_version}"
		# shellcheck disable=SC2046
		curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "$(go env GOPATH)/bin" "v${linter_version}"
		command -v "${linter}" &>/dev/null || \
			die "${linter} command not found. Ensure that \"\$GOPATH/bin\" is in your \$PATH."
	fi

	local linter_args="run -c ${cidir}/.golangci.yml"

	# Non-option arguments other than "./..." are
	# considered to be directories by $linter, not package names.
	# Hence, we need to obtain a list of package directories to check,
	# excluding any that relate to submodules.
	local dirs

	for pkg in ${go_packages}
	do
		path=$(pkg_to_path "${pkg}")

		makefile="${path}/Makefile"

		# perform a basic build since some repos generate code which
		# is required for the package to be buildable (and thus
		# checkable).
		[[ -f "${makefile}" ]] && (cd "${path}" && make)

		dirs+=" ${path}"
	done

	info "Running ${linter} checks on the following packages:\n"
	echo "${go_packages}"
	echo
	info "Package paths:\n"
	echo "${dirs}" | sed 's/^ *//g' | tr ' ' '\n'
	for d in ${dirs};do
		info "Running ${linter} on ${d}"
		(cd "${d}" && GO111MODULE=auto eval "${linter}" "${linter_args}" ".")
	done
	popd

}

# Install yamllint in the different Linux distributions
install_yamllint()
{
	package="yamllint"

	# shellcheck disable=SC2154
	case "${ID}" in
		centos|rhel) sudo yum -y install "${package}" ;;
		ubuntu) sudo apt-get -y install "${package}" ;;
		fedora) sudo dnf -y install "${package}" ;;
		*) die "Please install yamllint on ${ID}" ;;
	esac

	have_yamllint_cmd=$(command -v "${yamllint_cmd}" || true)

	if [[ -z "${have_yamllint_cmd}" ]]; then
		info "Cannot install ${package}" && return
	fi
}

# Check the "versions database".
#
# Some repositories use a versions database to maintain version information
# about non-golang dependencies. If found, check it for validity.
static_check_versions()
{
	local db="versions.yaml"

	if [[ -z "${have_yamllint_cmd}" ]]; then
		info "Installing yamllint"
		install_yamllint
	fi

	pushd "${repo_path}"

	[[ ! -e "${db}" ]] && popd && return

	if [[ -n "${have_yamllint_cmd}" ]]; then
		eval "${yamllint_cmd}" "${db}"
	else
		info "Cannot check versions as ${yamllint_cmd} not available"
	fi

	popd
}

static_check_labels()
{
	[[ "$(uname -s)" != Linux ]] && info "Can only check labels under Linux" && return

	# Handle SLES which doesn't provide the required command.
	[[ -z "${have_yamllint_cmd}" ]] && info "Cannot check labels as ${yamllint_cmd} not available" && return

	# Since this script is called from another repositories directory,
	# ensure the utility is built before the script below (which uses it) is run.
	(cd "${test_dir}/cmd/github-labels" && make)

	tmp=$(mktemp)

	files_to_remove+=("${tmp}")

	info "Checking labels for repo ${repo} using temporary combined database ${tmp}"

	bash -f "${test_dir}/cmd/github-labels/github-labels.sh" "generate" "${repo}" "${tmp}"
}

# Ensure all files (where possible) contain an SPDX license header
static_check_license_headers()
{
	# The branch is the baseline - ignore it.
	[[ "${specific_branch}" = "true" ]] && return

	# See: https://spdx.org/licenses/Apache-2.0.html
	local -r spdx_tag="SPDX-License-Identifier"
	local -r spdx_license="Apache-2.0"
	local -r license_pattern="${spdx_tag}: ${spdx_license}"
	local -r copyright_pattern="Copyright"

	local header_checks=()

	header_checks+=("SPDX license header::${license_pattern}")
	header_checks+=("Copyright header:-i:${copyright_pattern}")

	pushd "${repo_path}"

	files=$(get_pr_changed_file_details || true)

	# Strip off status and convert to array
	mapfile -t files < <(echo "${files}"|awk '{print $NF}')

	text_files=()
	# Filter out non-text files
	for file in "${files[@]}"; do
		if [[ -f "${file}" ]] && file --mime-type "${file}" | grep -q "text/"; then
			text_files+=("${file}")
		else
			info "Ignoring non-text file: ${file}"
		fi
	done
	files=("${text_files[@]}")

	# no text files were changed
	[[ "${#files[@]}" -eq 0 ]] && info "No files found" && popd && return

	local header_check

	for header_check in "${header_checks[@]}"
	do
		local desc
		desc=$(echo "${header_check}"|cut -d: -f1)
		local extra_args
		extra_args=$(echo "${header_check}"|cut -d: -f2)
		local pattern
		pattern=$(echo "${header_check}"|cut -d: -f3-)

		info "Checking ${desc}"

		local missing
		# shellcheck disable=SC2086
		missing=$(grep \
			--exclude=".git/*" \
			--exclude=".editorconfig" \
			--exclude=".gitignore" \
			--exclude=".dockerignore" \
			--exclude="Gopkg.lock" \
			--exclude="*.gpl.c" \
			--exclude="*.ipynb" \
			--exclude="*.jpg" \
			--exclude="*.json" \
			--exclude="LICENSE*" \
			--exclude="THIRD-PARTY" \
			--exclude="*.md" \
			--exclude="*.pb.go" \
			--exclude="*pb_test.go" \
			--exclude="*.bin" \
			--exclude="*.png" \
			--exclude="*.pub" \
			--exclude="*.service" \
			--exclude="*.svg" \
			--exclude="*.drawio" \
			--exclude="*.toml" \
			--exclude="*.txt" \
			--exclude="*.dtd" \
			--exclude="vendor/*" \
			--exclude="VERSION" \
			--exclude="kata_config_version" \
			--exclude="tools/packaging/kernel/configs/*" \
			--exclude="virtcontainers/pkg/firecracker/*" \
			--exclude="${ignore_clh_generated_code}*" \
			--exclude="*.xml" \
			--exclude="*.yaml" \
			--exclude="*.yml" \
			--exclude="go.mod" \
			--exclude="go.sum" \
			--exclude="*.lock" \
			--exclude="grpc-rs/*" \
			--exclude="target/*" \
			--exclude="*.patch" \
			--exclude="*.diff" \
			--exclude="tools/packaging/static-build/qemu.blacklist" \
			--exclude="tools/packaging/qemu/default-configs/*" \
			--exclude="src/libs/protocols/protos/gogo/*.proto" \
			--exclude="src/libs/protocols/protos/google/*.proto" \
			--exclude="src/libs/protocols/protos/cri-api/api.proto" \
			--exclude="src/mem-agent/example/protocols/protos/google/protobuf/*.proto" \
			--exclude="src/runtime-rs/crates/hypervisor/src/openvmm/protos/*.proto" \
			--exclude="src/runtime-rs/crates/hypervisor/src/openvmm/protos/google/protobuf/*.proto" \
			--exclude="src/libs/*/test/texture/*" \
			--exclude="*.dic" \
			-EL ${extra_args} -E "\<${pattern}\>" \
			"${files[@]}" || true)

		if [[ -n "${missing}" ]]; then
			cat >&2 <<-EOF
		ERROR: Required ${desc} check ('${pattern}') failed for the following files:

		${missing}

EOF
			exit 1
		fi
	done
	popd
}

run_url_check_cmd()
{
	local url="${1:-}"
	[[ -n "${url}" ]] || die "need URL"

	local out_file="${2:-}"
	[[ -n "${out_file}" ]] || die "need output file"

	# Can be blank
	local extra_args="${3:-}"

	local curl_extra_args=()

	curl_extra_args+=("${extra_args}")

	# Authenticate for github to increase threshold for rate limiting
	if [[ "${url}" =~ github\.com && -n "${GITHUB_USER}" && -n "${GITHUB_TOKEN}" ]]; then
		curl_extra_args+=("-u ${GITHUB_USER}:${GITHUB_TOKEN}")
	fi

	# Some endpoints return 403 to HEAD but 200 for GET,
	# so perform a GET but only read headers.
	# shellcheck disable=SC2048,SC2086
	curl \
		${curl_extra_args[*]} \
		-sIL \
		-X GET \
		-c - \
		-H "Accept-Encoding: zstd, none, gzip, deflate" \
		--max-time "${url_check_timeout_secs}" \
		--retry "${url_check_max_tries}" \
		"${url}" \
		&>"${out_file}"
}

check_url()
{
	local url="${1:-}"
	[[ -n "${url}" ]] || die "need URL to check"

	local invalid_urls_dir="${2:-}"
	[[ -n "${invalid_urls_dir}" ]] || die "need invalid URLs directory"

	local curl_out
	curl_out=$(mktemp)

	files_to_remove+=("${curl_out}")

	# Process specific file to avoid out-of-order writes
	local invalid_file
	invalid_file=$(printf "%s/%d" "${invalid_urls_dir}" "$$")

	local ret

	local -a errors=()

	local -a user_agents=()

	# Test an unspecified UA (curl default)
	user_agents+=('')

	# Test an explictly blank UA
	user_agents+=('""')

	# Single space
	user_agents+=(' ')

	# CLI HTTP tools
	user_agents+=('Wget')
	user_agents+=('curl')

	# console based browsers
	# Hopefully, these will always be supported for a11y.
	user_agents+=('Lynx')
	user_agents+=('Elinks')

	# Emacs' w3m browser
	user_agents+=('Emacs')

	# The full craziness
	user_agents+=('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36')

	local user_agent

	# Cycle through the user agents until we find one that works.
	#
	# Note that we also test an unspecified user agent
	# (no '-A <value>').
	for user_agent in "${user_agents[@]}"
	do
		info "Checking URL ${url} with User Agent '${user_agent}'"

		local curl_ua_args=""
		# shellcheck disable=SC2034
		[[ -n "${user_agent}" ]] && curl_ua_args="-A '${user_agent}'"

		local http_statuses

		http_statuses=$(grep -E "^HTTP" "${curl_out}" |\
			awk '{print $2}' || true)

		if [[ -z "${http_statuses}" ]]; then
			errors+=("no HTTP status codes for URL '${url}' (user agent: '${user_agent}')")

			continue
		fi

		local status

		local -i fail_count=0

		# Check all HTTP status codes
		for status in ${http_statuses}
		do
			# Ignore the following ranges of status codes:
			#
			# - 1xx: Informational codes.
			# - 2xx: Success codes.
			# - 3xx: Redirection codes.
			# - 405: Specifically to handle some sites
			#   which get upset by "curl -L" when the
			#   redirection is not required.
			#
			# Anything else is considered an error.
			#
			# See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes

			{ grep -qE "^(1[0-9][0-9]|2[0-9][0-9]|3[0-9][0-9]|405)" <<< "${status}"; ret=$?; } || true

			[[ "${ret}" -eq 0 ]] && continue

			fail_count+=1
		done

		# If we didn't receive any unexpected HTTP status codes for
		# this UA, the URL is valid so we don't need to check with any
		# further UAs, so clear any (transitory) errors we've
		# recorded.
		[[ "${fail_count}" -eq 0 ]] && errors=() && break

		echo "${url}" >> "${invalid_file}"
		errors+=("found HTTP error status codes for URL ${url} (status: '${status}', user agent: '${user_agent}')")
	done

	# shellcheck disable=SC2128
	[[ "${#errors}" = 0 ]] && return 0

	die "failed to check URL '${url}': errors: '${errors[*]}'"
}

static_check_eof()
{
	local file="$1"
	local anchor="EOF"


	[[ -z "${file}" ]] && info "No files to check" && return

	# Skip the itself
	[[ "${file}" == "${script_name}" ]] && return

	# Skip the Vagrantfile
	[[ "${file}" == "Vagrantfile" ]] && return

	local invalid
	invalid=$(grep -o -E '<<-* *\w*' "${file}" |\
		sed -e 's/^<<-*//g' |\
		tr -d ' ' |\
		sort -u |\
		grep -v -E '^$' |\
		grep -v -E "${anchor}" || true)
	[[ -z "${invalid}" ]] || die "Expected '${anchor}' here anchor, in ${file} found: ${invalid}"
}

# Tests to apply to all files.
#
# Currently just looks for TODO/FIXME comments that should be converted to
# (or annotated with) an Issue URL.
static_check_files()
{
	local file
	local files

	if [[ "${force}" = "false" ]]
	then
		info "Skipping check_files: see https://github.com/kata-containers/tests/issues/469"
		return
	else
		info "Force override of check_files skip"
	fi

	info "Checking files"

	if [[ "${specific_branch}" = "true" ]]
	then
		info "Checking all files in ${branch} branch"

		files=$(git ls-files | grep -v -E "/(.git|vendor|grpc-rs|target)/" || true)
	else
		info "Checking local branch for changed files only"

		files=$(get_pr_changed_file_details || true)

		# Strip off status
		files=$(echo "${files}"|awk '{print $NF}')
	fi

	[[ -z "${files}" ]] && info "No files changed" && return

	local matches=""

	pushd "${repo_path}"

	for file in ${files}
	do
		local match

		# Look for files containing the specified comment tags but
		# which do not include a github URL.
		match=$(grep -H -E "\<FIXME\>|\<TODO\>" "${file}" |\
			grep -v "https://github.com/.*/issues/[0-9]" |\
			cut -d: -f1 |\
			sort -u || true)

		[[ -z "${match}" ]] && continue

		# Don't fail if this script contains the patterns
		# (as it is guaranteed to ;)
		echo "${file}" | grep -q "${script_name}$" && info "Ignoring special file ${file}" && continue

		# We really only care about comments in code. But to avoid
		# having to hard-code the list of file extensions to search,
		# invert the problem by simply ignoring document files and
		# considering all other file types.
		echo "${file}" | grep -q ".md$" && info "Ignoring comment tag in document ${file}" && continue

		matches+=" ${match}"
	done

	popd

	[[ -z "${matches}" ]] && return

	echo >&2 -n \
		"ERROR: The following files contain TODO/FIXME's that need "
	echo >&2 -e "converting to issues:\n"

	for file in ${matches}
	do
		echo >&2 "${file}"
	done

	# spacer
	echo >&2

	exit 1
}

static_check_xml()
{
	local all_xml
	local files

	pushd "${repo_path}"

	need_chronic

	all_xml=$(git ls-files "*.xml" | grep -Ev "/(vendor|grpc-rs|target)/" | sort || true)

	if [[ "${specific_branch}" = "true" ]]
	then
		info "Checking all XML files in ${branch} branch"
		files="${all_xml}"
	else
		info "Checking local branch for changed XML files only"

		local xml_status

		xml_status=$(get_pr_changed_file_details || true)
		xml_status=$(echo "${xml_status}" | grep "\.xml$" || true)

		files=$(echo "${xml_status}" | awk '{print $NF}')
	fi

	[[ -z "${files}" ]] && info "No XML files to check" && popd && return

	local file

	for file in ${files}
	do
		info "Checking XML file '${file}'"

		local contents

		# Most XML documents are specified as XML 1.0 since, with the
		# advent of XML 1.0 (Fifth Edition), XML 1.1 is "almost
		# redundant" due to XML 1.0 providing the majority of XML 1.1
		# features. xmllint doesn't support XML 1.1 seemingly for this
		# reason, so the only check we can do is to (crudely) force
		# the document to be an XML 1.0 one since XML 1.1 documents
		# can mostly be represented as XML 1.0.
		#
		# This is only really required since Jenkins creates XML 1.1
		# documents.
		contents=$(sed "s/xml version='1.1'/xml version='1.0'/g" "${file}")

		local ret

		{ ${chronic} xmllint -format - <<< "${contents}"; ret=$?; } || true

		[[ "${ret}" -eq 0 ]] || die "failed to check XML file '${file}'"
	done

	popd
}

static_check_shell()
{
	local all_scripts
	local scripts

	pushd "${repo_path}"

	need_chronic

	all_scripts=$(git ls-files "*.sh" "*.bash" | grep -Ev "/(vendor|grpc-rs|target)/" | sort || true)

	if [[ "${specific_branch}" = "true" ]]
	then
		info "Checking all scripts in ${branch} branch"
		scripts="${all_scripts}"
	else
		info "Checking local branch for changed scripts only"

		local scripts_status
		scripts_status=$(get_pr_changed_file_details || true)
		scripts_status=$(echo "${scripts_status}" | grep -E "\.(sh|bash)$" || true)

		scripts=$(echo "${scripts_status}" | awk '{print $NF}')
	fi

	[[ -z "${scripts}" ]] && info "No scripts to check" && popd && return 0

	local script

	for script in ${scripts}
	do
		info "Checking script file '${script}'"

		local ret

		{ ${chronic} bash -n "${script}"; ret=$?; } || true

		[[ "${ret}" -eq 0 ]] || die "check for script '${script}' failed"

		static_check_eof "${script}"
	done

	popd
}

static_check_json()
{
	local all_json
	local json_files

	pushd "${repo_path}"

	need_chronic

	all_json=$(git ls-files "*.json" | grep -Ev "/(vendor|grpc-rs|target)/" | sort || true)

	if [[ "${specific_branch}" = "true" ]]
	then
		info "Checking all JSON in ${branch} branch"
		json_files="${all_json}"
	else
		info "Checking local branch for changed JSON only"

		local json_status
		json_status=$(get_pr_changed_file_details || true)
		json_status=$(echo "${json_status}" | grep "\.json$" || true)

		json_files=$(echo "${json_status}" | awk '{print $NF}')
	fi

	[[ -z "${json_files}" ]] && info "No JSON files to check" && popd && return 0

	local json

	for json in ${json_files}
	do
		info "Checking JSON file '${json}'"

		local ret

		{ ${chronic} jq -S . "${json}"; ret=$?; } || true

		[[ "${ret}" -eq 0 ]] || die "failed to check JSON file '${json}'"
	done

	popd
}

# The dockerfile checker relies on the hadolint tool. This function handle its
# installation if it is not found on PATH.
# Note that we need a specific version of the tool as it seems to not have
# backward/forward compatibility between versions.
has_hadolint_or_install()
{
	# Global variable set by the caller. It might be overwritten here.
	linter_cmd=${linter_cmd:-"hadolint"}
	local linter_version
	linter_version=$(get_test_version "externals.hadolint.version")
	local linter_url
	linter_url=$(get_test_version "externals.hadolint.url")
	# shellcheck disable=SC2154
	local linter_dest="${GOPATH}/bin/hadolint"

	local has_linter
	has_linter=$(command -v "${linter_cmd}" || true)
	if [[ -n "${has_linter}" ]]; then
		# Check if the expected linter version
		if "${linter_cmd}" --version | grep -v "${linter_version}" &>/dev/null; then
			warn "${linter_cmd} command found but not the required version ${linter_version}"
			has_linter=""
		fi
	fi

	if [[ -z "${has_linter}" ]]; then
		local download_url="${linter_url}/releases/download/v${linter_version}/hadolint-Linux-x86_64"
		info "Installing ${linter_cmd} ${linter_version} at ${linter_dest}"

		curl -sfL "${download_url}" -o "${linter_dest}" || \
			die "Failed to download ${download_url}"
		chmod +x "${linter_dest}"

		# Overwrite in case it cannot be found in PATH.
		linter_cmd="${linter_dest}"
	fi
}

static_check_dockerfiles()
{
	local all_files
	local files
	local ignore_files
	# Put here a list of files which should be ignored.
	local ignore_files=(
	)

	pushd "${repo_path}"

	local linter_cmd="hadolint"

	all_files=$(git ls-files "*/Dockerfile*" | grep -Ev "/(vendor|grpc-rs|target)/" | sort || true)

	if [[ "${specific_branch}" = "true" ]]; then
		info "Checking all Dockerfiles in ${branch} branch"
		files="${all_files}"
	else
		info "Checking local branch for changed Dockerfiles only"

		local files_status
		files_status=$(get_pr_changed_file_details || true)
		files_status=$(echo "${files_status}" | grep -E "Dockerfile.*$" || true)

		files=$(echo "${files_status}" | awk '{print $NF}')
	fi

	[[ -z "${files}" ]] && info "No Dockerfiles to check" && popd && return 0

	# As of this writing hadolint is only distributed for x86_64
	if [[ "$(uname -m)" != "x86_64" ]]; then
		info "Skip checking as ${linter_cmd} is not available for $(uname -m)"
		popd
		return 0
	fi
	has_hadolint_or_install

	linter_cmd+=" --no-color"

	# Let's not fail with INFO rules.
	linter_cmd+=" --failure-threshold warning"

	# Some rules we don't want checked, below we ignore them.
	#
	# "DL3008 warning: Pin versions in apt get install"
	linter_cmd+=" --ignore DL3008"
	# "DL3041 warning: Specify version with `dnf install -y <package>-<version>`"
	linter_cmd+=" --ignore DL3041"
	# "DL3033 warning: Specify version with `yum install -y <package>-<version>`"
	linter_cmd+=" --ignore DL3033"
	# "DL3018 warning: Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`"
	linter_cmd+=" --ignore DL3018"
	# "DL3003 warning: Use WORKDIR to switch to a directory"
	# See https://github.com/hadolint/hadolint/issues/70
	linter_cmd+=" --ignore DL3003"
	# "DL3048 style: Invalid label key"
	linter_cmd+=" --ignore DL3048"
	# DL3037 warning: Specify version with `zypper install -y <package>=<version>`.
	linter_cmd+=" --ignore DL3037"

	# Temporary add to prevent failure for test migration
	# DL3040 warning: `dnf clean all` missing after dnf command.
	linter_cmd+=" --ignore DL3040"

	local file
	for file in ${files}; do
		if echo "${ignore_files[@]}" | grep -q "${file}" ; then
			info "Ignoring Dockerfile '${file}'"
			continue
		fi

		info "Checking Dockerfile '${file}'"
		local ret
		# The linter generates an Abstract Syntax Tree (AST) from the
		# dockerfile. Some of our dockerfiles are actually templates
		# with special syntax, thus the linter might fail to build
		# the AST. Here we handle Dockerfile templates.
		if [[ "${file}" =~ Dockerfile.*\.(in|template)$ ]]; then
			# In our templates, text with marker as @SOME_NAME@ is
			# replaceable. Usually it is used to replace in a
			# FROM command (e.g. `FROM @UBUNTU_REGISTRY@/ubuntu`)
			# but also to add an entire block of commands. Example
			# of later:
			# ```
			# RUN apt-get install -y package1
			# @INSTALL_MUSL@
			# @INSTALL_RUST@
			# ```
			# It's known that the linter will fail to parse lines
			# started with `@`. Also it might give false-positives
			# on some cases. Here we remove all markers as a best
			# effort approach. If the template file is still
			# unparseable then it should be added in the
			# `$ignore_files` list.
			{ sed -e 's/^@[A-Z_]*@//' -e 's/@\([a-zA-Z_]*\)@/\1/g' "${file}" | ${linter_cmd} -; ret=$?; }\
				|| true
		else
			# Non-template Dockerfile.
			{ ${linter_cmd} "${file}"; ret=$?; } || true
		fi

		[[ "${ret}" -eq 0 ]] || die "failed to check Dockerfile '${file}'"
	done
	popd
}

static_check_rego()
{
	local rego_files
	rego_files=$(git ls-files | grep -E '.*\.rego$')

	interpreters=("opa" "regorus")
	for interpreter in "${interpreters[@]}"
	do
		if ! command -v "${interpreter}" &>/dev/null; then
			die "Required rego interpreter '${interpreter}' not found in PATH"
		fi
	done

	found_unparsable=0
	for file in ${rego_files}
	do
		for interpreter in "${interpreters[@]}"
			do
			if ! "${interpreter}" parse "${file}" > /dev/null; then
				info "Failed to parse Rego file '${file}' with ${interpreter}"
				found_unparsable=1
			else
				info "Successfully parsed Rego file '${file}' with ${interpreter}"
			fi
		done
	done

	if [[ ${found_unparsable} -ne 0 ]]; then
		die "Unparsable rego files found"
	fi
}

# Run the specified function (after first checking it is compatible with the
# users architectural preferences), or simply list the function name if list
# mode is active.
run_or_list_check_function()
{
	local name="$1"

	func_is_valid "${name}"

	local arch_func
	local handler

	arch_func=$(func_is_arch_specific "${name}")

	handler="info"

	# If the user requested only a single function to run, we should die
	# if the function cannot be run due to the other options specified.
	#
	# Whereas if this script is running all functions, just display an
	# info message if a function cannot be run.
	[[ "${single_func_only}" = "true" ]] && handler="die"

	if [[ "${handle_funcs}" = "arch-agnostic" ]] && [[ "${arch_func}" = "yes" ]]; then
		if [[ "${list_only}" != "true" ]]; then
			"${handler}" "Not running '${func}' as requested no architecture-specific functions"
		fi

		return 0
	fi

	if [[ "${handle_funcs}" = "arch-specific" ]] && [[ "${arch_func}" = "no" ]]; then
		if [[ "${list_only}" != "true" ]]; then
			"${handler}" "Not running architecture-agnostic function '${func}' as requested only architecture specific functions"
		fi

		return 0
	fi

	if [[ "${list_only}" = "true" ]]; then
		echo "${func}"
		return 0
	fi

	info "Running '${func}' function"
	eval "${func}"
}

setup()
{
	# shellcheck source=/dev/null
	source /etc/os-release || source /usr/lib/os-release

	trap remove_tmp_files EXIT
}

# Display a message showing some system details.
announce()
{
	local arch
	arch=$(uname -m)

	local file='/proc/cpuinfo'

	local detail
	detail=$(grep -m 1 -E '\<vendor_id\>|\<cpu\> *	*:' "${file}" \
		2>/dev/null |\
		cut -d: -f2- |\
		tr -d ' ' || true)

	local arch="${arch}"

	[[ -n "${detail}" ]] && arch+=" ('${detail}')"

	local kernel
	kernel=$(uname -r)

	local distro_name
	local distro_version

	distro_name="${NAME:-}"
	distro_version="${VERSION:-}"

	local -a lines

	local IFS=$'\n'

    mapfile -t lines <<-EOF
	Running static checks:
	  script: ${script_name}
	  architecture: ${arch}
	  kernel: ${kernel}
	  distro:
	    name: ${distro_name}
	    version: ${distro_version}
	EOF

	local line

	for line in "${lines[@]}"
	do
		info "${line}"
	done
}

main()
{
	setup

	local long_option_names="${!long_options[*]}"

	local args

	args=$(getopt \
		-n "${script_name}" \
		-a \
		--options="h" \
		--longoptions="${long_option_names}" \
		-- "$@")
	# shellcheck disable=SC2181
	[[ $? -eq 0 ]] || { usage >&2; exit 1; }

	eval set -- "${args}"

	local func=

	repo_path=""

	while [[ $# -gt 1 ]]
	do
		case "$1" in
			--all) specific_branch="true" ;;
			--branch) branch="$2"; shift ;;
			--commits) func=static_check_commits ;;
			--dockerfiles) func=static_check_dockerfiles ;;
			--files) func=static_check_files ;;
			--force) force="true" ;;
			--golang) func=static_check_go_arch_specific ;;
			-h|--help) usage; exit 0 ;;
			--json) func=static_check_json ;;
			--labels) func=static_check_labels;;
			--licenses) func=static_check_license_headers ;;
			--list) list_only="true" ;;
			--no-arch) handle_funcs="arch-agnostic" ;;
			--only-arch) handle_funcs="arch-specific" ;;
			--rego) func=static_check_rego ;;
			--repo) repo="$2"; shift ;;
			--repo-path) repo_path="$2"; shift ;;
			--scripts) func=static_check_shell ;;
			--versions) func=static_check_versions ;;
			--xml) func=static_check_xml ;;
			--) shift; break ;;
		esac

		shift
	done

	# Consume getopt cruft
	[[ "$1" = "--" ]] && shift

	[[ "$1" = "help" ]] && usage && exit 0

	# Set if not already set by options
	[[ -z "${repo}" ]] && repo="$1"
	[[ "${specific_branch}" = "false" ]] && specific_branch="$2"

	if [[ -z "${repo}" ]]
	then
		# No repo param provided so assume it's the current
		# one to avoid developers having to specify one now
		# (backwards compatability).
		repo=$(git config --get remote.origin.url |\
			sed 's!https://!!g' || true)
		info "Auto-detected repo as ${repo}"
	fi

	test_path="${test_path:-"${repo}/tests"}"
	test_dir="${GOPATH}/src/${test_path}"

	if [[ -z "${repo_path}" ]]; then
		repo_path="${GOPATH}/src/${repo}"
	else
		test_dir="${repo_path}/${test_path}"
	fi

	announce

	local all_check_funcs
	all_check_funcs=$(typeset -F|awk '{print $3}'|grep "${check_func_regex}"|sort || true)

	# Run user-specified check and quit
	if [[ -n "${func}" ]]; then
		single_func_only="true"
		run_or_list_check_function "${func}"
		exit 0
	fi

	for func in ${all_check_funcs}
	do
		run_or_list_check_function "${func}"
	done
}

main "$@"
