#!/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 sed, tr, sort and other simple utiltiies
# grep - for grep with -P

export LC_ALL=C

scan() {
	local rx="$1" msg="$2" tag="$3" severity="$4"
	grep -P -Hn -e "$rx" "$apkbuild" |
		sed "s/^\([^:]*:[^:]*:\)\(.*\)/$severity:[$tag]:\1$msg/"
}

variables=$(echo -n "#.*
_.*
startdir
srcdir
pkgdir
subpkgdir
builddir
arch
depends
depends_dev
checkdepends
giturl
install
.*.pre-install
.*.post-install
.*.pre-upgrade
.*.post-upgrade
.*.pre-deinstall
.*.post-deinstall
install_if
license
makedepends
md5sums
sha256sums
sha512sums
options
pkgdesc
pkggroups
pkgname
pkgrel
pkgusers
pkgver
provides
provider_priority
replaces
replaces_priority
source
subpackages
triggers
ldpath
sonameprefix
url" | 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' 'IC'
	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' 'MP'
}

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

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
	scan '^(?!\s*^('"$variables"'))[^\s=-]+=' \
		"prefix custom variable with _: \2" 'AL6' 'IC'
}

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 '[\t ]$' "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 '^\t*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 '^\t*[^ ]*  *\(\)' 'do not use space before function parenthesis' 'AL10' 'IC'
}

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

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

superfluous_cd_builddir() {
	[ "$SKIP_SUPERFLUOUS_CD_BUILDDIR" ] && return 0
	[ "$SKIP_AL13" ] && return 0
	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].*\t$phase() {/,/[0-9].*\t}/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 "IP:[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=[A-Za-z0-9_\-\.]*(-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
}

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'" ;
		continue
	}
	default_builddir_value &
	_builddir_is_set &

	unnecessary_return_1 &
	pkgname_quoted &
	pkgver_quoted &
	empty_variable &
	custom_variable &
	indent_tabs &
	trailing_whitespace &
	backticks_usage &
	function_keyword &
	space_before_function_parenthesis &
	space_after_function_parenthesis &
	newline_opening_brace &
	pkgname_has_uppercase &
	pkgver_has_pkgrel &

	# Don't perform these checks on packages from main
	if ! [ -z "${apkbuild##*main/*}" ]; then
	for phase in prepare build check package; do
		superfluous_cd_builddir "$phase" &
	done
	fi
	wait
	else
	echo no such apkbuild "$apkbuild" 1>&2
	fi | sort -t: -V | grep . && ret=1
done
exit $ret
