#!/bin/sh
#
# Analyse a Pascal file using p5c Pascal & gcc's static analysis options
# compile the .c file with various warning levels,
# then process and reformat the output
# translating from c terms to pascal (eg "struct" --> "record")
#
#  pan <-cpp> <-level> <file>
#
# <file> is the pascal filename without extention.
# <-cpp> optionally runs the c preprocessor
# <-level> is analysis level
# use c preprocessor for conditional compilation, include, etc
#

CC=gcc

# show brief help if no program specified on command line
if [ -z "$1" ]
then
   echo usage:
   echo "$0 [-cpp] [-level] file"
   echo "where file is pascal file, without the extension (.pas or .p)"
   echo '      -cpp is optional and if present runs the c preprocessor'
   echo '      -level is analysis level:'
   echo '            -0 => quick check only, eg unused variables'
   echo '            -1 => other useful details, eg uninitialised variables'
   echo '            -2 => more info, but maybe lower priority'
   echo '            -3 => even more details, false positives likely'
   echo ' level 1 is the default'
   exit 0
fi


##
# set the gcc warnings for the analysis levels
# details of each of the warnings can be found in your gcc documentation,
# or find the latest version here:
#             https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
#
# edit this if you want different warnings in the different levels
# add warnings if your version of gcc supports them
# shouldn't be necessary to remove any that are not available
##

if $CC --help | grep -q "=.\+warnings" ; then

# line wrap causes problems, cat prevents this
export COLUMNS=200

# filter out the warnings that the installed gcc does not provide
COPTS0=`$CC --help=warnings | cat | awk ' \
                   /-Wdiv-by-zero/ {print $1} \
                   /-Wmaybe-uninitialized/ {print "-Wno-maybe-uninitialized" } \
                   /-Wunused-local-typedefs/ {print "-Wno-unused-local-typedefs" } \
                   /-Wjump-misses-init/ {print $1} \
                   /-Warray-bounds[^=]/ {print "-ftree-vrp", $1 } \
                   /-Wdisabled-optimization/ {print $1} \
                   /-Wunused[^-]/ {print $1} '`

COPTS1=`$CC --help=warnings | cat | awk ' \
                   /-Wuninitialized/ {print $1} \
                   /-Winit-self/ {print $1} \
                   /-Wtautological-compare/ {print $1} \
                   /-Wduplicated-cond/ {print $1} \
                   /-Woverflow/ {print $1} '`

COPTS2=`$CC --help=warnings | cat | awk ' \
                   /-Wswitch-enum/ {print $1} \
                   /-Wfloat-equal/ {print $1} \
                   /-Wempty-body/ {print $1} \
                   /-Wdangling-else/ {print $1} \
                   /-Wparentheses/ {print $1} \
                   /-Wshadow[^-]/ {print $1} \
                   /-Wtype-limits/ {print $1} '`

COPTS3=`$CC --help=warnings | cat | awk ' \
                  /-Wmaybe-uninitialized/ {print $1} \
                  /-Wstrict-overflow[^=]/ {print "-fstrict-overflow", $1} \
                  /-Wclobbered/ {print $1} '`

else
# older gcc, can't determine available warnings, use default
COPTS0="-Wunused -Wdiv-by-zero"
COPTS1="-Wuninitialized -Winit-self"
COPTS2="-Wswitch-enum -Wfloat-equal -Wparentheses -Wshadow"
COPTS3=
fi
# the defaults
WITHCPP=0
COPTS="-O $COPTS0 $COPTS1"

# set up the compiler command line
# -O optimises without messing up line numbers too much
# -O3 has better code analysis, but line numbers become more approximate
for arg in "$@"
do
    case $arg in

        -x)
            COPTS="-O -Wall";
            shift
            ;;
        -0)
            COPTS="$COPTS0";
            shift
            ;;
        -1)
            COPTS="-O $COPTS0 $COPTS1";
            shift
            ;;
        -2)
            COPTS="-O2 $COPTS0 $COPTS1 $COPTS2";
            shift
            ;;
        -3)
            COPTS="-O3 $COPTS0 $COPTS1 $COPTS2 $COPTS3";
            shift
            ;;
        -cpp)
            WITHCPP=1
            shift
            ;;
    esac
done

echo $COPTS

# now compile the pascal program, checking that everything is OK

if [ -z $P5CDIR ] ; then
   echo "\$P5CDIR is not set, bailing out"
  exit 1
fi

if [ ! -x $P5CDIR/p5c ]
then
  echo "can't find p5c compiler, or it is not executable"
  exit 1
fi

if [ -f $1 ] && [ "`basename $1 .PAS`.PAS" = "`basename $1`" ] ; then
    PASFILE=`dirname $1`/`basename $1 .PAS`
    EXT=PAS
elif [ -f $1 ] && [ "`basename $1 .pas`.pas" = "`basename $1`" ] ; then
    PASFILE=`dirname $1`/`basename $1 .pas`
    EXT=pas
elif [ -f $1 ] && [ `basename $1 .p`.p = "`basename $1`" ] ; then
    PASFILE=`dirname $1`/`basename $1 .p`
    EXT=p
elif [ -f $1 ] && [ `basename $1 .pp`.pp = "`basename $1`" ] ; then
    PASFILE=`dirname $1`/`basename $1 .pp`
    EXT=pp
elif [ -f $1.pas ] ; then
    PASFILE=$1;
    EXT=pas
elif [ -f $1.p ] ; then
    PASFILE=$1;
    EXT=p
elif [ -f $1.pp ] ; then
    PASFILE=$1;
    EXT=pp
else
    echo "can't find $1 or $1.pas or $1.pp or $1.p"
    exit 1
fi

if head --lines=10 $PASFILE.$EXT | grep -qe "^#" ; then
    WITHCPP=1
fi

echo '{$n+,d-,v-  -- force line numbers, omit debug code }' > $PASFILE.1.$EXT
if [ $WITHCPP -ne 0 ]; then
    cpp -E -nostdinc -I $P5CDIR $PASFILE.$EXT -w | grep -v -e "^# 1 \"<" >> $PASFILE.1.$EXT
else
    echo "#1 \"$PASFILE.$EXT\"" >> $PASFILE.1.$EXT
    cat $PASFILE.$EXT >> $PASFILE.1.$EXT
fi

$P5CDIR/p5c $PASFILE.1.$EXT $PASFILE.c >  $PASFILE.lst

# The status of the compile is not returned,
# so convert a non-zero error message to fail status
if ! grep -qF "Errors in program: 0" $PASFILE.lst
then
    #tail  $PASFILE.lst
    awk '{ if( $2=="****" ) {print l0 "\n" l1 "\n" $0 ;} \
           else { l0=l1; l1 = $0; }; } \
           /^Errors in program/,0; \
        ' $PASFILE.lst > /dev/stderr

    echo "cannot compile $PASFILE, quitting" > /dev/stderr
    exit 1
fi

$CC -std=gnu99 $COPTS -I $P5CDIR -S -o /dev/null $PASFILE.c 2> $PASFILE.err
if [ -s $PASFILE.err ]
then
    if grep -qF ": error: unrecognized command line option" $PASFILE.err
    then
        awk '/: error: unrecognized command line option/ \
             {$1 = $2 = ""; print }' $PASFILE.err
        echo "gcc compile failed, quitting" > /dev/stderr
        exit 1
    fi
    if grep -qF ": error: " $PASFILE.err
    then
        echo "gcc compile failed, quitting" > /dev/stderr
        exit 1
    fi
fi
rm $PASFILE.1.$EXT

# now process gcc's output and generate a report
echo " - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ";
echo
echo "  ---- pascal analysis for $PASFILE.$EXT ---- " `date` " ---- "
echo
echo $COPTS | awk '{for(i=1; i<=NF; i++) if($i !~ /^-W/) $i=""; \
                    gsub(/-W/,""); print "checking", $0}'
echo

awk ' {gsub(/[\`‘]/, "'\''"); gsub(/['\''’]/, "'\''"); } \
      /'\''__[a-z0-9_]+'\''/ {$0="";}  \
      /p5c\.h/ {$0="";} \
      /['\''‘]_P/ {$0="";} \
      /unused variable ['\''\`‘]_PJB/ {$0="";} \
      /shadowed declaration is here/ {$0="";} \
      $NF ~ /\[-W.*\]$/ { sub(/-W/,"",$NF); }
      $1 ~ /:[0-9]+:[0-9]+:$/ { split($1,a,":"); \
                                $1 = a[1] ": near line " a[2] ":" } \
      /:\r?$/ \
                         { $1="\n"$1; \
                           sub(/function ['\''‘]main['\''’]/, "main program"); \
                           sub( /:[0-9]+:$/, ":" ); \
                           sub( /_[0-9]['\''’]/, "'\''" ); \
                           fName = $0;} \
      /\$result/ {sub(/['\''‘]\$result[0-9]+['\''’]/, "function result");} \
      /warning: unused variable .\$tw/ && !/p5x\.h/ \
                         { print $1 " no fields of record used in with statement"; } \
       /warning:/ \
       && (!/['\''\`‘][A-Za-z\$][a-z0-9]*['\''’]/ || /statement/ || /['\''\`‘][A-Za-z0-9]+_[0-9]+['\''’]/ || /else/) \
                    { sub( /_[0-9]+['\''’]/, "'\''" ); \
                      sub( / *warning:/, "" ); \
                      gsub( /_[0-9]+\./, "." ); \
                      gsub( /typedef/, "type" ); \
                      sub( /['\''\`‘]longjmp.+vfork['\''’]/, "'\''goto'\''" ); \
                      sub( /['\''\`‘]do['\''’] statement/, "'\''for'\'' statement" ); \
                      sub( /struct/, "record" ); \
                      sub( /&&/, "and" ); sub( /\|\|/, "or" ); \
                      gsub( /\.component\[/, "[+" ); \
                      if ($0 ~ /\.element\[(.+)\]/) { \
                            elnr = 8*gensub(/(.+)\.element\[(.+)\]/, "\\2", 1, $5); \
                            name = gensub(/(.+)\.element\[(.+)\]/, "\\1", 1, $5); \
                            $5 = "in set " name ", at least one element at or immediately after position " elnr; \
                      } \
                      sub( /u\]/, "]" ); \
                      $0 = gensub(/suggest(.+)braces/, "suggest\\1'\''begin end'\''", 1);
                      sub(/== or !=/, "= or <>");
                      sub(/ in this function/, "");
                      if( fName != "" ) {print fName; fName = ""}; \
                      print $0; getline; print $0 "\n"; \
                      }' \
                               $PASFILE.err

############################ end of pan ##############################

