#! /bin/sh
#
#  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.
#
#  NOTE
#  This script uses [ FILE1 -nt FILE2 ] to test if FILE1 is newer than FILE2.
#  But it behaves differently in different shells. Bash behaves as the info
#  page for the test program from GNU Coreutils explains:
#    `FILE1 -nt FILE2'
#         True if FILE1 is newer (according to modification date) than
#         FILE2, or if FILE1 exists and FILE2 does not.
#
#  But zsh and dash behave differently in the case when FILE1 exits and FILE2
#  does not. Therefore the FILE1 -nt FILE2 tests in this script are followed by
#  || [ ! -e FILE2 ].
#
#  So if you are reading this script, only know bash and wonder about the
#  reduanancy; this is the reason for it.

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
elif [ $CHECKER.adb -nt $CHECKER ]; then
	echo "ERROR: $CHECKER is out of date!!!"
	echo "ERROR:    Go to utils/spurious_source_code and rebuild it with make!"
	exit
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
#          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
#          operators: use
#        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
#       contractions: use
#            defines: undecided
#             kdebug: only for KDE
#       multiclasses: undecided
#               null: use
#             qenums: only for Qt
#              style: gives false positives
#        tipsandthis: only for Qt
KRAZY2CHECKS=captruefalse,copyright,explicit,includes,inline,license,operators,passbyvalue,postfixop,spelling,strings,contractions,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
	if
		[ -d $r ]                                                              &&
		[ ! -f $r/disabled ]                                                   &&
		( [ ! $CHECKER ]  || [ ! -f $r/redundant_with_whitespace_checker ] )   &&
		( [ ! $KRAZY2  ]  || [ ! -f $r/redundant_with_krazy2             ] )
	then
		cat $r/regexps >> $REGEXPS_FILE
	fi
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.
check () {
	egrep --with-filename --line-number $([ $COLOUR ] && echo --colour=always) -f $REGEXPS_FILE $1 >  $RESULT_FILE
	if [ $CHECKER ]; then
		$CHECKER $1                                                                                 >> $RESULT_FILE
	else
		utils/detect_spurious_indentation.py $1                                                     >> $RESULT_FILE
	fi
	[ $KRAZY2 ] && $KRAZY2 --brief --export=textedit --check=$KRAZY2CHECKS $1                      >> $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.
detect () {
	for d in $1/*; do
		[ -d $d ] && detect $d
	done
	TIMESTAMP=$CACHE_DIR/$1/timestamp
	PREVIOUS_FILE=$1
	FILES=$(find $1 -maxdepth 1 -name "*.h" -or -name "*.cc")
	if [ -n "$FILES" ]; then
		FILES=$(ls -tr $FILES)
		mkdir -p $CACHE_DIR/$1
		for f in $(echo $FILES); do
			CACHE_BASE=$CACHE_DIR/$f.stylecheck
			CACHE_COLOUR=$CACHE_BASE.colourized
			CACHE=$CACHE_BASE$COLOUR
			if
				[ $f -nt $TIMESTAMP ] || [ ! -e $TIMESTAMP ]                     ||
				[ -s $CACHE_BASE ] || [ -s $CACHE_COLOUR ]
			then
				if [ $f -nt $CACHE ] || [ ! -e $CACHE ]; then
					echo "Checking for errors in $f ..."
					check $f
					if [ -s $RESULT_FILE ]; then
						mv $RESULT_FILE $CACHE
						cat $CACHE
					else
						rm -f $CACHE_BASE $CACHE_COLOUR
					fi
					if
						[ $f -nt $PREVIOUS_FILE ] &&
						( [ $PREVIOUS_FILE -nt $TIMESTAMP ] || [ ! -e $TIMESTAMP ] )
					then
						touch --reference=$PREVIOUS_FILE $TIMESTAMP
					fi
				else
					echo "Showing cached errors in $f ..."
					cat $CACHE
				fi
			fi
			PREVIOUS_FILE=$f
		done
		if [ $PREVIOUS_FILE -nt $TIMESTAMP ] || [ ! -e $TIMESTAMP ]; then
			touch --reference=$PREVIOUS_FILE $TIMESTAMP
		fi
	fi
}


detect src

rm -fr $TMP_DIR
