#!/bin/bash
#
#  Detects style errors in the C++ source code. Run this from the top directory
#  (which has the subdirectories build, src and utils).
#
#  The output may include colour codes if and only if standard output is a
#  terminal.
#
#  Several check commands are used:
#  * grep with a set of regular expressions
#  * whitespace_checker (with detect_spurious_indentation.py as an inferior
#    fallback)
#  * krazy2 with a selected set of checks
#
#  Results are cached (there are separate caches for colour/nocolour). The
#  check commands do not write directly to a file at the cahce file location
#  because that could leave behind an incomplete cache file that is newer than
#  the source file in case that the check commands are interrupted. Instead
#  write the output from the check commands to a temporary file and when that
#  file is complete, move it to the cache file location.
#
#  Instead of storing empty cache files for source files that do not give any
#  errors, a per-directory timestamp file is stored. This drastically reduces
#  the number of cache files. The drawback is that if the script is interrupted
#  whilst checking a sequence of files with equal mtime, it will have to start
#  over with that sequence the next time it is executed. But reducing the
#  number of cache files is worth that minor inconvenience.

CHECKER=utils/spurious_source_code/whitespace_checker
if [ ! -f $CHECKER ]; then
	echo "WARNING: $CHECKER does not exist!"
	echo "WARNING:    Go to utils/spurious_source_code and build it with make!"
	echo "WARNING:    Else many checks will be done much slower or not at all!"
	unset CHECKER
fi

KRAZY2=$(which krazy2 2>/dev/null)
if [ -z $KRAZY2 ]; then
	KRAZY2=/usr/local/Krazy2/bin/krazy2
	if [ ! -f $KRAZY2 ]; then
		echo "WARNING: krazy2 does not exist!"
		echo "WARNING:    Get it from:"
		echo "WARNING:       svn://anonsvn.kde.org/home/kde/trunk/quality/krazy2"
		echo "WARNING:    Else you will submit code with i. a. spelling errors!"
		unset KRAZY2
	fi
fi

# Which C++ checks from krazy2 should we use?
#       captruefalse: use (fallback: regexps/using_macro_TRUE_FALSE)
#           constref: undecided
#       contractions: use
#          copyright: use
#                cpp: undecided (complains about __APPLE__ and __GNUC__)
#  doublequote_chars: only for Qt
#           doxytags: undecided
#           dpointer: only for shared libraries with binary compatibility reqs
#    emptystrcompare: only for Qt
#    endswithnewline: taken care of by whitespace_checker
#           explicit: use
#            foreach: undecided
#       i18ncheckarg: only for Qt
#          iconnames: only for KDE
#           includes: use
#             inline: use
#            license: use
#      nullstrassign: only for Qt
#     nullstrcompare: only for Qt
#        passbyvalue: use
#          postfixop: use
#         qbytearray: only for Qt
#           qclasses: only for Qt
#           qconnect: only for Qt
#           qmethods: only for Qt
#            qminmax: only for Qt
#            qobject: only for Qt
#       sigsandslots: only for Qt
#           spelling: use
#            strings: use
#           syscalls: only for Qt
#           typedefs: only for Qt
#          camelcase: undecided
#            defines: undecided
#             kdebug: only for KDE
#       multiclasses: undecided
#               null: use
#             qenums: only for Qt
#              style: gives false positives
#        tipsandthis: only for Qt
KRAZY2CHECKS=captruefalse,contractions,copyright,explicit,includes,inline,license,passbyvalue,postfixop,spelling,strings,null

#  Make sure that several instances of this script running concurrently each
#  have a separate temporary directory.
TMP_DIR=/tmp/spurious_source_code_detect.$PPID
rm -fr $TMP_DIR
mkdir $TMP_DIR || { echo "ERROR: could not create $TMP_DIR"; exit; }


REGEXPS_FILE=$TMP_DIR/regexps
for r in utils/spurious_source_code/regexps/*; do
	[[ -d $r && ! -f $r/disabled && ( ! $CHECKER || ! -f $r/redundant_with_whitespace_checker ) && ( ! $KRAZY2 || ! -f $r/redundant_with_krazy2 ) ]] &&
		cat $r/regexps >> $REGEXPS_FILE
done


[[ -t 1 ]] && COLOUR=.colourized || unset COLOUR


RESULT_FILE=$TMP_DIR/result

#  Runs all checks on single source file. The result is written to
#  $RESULT_FILE.
#
#  Requirements on the result:
#  * Error message lines must begin with <filename>:<linenumber> so that they
#    can be parsed by tools like KDevelop.
#  * Any colour codes produced by commands like "grep --colour" must be
#    included if and only if $COLOUR is set.
#
#  Parameters:
#    $1
#      The name of the source file to be checked.
function check {
	egrep --with-filename --line-number $([ $COLOUR ] && echo --colour=always) -f $REGEXPS_FILE $1 >  $RESULT_FILE
	([[ $CHECKER ]] && $CHECKER $1 || utils/detect_spurious_indentation.py $1)                     >> $RESULT_FILE
	([[ $KRAZY2 ]] && $KRAZY2 --export=textedit --check=$KRAZY2CHECKS $1 2>/dev/null)              >> $RESULT_FILE
}


CACHE_DIR="build/stylecheck"

#  Detects errors recursively and stores the result in a cache. The cache
#  content (if any) is echoed to standard output.
#
#  If a cached entry exists and is not older than the source file, no checks
#  are done on that source file.
#
#  Calls itself recursively for each subdirectory. Then checks all the source
#  files in the current directory that are newer than the timestamp file there.
#  Files are checked in mtime order, oldest first. After checking group of
#  files that have the same mtime, the timestamp is is set to that mtime.
#
#  Parameters:
#    $1
#      The name of the directory to start the search and check in.
function detect {
	for d in $1/*; do
		[[ -d $d ]] && detect $d
	done
	TIMESTAMP=$CACHE_DIR/$1/timestamp
	PREVIOUS_FILE=$1
	FILES=$(ls -tr $1/*.h $1/*.cc 2>/dev/null)
	if [[ -n $FILES ]]; then
		mkdir -p $CACHE_DIR/$1
		for f in $FILES; do
			CACHE_BASE=$CACHE_DIR/$f.stylecheck
			CACHE_COLOUR=$CACHE_BASE.colourized
			CACHE=$CACHE_BASE$COLOUR
			if [[ $f -nt $TIMESTAMP || -s $CACHE_BASE || -s $CACHE_COLOUR ]]; then
				if [ $f -nt $CACHE ]; then
					echo "Checking for errors in $f ..."
					check $f
					if [ -s $RESULT_FILE ]; then
						mv $RESULT_FILE $CACHE
						[[ $f -nt $PREVIOUS_FILE && $PREVIOUS_FILE -nt $TIMESTAMP ]] &&
							touch --reference=$PREVIOUS_FILE $TIMESTAMP
						cat $CACHE
					else
						rm -f $CACHE_BASE $CACHE_COLOUR
					fi
				else
					echo "Showing cached errors in $f ..."
					cat $CACHE
				fi
			fi
			PREVIOUS_FILE=$f
		done
		[[ $PREVIOUS_FILE -nt $TIMESTAMP ]] &&
			touch --reference=$PREVIOUS_FILE $TIMESTAMP
	fi
}


detect src

rm -fr $TMP_DIR
