#!/bin/sh
# xlint TEMPLATE - scan XBPS template for common mistakes
# xlint :PKGNAME - lint template as staged in the git index
# xlint :        - lint all templates staged in the git index

export LC_ALL=C

scan() {
	local rx="$1" msg="$(printf '%s\n' "$2" | sed 's,/,\\/,g')"
	grep -P -hn -e "$rx" "$template" |
		grep -v -P -e "[^:]*:\s*#" |
		sed "s/^\([^:]*\):\(.*\)/\1: $msg/" |
		while read line; do
			echo "$argument:$line"
		done
}

check_old_vars() {
	sed -E 's/^([^:]+:[0-9]+:).*: ('"$old_variables"')=.*$/\1 \2 is deprecated and should not be used/'
}

regex_escape() {
	sed 's/\([.*+]\)/\\\1/g'
}

once() {
	head -n 1
}

header() {
	if [ "$(head -n1 "$template")" != "# Template file for '$pkgname'" ]; then
		echo "$argument:1: Header should be: # Template file for '$pkgname'"
	fi
}

exists_once() {
	for var in pkgname version revision short_desc maintainer license \
		   homepage; do
		case "$(grep -c "^${var}=" "$template")" in
			0) echo "$argument:1: '$var' missing!";;
			1) ;;
			*)
				lines="$(grep -n "^${var}=" "$template" | awk -F: 'NR>1 { printf ", " } { printf "%s", $1 }')"
				echo "$argument:${lines##*, }: '$var' defined more than once: previously on line(s) ${lines%,*}"
				;;
		esac
	done
}

explain_make_check() {
	awk "-vargument=$argument" -vOFS=: '
		/make_check=[^#]*$/ && prev !~ /^[[:blank:]]*#/ {print(argument, FNR, " explain why the tests fail")}
		{prev=$0}
	' $template
}

variables_order() {
	local curr_index max_index max_index_line variables_end message line
	local line_number max_index_line_number
	max_index=0
	line_number=0
	while IFS="" read -r line; do
		line_number=$((line_number + 1))
		case "$line" in
			pkgname=*) curr_index=1;;
			reverts=*) curr_index=2;;
			version=*) curr_index=3;;
			revision=*) curr_index=4;;
			archs=*) curr_index=5;;
			create_wrksrc=*) curr_index=8;;
			build_wrksrc=*) curr_index=9;;
			build_style=*) curr_index=10;;
			build_helper=*) curr_index=10;;
			metapackage=*) curr_index=11;;
			cmake_args=*) curr_index=12;;
			cmake_builddir=*) curr_index=12;;
			configure_args=*) curr_index=12;;
			configure_script=*) curr_index=12;;
			go_build_tags=*) curr_index=12;;
			go_get=*) curr_index=12;;
			go_import_path=*) curr_index=12;;
			go_ldflags=*) curr_index=12;;
			go_package=*) curr_index=12;;
			go_mod_mode=*) curr_index=12;;
			make_build_args=*) curr_index=12;;
			make_build_target=*) curr_index=12;;
			make_check_args=*) curr_index=12;;
			make_check_target=*) curr_index=12;;
			make_cmd=*) curr_index=12;;
			make_install_args=*) curr_index=12;;
			make_install_target=*) curr_index=12;;
			make_use_env=*) curr_index=12;;
			meson_builddir=*) curr_index=12;;
			meson_cmd=*) curr_index=12;;
			meson_crossfile=*) curr_index=12;;
			perl_configure_dirs=*) curr_index=12;;
			pycompile_dirs=*) curr_index=12;;
			pycompile_module=*) curr_index=12;;
			python_versions=*) curr_index=12;;
			scons_use_destdir=*) curr_index=12;;
			stackage=*) curr_index=12;;
			cabal_index_state=*) curr_index=12;;
			conf_files=*) continue;;
			make_dirs=*) continue;;
			hostmakedepends=*) curr_index=15;;
			makedepends=*) curr_index=16;;
			depends=*) curr_index=17;;
			checkdepends=*) curr_index=18;;
			short_desc=*) curr_index=19;;
			maintainer=*) curr_index=20;;
			license=*) curr_index=21;;
			homepage=*) curr_index=22;;
			changelog=*) curr_index=23;;
			distfiles=*) curr_index=24;;
			checksum=*) curr_index=25;;
			alternatives=*) curr_index=26;;
			fetch_cmd=*) curr_index=26;;
			bootstrap=*) continue;;
			"#"*) continue;;
			" "*) continue;;
			"	"*) continue;;
			"_"*"="*) continue;;
			*"="*) curr_index=27;;
			"") variables_end=1;;
			*"{") variables_end=1;;
			*) continue;;
		esac

		if [ "$variables_end" ]; then
			break
		elif [ "$curr_index" -lt "$max_index" ]; then
			message="$argument:$max_index_line_number: Place $max_index_line= after ${line%%=*}="
		elif [ "$curr_index" -gt "$max_index" ]; then
			max_index="$curr_index"
			max_index_line="${line%%=*}"
			max_index_line_number=$line_number
			if [ "$message" ]; then
				echo "$message"
				message=
			fi
		fi
	done < "$template"
	[ "$message" ] && echo "$message"
}

file_end() {
	if [ "$(tail -c 1 $template)" ]; then
		echo "$argument:$(( $(wc -l < $template) + 1 )): File does not end with newline character"
	elif [ -z "$(tail -c 2 $template)" ]; then
		echo "$argument:$(wc -l < $template): Last line is empty"
	fi
}

variables=$(echo -n "#.*
_.*
.*_descr
.*_groups
.*_homedir
.*_pgroup
.*_shell
desc_option_.*
AR
AS
CC
CFLAGS
CPP
CPPFLAGS
CXX
CXXFLAGS
GCC
LD
LDFLAGS
LD_LIBRARY_PATH
NM
OBJCOPY
OBJDUMP
RANLIB
READELF
STRIP
XBPS_FETCH_CMD
allow_unknown_shlibs
alternatives
archs
binfmts
bootstrap
broken
build_options
build_options_default
build_style
build_helper
build_wrksrc
cabal_index_state
changelog
checkdepends
checksum
cmake_builddir
conf_files
configure_args
configure_script
conflicts
create_wrksrc
depends
disable_parallel_build
disable_parallel_check
distfiles
dkms_modules
fetch_cmd
font_dirs
force_debug_pkgs
go_build_tags
go_get
go_import_path
go_ldflags
go_package
go_mod_mode
homepage
hostmakedepends
ignore_elf_dirs
ignore_elf_files
keep_libtool_archives
kernel_hooks_version
lib32depends
lib32disabled
lib32files
lib32mode
lib32symlinks
license
maintainer
make_build_args
make_build_target
make_check
make_check_args
make_check_target
make_cmd
make_dirs
make_install_args
make_install_target
make_use_env
makedepends
meson_builddir
meson_cmd
meson_crossfile
metapackage
mutable_files
nofixperms
nocheckperms
no_generic_pkgconfig_link
nocross
nodebug
nopie
nopyprovides
noshlibprovides
nostrip
nostrip_files
noverifypydeps
noverifyrdeps
patch_args
pkgname
preserve
provides
pycompile_dirs
pycompile_module
python_extras
python_version
register_shell
replaces
repository
restricted
reverts
revision
run_depends
scons_use_destdir
sgml_catalogs
sgml_entries
shlib_provides
shlib_requires
short_desc
skip_extraction
skiprdeps
stackage
subpackages
system_accounts
system_groups
tags
triggers
version
xml_catalogs
xml_entries" | tr '\n' '|')

old_variables=$(echo -n "long_desc
wrksrc" | tr '\n' '|')

old_accounts=$(echo -n "at
avahi
bitlbee
boinc
caddy
chrony
clocksd
colord
couchpotato
cups
dbus
deluge
dictd
dnscrypt_proxy
dnsmasq
elastic
elog
etcd
fcron
ftp
gdm
gerbera
gitolite
gogs
gpsd
h2o
haproxy
icinga
inadyn
inspircd
jenkins
kube
ldap
libvirt
lidarr
lightdm
mail
minidlna
monero
mopidy
mpd
munge
mysql
named
nbd
ndhc
nethack
nginx
nsd
nslcd
ntpd
nut
openntpd
orientdb
polkitd
postfix
postgres
privoxy
prosody
pulse
puppet
quassel
radicale
redis
relaysrv
rmilter
rpc
rspamd
rsvlog
rtkit
sddm
shadowsocks
sndiod
squid
storm
suricata
synapse
taskd
tomcat
tor
transmission
tss
tuntox
umurmur
usbmux
voidupdates
x2gouser
xbmc
znc" | tr '\n' '|')

void_packages="$(xdistdir 2>/dev/null)/"

ret=0

if [ "$1" = ":" ]; then
	# get a list of all templates staged in the git index
	set -- $(git -C "$void_packages" diff --cached --name-only |
		sed -ne 's|^srcpkgs/\([^/]*\)/template$|:\1|p')
fi

for argument; do
	template=
	if [ -f "$argument" ]; then
		template="$argument"
	elif [ "${argument#:}" != "$argument" ]; then
		argument="${argument#:}"
		trap "rm -- ${tmpfile:=$(mktemp)}" EXIT INT TERM
		# get template as staged in the git index
		git -C "$void_packages" show ":srcpkgs/$argument/template" \
			> ${template:=$tmpfile} || continue
	else
		_template="${void_packages}srcpkgs/$argument/template"
		[ -f "$_template" ] && template="$_template"
	fi

	if [ "$template" ]; then
	exists_once "$template"
	scan 'short_desc=.*\."' "unwanted trailing dot in short_desc"
	scan 'short_desc=.*[[:space:]]"' "unwanted trailing space in short_desc"
	scan 'short_desc=["'\''][a-z]' "short_desc should start uppercase"
	scan 'short_desc=["'\''](An?|The) ' "short_desc should not start with an article"
	scan 'short_desc=["'\''][\t ]' "short_desc should not start with whitespace"
	scan 'short_desc=["'\''].{74}' "short_desc should be at most 72 chars"
	scan 'license=.*[^NL]GPL[^-]' "license GPL without version"
	scan 'license=.*SSPL' "Uses the SSPL license, which is not packageable"
	scan 'license=.*LGPL[^-]' "license LGPL without version"
	if ! grep -q vlicense "$template"; then
		for l in custom AGPL MIT BSD ISC X11; do
			scan "license=.*$l" "license '$l', but no use of vlicense"
		done
	else
		if ! grep license= "$template" | grep -Pqv -e 'license="(\b(LGPL|GPL|GFDL|Apache)-\S+(, )?)+"'; then
			scan "vlicense" 'license '"$(grep -Po license='\K.+' "$template" | tr -d '",' | tr '\n' ' ')"'should not be installed' | once
		fi
	fi

	: "${LICENSE_LIST:=/usr/share/spdx/license.lst}"
	if [ -f "${LICENSE_LIST}" ]; then
		sed -n 's/license="\(.*\)"/\1/p' "$template" |
		sed -E 's/ (WITH|OR|AND) /,/g' |
		sed 's/[()]//g' |
		tr , "\n" |
		while read -r l; do
			case "$l" in
				custom:*|'Public Domain') continue ;;
			esac

			if ! grep -q "^${l}$" "${LICENSE_LIST}"; then
				scan "license=.*$l" "use SPDX id for '$l' license or see Manual.md"
			fi
		done
	fi

	if ! sed -n '/^version=/{n;/revision=/b;q1}' "$template"; then
		scan 'revision=' "revision does not appear immediately after version"
	fi
	scan 'vinstall.* 0?755.*usr/bin' "use vbin"
	scan 'vinstall.* usr/share/man' "use vman"
	scan 'vinstall.* usr/share/licenses' "use vlicense"
	scan '^  ' "please indent with tabs" | once
	scan '^ +\t' "bad indentation: space before tabs"
	scan '[\t ]$' "trailing whitespace"
	scan '[^\\]`' "use \$() instead of backticks"
	scan '^pkgname="[^$]+"' "pkgname must not be quoted"
	scan '^revision=0' "revision must not be zero"
	scan '^version=.*[-:_].*' "version must not contain the characters - or : or _"
	scan '^version=.*\${.*[:!#%/^,@].*}.*' "version must not use shell variable substitution mechanism"
	scan '^version="[^$]+"' "version must not be quoted"
	scan '^reverts=.*-.*' "reverts must not contain package name"
	scan '^reverts=(?!.*_.*).*' "reverts without revision"
	scan 'archs=.?noarch.?' "noarch is deprecated and should no longer be used"
	scan 'replaces=(?=.*\w)[^<>]*$' "replaces needs depname with version"
	scan 'homepage=.*\$' "homepage should not use variables"
	scan 'maintainer=(?!.*<.*@.*>).*' "maintainer needs email address"
	scan 'maintainer=.*<.*@users.noreply.github.com>.*' "maintainer needs a valid address for sending mail"
	scan '^(?!\s*('"$variables"'))[^\s=-]+=' "custom variables should use _ prefix: \\2" | check_old_vars
	scan '^[^ =]*=(""|''|)$' "variable set to empty string: \\2"
	scan '^(.*)-docs_package().*' 'use <pkgname>-doc subpackage for documentation'
	scan 'distfiles=.*github.com.*/archive/.*\.zip[\"]?$' 'Use the distfile .tar.gz instead of .zip'
	scan 'distfiles=.*downloads\.sourceforge\.net' 'use $SOURCEFORGE_SITE'
	scan 'distfiles=.*savannah.nongnu\.org' 'use $NONGNU_SITE'
	scan 'distfiles=.*archive\.ubuntu\.com' 'use $UBUNTU_SITE'
	scan 'distfiles=.*x\.org/releases/individual' 'use $XORG_SITE'
	scan 'distfiles=.*ftp.*debian\.org' 'use $DEBIAN_SITE'
	scan 'distfiles=.*gnome\.org/pub' 'use $GNOME_SITE'
	scan 'distfiles=.*www\.kernel\.org/pub/linux' 'use $KERNEL_SITE'
	scan 'distfiles=.*cpan\.org/modules/by-module' 'use $CPAN_SITE'
	scan 'distfiles=.*files\.pythonhosted\.org/packages' 'use $PYPI_SITE'
	scan 'distfiles=.*ftp\.mozilla\.org' 'use $MOZILLA_SITE'
	scan 'distfiles=.*ftp\.gnu\.org/(pub/)?gnu' 'use $GNU_SITE'
	scan 'distfiles=.*freedesktop\.org/software' 'use $FREEDESKTOP_SITE'
	scan 'distfiles=.*download.kde.org/stable' 'use $KDE_SITE'
	scan 'distfiles=.*xorg\.freedesktop\.org/wiki/' 'use $XORG_HOME'
	scan 'distfiles=.*crates.io/api/' 'use https://static.crates.io/crates/pkgname/pkgname-${version}.crate'
	scan 'usr/lib/python3.[0-9]/site-packages' 'use $py3_sitelib'
	scan 'pycompile_module=' 'do not set pycompile_module, it is autodetected'
	scan '^\t*function\b' 'do not use the function keyword'
	scan '^\t*[^ ]*  *\(\)' 'do not use space before function parenthesis'
	scan '^\t*[^ ]*\(\)(|   *){' 'use one space after function parenthesis'
	scan '^\t*[^ ]*\(\)$' 'do not use a newline before function opening brace'
	scan 'python_version=.*#[[:space:]]*unverified' 'verify python_version and remove "#unverified"'
	pkgname=$(grep -Po "^pkgname=\K.*" "$template" | once)
	pkgname_re=$(echo "$pkgname" | regex_escape)
	version=$(grep -Po "^version=\K.*" "$template" | once)
	scan "distfiles=.*\Q$version\E" 'use ${version} in distfiles instead'
	scan "system_accounts=.*\b(?!($old_accounts))[a-zA-Z]" 'new accounts should be prefixed with underscore'
	scan "cargo update (--package|-p) [A-Za-z_][A-Za-z0-9_]*(?!\:[0-9]+\.[0-9]+\.[0-9]+)(\s.+)?$" '"cargo update" commands should include the specific version we are updating from in the --package SPEC' 
	scan "cargo update (--package|-p) [A-Za-z_][A-Za-z0-9_-]*\:[0-9]+\.[0-9]+\.[0-9]+(?!--precise [0-9]+\.[0-9]+\.[0-9]+)$" '"cargo update" commands should include the specific version we are updating to, using --precise' 
	scan '-D([[:alnum:]_-]+)=\$\(vopt_if \1 true false\)' 'use $(vopt_bool ...) instead of -D...=$(vopt_if ...)'
	variables_order | grep -Pv "($old_variables)="
	header
	file_end
	explain_make_check
	else
	echo no such template "$argument" >&2
	fi | sort -t: -n -k2 -k3 | grep . && ret=1
done
exit $ret
