#!/usr/bin/perl -w
###############################################################################
# Sanity checks for your KDE source code                                      #
# Copyright (C) 2005-2007 by Allen Winter <winter@kde.org>                    #
#                                                                             #
# This program is free software; you can redistribute it and/or modify        #
# it under the terms of the GNU General Public License as published by        #
# the Free Software Foundation; either version 2 of the License, or           #
# (at your option) any later version.                                         #
#                                                                             #
# This program is distributed in the hope that it will be useful,             #
# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the                #
# GNU General Public License for more details.                                #
#                                                                             #
# You should have received a copy of the GNU General Public License           #
# along with this program; if not, write to the Free Software                 #
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. #
#                                                                             #
###############################################################################
# Plugin-based: simply put your sanity checker somewhere in $KRAZY_PLUGIN_PATH.
# the sanity checker program can be written in the language of your choice,
# but it must follow the following rules:
#
# Plugin Rules:
#   1. must accept the following optional command line args:
#        --help:      print one-line help message and exit
#        --version:   print one-line version information and exit
#        --explain:   print an explanation with solving instructions, then exit
#        --quiet:     suppress all output messages
#        --verbose:   print the offending content for each file processed
#   2. must require one command line argument which is the file to test
#   3. must exit with status 0, and print "okay" if the file passes the test
#   4. must exit with non-zero status (=total failures) if file fails the test
#   5. must print a string to standard output showing line number(s) that
#      fail the test.
#   6. the plugin should be a quick test of a source code file
#   7. the --explain option must print an explanation of why the offending
#      code is a problem, along with instructions on how to fix the code.
#
# Program options:
#   --help:          display help message and exit
#   --version:       display version information and exit
#   --list:          list all the sanity programs
#   --explain:       if issues found, print an explanation along with
#                    solving instructions at the end of each test
#   --check <prog[,prog1,prog2,...,progN]>:
#                    run the specified sanity checker program(s) only
#   --exclude <prog[,prog1,prog2,...,progN]>:
#                    do NOT run the specified sanity checker program(s)
#   --export <text|textlist|ebn|html>
#                    output in one of the following formats:
#                      text (default)
#                      textlist -> plain old text, 1 offending file-per-line
#                      ebn -> English Breakfast Network style
#                      html -> plain old html
#   --title:         give the output a project title.
#   --cms:           component/module/subdir triple for html and ebn exports
#   --quiet:         suppress all output messages
#   --verbose:       print the offending content for each file processed
#
use strict;
use Getopt::Long;
use Env qw (HOME KRAZY_PLUGIN_PATH);
use File::Basename;
use File::Find;
use Text::Wrap;
use HTML::Entities;
use POSIX qw (strftime);

my($Prog) = 'krazy';
my
$VERSION = '1.3'; #split line so MakeMaker can find the version here

my($help) = '';
my($version) = '';
my($explain) = '';
my($list) = '';
my($quiet) = '';
my($verbose) = '';
my($only) = '';
my($exclude) = '';
my($export) = 'text';
my($title) = "$Prog Analysis";
my($cms) = "KDE";

exit 1
if(!GetOptions('help' => \$help, 'version' => \$version, 'explain' => \$explain,
               'list' => \$list, 'verbose' => \$verbose, 'quiet' => \$quiet,
	       'check=s' => \$only, 'exclude=s' => \$exclude,
	       'export=s' => \$export,
	       'title=s' => \$title, 'cms=s' => \$cms));

&Help() if ($help);
if (!$list && $#ARGV < 0){ &Help(); exit 0; }
&Version() if ($version);

if ($export) {
  $export = lc($export);
  if (($export ne "text") && ($export ne "textlist") &&
      ($export ne "ebn") && ($export ne "html")) {
    print "Unsupported export type \"$export\"... exiting\n";
    exit 1;
  }
}

############################################################
# This section builds the list of checker programs to run. #
############################################################
$KRAZY_PLUGIN_PATH = "/usr/libexec/krazy-plugins:/usr/local/libexec/krazy-plugins:krazy-plugins" if (!$KRAZY_PLUGIN_PATH);

my(@sanity_paths);
my(@sanity_progs);

my($sp);
for $sp (split(/:/, $KRAZY_PLUGIN_PATH)) {
  push(@sanity_paths,$sp) if ( -d $sp );
}
if ($#sanity_paths < 0 ) {
  print "No plugin paths found.\nPlease check your KRAZY_PLUGIN_PATH environment variable... exiting\n";
  exit 1;
}
find (\&buildProgList, @sanity_paths);
sub buildProgList {
  -x && ! -d && push (@sanity_progs, $File::Find::name);
}

# Print the list of available checker programs and exit.
&List() if $list;

my(@progs);
# If only, then run the specified check program only
if ($only) {
  my(@only_progs) = split(",",$only);
  my($i,$o);
  for $o (@only_progs) {
    # Make sure specified only program is in the list
    my($matchidx) = -1;
    for ($i=0; $i<@sanity_progs; $i++) {
      if (&basename($sanity_progs[$i]) eq $o) {
	$matchidx = $i;
	last;
      }
    }
    if ($matchidx == -1) {
      print "No such sanity program \"$o\"... exiting\n";
      exit 1;
    } else {
      push(@progs,$sanity_progs[$matchidx]);
    }
  }
} else {
  my($s);
  for $s (@sanity_progs) {
    push(@progs,$s);
  }
}
## process program exclusions
if ($exclude) {
  my(@exclude_progs) = split(",",$exclude);
  my($i,$e);
  for $e (@exclude_progs) {
    # Make sure specified exclude program is in the list
    my($matchidx) = -1;
    for ($i=0; $i<@progs; $i++) {
      if (&basename($progs[$i]) eq $e) {
	$matchidx = $i;
	last;
      }
    }
    if ($matchidx == -1) {
      print "\"$e\" is not in the list of checker programs... exiting\n";
      exit 1;
    } else {
      splice(@progs,$matchidx,1);
    }
  }
}

if ($#progs < 0) {
  print "No checker programs to run... exiting...\n";
  exit 1;
}

#####################################################################
# This section runs each checker program for each specified file,   #
# collecting the output and exit status into a hash on the checker. #
#####################################################################

# Options to pass to the checker programs
my($opts) = "";
$opts .= "--quiet "  if ($quiet);
$opts .= "--verbose " if ($verbose);

my($overall_status) = 0;
my($p, $f, $use, %result, $pid, %status);
for $p (sort @progs) {
  my($bp)=&basename($p);
  print STDERR "=>$bp test in-progress." unless ($quiet);
  $result{$p} = "";
  my($nf) = 0;
  for $f (@ARGV) {
    $nf++;
    # skip the following files because they are auto-generated but do not
    # contain text that can be tested to determine that situation.
    next if ($f =~ m/la\.all_cpp\.cpp$/);

    # skip auto-generated files -- test the first 5 lines for known signatures
    open(F, "<$f") || die "Couldn't open $f";
    my(@c) = <F>;
    my($tt) = join '', @c[0.. ($#c > 5 ? 5 : $#c)];
    close(F);
    next if ($tt =~ /(All changes made in this file will be lost|DO NOT EDIT|DO NOT delete this file|[Gg]enerated by|uicgenerated)|Bison parser|define BISON_/);

    # run the checker, concatentating the output
    $pid = open(SANE, "$p $opts $f |") or print STDERR "Cannot run: &basename($p)\n";
    while (<SANE>) {
      chomp($_);
      $result{$p} .= "\t" . $f . ": " . $_ . "\n"
	unless ($_ =~ m+[Oo][Kk][Aa][Yy]$+ || $_ =~ m+[Nn]/[Aa]+);
    }
    close(SANE);
    $status{$p} += $?>>8;
    print STDERR "." unless ($nf%10 || $quiet);
  }
  $overall_status += $status{$p};
  print STDERR "done\n" unless ($quiet);
}

###############################
# This section prints results #
###############################
if (!$quiet) {

  &printHeader($overall_status,$#progs+1);

  my($st) = 0;
  my($cline,$rline);
  for $p (sort @progs) {
    $st++;
    $use = `$p --help`;
    chomp($use);
    $use = "no description available" if (length($use) < 4);
    $cline = '';
    $cline .= "$st. " if ($export eq "text");
    $cline .= "$use... ";
    if ($status{$p} > 0) {
      my($si) = ($status{$p}>1?"issues":"issue");
      $rline = "OOPS! $status{$p} $si found!";
    } else {
      $rline = "okay!";
    }
    &printCheck($cline,$rline);
    if ($status{$p} && length($result{$p}) > 0) {
      printOOPS($result{$p});
      if ($explain) {
	my($use) = `$p --explain 2>/dev/null`;
	chomp($use);
	$use = "(no explanation available)" if (length($use) < 4);
	&printExplain(wrap("        ", "        ", $use));
      }
    }
    print "\n" if ($export ne "textlist");
  }

  &printFooter();
}

# This program exits with a sum of all issues for each file processed.
exit $overall_status;


#==============================================================================
# Help function: print help message and exit.
sub Help {
  &Version();
  print "A KDE source code sanitizer.\n\n";
  print "Usage: $Prog [OPTION]... FILE...\n";
  print "  --help             display help message and exit\n";
  print "  --version          display version information and exit\n";
  print "  --list             list all the sanity programs\n";
  print "  --explain          print explanations with solving instructions\n";
  print "  --check <prog[,prog1,prog2,...,progN]>\n";
  print "                     run the specified sanity checker program(s) only\n";
  print "  --exclude <prog[,prog1,prog2,...,progN]>\n";
  print "                     do NOT run the specified sanity checker program(s)\n";
  print "  --export <text|textlist|ebn|html>\n";
  print "                     output in one of the following formats:\n";
  print "                       text (default)\n";
  print "                       textlist -> plain old text, 1 offending file-per-line\n";
  print "                       ebn -> English Breakfast Network style\n";
  print "                       html -> plain old html\n";
  print "  --title            give the output a project title\n";
  print "  --cms              component/module/subdir triple for html and ebn exports\n";
  print "  --quiet            suppress all output messages\n";
  print "  --verbose          print the offending content for each file processed\n";
  print "\n";
  exit 0 if $help;
}

# Version function: print the version number and exit.
sub Version {
  print "$Prog, version $VERSION\n";
  exit 0 if $version;
}

# List function: print a list of all checker programs available to run.
sub List{
  my($prog, $use);
  print "Available KDE source code sanitizer checks:\n";
  for $prog (sort @sanity_progs) {
    $use = `$prog --help`;
    chomp($use);
    $use = "(no description available)" if (length($use) < 4);
    printf("%18.18s: %s\n", &basename($prog), $use);
    if ($explain) {
      $use = `$prog --explain 2>/dev/null`;
      chomp($use);
      $use = "(no explanation available)" if (length($use) < 4);
      $use = "=>" . $use;
      print wrap("                  ",
                 "                    ",
		 $use); print "\n\n";
    }
  }
  exit 0 if $list;
}

# asOf function: return nicely formatted string containing the current time
sub asOf{
  return strftime("%B %d %Y %H:%M:%S", localtime(time()));
}

# printHeader function: print the header string, according to export type.
sub printHeader{
  my($num_issues,$num_checks)=@_;
  my($component,$module,$subdir)=split("/",$cms);
  my($upcomp) = uc($component);
  $upcomp =~ s/-/ /;

  if ($export eq "ebn") {
    print "<html>\n";
    print "<head>\n";
    print "<title>$title</title>\n";
    print "<link rel=\"stylesheet\" type=\"text/css\" title=\"Normal\" href=\"/style.css\" />\n";
    print "</head>\n";
    print "<body>\n";
    print "<div id=\"title\">\n";
    print "<div class=\"logo\">&nbsp;</div>\n";
    print "<div class=\"header\">\n";
    print "<h1><a href=\"/\">English Breakfast Network</a></h1>\n";
    print "<p><a href=\"/\">Almost, but not quite, entirely unlike tea.</a></p>\n";
    print "</div>\n";
    print "</div>\n";
    print "<div id=\"content\">\n";
    print "<div class=\"inside\">\n";

    # Breadcrumbs
    print "<p style=\"font-size: x-small;font-style: sans-serif;\">\n";
    print "<a href=\"/index.php\">Home</a>&nbsp;&gt;&nbsp;\n";
    print "<a href=\"/$Prog/index.php\">Source Code Sanitizer Results</a>&nbsp;&gt;&nbsp;\n";
    print "<a href=\"/$Prog/index.php?component=$component\">$upcomp</a>&nbsp;&gt;&nbsp;\n" if ($component);
    print "<a href=\"/$Prog/index.php?component=$component&module=$module\">$module</a>&nbsp;&gt;&nbsp;\n" if ($component && $module);
    print "$subdir\n" if ($subdir);
    print "</p>\n";

    # Links to other available reports
    if ($component && $module && $subdir) {
      print "<p style=\"font-size: x-small;font-style: sans-serif;\">\n";
      print "Other $module/$subdir reports:\n";
      print "[<a href=\"/apidocs/apidox-$component/$module-$subdir.html\">APIDOX</a>]\n";
      print "[<a href=\"/sanitizer/reports/$component/$module/$subdir/index.html\">Docs</a>]\n";
      print "</p>\n";
    }

    print "<h1>$title</h1>\n";
    print "<p>Checkers Run = $num_checks<br>\n";
    if ($num_issues) {
      print "Total Issues = $num_issues";
    } else {
      print "No Issues Found!";
    }
    print " ...as of "; print &asOf(); print "</p>\n";
    print "<ol>\n";
  } else {
    if ($export eq "html") {
      print "<html>\n";
      print "<head>\n";
      print "<title>$title</title>\n";
      print "</head>\n";
      print "<body>\n";
      print "<h1>$title</h1>\n";
      print "<p>Checkers Run = $num_checks<br>\n";
      if ($num_issues) {
	print "Total Issues = $num_issues";
      } else {
	print "No Issues Found!";
      }
      print " ...as of "; print &asOf(); print "</p>\n";
      print "<ol>\n";
    } else {
      if ($export eq "text") {
	print "\n$title\n" if ($title);
	print "\nCheckers Run = $num_checks\n";
	if ($num_issues) {
	  print "Total Issues = $num_issues";
	} else {
	  print "No Issues Found!";
	}
	print " ...as of "; print &asOf(); print "\n\n";
      }
    }
  }
}

# printFooter function: print the footer string, according to export type.
sub printFooter{

  if ($export eq "ebn") {
    print "</ol>\n";
    print "</div>\n";
    print "</div>\n";
    print "<div id=\"footer\">\n";
    print "<p>Site content Copyright 2005-2007 by Adriaan de Groot,<br/>\n";
    print "except images as indicated.</p>\n";
    print "</div>\n";
    print "</body>\n";
    print "</html>\n";
  } else {
    if ($export eq "html") {
      print "</ol>\n";
      print "</body>\n";
      print "</html>\n";
    } else {
      if ($export eq "text") {
	print "";
      }
    }
  }
}

# printCheck function: print the "check" and "result" strings, according
# to export type.
sub printCheck(){
  my($check,$result) = @_;
  if ($export eq "text") {
    print "$check $result\n";
  } else {
    if ($export eq "ebn" || $export eq "html") {
      # export is "ebn" or "html"
      print "<li>";
      print "<span class=\"toolmsg\">" if ($export eq "ebn");
      print "$check\n<b>$result</b>";
      print "</span>" if ($export eq "ebn");
    }
  }
}

# printOOPS function: print the OOPS lines, according to export type.
sub printOOPS{

  my($o);
  print "\n<ul>\n" if ($export !~ "text");
  for $o (split("\n",$_[0])) {
    chomp($o);
    print "<li>" if ($export !~ "text");
    if ($export eq "textlist") {
      my($t) = split(":",$o); $t =~ s+^\s++;
      print "$t\n";
    } else {
      print "$o\n";
    }
    print "</li>\n" if ($export !~ "text");
  }
  print "</ul>\n" if ($export !~ "text");
}

# printExplain function: print the explanation lines, according to export type.
sub printExplain{
  if ($export eq "ebn") {
    print "<p class=\"explanation\">";
    print &htmlify($_[0]);
    print "</p>\n</li>\n";
  } else {
    if ($export eq "html"){
      print "<p>";
      print "<p><p><b>Why should I care?</b>";
      print &htmlify($_[0]);
      print "</p>\n</li>\n";
    } else {
      # text export
      print "$_[0]\n";
    }
  }
}

# htmlify function: turn plain text into html
sub htmlify{
  my($s) = @_;
  $s = encode_entities($s);
  $s =~ s+&lt;http:(.*?)&gt;+<a href="http:$1">http:$1</a>+gs;
  $s =~ s=\*(\S+)\*=<strong>$1</strong>=g; # *word* becomes boldified
  $s =~ s=\b\_(\S+)\_\b=<em>$1</em>=g;     # _word_ becomes italicized
  return($s);
}

__END__
#==============================================================================

=head1 NAME

krazy - Sanity checks KDE source code.

=head1 SYNOPSIS

krazy [options] file1 file2 file3 ...

=head1 DESCRIPTION

krazy scans KDE source code looking for issues that should be fixed
for reasons of policy, good coding practice, optimization, or any other
good reason.  In typical use, krazy simply counts up the issues
and provides the line numbers where those issues occurred in each
file processed.  With the verbose option, the offending content will
be printed as well.

krazy uses "sanity checker programs" which are small plugin programs
to do the real work of the scanning.  It is easy to write your own plugins
(see B<PLUGINS>) and tell krazy how to use them (see B<ENVIRONMENT>).

=head1 OPTIONS

=over 4

=item B<--help>

Print help message and exit.

=item B<--version>

Print version information and exit.

=item B<--list>

Print a list of all available sanity checker programs and exit.

=item B<--explain>

For each sanity program, if any issues are found, print an explanation
of the problem along with solving instructions.  May be used in conjunction
with the B<--list> option to provide a more detailed description of the
sanity checker programs.

=item B<--check> <prog[,prog1,prog2,...,progN]>

Run the specified sanity checker program(s) only.

=item B<--exclude> <prog[,prog1,prog2,...,progN]>

Do B<NOT> run the specified sanity checker program(s).

=item B<--export> <text|textlist|ebn|html>

Output in one of the following formats:
     text (default)
     textlist -> plain old text, 1 offending file-per-line
     ebn -> English Breakfast Network style
     html -> plain old html

=item B<--title>

Give the output a project title.

=item B<--cms>

An acronym for "component/module/subdir".  Used to write the breadcrumbs line
in the ebn and html export.  Must be a slash-delimited triple containing the
component, module, and subdir which is being scanned.

=item B<--quiet>

Suppress all output messages.

=item B<--verbose>

Print the offending content for each file processed

=back

=head1 EXAMPLES

=over 4

=item Print a list of all available sanity checker programs along with a short description:

 % krazy --list

 Available KDE source code sanitizer checks:
         capfalse: Check for FALSE macro
          captrue: Check for TRUE macro
        copyright: Check for an acceptable copyright
 doublequote_char: Check for adding single char string to a QString
          license: Check for an acceptable license
    nullstrassign: Check for assignments to QString::null
   nullstrcompare: Check for compares to QString::null
             qmax: Check for QMAX macros
             qmin: Check for QMIN macros

=item Run all sanity checker programs on a file:

 % krazy fred.cc

 =>capfalse test in-progress.done
 =>captrue test in-progress.done
 =>copyright test in-progress.done
 =>doublequote_chars test in-progress.done
 =>license test in-progress.done
 =>nullstrassign test in-progress.done
 =>nullstrcompare test in-progress.done
 =>qmax test in-progress.done
 =>qmin test in-progress.done

 No Issues Found!

 1. Check for FALSE macro... okay!

 2. Check for TRUE macro... okay!

 3. Check for an acceptable copyright... okay!

 4. Check for adding single char string to a QString... okay!

 5. Check for an acceptable license... okay!

 6. Check for assignments to QString::null... okay!

 7. Check for compares to QString::null... okay!

 8. Check for QMAX macros... okay!

 9. Check for QMIN macros... okay!

=item Run all sanity checker programs B<except> F<license> and F<copyright> the .cpp files in the current working directory:

 % krazy --exclude license,copyright *.cpp

=item Run the C<capfalse> sanity checker programs on the *.cpp, *.cc, and *.h found in the current working directory tree, printing explanations if any issues are encountered:

 % find . -name "*.cpp" -o -name "*.cc" -o -name "*.h" | \
 xargs krazy --check capfalse --explain

 =>capfalse test in-progress........done

 Total Issues = 10

 1. Check for FALSE macro... OOPS! 232 issues found!
        ./fred.h: line#176 (1)
        ./fredmsg.h: line#41,54 (2)
        ./fred.cpp.cpp: line#436,530,702,724,1030,1506,1525 (7)

        The FALSE macro is obsolete and should be replaced by the
        false (all lower case) macro.

=back

=head1 PLUGINS

Write your own plugin:

=over 4

=item
Copy TEMPLATE.pl to your new file.

=item
Make the new file executable C<chmod +x file>.

=item
Move the new file into the sanity_plugins directory, or create
your own krazy-plugins directory and add it to $KRAZY_PLUGIN_PATH.

=back

You may write the plugin in the language of your choice,
but it must follow these rules:

=over 4

=item 1.
must accept the following optional command line args:

 --help:     print one-line help message and exit
 --version:  print one-line version information and exit
 --explain:  print an explanation with solving instructions and exit
 --quiet:    suppress all output messages
 --verbose:  print the offending content for each file processed

=item 2.
must require one command line argument which is the file to test.

=back

=over

=item 3.
must exit with status 0, and print "okay" if the file passes the test.

=item 4.
must exit with non-zero status (=total issues) if issues are encountered.

=item 5.
must print a string to standard output showing line number(s) that fail the test.

=item 6.
the plugin should be a quick test of a source code file.

=item 7.
the --explain option must print an explanation of why the offending code is a problem, along with instructions on how to fix the code.

=item 8.
I<finally, and importantly, the plugin must eliminate false positives as much as possible.>

=back

=head1 ENVIRONMENT

KRAZY_PLUGIN_PATH - this is a colon-separated list of paths which is
searched when locating plugins. By default, plugins are searched for in
the path F</usr/libexec/krazy-plugins:/usr/local/libexec/krazy-plugins:krazy-plugins>.

=head1 EXIT STATUS

In normal operation, krazy exits with a status equal to the total number
of issues encountered during processing.

If a command line option was incorrectly provided, krazy exits with
status=1.

If krazy was envoked with the B<--help>, B<--version>, or B<--list>
options it will exit with status=0.

=head1 COPYRIGHT

Copyright (c) 2005-2007 by Allen Winter <winter@kde.org>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

=head1 SEE ALSO

Ben Meyer's kdetestscripts - Automated scripts are to catch problems in KDE,
L<http://websvn.kde.org/trunk/playground/base/kdetestscripts>.

flawfinder - Examines source code looking for security weaknesses,
L<http://www.dwheeler.com/flawfinder>.

=head1 AUTHORS

Allen Winter, <winter@kde.org>

=cut
