#!/bin/sh
# alint APKBUILD - scan APKBUILD template for common mistakes
#
# Adapted from xlint from Void Linux's xtools to Alpine Linux
# https://github.com/leahneukirchen/xtools/
#
# Required packages (names are Alpine Linux pkgs):
# busybox - for grep, sed, tr, sort and other simple utiltiies

export LC_ALL=C

scan() {
	# shellcheck disable=2039
	# 2039: local operator is not posix but we can use it in ash/dash
	local rx="$1" msg="$2" tag="$3" severity="$4"
	grep -E -Hn -e "$rx" "$apkbuild" |
		sed "s/^\([^:]*:[^:]*:\)\(.*\)/$severity:[$tag]:\1$msg/"
}

variables=$(echo -n "#.*
_.*
startdir
srcdir
pkgdir
subpkgdir
builddir
arch
depends
depends_dev
depends_doc
depends_openrc
depends_libs
depends_static
checkdepends
giturl
install
.*.pre-install
.*.post-install
.*.pre-upgrade
.*.post-upgrade
.*.pre-deinstall
.*.post-deinstall
install_if
license
makedepends
makedepends_build
makedepends_host
md5sums
sha256sums
sha512sums
options
pkgdesc
pkggroups
pkgname
pkgrel
pkgusers
pkgver
provides
provider_priority
replaces
replaces_priority
source
subpackages
triggers
ldpath
linguas
sonameprefix
somask
url
langdir
patch_args
cpandepends
cpanmakedepends
cpancheckdepends
HOSTCC" | tr '\n' '|')

valid_options="
!archcheck
!check
checkroot
net
!strip
suid
!tracedeps
chmod-clean
!dbg
toolchain
!fhs
libtool
charset.alias
textrels
!spdx
ldpath-recursive
sover-namecheck
$(echo "$CUSTOM_VALID_OPTIONS" | tr ' ' '\n')
"

valid_arches="
x86_64
!x86_64
x86
!x86
armel
!armel
armhf
!armhf
armv7
!armv7
s390x
!s390x
ppc
!ppc
ppc64
!ppc64
ppc64le
!ppc64le
all
noarch
mips
!mips
mipsel
!mipsel
mips64
!mips64
mips64el
!mips64el
aarch64
!aarch64
$(echo "$CUSTOM_VALID_ARCHES" | tr ' ' '\n')
"

default_builddir_value() {
	[ "$SKIP_DEFAULT_BUILDDIR_VALUE" ] && return 0
	[ "$SKIP_AL1" ] && return 0
	if [ "$builddir" = "/$pkgname-$pkgver" ]; then
		scan '^builddir=' "builddir can be removed as it is the default value" 'AL1' 'MC'
	fi
}

unnecessary_return_1() {
	[ "$SKIP_UNNECESSARY_RETURN_1" ] && return 0
	[ "$SKIP_AL2" ] && return 0
	scan '\|\| return 1' "|| return 1 is not required as set -e is used" 'AL2' 'MC'
}

pkgname_quoted() {
	[ "$SKIP_PKGNAME_QUOTED" ] && return 0
	[ "$SKIP_AL3" ] && return 0
	scan "^pkgname=[\"'][^$]+[\"']" "pkgname must not be quoted" 'AL3' 'TP'
}

pkgver_quoted() {
	[ "$SKIP_PKGVER_QUOTED" ] && return 0
	[ "$SKIP_AL4" ] && return 0
	scan "^pkgver=[\"'][^$]+[\"']" "pkgver must not be quoted" 'AL4' 'TP'
}

empty_variable() {
	[ "$SKIP_EMPTY_VARIABLE" ] && return 0
	[ "$SKIP_AL5" ] && return 0
	scan '^[A-Za-z0-9_]*=(""|''|)$' "variable set to empty string: \2" 'AL5' 'MC'
}

custom_variable() {
	[ "$SKIP_CUSTOM_VARIABLE" ] && return 0
	[ "$SKIP_AL6" ] && return 0
	grep -E -oHn '^[\sA-Za-z0-9_-]*=' $apkbuild | \
		sed "s|:[ 	]*|:|g" | \
		grep -E -v ':('"$variables"')=' | \
		sed "s/^\([^:]*:[^:]*:\)\(.*\)/IC:[AL6]:\1prefix custom variable with _: \2/"
}

indent_tabs() {
	[ "$SKIP_INDENT_TABS" ] && return 0
	[ "$SKIP_AL7" ] && return 0
	scan '^  ' "indent with tabs" 'AL7' 'IC'
}

trailing_whitespace() {
	[ "$SKIP_TRAILING_WHITESPACE" ] && return 0
	[ "$SKIP_AL8" ] && return 0
	scan '[	 ]$' "trailing whitespace" 'AL8' 'IC'
}

backticks_usage() {
	[ "$SKIP_BACKTICKS_USAGE" ] && return 0
	[ "$SKIP_AL25" ] && return 0
	scan '[^\\]`' "use \$() instead of backticks" 'AL25' 'SP'
}

function_keyword() {
	[ "$SKIP_FUNCTION_KEYWORD" ] && return 0
	[ "$SKIP_AL9" ] && return 0
	scan '^	*function\b' 'do not use the function keyword' 'AL9' 'SC'
}

space_before_function_parenthesis() {
	[ "$SKIP_SPACE_BEFORE_FUNCTION_PARENTHESIS" ] && return 0
	[ "$SKIP_AL10" ] && return 0
	scan '^	*[^ ]*  *\(\)' 'do not use space before function parenthesis' 'AL10' 'TC'
}

space_after_function_parenthesis() {
	[ "$SKIP_SPACE_AFTER_FUNCTION_PARENTHESIS" ] && return 0
	[ "$SKIP_AL11" ] && return 0
	scan '^	*[^ ]*\(\)(|   *)\{' 'use one space after function parenthesis' 'AL11' 'TC'
}

newline_opening_brace() {
	[ "$SKIP_NEWLINE_OPENING_BRACE" ] && return 0
	[ "$SKIP_AL12" ] && return 0
	scan '^	*[^ ]*\(\)$' 'do not use a newline before function opening brace' 'AL12' 'TC'
}

superfluous_cd_builddir() {
	[ "$SKIP_SUPERFLUOUS_CD_BUILDDIR" ] && return 0
	[ "$SKIP_AL13" ] && return 0
	# shellcheck disable=2039
	# 2039: local operator is not posix but we can use it in ash/dash
	local cds='' cdscount='' prevcd='' phase="$1"

	# All ocurrences of the 'cd' command being used
	# 1. Print file with line numbers.
	# 2. Print the function from the opening declaration up to the closing bracked
	# 3. grep for all ocurrences of the 'cd' command (ignore obviously invalid ones
	#	like matching 'cd' until the end of the line)
	cds="$(cat -n "$apkbuild" \
		   | sed -n "/^\s\+[0-9].*	$phase() {/,/[0-9].*	}/p" \
		   | grep '\bcd ')"

	# Number of ocurrences of the 'cd' command being used
	# Used to tell if we are in a phase() with a single cd statement
	# in that case we can be free to warn the user that their cd statement
	# is superfluous if it is to "$builddir", this avoids problems of previous
	# 'cd' statements to other places giving false positives
	cdscount="$(printf "%s\\n" "$cds" | wc -l)"

	# if the previous line had a 'cd "$builddir"' statement
	prevcd=0

	# If it is the first cd of the program
	firstcd=1

	# Use newline as our IFS delimiter, so we can iterate over lines with
	# the for construct, since the while loop will create a subshell that
	# prevents the value of the prevcd variable from being propagated
	# to future runs
	OLDIFS="$IFS"
	IFS="
"
	for line in $(printf "%s\\n" "$cds"); do
		linenum="$(printf "%s\\n" "$line" | awk '{ print $1 }')"
		statement="$(printf "%s\\n" "$line" | awk '{ $1="" ; print $0 }')"
		[ -z "$statement" ] && continue
		if echo "$statement" | grep -E -q 'cd ["]?\$[{]?builddir["}]?+($| )' ; then
			if [ "$prevcd" -eq 1 ] || [ "$cdscount" -eq 1 ] || [ "$firstcd" -eq 1 ]; then
				printf "MP:[AL13]:%s:%s:cd \"\$builddir\" can be removed in phase '%s'\\n" \
					"$apkbuild" \
					"$linenum" \
					"$phase"
			fi
			prevcd=1
		else
			prevcd=0
		fi
		# Can be set to 0 in the first loop and the re-set it to 0 in any next loops
		firstcd=0
	done
	IFS="$OLDIFS"
}

pkgname_has_uppercase() {
	[ "$SKIP_PKGNAME_HAS_UPPERCASE" ] && return 0
	[ "$SKIP_AL14" ] && return 0
	scan '^pkgname=[a-z0-9\._\-]*[A-Z]' 'pkgname must not have uppercase characters' 'AL14' 'SC'
}

pkgver_has_pkgrel() {
	[ "$SKIP_PKGVER_HAS_PKGREL" ] && return 0
	[ "$SKIP_AL15" ] && return 0
	scan '^pkgver=.*(-r|_r[^c])' 'pkgver must not have -r or _r' 'AL15' 'SC'
}

_builddir_is_set() {
	[ "$SKIP__BUILDDIR_IS_SET" ] && return 0
	[ "$SKIP_AL26" ] && return 0
	if [ -z "$builddir" ] && [ -n "$_builddir" ]; then
		scan '^_builddir=' 'rename _builddir to builddir' 'AL26' 'SP'
	fi
}

literal_integer_is_quoted() {
	[ "$SKIP_LITERAL_INTEGER_IS_QUOTED" ] && return 0
	[ "$SKIP_AL28" ] && return 0
	scan '^[A-Za-z0-9_]*=('\''|")[0-9]+("|'\'')' 'literal integers must not be quoted' 'AL28' 'MC'
}

pkgname_used_in_source() {
    [ "$SKIP_PKGNAME_USED_IN_SOURCE" ] && return 0
    [ "$SKIP_AL29" ] && return 0
    scan '^source="(.*?::)?[a-z]+://[^"]+\$\{?pkgname\}?' '$pkgname should not be used in the source url' 'AL29' 'MC'
}

double_underscore_in_variable() {
	[ "$SKIP_DOUBLE_UNDERSCORE_IN_VARIABLE" ] && return 0
	[ "$SKIP_AL30" ] && return 0
	# Run this twice, once which will detect variables without the local keyword
	# which requires matching the = sign at the end. The second time will match
	# for variables declared with the local keyword which do not require the =
	# sign
	scan '(^|	)__[A-Za-z0-9_].*=' 'double underscore on variables are reserved' 'AL30' 'MC'
	scan '(^|	)local ([A-Za-z0-9_])?.*__[A-Za-z0-9_].*(=)?' 'double underscore on variables are reserved' 'AL30' 'MC'
}

variable_capitalized() {
	[ "$SKIP_VARIABLE_CAPITALIZED" ] && return 0
	[ "$SKIP_AL31" ] && return 0
	scan '^[a-z0-9_]*[A-Z].*=' 'variables must not have capital letters' 'AL31' 'MC'
	scan '(	)?local [a-z0-9_ ]*[A-Z].*(=)?' 'variables must not have capital letters' 'AL31' 'MC'
}

braced_variable() {
	[ "$SKIP_BRACED_VARIABLE" ] && return 0
	[ "$SKIP_AL32" ] && return 0
	# Match a Sigil ($) then a brace and any valid value until the end brace and then
	# match end-of-line or a character that can't be in the name of a variable
	grep -Eo -Hn -e '\$\{[A-Za-z0-9_]+\}($|["\./\(\)= -])' "$apkbuild" |
		sed "s/^\([^:]*:[^:]*:\)\(.*\)/MP:[AL32]:\1unnecesary usage of braces: \2/" |
		sed 's|[^}]$||g' # This strips the last match in the grep
}

cpan_variable() {
	[ "$SKIP_CPAN_VARIABLE" ] && return 0
	[ "$SKIP_AL35" ] && return 0
	scan '^cpandepends=' 'merge the contents of cpandepends into depends and remove it' 'AL35' 'MC'
	scan '^cpanmakedepends=' 'merge the contents of cpanmakedepends into makedepends and remove it' 'AL35' 'MC'
	scan '^cpancheckdepends=' 'merge the contents of cpancheckdepends into checkdepends and remove it' 'AL35' 'MC'
}

overwrite_xflags() {
	[ "$SKIP_OVERWRITE_XFLAGS" ] && return 0
	[ "$SKIP_AL36" ] && return 0
	# shellcheck disable=SC2016 disable=SC2086
	# We need to match either the start of the line or a whitespace
	# otherwise we can end up matching variables wrongly, like a variable
	# can be called 'LUA_CFLAGS' and we end up matching it because we only check
	# 'CFLAGS'
	#
	# The sed call is to strip any leading whitespace that is created
	grep -E -oHn "(^| |	)$1=\".*\"" $apkbuild |
		sed "s|:[ 	]*$1|:$1|g" |
		grep -vF "\$$1" |
		sed "s/^\([^:]*:[^:]*:\)\(.*\)/SP:[AL36]:\1$1 should not be overwritten, add \$$1 to it/"
}

invalid_option() {
	[ "$SKIP_INVALID_OPTION" ] && return 0
	[ "$SKIP_AL49" ] && return 0
	for i in $options; do
		if ! echo "$valid_options" | grep -q -x "$i"; then
			scan "options=.*$i" "invalid option '$i'" 'AL49' 'MC'
		fi
	done
}

missing_default_prepare() {
	[ "$SKIP_MISSING_DEFAULT_PREPARE" ] && return 0
	[ "$SKIP_AL54" ] && return 0

	# Check if we have prepare() defined
	# shellcheck disable=SC2086
	grep -q "^prepare() {" $apkbuild || return 0

	# shellcheck disable=SC2086
	# The '( | )' part of the sed call is not by mistake, the first component is a whitespace
	# the second component is a literal tab character, created by doing 'CTRL + V, <tab>'
	if ! sed -n '/^prepare() {/,/^}/p' $apkbuild | grep -q -E "^( |	)*default_prepare"; then
		scan "^prepare\(\) \{" "prepare() is missing call to 'default_prepare'" 'AL54' 'SC'
	fi
}

missing_patch_description() {
	[ "$SKIP_MISSING_PATCH_DESCRIPTION" ] && return 0
	[ "$SKIP_AL56" ] && return 0

	ret=0
	for i in $source; do
		case ${i%::*} in
			http://*|ftp://*|https://*) ;;
			*.patch)
				awk -vAPKBUILD="$apkbuild" '
				function desc_check() {
				  if (NR == 1) {
				    printf("MP:[AL56]:%s:Patch file %s is missing a description\n",
				       APKBUILD,
				       FILENAME)
				    exit(1)
				  } else {
				    exit(0)
			          }
				}

				/^--- .*$/  { desc_check() }
				/^diff .*$/ { desc_check() }
				' "$(dirname "${apkbuild}")/$i"

				[ $? -ne 0 ] && ret=1

				;;
		esac
	done

	return $ret
}

build_type_not_none() {
	[ "$SKIP_BUILD_TYPE_NOT_NONE" ] && return 0
	[ "$SKIP_AL56" ] && return 0

	# Check inside the build function if we have a call to cmake
	if sed -n '/^build() {/,/^}/p' $apkbuild | grep -q -F cmake; then
		# Check if we have -DCMAKE_BUILD_TYPE= set
		_call="$(sed -n '/^build() {/,/^}/p' $apkbuild \
				| grep -E -o -- '.*-DCMAKE_BUILD_TYPE=[A-Za-z]*( |$)' \
				| sed 's| *$||g')"
		if [ "$(echo "$_call" | sed 's|.*-DCMAKE_BUILD_TYPE=||g' )" != None ]; then
			grep -Eo -Hn -e "$_call" "$apkbuild" |
				sed "s/^\([^:]*:[^:]*:\)\(.*\)/SP:[AL56]:\1CMAKE_BUILD_TYPE must be None: \2/" |
					sed -r 's|(: )[	]*|\1|g' # Strip all space and tabs caught by the grep
		fi
	fi
}

invalid_arch() {
	[ "$SKIP_INVALID_ARCH" ] && return 0
	[ "$SKIP_AL57" ] && return 0
	for i in $arch; do
		if ! echo "$valid_arches" | grep -q -x "$i"; then
			scan "arch=.*$i" "invalid arch '$i'" 'AL57' 'SC'
		fi
	done
}

ret=0
for apkbuild; do
	if [ -f "$apkbuild" ]; then

	# Source apkbuild, we need some nice values
	srcdir="" . "$apkbuild" || {
		echo "Failed to source APKBUILD in '$apkbuild'" ;
		exit 1;
	}
	default_builddir_value &
	_builddir_is_set &

	pkgname_quoted &
	pkgver_quoted &
	unnecessary_return_1 &
	empty_variable &
	custom_variable &
	indent_tabs &
	trailing_whitespace &
	backticks_usage &
	function_keyword &
	pkgname_has_uppercase &
	pkgver_has_pkgrel &
	space_before_function_parenthesis &
	space_after_function_parenthesis &
	newline_opening_brace &
	literal_integer_is_quoted &
	pkgname_used_in_source &
	double_underscore_in_variable &
	variable_capitalized &
	braced_variable &
	cpan_variable &
	overwrite_xflags "CFLAGS" &
	overwrite_xflags "GOFLAGS" &
	overwrite_xflags "CPPFLAGS" &
	overwrite_xflags "CXXFLAGS" &
	overwrite_xflags "FFLAGS" &
	overwrite_xflags "LDFLAGS" &
	overwrite_xflags "GOFLAGS" &
	missing_default_prepare &
	missing_patch_description &
	build_type_not_none &
	[ "$arch" ] && invalid_arch &
	[ "$options" ] && invalid_option &

	for phase in prepare build check package; do
		superfluous_cd_builddir "$phase" &
	done
	wait
	else
	echo no such apkbuild "$apkbuild" 1>&2
	fi | sort -t: -V | grep . && ret=1
done
exit $ret
